diff --git a/package.json b/package.json index 9a97fc1b5..21bb6a260 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@paraswap/dex-lib", - "version": "4.0.3", + "version": "4.0.4", "main": "build/index.js", "types": "build/index.d.ts", "repository": "https://github.com/paraswap/paraswap-dex-lib", diff --git a/src/abi/cables/CablesMainnetRFQ.json b/src/abi/cables/CablesMainnetRFQ.json new file mode 100644 index 000000000..11cd0fade --- /dev/null +++ b/src/abi/cables/CablesMainnetRFQ.json @@ -0,0 +1,1083 @@ +[ + { + "inputs": [], + "name": "AccessControlBadConfirmation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "neededRole", + "type": "bytes32" + } + ], + "name": "AccessControlUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "AddressInsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "EnforcedPause", + "type": "error" + }, + { + "inputs": [], + "name": "ExpectedPause", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "SafeERC20FailedOperation", + "type": "error" + }, + { + "inputs": [], + "name": "addressCannotBeZero", + "type": "error" + }, + { + "inputs": [], + "name": "atLeastOneAdminNeeded", + "type": "error" + }, + { + "inputs": [], + "name": "batchArraysMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "expired", + "type": "error" + }, + { + "inputs": [], + "name": "expiredByOverride", + "type": "error" + }, + { + "inputs": [], + "name": "invalidMsgValue", + "type": "error" + }, + { + "inputs": [], + "name": "invalidNonce", + "type": "error" + }, + { + "inputs": [], + "name": "invalidSignature", + "type": "error" + }, + { + "inputs": [], + "name": "swapSignerCannotBeZero", + "type": "error" + }, + { + "inputs": [], + "name": "transferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "wethAddressCannotBeZero", + "type": "error" + }, + { + "inputs": [], + "name": "zeroAmount", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "actionName", + "type": "string" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAddress", + "type": "address" + } + ], + "name": "AddressSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "EIP712DomainChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "RebalancerWithdraw", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "previousAdminRole", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "newAdminRole", + "type": "bytes32" + } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "actionName", + "type": "string" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "updatedRole", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "address", + "name": "updatedAddress", + "type": "address" + } + ], + "name": "RoleUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "nonceAndMeta", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "taker", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "destTrader", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "destChainId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "srcAsset", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "destAsset", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "srcAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "destAmount", + "type": "uint256" + } + ], + "name": "SwapExecuted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "nonceAndMeta", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "SwapExpired", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "newSwapSigner", + "type": "address" + } + ], + "name": "SwapSignerUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "REBALANCER_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "VERSION", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "name": "addAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "name": "addRebalancer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_assets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_amounts", + "type": "uint256[]" + } + ], + "name": "batchClaimBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_asset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "claimBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "completedSwaps", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "eip712Domain", + "outputs": [ + { + "internalType": "bytes1", + "name": "fields", + "type": "bytes1" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "version", + "type": "string" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "verifyingContract", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "uint256[]", + "name": "extensions", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "expiredSwaps", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "getRoleMember", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleMemberCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "hasRole", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_swapSigner", + "type": "address" + }, + { + "internalType": "address", + "name": "_wethAddress", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "name": "isAdmin", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "name": "isRebalancer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_hash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "_signature", + "type": "bytes" + } + ], + "name": "isValidSignature", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "nonceAndMeta", + "type": "uint256" + }, + { + "internalType": "uint128", + "name": "expiry", + "type": "uint128" + }, + { + "internalType": "address", + "name": "makerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "takerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "taker", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makerAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takerAmount", + "type": "uint256" + } + ], + "internalType": "struct CablesRFQ.Order", + "name": "_order", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "_signature", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "_takerAmount", + "type": "uint256" + } + ], + "name": "partialSwap", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "name": "removeAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "name": "removeRebalancer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "callerConfirmation", + "type": "address" + } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_swapSigner", + "type": "address" + } + ], + "name": "setSwapSigner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "nonceAndMeta", + "type": "uint256" + }, + { + "internalType": "uint128", + "name": "expiry", + "type": "uint128" + }, + { + "internalType": "address", + "name": "makerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "takerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "taker", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makerAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takerAmount", + "type": "uint256" + } + ], + "internalType": "struct CablesRFQ.Order", + "name": "_order", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "_signature", + "type": "bytes" + } + ], + "name": "simpleSwap", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "swapSigner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_nonceAndMeta", + "type": "uint256" + } + ], + "name": "updateSwapExpiry", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "wethAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/src/dex/bebop/bebop.ts b/src/dex/bebop/bebop.ts index 46e0bb08e..fe69a4f19 100644 --- a/src/dex/bebop/bebop.ts +++ b/src/dex/bebop/bebop.ts @@ -723,7 +723,7 @@ export class Bebop extends SimpleExchange implements IDex { } } - if (side == SwapSide.SELL) { + if (side === SwapSide.SELL) { const requiredAmount = BigInt(optimalSwapExchange.destAmount); const quoteAmount = BigInt( response.buyTokens[utils.getAddress(destToken.address)].amount, diff --git a/src/dex/cables/cables-e2e.test.ts b/src/dex/cables/cables-e2e.test.ts new file mode 100644 index 000000000..f1cdf62dd --- /dev/null +++ b/src/dex/cables/cables-e2e.test.ts @@ -0,0 +1,207 @@ +import dotenv from 'dotenv'; +dotenv.config(); +import { testE2E } from '../../../tests/utils-e2e'; +import { Tokens, Holders } from '../../../tests/constants-e2e'; +import { Network, ContractMethod, SwapSide } from '../../constants'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { generateConfig } from '../../config'; + +const sleepMs: number = 10000; +const slippage: number = 100; + +describe('Cables E2E', () => { + const dexKey = 'Cables'; + + const sideToContractMethods = new Map([ + [SwapSide.SELL, [ContractMethod.swapExactAmountIn]], + [SwapSide.BUY, [ContractMethod.swapExactAmountOut]], + ]); + + describe('Arbitrum', () => { + const network = Network.ARBITRUM; + const provider = new StaticJsonRpcProvider( + generateConfig(network).privateHttpProvider, + network, + ); + const tokens = Tokens[network]; + const holders = Holders[network]; + + const pairs: { name: string; sellAmount: string; buyAmount: string }[][] = [ + [ + { + name: 'USDC', + sellAmount: '500000', + buyAmount: '700000', + }, + { + name: 'USDT', + sellAmount: '600000', + buyAmount: '850000', + }, + ], + [ + { + name: 'WETH', + sellAmount: '100000000000000000', + buyAmount: '200000000000000000', + }, + { + name: 'USDT', + sellAmount: '6000000', + buyAmount: '8000000', + }, + ], + ]; + + sideToContractMethods.forEach((contractMethods, side) => + describe(`${side}`, () => { + contractMethods.forEach((contractMethod: ContractMethod) => { + pairs.forEach(pair => { + describe(`${contractMethod}`, () => { + it(`${pair[0].name} -> ${pair[1].name}`, async () => { + await testE2E( + side === SwapSide.SELL + ? tokens[pair[0].name] + : tokens[pair[1].name], + side === SwapSide.SELL + ? tokens[pair[1].name] + : tokens[pair[0].name], + side === SwapSide.SELL + ? holders[pair[0].name] + : holders[pair[1].name], + side === SwapSide.SELL + ? pair[0].sellAmount + : pair[0].buyAmount, + side, + dexKey, + contractMethod, + network, + provider, + undefined, + undefined, + undefined, + slippage, + sleepMs, + ); + }); + it(`${pair[1].name} -> ${pair[0].name}`, async () => { + await testE2E( + side === SwapSide.SELL + ? tokens[pair[1].name] + : tokens[pair[0].name], + side === SwapSide.SELL + ? tokens[pair[0].name] + : tokens[pair[1].name], + side === SwapSide.SELL + ? holders[pair[1].name] + : holders[pair[0].name], + side === SwapSide.SELL + ? pair[1].sellAmount + : pair[1].buyAmount, + side, + dexKey, + contractMethod, + network, + provider, + undefined, + undefined, + undefined, + slippage, + sleepMs, + ); + }); + }); + }); + }); + }), + ); + }); + + describe('Avalanche', () => { + const network = Network.AVALANCHE; + const provider = new StaticJsonRpcProvider( + generateConfig(network).privateHttpProvider, + network, + ); + const tokens = Tokens[network]; + const holders = Holders[network]; + + const pairs: { name: string; sellAmount: string; buyAmount: string }[][] = [ + [ + { + name: 'USDC', + sellAmount: '1000000', + buyAmount: '700000', + }, + { + name: 'USDT', + sellAmount: '1000000', + buyAmount: '850000', + }, + ], + ]; + + sideToContractMethods.forEach((contractMethods, side) => + describe(`${side}`, () => { + contractMethods.forEach((contractMethod: ContractMethod) => { + pairs.forEach(pair => { + describe(`${contractMethod}`, () => { + it(`${pair[0].name} -> ${pair[1].name}`, async () => { + await testE2E( + side === SwapSide.SELL + ? tokens[pair[0].name] + : tokens[pair[1].name], + side === SwapSide.SELL + ? tokens[pair[1].name] + : tokens[pair[0].name], + side === SwapSide.SELL + ? holders[pair[0].name] + : holders[pair[1].name], + side === SwapSide.SELL + ? pair[0].sellAmount + : pair[0].buyAmount, + side, + dexKey, + contractMethod, + network, + provider, + undefined, + undefined, + undefined, + slippage, + sleepMs, + ); + }); + it(`${pair[1].name} -> ${pair[0].name}`, async () => { + await testE2E( + side === SwapSide.SELL + ? tokens[pair[1].name] + : tokens[pair[0].name], + side === SwapSide.SELL + ? tokens[pair[0].name] + : tokens[pair[1].name], + side === SwapSide.SELL + ? holders[pair[1].name] + : holders[pair[0].name], + side === SwapSide.SELL + ? pair[1].sellAmount + : pair[1].buyAmount, + side, + dexKey, + contractMethod, + network, + provider, + undefined, + undefined, + undefined, + slippage, + sleepMs, + ); + }); + }); + }); + }); + }), + ); + }); +}); diff --git a/src/dex/cables/cables-integration.test.ts b/src/dex/cables/cables-integration.test.ts new file mode 100644 index 000000000..62dc90c58 --- /dev/null +++ b/src/dex/cables/cables-integration.test.ts @@ -0,0 +1,391 @@ +/* eslint-disable no-console */ +import dotenv from 'dotenv'; +dotenv.config(); + +import { DummyDexHelper } from '../../dex-helper/index'; +import { Network, SwapSide } from '../../constants'; +import { BI_POWS } from '../../bigint-constants'; +import { Cables } from './cables'; +import { + checkPoolPrices, + checkPoolsLiquidity, + checkConstantPoolPrices, + sleep, +} from '../../../tests/utils'; +import { Tokens } from '../../../tests/constants-e2e'; + +async function testPricingOnNetwork( + cables: Cables, + network: Network, + dexKey: string, + blockNumber: number, + srcTokenSymbol: string, + destTokenSymbol: string, + side: SwapSide, + amounts: bigint[], +) { + const networkTokens = Tokens[network]; + + const pools = await cables.getPoolIdentifiers( + networkTokens[srcTokenSymbol], + networkTokens[destTokenSymbol], + side, + blockNumber, + ); + console.log( + `${srcTokenSymbol} <> ${destTokenSymbol} Pool Identifiers: `, + pools, + ); + + expect(pools.length).toBeGreaterThan(0); + + const poolPrices = await cables.getPricesVolume( + networkTokens[srcTokenSymbol], + networkTokens[destTokenSymbol], + amounts, + side, + blockNumber, + pools, + ); + console.log( + `${srcTokenSymbol} <> ${destTokenSymbol} Pool Prices: `, + poolPrices, + ); + + expect(poolPrices).not.toBeNull(); + if (cables.hasConstantPriceLargeAmounts) { + checkConstantPoolPrices(poolPrices!, amounts, dexKey); + } else { + checkPoolPrices(poolPrices!, amounts, side, dexKey); + } +} + +describe('Cables', function () { + const dexKey = 'Cables'; + let blockNumber: number; + let cables: Cables; + + describe('Avalanche', () => { + const network = Network.AVALANCHE; + const dexHelper = new DummyDexHelper(network); + + const tokens = Tokens[network]; + + const tokenASymbol = 'USDC'; + const tokenBSymbol = 'USDT'; + + const amountsForTokenA = [ + 0n, + 1n * BI_POWS[tokens[tokenASymbol].decimals], + 2n * BI_POWS[tokens[tokenASymbol].decimals], + 3n * BI_POWS[tokens[tokenASymbol].decimals], + 4n * BI_POWS[tokens[tokenASymbol].decimals], + 5n * BI_POWS[tokens[tokenASymbol].decimals], + 6n * BI_POWS[tokens[tokenASymbol].decimals], + 7n * BI_POWS[tokens[tokenASymbol].decimals], + 8n * BI_POWS[tokens[tokenASymbol].decimals], + 9n * BI_POWS[tokens[tokenASymbol].decimals], + 10n * BI_POWS[tokens[tokenASymbol].decimals], + ]; + + const amountsForTokenB = [ + 0n, + 1n * BI_POWS[tokens[tokenBSymbol].decimals], + 2n * BI_POWS[tokens[tokenBSymbol].decimals], + 3n * BI_POWS[tokens[tokenBSymbol].decimals], + 4n * BI_POWS[tokens[tokenBSymbol].decimals], + 5n * BI_POWS[tokens[tokenBSymbol].decimals], + 6n * BI_POWS[tokens[tokenBSymbol].decimals], + 7n * BI_POWS[tokens[tokenBSymbol].decimals], + 8n * BI_POWS[tokens[tokenBSymbol].decimals], + 9n * BI_POWS[tokens[tokenBSymbol].decimals], + 10n * BI_POWS[tokens[tokenBSymbol].decimals], + ]; + + beforeEach(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + cables = new Cables(network, dexKey, dexHelper); + await cables.initializePricing(blockNumber); + await sleep(5000); + }); + + afterEach(async () => { + if (cables.releaseResources) cables.releaseResources(); + await sleep(5000); + }); + + it(`getPoolIdentifiers and getPricesVolume SELL ${tokenASymbol} ${tokenBSymbol}`, async function () { + await testPricingOnNetwork( + cables, + network, + dexKey, + blockNumber, + tokenASymbol, + tokenBSymbol, + SwapSide.SELL, + amountsForTokenA, + ); + }); + + it(`getPoolIdentifiers and getPricesVolume BUY ${tokenASymbol} ${tokenBSymbol}`, async function () { + await testPricingOnNetwork( + cables, + network, + dexKey, + blockNumber, + tokenASymbol, + tokenBSymbol, + SwapSide.BUY, + amountsForTokenB, + ); + }); + + it(`getPoolIdentifiers and getPricesVolume SELL ${tokenBSymbol} ${tokenASymbol}`, async function () { + await testPricingOnNetwork( + cables, + network, + dexKey, + blockNumber, + tokenBSymbol, + tokenASymbol, + SwapSide.SELL, + amountsForTokenB, + ); + }); + + it(`getPoolIdentifiers and getPricesVolume BUY ${tokenBSymbol} ${tokenASymbol}`, async function () { + await testPricingOnNetwork( + cables, + network, + dexKey, + blockNumber, + tokenBSymbol, + tokenASymbol, + SwapSide.BUY, + amountsForTokenA, + ); + }); + + it.skip('getTopPoolsForToken', async function () { + // We have to check without calling initializePricing, because + // pool-tracker is not calling that function + const cables = new Cables(network, dexKey, dexHelper); + const poolLiquidity = await cables.getTopPoolsForToken( + tokens[tokenASymbol].address, + 10, + ); + console.log( + `${tokenASymbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + console.log( + `${tokenASymbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + + if (!cables.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity( + poolLiquidity, + Tokens[network][tokenASymbol].address, + dexKey, + ); + } + }); + }); + + describe('Arbitrum', () => { + const network = Network.ARBITRUM; + const dexHelper = new DummyDexHelper(network); + + const tokens = Tokens[network]; + + const tokenASymbol = 'USDC'; + const tokenBSymbol = 'USDT'; + + const amountsForTokenA = [ + 0n, + 1n * BI_POWS[tokens[tokenASymbol].decimals], + 2n * BI_POWS[tokens[tokenASymbol].decimals], + 3n * BI_POWS[tokens[tokenASymbol].decimals], + 4n * BI_POWS[tokens[tokenASymbol].decimals], + 5n * BI_POWS[tokens[tokenASymbol].decimals], + 6n * BI_POWS[tokens[tokenASymbol].decimals], + 7n * BI_POWS[tokens[tokenASymbol].decimals], + 8n * BI_POWS[tokens[tokenASymbol].decimals], + 9n * BI_POWS[tokens[tokenASymbol].decimals], + 10n * BI_POWS[tokens[tokenASymbol].decimals], + ]; + + const amountsForTokenB = [ + 0n, + 1n * BI_POWS[tokens[tokenBSymbol].decimals], + 2n * BI_POWS[tokens[tokenBSymbol].decimals], + 3n * BI_POWS[tokens[tokenBSymbol].decimals], + 4n * BI_POWS[tokens[tokenBSymbol].decimals], + 5n * BI_POWS[tokens[tokenBSymbol].decimals], + 6n * BI_POWS[tokens[tokenBSymbol].decimals], + 7n * BI_POWS[tokens[tokenBSymbol].decimals], + 8n * BI_POWS[tokens[tokenBSymbol].decimals], + 9n * BI_POWS[tokens[tokenBSymbol].decimals], + 10n * BI_POWS[tokens[tokenBSymbol].decimals], + ]; + + beforeEach(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + cables = new Cables(network, dexKey, dexHelper); + await cables.initializePricing(blockNumber); + await sleep(5000); + }); + + afterEach(async () => { + if (cables.releaseResources) cables.releaseResources(); + await sleep(5000); + }); + + it(`getPoolIdentifiers and getPricesVolume SELL ${tokenASymbol} ${tokenBSymbol}`, async function () { + await testPricingOnNetwork( + cables, + network, + dexKey, + blockNumber, + tokenASymbol, + tokenBSymbol, + SwapSide.SELL, + amountsForTokenA, + ); + }); + + it(`getPoolIdentifiers and getPricesVolume BUY ${tokenASymbol} ${tokenBSymbol}`, async function () { + await testPricingOnNetwork( + cables, + network, + dexKey, + blockNumber, + tokenASymbol, + tokenBSymbol, + SwapSide.BUY, + amountsForTokenB, + ); + }); + + it(`getPoolIdentifiers and getPricesVolume SELL ${tokenBSymbol} ${tokenASymbol}`, async function () { + await testPricingOnNetwork( + cables, + network, + dexKey, + blockNumber, + tokenBSymbol, + tokenASymbol, + SwapSide.SELL, + amountsForTokenB, + ); + }); + + it(`getPoolIdentifiers and getPricesVolume BUY ${tokenBSymbol} ${tokenASymbol}`, async function () { + await testPricingOnNetwork( + cables, + network, + dexKey, + blockNumber, + tokenBSymbol, + tokenASymbol, + SwapSide.BUY, + amountsForTokenA, + ); + }); + + describe.skip('getTopPoolsForToken', () => { + it('USDC getTopPoolsForToken', async function () { + // We have to check without calling initializePricing, because + // pool-tracker is not calling that function + const tokenSymbol = 'USDC'; + const cables = new Cables(network, dexKey, dexHelper); + const poolLiquidity = await cables.getTopPoolsForToken( + tokens[tokenSymbol].address, + 10, + ); + console.log( + `${tokenASymbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + + if (!cables.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity( + poolLiquidity, + Tokens[network][tokenASymbol].address, + dexKey, + ); + } + }); + + it('WETH getTopPoolsForToken', async function () { + // We have to check without calling initializePricing, because + // pool-tracker is not calling that function + const tokenSymbol = 'WETH'; + const cables = new Cables(network, dexKey, dexHelper); + const poolLiquidity = await cables.getTopPoolsForToken( + tokens[tokenSymbol].address, + 10, + ); + console.log( + `${tokenASymbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + + if (!cables.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity( + poolLiquidity, + Tokens[network][tokenASymbol].address, + dexKey, + ); + } + }); + + it('ETH getTopPoolsForToken', async function () { + // We have to check without calling initializePricing, because + // pool-tracker is not calling that function + const tokenSymbol = 'ETH'; + const cables = new Cables(network, dexKey, dexHelper); + const poolLiquidity = await cables.getTopPoolsForToken( + tokens[tokenSymbol].address, + 10, + ); + console.log( + `${tokenASymbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + + if (!cables.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity( + poolLiquidity, + Tokens[network][tokenASymbol].address, + dexKey, + ); + } + }); + + it('ARB getTopPoolsForToken', async function () { + // We have to check without calling initializePricing, because + // pool-tracker is not calling that function + const tokenSymbol = 'ARB'; + const cables = new Cables(network, dexKey, dexHelper); + const poolLiquidity = await cables.getTopPoolsForToken( + tokens[tokenSymbol].address, + 10, + ); + console.log( + `${tokenASymbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + + if (!cables.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity( + poolLiquidity, + Tokens[network][tokenASymbol].address, + dexKey, + ); + } + }); + }); + }); +}); diff --git a/src/dex/cables/cables.ts b/src/dex/cables/cables.ts new file mode 100644 index 000000000..eb14b3f8e --- /dev/null +++ b/src/dex/cables/cables.ts @@ -0,0 +1,823 @@ +import { + Address, + NumberAsString, + OptimalSwapExchange, + SwapSide, +} from '@paraswap/core'; +import { assert, AsyncOrSync } from 'ts-essentials'; +import * as CALLDATA_GAS_COST from '../../calldata-gas-cost'; +import { ETHER_ADDRESS, Network, NULL_ADDRESS } from '../../constants'; +import { IDexHelper } from '../../dex-helper'; +import { + AdapterExchangeParam, + DexExchangeParam, + ExchangePrices, + ExchangeTxInfo, + Logger, + PoolLiquidity, + PoolPrices, + PreprocessTransactionOptions, + Token, + TransferFeeParams, +} from '../../types'; +import { getDexKeysWithNetwork, Utils } from '../../utils'; +import { IDex } from '../idex'; +import { SimpleExchange } from '../simple-exchange'; +import { CablesConfig } from './config'; +import { + CABLES_API_BLACKLIST_POLLING_INTERVAL_MS, + CABLES_API_PAIRS_POLLING_INTERVAL_MS, + CABLES_API_PRICES_POLLING_INTERVAL_MS, + CABLES_API_TOKENS_POLLING_INTERVAL_MS, + CABLES_API_URL, + CABLES_BLACKLIST_CACHES_TTL_S, + CABLES_ERRORS_CACHE_KEY, + CABLES_FIRM_QUOTE_TIMEOUT_MS, + CABLES_GAS_COST, + CABLES_PAIRS_CACHES_TTL_S, + CABLES_PRICES_CACHES_TTL_S, + CABLES_RESTRICT_CHECK_INTERVAL_MS, + CABLES_RESTRICT_COUNT_THRESHOLD, + CABLES_RESTRICT_TTL_S, + CABLES_RESTRICTED_CACHE_KEY, + CABLES_TOKENS_CACHES_TTL_S, +} from './constants'; +import { CablesRateFetcher } from './rate-fetcher'; +import { + CablesData, + CablesRFQResponse, + RestrictData, + SlippageError, +} from './types'; +import mainnetRFQAbi from '../../abi/cables/CablesMainnetRFQ.json'; +import { Interface } from 'ethers/lib/utils'; +import BigNumber from 'bignumber.js'; +import { ethers } from 'ethers'; +import { BI_MAX_UINT256 } from '../../bigint-constants'; +import _ from 'lodash'; +import { BebopData } from '../bebop/types'; + +export class Cables extends SimpleExchange implements IDex { + public static dexKeysWithNetwork: { key: string; networks: Network[] }[] = + getDexKeysWithNetwork(CablesConfig); + + readonly isStatePollingDex = true; + + private rateFetcher: CablesRateFetcher; + + logger: Logger; + private tokensMap: { [address: string]: Token } = {}; + + hasConstantPriceLargeAmounts: boolean = false; + + constructor( + readonly network: Network, + readonly dexKey: string, + readonly dexHelper: IDexHelper, + readonly mainnetRFQAddress: string = CablesConfig['Cables'][network] + .mainnetRFQAddress, + protected rfqInterface = new Interface(mainnetRFQAbi), + ) { + super(dexHelper, dexKey); + this.logger = dexHelper.getLogger(dexKey); + + this.rateFetcher = new CablesRateFetcher( + this.dexHelper, + this.dexKey, + this.network, + this.logger, + { + rateConfig: { + pairsReqParams: { + url: CABLES_API_URL + '/pairs', + }, + pricesReqParams: { + url: CABLES_API_URL + '/prices', + }, + blacklistReqParams: { + url: CABLES_API_URL + '/blacklist', + }, + tokensReqParams: { + url: CABLES_API_URL + '/tokens', + }, + + pricesIntervalMs: CABLES_API_PRICES_POLLING_INTERVAL_MS, + pricesCacheTTLSecs: CABLES_PRICES_CACHES_TTL_S, + pricesCacheKey: 'prices', + + pairsIntervalMs: CABLES_API_PAIRS_POLLING_INTERVAL_MS, + pairsCacheTTLSecs: CABLES_PAIRS_CACHES_TTL_S, + pairsCacheKey: 'pairs', + + tokensIntervalMs: CABLES_API_TOKENS_POLLING_INTERVAL_MS, + tokensCacheTTLSecs: CABLES_TOKENS_CACHES_TTL_S, + tokensCacheKey: 'tokens', + + blacklistIntervalMs: CABLES_API_BLACKLIST_POLLING_INTERVAL_MS, + blacklistCacheTTLSecs: CABLES_BLACKLIST_CACHES_TTL_S, + blacklistCacheKey: 'blacklist', + }, + }, + ); + } + + async preProcessTransaction?( + optimalSwapExchange: OptimalSwapExchange, + srcToken: Token, + destToken: Token, + side: SwapSide, + options: PreprocessTransactionOptions, + ): Promise<[OptimalSwapExchange, ExchangeTxInfo]> { + if (await this.isBlacklisted(options.txOrigin)) { + this.logger.warn( + `${this.dexKey}-${this.network}: blacklisted TX Origin address '${options.txOrigin}' trying to build a transaction. Bailing...`, + ); + throw new Error( + `${this.dexKey}-${ + this.network + }: user=${options.txOrigin.toLowerCase()} is blacklisted`, + ); + } + + if (BigInt(optimalSwapExchange.srcAmount) === 0n) { + throw new Error('getFirmRate failed with srcAmount === 0'); + } + + const normalizedSrcToken = this.normalizeToken(srcToken); + const normalizedDestToken = this.normalizeToken(destToken); + const swapIdentifier = `${this.dexKey}_${normalizedSrcToken.address}_${normalizedDestToken.address}_${side}`; + + try { + let makerToken = normalizedDestToken; + let takerToken = normalizedSrcToken; + + const isSell = side === SwapSide.SELL; + const isBuy = side === SwapSide.BUY; + + const rfqParams = { + makerAsset: ethers.utils.getAddress(makerToken.address), + takerAsset: ethers.utils.getAddress(takerToken.address), + ...(isBuy && { makerAmount: optimalSwapExchange.destAmount }), + ...(isSell && { takerAmount: optimalSwapExchange.srcAmount }), + userAddress: options.executionContractAddress, + chainId: String(this.network), + }; + + const rfq: CablesRFQResponse = await this.dexHelper.httpRequest.post( + `${CABLES_API_URL}/quote`, + rfqParams, + CABLES_FIRM_QUOTE_TIMEOUT_MS, + ); + + if (!rfq) { + throw new Error( + 'Failed to fetch RFQ' + + swapIdentifier + + JSON.stringify(rfq + 'params' + rfqParams), + ); + } + + const { order } = rfq; + + assert( + order.makerAsset.toLowerCase() === makerToken.address, + `QuoteData makerAsset=${order.makerAsset} is different from Paraswap makerAsset=${makerToken.address}`, + ); + assert( + order.takerAsset.toLowerCase() === takerToken.address, + `QuoteData takerAsset=${order.takerAsset} is different from Paraswap takerAsset=${takerToken.address}`, + ); + if (isSell) { + assert( + order.takerAmount === optimalSwapExchange.srcAmount, + `QuoteData takerAmount=${order.takerAmount} is different from Paraswap srcAmount=${optimalSwapExchange.srcAmount}`, + ); + } else { + assert( + order.makerAmount === optimalSwapExchange.destAmount, + `QuoteData makerAmount=${order.makerAmount} is different from Paraswap destAmount=${optimalSwapExchange.destAmount}`, + ); + } + + const expiryAsBigInt = BigInt(order.expiry); + const minDeadline = expiryAsBigInt > 0 ? expiryAsBigInt : BI_MAX_UINT256; + + if (side === SwapSide.BUY) { + const requiredAmount = BigInt(optimalSwapExchange.srcAmount); + const quoteAmount = BigInt(order.takerAmount); + const requiredAmountWithSlippage = new BigNumber( + requiredAmount.toString(), + ) + .multipliedBy(options.slippageFactor) + .toFixed(0); + if (quoteAmount > BigInt(requiredAmountWithSlippage)) { + throw new SlippageError( + `Slipped, factor: ${quoteAmount.toString()} > ${requiredAmountWithSlippage}`, + ); + } + } else { + const requiredAmount = BigInt(optimalSwapExchange.destAmount); + const quoteAmount = BigInt(order.makerAmount); + const requiredAmountWithSlippage = new BigNumber( + requiredAmount.toString(), + ) + .multipliedBy(options.slippageFactor) + .toFixed(0); + if (quoteAmount < BigInt(requiredAmountWithSlippage)) { + throw new SlippageError( + `Slipped, factor: ${ + options.slippageFactor + } ${quoteAmount.toString()} < ${requiredAmountWithSlippage}`, + ); + } + } + + return [ + { + ...optimalSwapExchange, + data: { + quoteData: order, + }, + }, + { deadline: minDeadline }, + ]; + } catch (e: any) { + const message = `${this.dexKey}-${this.network}: ${e}`; + this.logger.error(message); + if (!e?.isSlippageError) { + this.restrict(); + } + throw new Error(message); + } + } + + getAdapterParam( + srcToken: string, + destToken: string, + srcAmount: string, + destAmount: string, + data: BebopData, + side: SwapSide, + ): AdapterExchangeParam { + return { + targetExchange: this.mainnetRFQAddress, + payload: '0x', + networkFee: '0', + }; + } + + getDexParam( + srcToken: Address, + destToken: Address, + srcAmount: NumberAsString, + destAmount: NumberAsString, + recipient: Address, + data: CablesData, + side: SwapSide, + ): DexExchangeParam { + const { quoteData } = data; + + assert( + quoteData !== undefined, + `${this.dexKey}-${this.network}: quoteData undefined`, + ); + + const swapFunction = 'partialSwap'; + const swapFunctionParams = [ + [ + quoteData.nonceAndMeta, + quoteData.expiry, + quoteData.makerAsset, + quoteData.takerAsset, + quoteData.maker, + quoteData.taker, + quoteData.makerAmount, + quoteData.takerAmount, + ], + quoteData.signature, + // might be overwritten on Executors + quoteData.takerAmount, + ]; + + const exchangeData = this.rfqInterface.encodeFunctionData( + swapFunction, + swapFunctionParams, + ); + + const fromAmount = ethers.utils.defaultAbiCoder.encode( + ['uint256'], + [quoteData.takerAmount], + ); + + const filledAmountIndex = exchangeData + .replace('0x', '') + .lastIndexOf(fromAmount.replace('0x', '')); + + const filledAmountPos = + (filledAmountIndex !== -1 ? filledAmountIndex : exchangeData.length) / 2; + + return { + exchangeData, + needWrapNative: this.needWrapNative, + dexFuncHasRecipient: false, + targetExchange: this.mainnetRFQAddress, + returnAmountPos: undefined, + insertFromAmountPos: filledAmountPos, + }; + } + + normalizeToken(token: Token): Token { + return { + ...token, + address: this.normalizeTokenAddress(token.address), + }; + } + + normalizeTokenAddress(address: Address): Address { + return address.toLowerCase(); + } + + getTokenFromAddress(address: Address): Token { + return this.tokensMap[this.normalizeAddress(address)]; + } + + getPoolIdentifier(srcAddress: Address, destAddress: Address) { + return `${this.dexKey}_${srcAddress}_${destAddress}`.toLowerCase(); + } + + async getPoolIdentifiers( + srcToken: Token, + destToken: Token, + side: SwapSide, + blockNumber: number, + ): Promise { + if (!srcToken || !destToken) { + return []; + } + + if (srcToken.address.toLowerCase() === destToken.address.toLowerCase()) { + return []; + } + + const normalizedSrcToken = this.normalizeToken(srcToken); + const normalizedDestToken = this.normalizeToken(destToken); + + const pairData = await this.getPairData( + normalizedSrcToken, + normalizedDestToken, + ); + + if (!pairData) { + return []; + } + + return [ + this.getPoolIdentifier( + normalizedSrcToken.address, + normalizedDestToken.address, + ), + ]; + } + + calculateOrderPrice( + amounts: bigint[], + orderbook: string[][], + baseToken: Token, + quoteToken: Token, + isInputQuote: boolean, + ) { + let result = []; + + for (let i = 0; i < amounts.length; i++) { + let amt = amounts[i]; + if (amt === 0n) { + result.push(amt); + continue; + } + + let decimals = baseToken.decimals; + let out_decimals = quoteToken.decimals; + + let price = this.calculatePriceSwap( + orderbook, + Number(amt) / 10 ** decimals, + isInputQuote, + ); + result.push(BigInt(Math.round(price * 10 ** out_decimals))); + } + return result; + } + + calculatePriceSwap( + prices: string[][], + requiredQty: number, + qtyMode: Boolean, + ) { + let sumBaseQty = 0; + let sumQuoteQty = 0; + const selectedRows: string[][] = []; + + const isBase = qtyMode; + const isQuote = !qtyMode; + + for (const [price, volume] of prices) { + if (isBase) { + if (sumBaseQty >= requiredQty) { + break; + } + } + + if (isQuote) { + if (sumQuoteQty >= requiredQty) { + break; + } + } + + let currentBaseQty = Number(volume); + let currentQuoteQty = Number(volume) * Number(price); + + const overQty = isBase + ? currentBaseQty + sumBaseQty > requiredQty + : currentQuoteQty + sumQuoteQty > requiredQty; + + if (overQty) { + if (isBase) { + currentBaseQty = requiredQty - sumBaseQty; + currentQuoteQty = currentBaseQty * Number(price); + } + + if (isQuote) { + currentQuoteQty = requiredQty - sumQuoteQty; + currentBaseQty = + currentQuoteQty * + new BigNumber(1).dividedBy(new BigNumber(price)).toNumber(); + } + } + + sumBaseQty += currentBaseQty; + sumQuoteQty += currentQuoteQty; + selectedRows.push([price, currentBaseQty.toString()]); + } + + const vSumBase = selectedRows.reduce((sum: number, [price, volume]) => { + return sum + Number(price) * Number(volume); + }, 0); + + const price = new BigNumber(vSumBase) + .dividedBy(new BigNumber(sumBaseQty)) + .toNumber(); + + if (isBase) { + return requiredQty / price; + } else { + return requiredQty * price; + } + } + + async getPricesVolume( + srcToken: Token, + destToken: Token, + amounts: bigint[], + side: SwapSide, + blockNumber: number, + limitPools?: string[], + transferFees?: TransferFeeParams, + isFirstSwap?: boolean, + ): Promise | null> { + try { + const normalizedSrcToken = this.normalizeToken(srcToken); + const normalizedDestToken = this.normalizeToken(destToken); + // If: same token, return null + if ( + normalizedSrcToken.address.toLowerCase() === + normalizedDestToken.address.toLowerCase() + ) { + return null; + } + + const isRestricted = await this.isRestricted(); + if (isRestricted) { + return null; + } + + await this.setTokensMap(); + + normalizedSrcToken.symbol = + this.tokensMap[normalizedSrcToken.address].symbol; + normalizedDestToken.symbol = + this.tokensMap[normalizedDestToken.address!].symbol; + + // ---------- Pools ---------- + let pools = + limitPools || + (await this.getPoolIdentifiers(srcToken, destToken, side, blockNumber)); + if (pools.length === 0) return null; + + // ---------- Prices ---------- + const priceMap = await this.getCachedPrices(); + + if (!priceMap) return null; + + let isInputQuote = false; + let pairKey = `${normalizedSrcToken.symbol}/${normalizedDestToken.symbol}`; + const pairsKeys = Object.keys(priceMap); + + if (!pairsKeys.includes(pairKey)) { + // Revert + isInputQuote = true; + pairKey = `${normalizedDestToken.symbol}/${normalizedSrcToken.symbol}`; + if (!pairsKeys.includes(pairKey)) { + return null; + } + } + + /** + * Orderbook + */ + const priceData = priceMap[pairKey]; + + let orderbook: any[] = []; + if (side === SwapSide.BUY) { + orderbook = priceData.asks; + } else { + orderbook = priceData.bids; + } + if (orderbook?.length === 0) { + throw new Error(`Empty orderbook for ${pairKey}`); + } + + const prices = this.calculateOrderPrice( + amounts, + orderbook, + side === SwapSide.SELL ? srcToken : destToken, + side === SwapSide.SELL ? destToken : srcToken, + side === SwapSide.SELL ? isInputQuote : !isInputQuote, + ); + + const result = [ + { + prices: prices, + unit: BigInt(normalizedDestToken.decimals), + exchange: this.dexKey, + gasCost: CABLES_GAS_COST, + poolAddresses: [this.mainnetRFQAddress], + data: {}, + }, + ]; + + return result; + } catch (e: unknown) { + this.logger.error( + `Error in getPricesVolume`, + { + srcToken: srcToken.address || srcToken.symbol, + destToken: destToken.address || destToken.symbol, + side, + }, + e, + ); + return null; + } + } + + getCalldataGasCost(poolPrices: PoolPrices): number | number[] { + return ( + CALLDATA_GAS_COST.DEX_OVERHEAD + + // addresses: makerAsset, takerAsset, maker, taker + CALLDATA_GAS_COST.ADDRESS * 4 + + // uint256: expiry + CALLDATA_GAS_COST.wordNonZeroBytes(16) + + // uint256: nonceAndMeta, makerAmount, takerAmount + CALLDATA_GAS_COST.AMOUNT * 3 + + // bytes: _signature (65 bytes) + CALLDATA_GAS_COST.FULL_WORD * 2 + + CALLDATA_GAS_COST.OFFSET_SMALL + ); + } + + async initializePricing(blockNumber: number): Promise { + if (!this.dexHelper.config.isSlave) { + this.rateFetcher.start(); + } + return; + } + + getAdapters(side: SwapSide): { name: string; index: number }[] | null { + return null; + } + + releaseResources?(): AsyncOrSync { + if (!this.dexHelper.config.isSlave && this.rateFetcher) { + this.rateFetcher.stop(); + } + } + + normalizeAddress(address: string): string { + return address.toLowerCase() === ETHER_ADDRESS + ? NULL_ADDRESS + : address.toLowerCase(); + } + + async setTokensMap() { + const tokens = await this.getCachedTokens(); + + if (tokens) { + this.tokensMap = Object.keys(tokens).reduce((acc, key) => { + //@ts-ignore + acc[tokens[key].address.toLowerCase()] = tokens[key]; + return acc; + }, {}); + } + } + + async getTopPoolsForToken( + tokenAddress: Address, + limit: number, + ): Promise { + return []; + } + + /** + * CACHED UTILS + */ + async getCachedTokens(): Promise { + const cachedTokens = await this.dexHelper.cache.get( + this.dexKey, + this.network, + this.rateFetcher.tokensCacheKey, + ); + + return cachedTokens ? JSON.parse(cachedTokens) : {}; + } + + async getCachedPairs(): Promise { + const cachedPairs = await this.dexHelper.cache.get( + this.dexKey, + this.network, + this.rateFetcher.pairsCacheKey, + ); + + return cachedPairs ? JSON.parse(cachedPairs) : {}; + } + + async getCachedPrices(): Promise { + const cachedPrices = await this.dexHelper.cache.get( + this.dexKey, + this.network, + this.rateFetcher.pricesCacheKey, + ); + + return cachedPrices ? JSON.parse(cachedPrices) : {}; + } + + async getCachedTokensAddr(): Promise { + const tokens = await this.getCachedTokens(); + const tokensAddr: Record = {}; + for (const key of Object.keys(tokens)) { + tokensAddr[tokens[key].symbol.toLowerCase()] = tokens[key].address; + } + return tokensAddr; + } + + getPairString(baseToken: Token, quoteToken: Token): string { + return `${baseToken.symbol}/${quoteToken.symbol}`.toLowerCase(); + } + + // Function to find a key by address + private findKeyByAddress = ( + jsonData: Record, + targetAddress: string, + ): string | undefined => { + const entries = Object.entries(jsonData); + const foundEntry = entries.find( + ([_, value]) => + value.address.toLowerCase() === targetAddress.toLowerCase(), + ); + return foundEntry ? foundEntry[0] : undefined; + }; + + async getPairData(srcToken: Token, destToken: Token): Promise { + if (srcToken.address === destToken.address) { + return null; + } + + const cachedTokens = await this.getCachedTokens(); + + srcToken.symbol = this.findKeyByAddress(cachedTokens, srcToken.address); + destToken.symbol = this.findKeyByAddress(cachedTokens, destToken.address); + + const cachedPairs = await this.getCachedPairs(); + + const potentialPairs = [ + { + base: srcToken.symbol, + quote: destToken.symbol, + identifier: this.getPairString(srcToken, destToken), + isSrcBase: true, + }, + { + base: destToken.symbol, + quote: srcToken.symbol, + identifier: this.getPairString(destToken, srcToken), + isSrcBase: false, + }, + ]; + + for (const pair of potentialPairs) { + if (pair.identifier in cachedPairs) { + const pairData = cachedPairs[pair.identifier]; + pairData.isSrcBase = pair.isSrcBase; + return pairData; + } + } + return null; + } + + async isBlacklisted(txOrigin: Address): Promise { + const cachedBlacklist = await this.dexHelper.cache.get( + this.dexKey, + this.network, + this.rateFetcher.blacklistCacheKey, + ); + + if (cachedBlacklist) { + const blacklist = JSON.parse(cachedBlacklist) as string[]; + return blacklist.includes(txOrigin.toLowerCase()); + } + + return false; + } + + async isRestricted(): Promise { + const result = await this.dexHelper.cache.get( + this.dexKey, + this.network, + CABLES_RESTRICTED_CACHE_KEY, + ); + + return result === 'true'; + } + + async restrict() { + const errorsDataRaw = await this.dexHelper.cache.get( + this.dexKey, + this.network, + CABLES_ERRORS_CACHE_KEY, + ); + + const errorsData: RestrictData = Utils.Parse(errorsDataRaw); + const ERRORS_TTL_S = Math.floor(CABLES_RESTRICT_CHECK_INTERVAL_MS / 1000); + + if ( + !errorsData || + errorsData?.addedDatetimeMs + CABLES_RESTRICT_CHECK_INTERVAL_MS < + Date.now() + ) { + this.logger.warn( + `${this.dexKey}-${this.network}: First encounter of error OR error ocurred outside of threshold, setting up counter`, + ); + const data: RestrictData = { + count: 1, + addedDatetimeMs: Date.now(), + }; + await this.dexHelper.cache.setex( + this.dexKey, + this.network, + CABLES_ERRORS_CACHE_KEY, + ERRORS_TTL_S, + Utils.Serialize(data), + ); + return; + } else { + if (errorsData.count + 1 >= CABLES_RESTRICT_COUNT_THRESHOLD) { + this.logger.warn( + `${this.dexKey}-${this.network}: Restricting due to error count=${ + errorsData.count + 1 + } within ${CABLES_RESTRICT_CHECK_INTERVAL_MS / 1000 / 60} minutes`, + ); + await this.dexHelper.cache.setex( + this.dexKey, + this.network, + CABLES_RESTRICTED_CACHE_KEY, + CABLES_RESTRICT_TTL_S, + 'true', + ); + } else { + this.logger.warn( + `${this.dexKey}-${this.network}: Error count increased`, + ); + const data: RestrictData = { + count: errorsData.count + 1, + addedDatetimeMs: errorsData.addedDatetimeMs, + }; + await this.dexHelper.cache.setex( + this.dexKey, + this.network, + CABLES_RESTRICTED_CACHE_KEY, + ERRORS_TTL_S, + Utils.Serialize(data), + ); + } + } + } +} diff --git a/src/dex/cables/config.ts b/src/dex/cables/config.ts new file mode 100644 index 000000000..07c4ee343 --- /dev/null +++ b/src/dex/cables/config.ts @@ -0,0 +1,13 @@ +import { Network } from '../../constants'; +import { DexConfigMap } from '../../types'; + +export const CablesConfig: DexConfigMap<{ mainnetRFQAddress: string }> = { + Cables: { + [Network.AVALANCHE]: { + mainnetRFQAddress: '0xfA12DCB2e1FD72bD92E8255Db6A781b2c76adC20', + }, + [Network.ARBITRUM]: { + mainnetRFQAddress: '0xfA12DCB2e1FD72bD92E8255Db6A781b2c76adC20', + }, + }, +}; diff --git a/src/dex/cables/constants.ts b/src/dex/cables/constants.ts new file mode 100644 index 000000000..cd2ec378d --- /dev/null +++ b/src/dex/cables/constants.ts @@ -0,0 +1,33 @@ +import BigNumber from 'bignumber.js'; + +/** + * Cables + */ +export const CABLES_API_URL = + 'https://cables-evm-rfq-service.cryptosrvc.com/v1'; + +export const CABLES_PRICES_CACHES_TTL_S = 10; +export const CABLES_API_PRICES_POLLING_INTERVAL_MS = 2000; // 2 sec + +export const CABLES_PAIRS_CACHES_TTL_S = 12; +export const CABLES_API_PAIRS_POLLING_INTERVAL_MS = 10000; // 10 sec + +export const CABLES_BLACKLIST_CACHES_TTL_S = 60; +export const CABLES_API_BLACKLIST_POLLING_INTERVAL_MS = 30000; // 30 sec + +export const CABLES_TOKENS_CACHES_TTL_S = 60; +export const CABLES_API_TOKENS_POLLING_INTERVAL_MS = 30000; // 30 sec + +export const CABLES_FIRM_QUOTE_TIMEOUT_MS = 2000; + +export const CABLES_RESTRICTED_CACHE_KEY = 'restricted'; + +export const CABLES_ERRORS_CACHE_KEY = 'errors'; + +export const CABLES_RESTRICT_CHECK_INTERVAL_MS = 1000 * 60 * 3; // 3 min + +export const CABLES_RESTRICT_COUNT_THRESHOLD = 3; + +export const CABLES_RESTRICT_TTL_S = 10 * 60; // 10 min + +export const CABLES_GAS_COST = 120_000; diff --git a/src/dex/cables/rate-fetcher.ts b/src/dex/cables/rate-fetcher.ts new file mode 100644 index 000000000..3df0b0f83 --- /dev/null +++ b/src/dex/cables/rate-fetcher.ts @@ -0,0 +1,212 @@ +import { Network } from '../../constants'; +import { IDexHelper } from '../../dex-helper'; +import { Fetcher } from '../../lib/fetcher/fetcher'; +import { validateAndCast } from '../../lib/validators'; +import { Logger, Token } from '../../types'; +import { PairData } from '../cables/types'; +import { + CablesBlacklistResponse, + CablesPairsResponse, + CablesPricesResponse, + CablesRateFetcherConfig, + CablesTokensResponse, +} from './types'; +import { + blacklistResponseValidator, + pairsResponseValidator, + pricesResponseValidator, + tokensResponseValidator, +} from './validators'; + +export class CablesRateFetcher { + public tokensFetcher: Fetcher; + public tokensCacheKey: string; + public tokensCacheTTL: number; + + public pairsFetcher: Fetcher; + public pairsCacheKey: string; + public pairsCacheTTL: number; + + public pricesFetcher: Fetcher; + public pricesCacheKey: string; + public pricesCacheTTL: number; + + public blacklistFetcher: Fetcher; + public blacklistCacheKey: string; + public blacklistCacheTTL: number; + + constructor( + private dexHelper: IDexHelper, + private dexKey: string, + private network: Network, + private logger: Logger, + config: CablesRateFetcherConfig, + ) { + this.tokensCacheKey = config.rateConfig.tokensCacheKey; + this.tokensCacheTTL = config.rateConfig.tokensCacheTTLSecs; + + this.pairsCacheKey = config.rateConfig.pairsCacheKey; + this.pairsCacheTTL = config.rateConfig.pairsCacheTTLSecs; + + this.pricesCacheKey = config.rateConfig.pricesCacheKey; + this.pricesCacheTTL = config.rateConfig.pricesCacheTTLSecs; + + this.blacklistCacheKey = config.rateConfig.blacklistCacheKey; + this.blacklistCacheTTL = config.rateConfig.blacklistCacheTTLSecs; + + this.pairsFetcher = new Fetcher( + dexHelper.httpRequest, + { + info: { + requestOptions: config.rateConfig.pairsReqParams, + caster: (data: unknown) => { + return validateAndCast( + data, + pairsResponseValidator, + ); + }, + }, + handler: this.handlePairsResponse.bind(this), + }, + config.rateConfig.pairsIntervalMs, + logger, + ); + + this.pricesFetcher = new Fetcher( + dexHelper.httpRequest, + { + info: { + requestOptions: config.rateConfig.pricesReqParams, + caster: (data: unknown) => { + return validateAndCast( + data, + pricesResponseValidator, + ); + }, + }, + handler: this.handlePricesResponse.bind(this), + }, + config.rateConfig.pricesIntervalMs, + logger, + ); + + this.blacklistFetcher = new Fetcher( + dexHelper.httpRequest, + { + info: { + requestOptions: config.rateConfig.blacklistReqParams, + caster: (data: unknown) => { + return validateAndCast( + data, + blacklistResponseValidator, + ); + }, + }, + handler: this.handleBlacklistResponse.bind(this), + }, + config.rateConfig.blacklistIntervalMs, + logger, + ); + + this.tokensFetcher = new Fetcher( + dexHelper.httpRequest, + { + info: { + requestOptions: config.rateConfig.tokensReqParams, + caster: (data: unknown) => { + return validateAndCast( + data, + tokensResponseValidator, + ); + }, + }, + handler: this.handleTokensResponse.bind(this), + }, + config.rateConfig.tokensIntervalMs, + logger, + ); + } + + /** + * Utils + */ + start() { + this.pairsFetcher.startPolling(); + this.pricesFetcher.startPolling(); + this.blacklistFetcher.startPolling(); + this.tokensFetcher.startPolling(); + } + stop() { + this.pairsFetcher.stopPolling(); + this.pricesFetcher.stopPolling(); + this.blacklistFetcher.stopPolling(); + this.tokensFetcher.stopPolling(); + } + + private handlePairsResponse(res: CablesPairsResponse): void { + const networkId = String(this.network); + const pairs = res.pairs[networkId]; + + let normalized_pairs: { [token: string]: PairData } = {}; + Object.keys(pairs).forEach(key => { + normalized_pairs[key.toLowerCase()] = pairs[key]; + }); + + this.dexHelper.cache.setex( + this.dexKey, + this.network, + this.pairsCacheKey, + this.pairsCacheTTL, + JSON.stringify(normalized_pairs), + ); + } + + private handlePricesResponse(res: CablesPricesResponse): void { + const networkId = String(this.network); + const prices = res.prices[networkId]; + + this.dexHelper.cache.setex( + this.dexKey, + this.network, + this.pricesCacheKey, + this.pricesCacheTTL, + JSON.stringify(prices), + ); + } + + private handleBlacklistResponse(res: CablesBlacklistResponse): void { + const { blacklist } = res; + this.dexHelper.cache.setex( + this.dexKey, + this.network, + this.blacklistCacheKey, + this.blacklistCacheTTL, + JSON.stringify(blacklist.map(item => item.toLowerCase())), + ); + } + + // Convert addresses to lowercase + private normalizeAddressesToLowerCase = ( + jsonData: Record, + ) => { + Object.keys(jsonData).forEach(key => { + jsonData[key].address = jsonData[key].address.toLowerCase(); + }); + return jsonData; + }; + + private async handleTokensResponse(res: CablesTokensResponse): Promise { + const networkId = String(this.network); + const tokens = res.tokens[networkId]; + + const normalizedTokens = this.normalizeAddressesToLowerCase(tokens); + + this.dexHelper.cache.setex( + this.dexKey, + this.network, + this.tokensCacheKey, + this.tokensCacheTTL, + JSON.stringify(normalizedTokens), + ); + } +} diff --git a/src/dex/cables/types.ts b/src/dex/cables/types.ts new file mode 100644 index 000000000..b8f5d14de --- /dev/null +++ b/src/dex/cables/types.ts @@ -0,0 +1,113 @@ +import { RequestHeaders } from '../../dex-helper'; +import { Token } from '../../types'; +import { Method } from '../../dex-helper/irequest-wrapper'; +import { AugustusRFQOrderData } from '../augustus-rfq'; + +export type CablesRFQResponse = { + order: AugustusRFQOrderData; + signature: string; +}; + +export type CablesData = { + quoteData?: AugustusRFQOrderData; +}; +/** + * Types + */ +export type PairData = { + base: string; + quote: string; + liquidityUSD: number; +}; + +type PriceAndAmount = [string, string]; + +type PriceData = { + bids: PriceAndAmount[]; + asks: PriceAndAmount[]; +}; + +type PriceDataMap = { + [network: string]: { + [pair: string]: PriceData; + }; +}; + +type TokenDataMap = { + [network: string]: { + [token: string]: Token; + }; +}; + +type PairsDataMap = { + [network: string]: { + [token: string]: PairData; + }; +}; + +/** + * Responses + */ +export type CablesPricesResponse = { + prices: PriceDataMap; +}; +export type CablesBlacklistResponse = { + blacklist: string[]; +}; +export type CablesTokensResponse = { + tokens: TokenDataMap; +}; +export type CablesPairsResponse = { + pairs: PairsDataMap; +}; + +/** + * Rate Fetcher + */ +export type CablesRateFetcherConfig = { + rateConfig: { + pairsReqParams: { + url: string; + headers?: RequestHeaders; + params?: any; + }; + pricesReqParams: { + url: string; + headers?: RequestHeaders; + params?: any; + }; + blacklistReqParams: { + url: string; + headers?: RequestHeaders; + params?: any; + }; + tokensReqParams: { + url: string; + headers?: RequestHeaders; + params?: any; + }; + pairsIntervalMs: number; + pricesIntervalMs: number; + blacklistIntervalMs: number; + tokensIntervalMs: number; + + pairsCacheKey: string; + pricesCacheKey: string; + blacklistCacheKey: string; + tokensCacheKey: string; + + blacklistCacheTTLSecs: number; + pairsCacheTTLSecs: number; + pricesCacheTTLSecs: number; + tokensCacheTTLSecs: number; + }; +}; + +export type RestrictData = { + count: number; + addedDatetimeMs: number; +} | null; + +export class SlippageError extends Error { + isSlippageError = true; +} diff --git a/src/dex/cables/validators.test.ts b/src/dex/cables/validators.test.ts new file mode 100644 index 000000000..abc36cf82 --- /dev/null +++ b/src/dex/cables/validators.test.ts @@ -0,0 +1,151 @@ +import Joi from 'joi'; +import { + pairsResponseValidator, + pricesResponseValidator, + tokensResponseValidator, + blacklistResponseValidator, +} from './validators'; // + +describe('Validation Schemas', () => { + describe('pairsResponseValidator', () => { + it('should validate correct pairs response', () => { + const validData = { + pairs: { '43114': { 'USDC/USDT': { base: 'USDC', quote: 'USDT' } } }, + }; + const { error } = pairsResponseValidator.validate(validData); + expect(error).toBeUndefined(); + }); + + it('should invalidate incorrect pairs response', () => { + const invalidData = { pairs: { '43114': 'USDC/USDT' } }; + const { error } = pairsResponseValidator.validate(invalidData); + expect(error).toBeDefined(); + }); + }); + + describe('pricesResponseValidator', () => { + it('should validate correct prices response', () => { + const validData = { + prices: { + '43114': { + 'USDC/USDT': { + bids: [ + ['0.9996', '244305.9'], + ['0.9995', '236021.6'], + ], + asks: [ + ['0.9996', '244305.9'], + ['0.9995', '236021.6'], + ], + }, + }, + }, + }; + const { error } = pricesResponseValidator.validate(validData); + expect(error).toBeUndefined(); + }); + + it('should invalidate incorrect prices response', () => { + const invalidData = { + prices: { + chain1: { + bids: [ + ['1000'], // invalid entry length + ], + asks: [['1010', '1']], + }, + }, + }; + const { error } = pricesResponseValidator.validate(invalidData); + expect(error).toBeDefined(); + }); + }); + + describe('tokensResponseValidator', () => { + it('should validate correct tokens response', () => { + const validData = { + tokens: { + '43114': { + AVAX: { + symbol: 'AVAX', + decimals: 18, + name: 'AVAX', + address: '0x0000000000000000000000000000000000000000', + }, + WAVAX: { + symbol: 'WAVAX', + decimals: 18, + name: 'WAVAX', + address: '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7', + }, + 'WETH.e': { + symbol: 'WETH.e', + decimals: 18, + name: 'WETH.e', + address: '0x49D5c2BdFfac6CE2BFdB6640F4F80f226bc10bAB', + }, + USDT: { + symbol: 'USDT', + decimals: 6, + name: 'USDT', + address: '0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7', + }, + USDC: { + symbol: 'USDC', + decimals: 6, + name: 'USDC', + address: '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', + }, + 'USDC.e': { + symbol: 'USDC.e', + decimals: 6, + name: 'USDC.e', + address: '0xA7D7079b0FEaD91F3e65f86E8915Cb59c1a4C664', + }, + }, + }, + }; + const { error } = tokensResponseValidator.validate(validData); + expect(error).toBeUndefined(); + }); + + it('should invalidate incorrect tokens response', () => { + const invalidData = { + tokens: { + chain1: { + ETH: { + symbol: '', // invalid value + name: 'Ethereum', + description: 'A popular cryptocurrency', + address: '0x...', + decimals: 18, + type: 'ERC20', + }, + }, + }, + }; + const { error } = tokensResponseValidator.validate(invalidData); + expect(error).toBeDefined(); + }); + }); + + describe('blacklistResponseValidator', () => { + it('should validate correct blacklist response', () => { + const validData = { + blacklist: ['0xAddress1', '0xAddress2'], + }; + const { error } = blacklistResponseValidator.validate(validData); + expect(error).toBeUndefined(); + }); + + it('should invalidate incorrect blacklist response', () => { + const invalidData = { + blacklist: [ + '', // invalid value + ], + }; + const { error } = blacklistResponseValidator.validate(invalidData); + expect(error).toBeDefined(); + }); + }); +}); diff --git a/src/dex/cables/validators.ts b/src/dex/cables/validators.ts new file mode 100644 index 000000000..d95c65341 --- /dev/null +++ b/src/dex/cables/validators.ts @@ -0,0 +1,61 @@ +import joi from 'joi'; + +const pairValidator = joi.object({ + base: joi.string().min(1), + quote: joi.string().min(1), + liquidityUSD: joi.number().min(0), + baseAddress: joi.string().min(1), + quoteAddress: joi.string().min(1), + baseDecimals: joi.number().min(0), + quoteDecimals: joi.number().min(0), +}); + +const pairMap = joi.object().pattern( + joi.string(), // Pair name ETH/USDT + pairValidator, +); + +export const pairsResponseValidator = joi.object({ + pairs: joi.object().pattern( + joi.string(), // chain id + pairMap, + ), +}); + +const orderbookEntry = joi.array().items(joi.string().min(1)).length(2); + +const orderbookValidator = joi.object({ + bids: joi.array().items(orderbookEntry), + asks: joi.array().items(orderbookEntry), +}); + +const chainDataSchema = joi.object().pattern( + joi.string(), // pair name USDC/USDT + orderbookValidator, +); + +export const pricesResponseValidator = joi.object({ + prices: joi.object().pattern( + joi.string(), // chain id + chainDataSchema, + ), +}); + +const tokenValidator = joi.object({ + symbol: joi.string().min(1), + name: joi.string().min(1), + description: joi.string().min(1), + address: joi.string().min(1), + decimals: joi.number().min(0), + type: joi.string().min(1), +}); + +const chainTokens = joi.object().pattern(joi.string(), tokenValidator); + +export const tokensResponseValidator = joi.object({ + tokens: joi.object().pattern(joi.string(), chainTokens), +}); + +export const blacklistResponseValidator = joi.object({ + blacklist: joi.array().items(joi.string().min(1)), +}); diff --git a/src/dex/dexalot/dexalot.ts b/src/dex/dexalot/dexalot.ts index c27f7147a..6fda285df 100644 --- a/src/dex/dexalot/dexalot.ts +++ b/src/dex/dexalot/dexalot.ts @@ -593,6 +593,7 @@ export class Dexalot extends SimpleExchange implements IDex { .multipliedBy(10000) .toFixed(0) : options.slippageFactor.minus(1).multipliedBy(10000).toFixed(0); + const rfqParams = { makerAsset: ethers.utils.getAddress(makerToken.address), takerAsset: ethers.utils.getAddress(takerToken.address), diff --git a/src/dex/index.ts b/src/dex/index.ts index cf03b6c61..4161334ad 100644 --- a/src/dex/index.ts +++ b/src/dex/index.ts @@ -96,6 +96,7 @@ import { StkGHO } from './stkgho/stkgho'; import { BalancerV3 } from './balancer-v3/balancer-v3'; import { balancerV3Merge } from './balancer-v3/optimizer'; import { SkyConverter } from './sky-converter/sky-converter'; +import { Cables } from './cables/cables'; import { Stader } from './stader/stader'; const LegacyDexes = [ @@ -187,6 +188,7 @@ const Dexes = [ UsualBond, StkGHO, SkyConverter, + Cables, FluidDex, ]; diff --git a/src/executor/Executor01BytecodeBuilder.ts b/src/executor/Executor01BytecodeBuilder.ts index f5748d890..9d6e743b0 100644 --- a/src/executor/Executor01BytecodeBuilder.ts +++ b/src/executor/Executor01BytecodeBuilder.ts @@ -379,17 +379,21 @@ export class Executor01BytecodeBuilder extends ExecutorBytecodeBuilder< let fromAmountPos = 0; if (insertFromAmount) { - const fromAmount = ethers.utils.defaultAbiCoder.encode( - ['uint256'], - [swap.swapExchanges[swapExchangeIndex].srcAmount], - ); - - const fromAmountIndex = exchangeData - .replace('0x', '') - .indexOf(fromAmount.replace('0x', '')); - - fromAmountPos = - (fromAmountIndex !== -1 ? fromAmountIndex : exchangeData.length) / 2; + if (exchangeParam.insertFromAmountPos) { + fromAmountPos = exchangeParam.insertFromAmountPos; + } else { + const fromAmount = ethers.utils.defaultAbiCoder.encode( + ['uint256'], + [swap.swapExchanges[swapExchangeIndex].srcAmount], + ); + + const fromAmountIndex = exchangeData + .replace('0x', '') + .indexOf(fromAmount.replace('0x', '')); + + fromAmountPos = + (fromAmountIndex !== -1 ? fromAmountIndex : exchangeData.length) / 2; + } } return this.buildCallData( diff --git a/src/executor/Executor02BytecodeBuilder.ts b/src/executor/Executor02BytecodeBuilder.ts index 14969dec4..90cf76be8 100644 --- a/src/executor/Executor02BytecodeBuilder.ts +++ b/src/executor/Executor02BytecodeBuilder.ts @@ -335,16 +335,20 @@ export class Executor02BytecodeBuilder extends ExecutorBytecodeBuilder< let fromAmountPos = 0; if (insertFromAmount) { - const fromAmount = ethers.utils.defaultAbiCoder.encode( - ['uint256'], - [swapExchange.srcAmount], - ); - const fromAmountIndex = exchangeData - .replace('0x', '') - .indexOf(fromAmount.replace('0x', '')); + if (exchangeParam.insertFromAmountPos) { + fromAmountPos = exchangeParam.insertFromAmountPos; + } else { + const fromAmount = ethers.utils.defaultAbiCoder.encode( + ['uint256'], + [swapExchange.srcAmount], + ); + const fromAmountIndex = exchangeData + .replace('0x', '') + .indexOf(fromAmount.replace('0x', '')); - fromAmountPos = - (fromAmountIndex !== -1 ? fromAmountIndex : exchangeData.length) / 2; + fromAmountPos = + (fromAmountIndex !== -1 ? fromAmountIndex : exchangeData.length) / 2; + } } return this.buildCallData( diff --git a/src/executor/Executor03BytecodeBuilder.ts b/src/executor/Executor03BytecodeBuilder.ts index 2bd82aa67..c9331b80c 100644 --- a/src/executor/Executor03BytecodeBuilder.ts +++ b/src/executor/Executor03BytecodeBuilder.ts @@ -331,24 +331,31 @@ export class Executor03BytecodeBuilder extends ExecutorBytecodeBuilder< let fromAmountPos = 0; let toAmountPos = 0; if (insertAmount) { - const fromAmount = ethers.utils.defaultAbiCoder.encode( - ['uint256'], - [swap.swapExchanges[swapExchangeIndex].srcAmount], - ); + if (exchangeParam.insertFromAmountPos) { + fromAmountPos = exchangeParam.insertFromAmountPos; + } else { + const fromAmount = ethers.utils.defaultAbiCoder.encode( + ['uint256'], + [swap.swapExchanges[swapExchangeIndex].srcAmount], + ); + + const fromAmountIndex = exchangeData + .replace('0x', '') + .indexOf(fromAmount.replace('0x', '')); + + fromAmountPos = + (fromAmountIndex !== -1 ? fromAmountIndex : exchangeData.length) / 2; + } + const toAmount = ethers.utils.defaultAbiCoder.encode( ['uint256'], [swap.swapExchanges[swapExchangeIndex].destAmount], ); - const fromAmountIndex = exchangeData - .replace('0x', '') - .indexOf(fromAmount.replace('0x', '')); const toAmountIndex = exchangeData .replace('0x', '') .indexOf(toAmount.replace('0x', '')); - fromAmountPos = - (fromAmountIndex !== -1 ? fromAmountIndex : exchangeData.length) / 2; toAmountPos = (toAmountIndex !== -1 ? toAmountIndex : exchangeData.length) / 2; } diff --git a/src/types.ts b/src/types.ts index 57986873c..843cf67e8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -173,6 +173,7 @@ export type DexExchangeParam = { swappedAmountNotPresentInExchangeData?: boolean; preSwapUnwrapCalldata?: string; returnAmountPos: number | undefined; + insertFromAmountPos?: number; permit2Approval?: boolean; }; diff --git a/tests/constants-e2e.ts b/tests/constants-e2e.ts index a924a7192..79bd60b47 100644 --- a/tests/constants-e2e.ts +++ b/tests/constants-e2e.ts @@ -1900,7 +1900,7 @@ export const Holders: { BETS: '0x8cc2284c90d05578633418f9cde104f402375a65', HATCHY: '0x14ec295ec8def851ec6e2959df872dd24e422631', USDCe: '0x3a2434c698f8d79af1f5a9e43013157ca8b11a66', - USDC: '0xcc2da711D621A4491b338CAC88B9C0954db3e75B', + USDC: '0x64b4dE1b00EF830f3CC2FD68ee056aAD76C45BF6', USDTe: '0x84d34f4f83a87596cd3fb6887cff8f17bf5a7b83', WETHe: '0x9bdB521a97E95177BF252C253E256A60C3e14447', POPS: '0x5268c2331658cb0b2858cfa9db27d8f22f5434bc', @@ -1916,7 +1916,7 @@ export const Holders: { TSD: '0x691A89db352B72dDb249bFe16503494eC0D920A4', THO: '0xc40d16c47394a506d451475c8a7c46c1175c1da1', aAvaUSDT: '0x50B1Ba98Cf117c9682048D56628B294ebbAA4ec2', - USDT: '0x0d0707963952f2fba59dd06f2b425ace40b492fe', + USDT: '0xCddc5d0Ebeb71a08ffF26909AA6c0d4e256b4fE1', aAvaWAVAX: '0x1B18Df70863636AEe4BfBAb6F7C70ceBCA9bA404', oldFRAX: '0x4e3376018add04ebe4c46bf6f924ddec8c67aa7b', newFRAX: '0x4e3376018add04ebe4c46bf6f924ddec8c67aa7b',