diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 68f4c65..3abc3f3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [20.x] + node-version: [22.x] steps: - uses: actions/checkout@v3 diff --git a/.nvmrc b/.nvmrc index 2edeafb..8fdd954 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 \ No newline at end of file +22 \ No newline at end of file diff --git a/README.md b/README.md index ecb69ba..dba264f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,30 @@ This SDK is in beta. We cannot be held responsible for any losses caused by use ## Overview -The SDK is a READ-ONLY tool, intended to facilitate working with Carbon contracts. It's a convenient wrapper around the Carbon matching algorithm and Carbon contracts, allowing programs and users get a ready to use transaction data that will allow them to manage strategies and fulfill trades +The SDK is a READ-ONLY tool, intended to facilitate working with Carbon contracts. It's a convenient wrapper around the Carbon matching algorithm and Carbon contracts, allowing programs and users get a ready to use transaction data that will allow them to manage strategies and fulfill trades. + +The SDK supports two strategy families: + +- Standard Carbon strategies +- Gradient strategies + +Gradient strategies are time-based moving limit orders. Each side of the strategy has: + +- a start price +- an end price +- a budget +- a start time +- an end time +- a gradient type + +Supported gradient types: + +- `LinearIncrease` +- `LinearDecrease` +- `LinearInverseIncrease` +- `LinearInverseDecrease` +- `ExponentialIncrease` +- `ExponentialDecrease` ## Installation @@ -29,6 +52,9 @@ import { MatchActionBNStr, StrategyUpdate, EncodedStrategyBNStr, + GradientStrategyUpdate, + GradientEncodedStrategyBNStr, + GradientType, } from '@bancor/carbon-sdk'; import { Toolkit } from '@bancor/carbon-sdk/strategy-management'; import { ChainCache, initSyncedCache } from '@bancor/carbon-sdk/chain-cache'; @@ -78,13 +104,118 @@ const init = async ( }; ``` +## Gradient Contracts + +Gradient support is optional per chain. + +If `gradientControllerAddress` and `gradientVoucherAddress` are provided in `ContractsConfig`, the SDK will: + +- read and cache gradient strategies +- process gradient strategy events in `ChainSync` +- expose gradient strategy management methods + +If these addresses are omitted, the SDK will not issue calls to gradient contracts. This is useful for chains where gradient contracts have not been deployed yet. + +Example: + +```ts +const config: ContractsConfig = { + carbonControllerAddress: '0x...', + multiCallAddress: '0x...', + voucherAddress: '0x...', + carbonBatcherAddress: '0x...', + gradientControllerAddress: '0x...', + gradientVoucherAddress: '0x...', +}; +``` + +## Strategy Types + +### Standard Strategies + +The existing standard strategy flow is unchanged. Main helpers include: + +- `createBuySellStrategy` +- `updateStrategy` +- `deleteStrategy` +- `getStrategyById` +- `getStrategiesByPair` +- `getUserStrategies` + +### Gradient Strategies + +Gradient strategies are exposed in parallel to the standard strategy flow. + +Main helpers include: + +- `createBuySellGradientStrategy` +- `updateGradientStrategy` +- `deleteGradientStrategy` +- `getGradientStrategyById` +- `getGradientStrategiesByPair` +- `getUserGradientStrategies` + +Example: + +```ts +const tx = await carbonSDK.createBuySellGradientStrategy( + baseToken, + quoteToken, + '1800', // buyPriceStart + '1500', // buyPriceEnd + '1000', // buyBudget + GradientType.LinearDecrease, + 1710000000, // buyStartTime + 1712592000, // buyEndTime + '2200', // sellPriceStart + '2600', // sellPriceEnd + '1', // sellBudget + GradientType.ExponentialIncrease, + 1710000000, // sellStartTime + 1712592000 // sellEndTime +); +``` + +## Encoding Helpers + +The shared encoder module supports both strategy types. + +Standard helpers: + +- `encodeOrder` +- `decodeOrder` +- `encodeStrategy` +- `decodeStrategy` + +Gradient helpers: + +- `encodeGradientOrder` +- `decodeGradientOrder` +- `encodeGradientStrategy` +- `decodeGradientStrategy` + +These live in `@bancor/carbon-sdk/utils`. + +## Chain Cache + +`ChainCache` and `ChainSync` support both standard and gradient strategies. + +The synced cache: + +- caches standard strategies by pair and id +- caches gradient strategies by pair and id +- keeps standard trade orders for the existing matcher flow +- processes both standard and gradient strategy create/update/delete events + +Gradient cache lookups are available through the toolkit methods listed above. + ## Notes ### 1. The SDK Logger supports 3 verbosity levels: - `0` (default) only prints errors and important logs. - `1` (debug) prints highly verbose logs. -- `2` (debug readable) is same as `1` but also converts any BigNumber to an easy to read string (impacting performance). +- `2` (debug readable) is same as `1` but also converts any bigint to an easy to read string (impacting performance). To use it in Node, set the environment variable `CARBON_DEFI_SDK_VERBOSITY` to the desired level. To use it from a browser app do, before importing the SDK: diff --git a/demos/concentrated-amm-integration/demo.ts b/demos/concentrated-amm-integration/demo.ts index 63fcd37..b9c22e8 100644 --- a/demos/concentrated-amm-integration/demo.ts +++ b/demos/concentrated-amm-integration/demo.ts @@ -37,6 +37,8 @@ async function demonstrateCarbonIntegration() { voucherAddress: '0x3660F04B79751e31128f6378eAC70807e38f554E', carbonBatcherAddress: '0x0199f3A6C4B192B9f9C3eBE31FBC535CdD4B7D4e', multiCallAddress: '0xcA11bde05977b3631167028862bE2a173976CA11', + gradientControllerAddress: '0x5BDdF8EdeEaE66Cc8477c9282b8c0462CD7132aa', + gradientVoucherAddress: '0x4973fa43c4c4b0Bbe4071eB3e7c900810Df143E8', }); // const reader = new Reader(contracts); diff --git a/package.json b/package.json index ea581ce..1deef2b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@bancor/carbon-sdk", "type": "module", "source": "src/index.ts", - "version": "0.0.130-DEV", + "version": "0.0.131-DEV", "description": "The SDK is a READ-ONLY tool, intended to facilitate working with Carbon contracts. It's a convenient wrapper around our matching algorithm, allowing programs and users get a ready to use transaction data that will allow them to manage strategies and fulfill trades", "main": "dist/index.cjs", "module": "dist/index.js", @@ -40,7 +40,7 @@ "dist" ], "engines": { - "node": ">=18" + "node": ">=20" }, "typesVersions": { "*": { @@ -67,7 +67,7 @@ "types": "dist/index.d.ts", "scripts": { "clean": "rm -rf dist && rm -rf src/abis/types", - "compile-abis": "typechain --target ethers-v6 --out-dir 'src/abis/types' 'src/abis/**/*.json'", + "compile-abis": "typechain --target ethers-v6 --out-dir \"src/abis/types\" \"src/abis/**/*.json\"", "prebuild": "yarn clean && yarn compile-abis", "build": "yarn lint && rollup -c", "test": "yarn lint && mocha", @@ -125,5 +125,6 @@ }, "peerDependencies": { "ethers": "^6.15.0" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/rollup.config.js b/rollup.config.js index f990f25..2367477 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -2,7 +2,7 @@ import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import typescript from '@rollup/plugin-typescript'; import { terser } from 'rollup-plugin-terser'; -import pkg from './package.json' assert { type: 'json' }; +import pkg from './package.json' with { type: 'json' }; export default { input: { diff --git a/src/abis/GradientController.json b/src/abis/GradientController.json new file mode 100644 index 0000000..3c3e0d9 --- /dev/null +++ b/src/abis/GradientController.json @@ -0,0 +1,2179 @@ +{ + "address": "0x5BDdF8EdeEaE66Cc8477c9282b8c0462CD7132aa", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, + { + "inputs": [], + "name": "admin", + "outputs": [ + { + "internalType": "address", + "name": "admin_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [ + { + "internalType": "address", + "name": "implementation_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + }, + { + "inputs": [], + "name": "AccessControlBadConfirmation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "neededRole", + "type": "bytes32" + } + ], + "name": "AccessControlUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [], + "name": "AccessDenied", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyInitialized", + "type": "error" + }, + { + "inputs": [], + "name": "BalanceMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "DeadlineExpired", + "type": "error" + }, + { + "inputs": [], + "name": "EnforcedPause", + "type": "error" + }, + { + "inputs": [], + "name": "ExpOverflow", + "type": "error" + }, + { + "inputs": [], + "name": "ExpectedPause", + "type": "error" + }, + { + "inputs": [], + "name": "FailedCall", + "type": "error" + }, + { + "inputs": [], + "name": "GreaterThanMaxInput", + "type": "error" + }, + { + "inputs": [], + "name": "IdenticalAddresses", + "type": "error" + }, + { + "inputs": [], + "name": "InitialRateTooHigh", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientCapacity", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientFlashloanReturn", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientLiquidity", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientNativeTokenReceived", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidExpiry", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidFee", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidIndices", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOrderLiquidity", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOrderTargetAmount", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPrice", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRate", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidTradeActionAmount", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidTradeActionSourceToken", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidTradeActionStrategyId", + "type": "error" + }, + { + "inputs": [], + "name": "LowerThanMinReturn", + "type": "error" + }, + { + "inputs": [], + "name": "MultiFactorTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "NativeAmountMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [], + "name": "OrderDisabled", + "type": "error" + }, + { + "inputs": [], + "name": "OrderExpired", + "type": "error" + }, + { + "inputs": [], + "name": "OrderNotAvailable", + "type": "error" + }, + { + "inputs": [], + "name": "OutDated", + "type": "error" + }, + { + "inputs": [], + "name": "Overflow", + "type": "error" + }, + { + "inputs": [], + "name": "PairAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "PairDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "bits", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "SafeCastOverflowedUintDowncast", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "SafeERC20FailedOperation", + "type": "error" + }, + { + "inputs": [], + "name": "UnknownDelegator", + "type": "error" + }, + { + "inputs": [], + "name": "UnnecessaryNativeTokenReceived", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroValue", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "Token", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "FeesWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "Token", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + } + ], + "name": "FlashloanCompleted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint32", + "name": "prevFeePPM", + "type": "uint32" + }, + { + "indexed": true, + "internalType": "uint32", + "name": "newFeePPM", + "type": "uint32" + } + ], + "name": "FlashloanFeePPMUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint128", + "name": "pairId", + "type": "uint128" + }, + { + "indexed": true, + "internalType": "Token", + "name": "token0", + "type": "address" + }, + { + "indexed": true, + "internalType": "Token", + "name": "token1", + "type": "address" + } + ], + "name": "PairCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "Token", + "name": "token0", + "type": "address" + }, + { + "indexed": true, + "internalType": "Token", + "name": "token1", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "prevFeePPM", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "newFeePPM", + "type": "uint32" + } + ], + "name": "PairTradingFeePPMUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "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": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "Token", + "name": "token0", + "type": "address" + }, + { + "indexed": true, + "internalType": "Token", + "name": "token1", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "initialPrice", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "tradingStartTime", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "expiry", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "multiFactor", + "type": "uint32" + }, + { + "internalType": "enum GradientType", + "name": "gradientType", + "type": "uint8" + } + ], + "indexed": false, + "internalType": "struct Order", + "name": "order0", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "initialPrice", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "tradingStartTime", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "expiry", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "multiFactor", + "type": "uint32" + }, + { + "internalType": "enum GradientType", + "name": "gradientType", + "type": "uint8" + } + ], + "indexed": false, + "internalType": "struct Order", + "name": "order1", + "type": "tuple" + } + ], + "name": "StrategyCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "Token", + "name": "token0", + "type": "address" + }, + { + "indexed": true, + "internalType": "Token", + "name": "token1", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "initialPrice", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "tradingStartTime", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "expiry", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "multiFactor", + "type": "uint32" + }, + { + "internalType": "enum GradientType", + "name": "gradientType", + "type": "uint8" + } + ], + "indexed": false, + "internalType": "struct Order", + "name": "order0", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "initialPrice", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "tradingStartTime", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "expiry", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "multiFactor", + "type": "uint32" + }, + { + "internalType": "enum GradientType", + "name": "gradientType", + "type": "uint8" + } + ], + "indexed": false, + "internalType": "struct Order", + "name": "order1", + "type": "tuple" + } + ], + "name": "StrategyDeleted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "Token", + "name": "token0", + "type": "address" + }, + { + "indexed": true, + "internalType": "Token", + "name": "token1", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "liquidity0", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "liquidity1", + "type": "uint128" + } + ], + "name": "StrategyLiquidityUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "Token", + "name": "token0", + "type": "address" + }, + { + "indexed": true, + "internalType": "Token", + "name": "token1", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "initialPrice", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "tradingStartTime", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "expiry", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "multiFactor", + "type": "uint32" + }, + { + "internalType": "enum GradientType", + "name": "gradientType", + "type": "uint8" + } + ], + "indexed": false, + "internalType": "struct Order", + "name": "order0", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "initialPrice", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "tradingStartTime", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "expiry", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "multiFactor", + "type": "uint32" + }, + { + "internalType": "enum GradientType", + "name": "gradientType", + "type": "uint8" + } + ], + "indexed": false, + "internalType": "struct Order", + "name": "order1", + "type": "tuple" + } + ], + "name": "StrategyUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "Token", + "name": "sourceToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "Token", + "name": "targetToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "sourceAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "targetAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "tradingFeeAmount", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "bool", + "name": "byTargetAmount", + "type": "bool" + } + ], + "name": "TokensTraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint32", + "name": "prevFeePPM", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "newFeePPM", + "type": "uint32" + } + ], + "name": "TradingFeePPMUpdated", + "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": [ + { + "internalType": "Token", + "name": "token", + "type": "address" + } + ], + "name": "accumulatedFees", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Token", + "name": "sourceToken", + "type": "address" + }, + { + "internalType": "Token", + "name": "targetToken", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "strategyId", + "type": "uint256" + }, + { + "internalType": "uint128", + "name": "amount", + "type": "uint128" + } + ], + "internalType": "struct TradeAction[]", + "name": "tradeActions", + "type": "tuple[]" + } + ], + "name": "calculateTradeSourceAmount", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Token", + "name": "sourceToken", + "type": "address" + }, + { + "internalType": "Token", + "name": "targetToken", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "strategyId", + "type": "uint256" + }, + { + "internalType": "uint128", + "name": "amount", + "type": "uint128" + } + ], + "internalType": "struct TradeAction[]", + "name": "tradeActions", + "type": "tuple[]" + } + ], + "name": "calculateTradeTargetAmount", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "controllerType", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Token", + "name": "token0", + "type": "address" + }, + { + "internalType": "Token", + "name": "token1", + "type": "address" + } + ], + "name": "createPair", + "outputs": [ + { + "components": [ + { + "internalType": "uint128", + "name": "id", + "type": "uint128" + }, + { + "internalType": "Token[2]", + "name": "tokens", + "type": "address[2]" + } + ], + "internalType": "struct Pair", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Token", + "name": "token0", + "type": "address" + }, + { + "internalType": "Token", + "name": "token1", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "initialPrice", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "tradingStartTime", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "expiry", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "multiFactor", + "type": "uint32" + }, + { + "internalType": "enum GradientType", + "name": "gradientType", + "type": "uint8" + } + ], + "internalType": "struct Order[2]", + "name": "orders", + "type": "tuple[2]" + } + ], + "name": "createStrategy", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "strategyId", + "type": "uint256" + } + ], + "name": "deleteStrategy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Token", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "contract IFlashloanRecipient", + "name": "recipient", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "flashloan", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "flashloanFeePPM", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "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" + } + ], + "name": "getRoleMembers", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "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": [], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Token", + "name": "token0", + "type": "address" + }, + { + "internalType": "Token", + "name": "token1", + "type": "address" + } + ], + "name": "pair", + "outputs": [ + { + "components": [ + { + "internalType": "uint128", + "name": "id", + "type": "uint128" + }, + { + "internalType": "Token[2]", + "name": "tokens", + "type": "address[2]" + } + ], + "internalType": "struct Pair", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Token", + "name": "token0", + "type": "address" + }, + { + "internalType": "Token", + "name": "token1", + "type": "address" + } + ], + "name": "pairTradingFeePPM", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pairs", + "outputs": [ + { + "internalType": "Token[2][]", + "name": "", + "type": "address[2][]" + } + ], + "stateMutability": "view", + "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": "bool", + "name": "checkVersion", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "postUpgrade", + "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": [], + "name": "roleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "roleEmergencyStopper", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "roleFeesManager", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "newFlashloanFeePPM", + "type": "uint32" + } + ], + "name": "setFlashloanFeePPM", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Token", + "name": "token0", + "type": "address" + }, + { + "internalType": "Token", + "name": "token1", + "type": "address" + }, + { + "internalType": "uint32", + "name": "newPairTradingFeePPM", + "type": "uint32" + } + ], + "name": "setPairTradingFeePPM", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "newTradingFeePPM", + "type": "uint32" + } + ], + "name": "setTradingFeePPM", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Token", + "name": "token0", + "type": "address" + }, + { + "internalType": "Token", + "name": "token1", + "type": "address" + }, + { + "internalType": "uint256", + "name": "startIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endIndex", + "type": "uint256" + } + ], + "name": "strategiesByPair", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "Token[2]", + "name": "tokens", + "type": "address[2]" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "initialPrice", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "tradingStartTime", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "expiry", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "multiFactor", + "type": "uint32" + }, + { + "internalType": "enum GradientType", + "name": "gradientType", + "type": "uint8" + } + ], + "internalType": "struct Order[2]", + "name": "orders", + "type": "tuple[2]" + } + ], + "internalType": "struct Strategy[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Token", + "name": "token0", + "type": "address" + }, + { + "internalType": "Token", + "name": "token1", + "type": "address" + } + ], + "name": "strategiesByPairCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "strategy", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "Token[2]", + "name": "tokens", + "type": "address[2]" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "initialPrice", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "tradingStartTime", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "expiry", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "multiFactor", + "type": "uint32" + }, + { + "internalType": "enum GradientType", + "name": "gradientType", + "type": "uint8" + } + ], + "internalType": "struct Order[2]", + "name": "orders", + "type": "tuple[2]" + } + ], + "internalType": "struct Strategy", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Token", + "name": "sourceToken", + "type": "address" + }, + { + "internalType": "Token", + "name": "targetToken", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "strategyId", + "type": "uint256" + }, + { + "internalType": "uint128", + "name": "amount", + "type": "uint128" + } + ], + "internalType": "struct TradeAction[]", + "name": "tradeActions", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint128", + "name": "minReturn", + "type": "uint128" + } + ], + "name": "tradeBySourceAmount", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Token", + "name": "sourceToken", + "type": "address" + }, + { + "internalType": "Token", + "name": "targetToken", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "strategyId", + "type": "uint256" + }, + { + "internalType": "uint128", + "name": "amount", + "type": "uint128" + } + ], + "internalType": "struct TradeAction[]", + "name": "tradeActions", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint128", + "name": "maxInput", + "type": "uint128" + } + ], + "name": "tradeByTargetAmount", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "tradingFeePPM", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "strategyId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "initialPrice", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "tradingStartTime", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "expiry", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "multiFactor", + "type": "uint32" + }, + { + "internalType": "enum GradientType", + "name": "gradientType", + "type": "uint8" + } + ], + "internalType": "struct Order[2]", + "name": "currentOrders", + "type": "tuple[2]" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "initialPrice", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "tradingStartTime", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "expiry", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "multiFactor", + "type": "uint32" + }, + { + "internalType": "enum GradientType", + "name": "gradientType", + "type": "uint8" + } + ], + "internalType": "struct Order[2]", + "name": "newOrders", + "type": "tuple[2]" + } + ], + "name": "updateStrategy", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Token", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "withdrawFees", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_logic", + "type": "address" + }, + { + "internalType": "address", + "name": "admin_", + "type": "address" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "constructor" + } + ] +} diff --git a/src/abis/GradientVoucher.json b/src/abis/GradientVoucher.json new file mode 100644 index 0000000..e9f042f --- /dev/null +++ b/src/abis/GradientVoucher.json @@ -0,0 +1,1126 @@ +{ + "address": "0x4973fa43c4c4b0Bbe4071eB3e7c900810Df143E8", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, + { + "inputs": [], + "name": "admin", + "outputs": [ + { + "internalType": "address", + "name": "admin_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [ + { + "internalType": "address", + "name": "implementation_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + }, + { + "inputs": [], + "name": "AccessControlBadConfirmation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "neededRole", + "type": "bytes32" + } + ], + "name": "AccessControlUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [], + "name": "AccessDenied", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyInitialized", + "type": "error" + }, + { + "inputs": [], + "name": "BatchNotSupported", + "type": "error" + }, + { + "inputs": [], + "name": "ControllerAlreadySet", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "ERC721IncorrectOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ERC721InsufficientApproval", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC721InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "ERC721InvalidOperator", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "ERC721InvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC721InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC721InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ERC721NonexistentToken", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidIndices", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyController", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "newBaseExtension", + "type": "string" + } + ], + "name": "BaseExtensionUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "newBaseURI", + "type": "string" + } + ], + "name": "BaseURIUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "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": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "newUseGlobalURI", + "type": "bool" + } + ], + "name": "UseGlobalURIUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "controller", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "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" + } + ], + "name": "getRoleMembers", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "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": "bool", + "name": "newUseGlobalURI", + "type": "bool" + }, + { + "internalType": "string", + "name": "newBaseURI", + "type": "string" + }, + { + "internalType": "string", + "name": "newBaseExtension", + "type": "string" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "checkVersion", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "postUpgrade", + "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": [], + "name": "roleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "newBaseExtension", + "type": "string" + } + ], + "name": "setBaseExtension", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "newBaseURI", + "type": "string" + } + ], + "name": "setBaseURI", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "controllerAddress", + "type": "address" + } + ], + "name": "setController", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "startIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endIndex", + "type": "uint256" + } + ], + "name": "tokensByOwner", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "newUseGlobalURI", + "type": "bool" + } + ], + "name": "useGlobalURI", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_logic", + "type": "address" + }, + { + "internalType": "address", + "name": "admin_", + "type": "address" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "constructor" + } + ] +} diff --git a/src/adapters/uni-v3/adapter.ts b/src/adapters/uni-v3/adapter.ts index a4b8eb6..5e81f9a 100644 --- a/src/adapters/uni-v3/adapter.ts +++ b/src/adapters/uni-v3/adapter.ts @@ -1,7 +1,7 @@ -import { Decimal, ONE } from '../../utils/numerics'; +import { Decimal, ONE_48 } from '../../utils/numerics'; import { EncodedOrder, EncodedStrategy } from '../../common/types'; import { UniV3CastStrategy, UniV3Pool, UniV3Position } from './types'; -import { decodeFloat, decodeOrder } from '../../utils/encoders'; +import { decodeFloatInitialRate, decodeOrder } from '../../utils/encoders'; /** * Constants for Uniswap V3 calculations @@ -30,7 +30,7 @@ function calculateLConstant(order: EncodedOrder): string { if (order.A === 0n) { return Infinity.toString(); } - return ((order.z * ONE) / decodeFloat(order.A)).toString(); + return ((order.z * ONE_48) / decodeFloatInitialRate(order.A)).toString(); } function calculateSqrtPriceX96(marginal: Decimal, roundUp: boolean): string { diff --git a/src/chain-cache/ChainCache.ts b/src/chain-cache/ChainCache.ts index 26d41cc..7ad8480 100644 --- a/src/chain-cache/ChainCache.ts +++ b/src/chain-cache/ChainCache.ts @@ -10,6 +10,7 @@ import { BlockMetadata, EncodedOrder, EncodedStrategy, + GradientEncodedStrategy, OrdersMap, RetypeBigIntToString, TokenPair, @@ -20,20 +21,25 @@ import { BigIntish } from '../utils/numerics'; import { encodedStrategyBigIntToStr, encodedStrategyStrToBN, + encodedGradientStrategyBigIntToStr, + encodedGradientStrategyStrToBN, } from '../utils/serializers'; import { Logger } from '../common/logger'; const logger = new Logger('ChainCache.ts'); -const schemeVersion = 7; // bump this when the serialization format changes +const schemeVersion = 8; // bump this when the serialization format changes type PairToStrategiesMap = { [key: string]: EncodedStrategy[] }; +type GradientPairToStrategiesMap = { [key: string]: GradientEncodedStrategy[] }; type StrategyById = { [key: string]: EncodedStrategy }; +type GradientStrategyById = { [key: string]: GradientEncodedStrategy }; type PairToDirectedOrdersMap = { [key: string]: OrdersMap }; type SerializableDump = { schemeVersion: number; strategiesByPair: RetypeBigIntToString; + gradientStrategiesByPair: RetypeBigIntToString; tradingFeePPMByPair: { [key: string]: number }; latestBlockNumber: number; }; @@ -42,6 +48,8 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter TypedEventEmitter TypedEventEmitter ), + gradientStrategiesByPair: Object.entries( + this._gradientStrategiesByPair + ).reduce( + (acc, [key, strategies]) => { + acc[key] = strategies.map(encodedGradientStrategyBigIntToStr); + return acc; + }, + {} as RetypeBigIntToString + ), tradingFeePPMByPair: this._tradingFeePPMByPair, latestBlockNumber: this._latestBlockNumber, }; @@ -147,6 +184,8 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter TypedEventEmitter { + await this._checkAndHandleCacheMiss(token0, token1); + const key = toPairKey(token0, token1); + return this._gradientStrategiesByPair[key]; + } + public async getStrategiesByPairs(pairs: TokenPair[]): Promise< { pair: TokenPair; @@ -190,14 +238,32 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter strategies.length > 0) - .map(([key, _]) => fromPairKey(key)); + return Array.from( + new Set([ + ...Object.entries(this._strategiesByPair) + .filter(([_, strategies]) => strategies.length > 0) + .map(([key, _]) => key), + ...Object.entries(this._gradientStrategiesByPair) + .filter(([_, strategies]) => strategies.length > 0) + .map(([key, _]) => key), + ]) + ).map(fromPairKey); } - return Object.keys(this._strategiesByPair).map(fromPairKey); + return Array.from( + new Set([ + ...Object.keys(this._strategiesByPair), + ...Object.keys(this._gradientStrategiesByPair), + ]) + ).map(fromPairKey); } /** @@ -221,7 +287,7 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter TypedEventEmitter TypedEventEmitter { this._strategiesById[strategy.id.toString()] = strategy; this._addStrategyOrders(strategy); }); + gradientStrategies.forEach((strategy) => { + this._gradientStrategiesById[strategy.id.toString()] = strategy; + }); } /** @@ -287,9 +358,10 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter TypedEventEmitter 0 && !this._isCacheInitialized) { this._isCacheInitialized = true; @@ -387,6 +465,24 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter TypedEventEmitter TypedEventEmitter s.id !== strategy.id + ); + strategies.push(strategy); + this._gradientStrategiesByPair[key] = strategies; + this._gradientStrategiesById[strategy.id.toString()] = strategy; + } + + private _deleteGradientStrategy(strategy: GradientEncodedStrategy): void { + if (!this.hasCachedPair(strategy.token0, strategy.token1)) { + logger.error( + `Pair ${toPairKey( + strategy.token0, + strategy.token1 + )} is not cached, cannot delete gradient strategy` + ); + return; + } + const key = toPairKey(strategy.token0, strategy.token1); + delete this._gradientStrategiesById[strategy.id.toString()]; + const strategies = (this._gradientStrategiesByPair[key] || []).filter( + (s) => s.id !== strategy.id + ); + this._gradientStrategiesByPair[key] = strategies; + } + //#endregion cache updates } diff --git a/src/chain-cache/ChainSync.ts b/src/chain-cache/ChainSync.ts index 2024f50..b5794b9 100644 --- a/src/chain-cache/ChainSync.ts +++ b/src/chain-cache/ChainSync.ts @@ -1,6 +1,7 @@ import { ChainCache } from './ChainCache'; import { Logger } from '../common/logger'; import { BlockMetadata, Fetcher, TokenPair } from '../common/types'; +import { toPairKey } from './utils'; const logger = new Logger('ChainSync.ts'); @@ -222,8 +223,40 @@ export class ChainSync { const strategiesBatches = await Promise.all( batches.map((batch) => this._fetcher.strategiesByPairs(batch)) ); + const gradientStrategiesBatches = await Promise.all( + batches.map((batch) => this._fetcher.gradientStrategiesByPairs(batch)) + ); logger.debug('_syncPairDataBatch strategiesBatches', strategiesBatches); - this._chainCache.bulkAddPairs(strategiesBatches.flat()); + logger.debug( + '_syncPairDataBatch gradientStrategiesBatches', + gradientStrategiesBatches + ); + this._chainCache.bulkAddPairs( + batches.flatMap((batch, batchIndex) => { + const strategiesByPairKey = new Map( + strategiesBatches[batchIndex].map((entry) => [ + toPairKey(entry.pair[0], entry.pair[1]), + entry.strategies, + ]) + ); + const gradientStrategiesByPairKey = new Map( + gradientStrategiesBatches[batchIndex].map((entry) => [ + toPairKey(entry.pair[0], entry.pair[1]), + entry.strategies, + ]) + ); + + return batch.map((pair) => { + const pairKey = toPairKey(pair[0], pair[1]); + return { + pair, + strategies: strategiesByPairKey.get(pairKey) ?? [], + gradientStrategies: + gradientStrategiesByPairKey.get(pairKey) ?? [], + }; + }); + }) + ); this._uncachedPairs = []; } catch (error) { logger.error('Failed to fetch strategies for pairs batch:', error); @@ -238,9 +271,12 @@ export class ChainSync { ); } try { - const strategies = await this._fetcher.strategiesByPair(token0, token1); + const [strategies, gradientStrategies] = await Promise.all([ + this._fetcher.strategiesByPair(token0, token1), + this._fetcher.gradientStrategiesByPair(token0, token1), + ]); if (this._chainCache.hasCachedPair(token0, token1)) return; - this._chainCache.addPair(token0, token1, strategies); + this._chainCache.addPair(token0, token1, strategies, gradientStrategies); } catch (error) { logger.error( 'Failed to fetch strategies for pair:', @@ -302,7 +338,10 @@ export class ChainSync { // Process events and collect newly created pairs const newlyCreatedPairs: TokenPair[] = []; for (const event of events) { - if (event.type === 'StrategyCreated') { + if ( + event.type === 'StrategyCreated' || + event.type === 'GradientStrategyCreated' + ) { const strategy = event.data; if ( !this._chainCache.hasCachedPair( diff --git a/src/common/types.ts b/src/common/types.ts index 8526962..342048d 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -67,6 +67,27 @@ export type EncodedOrder = { export type EncodedOrderBNStr = RetypeBigIntToString; +export enum GradientType { + LinearIncrease = 0, + LinearDecrease = 1, + LinearInverseIncrease = 2, + LinearInverseDecrease = 3, + ExponentialIncrease = 4, + ExponentialDecrease = 5, +} + +export type GradientEncodedOrder = { + liquidity: bigint; + initialPrice: bigint; + tradingStartTime: bigint; + expiry: bigint; + multiFactor: bigint; + gradientType: bigint; +}; + +export type GradientEncodedOrderBNStr = + RetypeBigIntToString; + export type DecodedOrder = { liquidity: string; lowestRate: string; @@ -74,6 +95,15 @@ export type DecodedOrder = { marginalRate: string; }; +export type GradientDecodedOrder = { + liquidity: string; + initialPrice: string; + tradingStartTime: number; + expiry: number; + multiFactor: string; + gradientType: GradientType; +}; + export type OrdersMap = { [orderId: string]: EncodedOrder; }; @@ -90,6 +120,17 @@ export type EncodedStrategy = { export type EncodedStrategyBNStr = RetypeBigIntToString; +export type GradientEncodedStrategy = { + id: bigint; + token0: string; + token1: string; + order0: GradientEncodedOrder; + order1: GradientEncodedOrder; +}; + +export type GradientEncodedStrategyBNStr = + RetypeBigIntToString; + export type DecodedStrategy = { token0: string; token1: string; @@ -97,6 +138,13 @@ export type DecodedStrategy = { order1: DecodedOrder; }; +export type GradientDecodedStrategy = { + token0: string; + token1: string; + order0: GradientDecodedOrder; + order1: GradientDecodedOrder; +}; + export type TradingFeeUpdate = [string, string, number]; export type Action = { @@ -123,6 +171,26 @@ export type Strategy = { encoded: EncodedStrategyBNStr; // the encoded strategy }; +export type GradientStrategy = { + type: 'gradient'; + id: string; + baseToken: string; + quoteToken: string; + buyPriceStart: string; // in quote tkn per 1 base tkn + buyPriceEnd: string; // in quote tkn per 1 base tkn + buyBudget: string; // in quote tkn + buyGradientType: GradientType; + buyStartTime: number; // unix timestamp in seconds + buyEndTime: number; // unix timestamp in seconds + sellPriceStart: string; // in quote tkn per 1 base tkn + sellPriceEnd: string; // in quote tkn per 1 base tkn + sellBudget: string; // in base tkn + sellGradientType: GradientType; + sellStartTime: number; // unix timestamp in seconds + sellEndTime: number; // unix timestamp in seconds + encoded: GradientEncodedStrategyBNStr; +}; + export type AtLeastOneOf = { [K in keyof T]: { [key in K]: T[K] } & { [key in Exclude]?: T[key]; @@ -141,6 +209,13 @@ export type StrategyUpdate = AtLeastOneOf< > >; +export type GradientStrategyUpdate = AtLeastOneOf< + Omit< + GradientStrategy, + 'type' | 'id' | 'encoded' | 'baseToken' | 'quoteToken' + > +>; + export type BlockMetadata = { number: number; hash: string; @@ -153,6 +228,15 @@ export type SyncedEvent = logIndex: number; data: EncodedStrategy; } + | { + type: + | 'GradientStrategyCreated' + | 'GradientStrategyUpdated' + | 'GradientStrategyDeleted'; + blockNumber: number; + logIndex: number; + data: GradientEncodedStrategy; + } | { type: 'TradingFeePPMUpdated'; blockNumber: number; @@ -171,12 +255,22 @@ export type SyncedEvents = SyncedEvent[]; export interface Fetcher { pairs(): Promise; strategiesByPair(token0: string, token1: string): Promise; + gradientStrategiesByPair( + token0: string, + token1: string + ): Promise; strategiesByPairs(pairs: TokenPair[]): Promise< { pair: TokenPair; strategies: EncodedStrategy[]; }[] >; + gradientStrategiesByPairs(pairs: TokenPair[]): Promise< + { + pair: TokenPair; + strategies: GradientEncodedStrategy[]; + }[] + >; pairTradingFeePPM(token0: string, token1: string): Promise; pairsTradingFeePPM(pairs: TokenPair[]): Promise<[string, string, number][]>; tradingFeePPM(): Promise; diff --git a/src/contracts-api/Composer.ts b/src/contracts-api/Composer.ts index c941390..f84c183 100644 --- a/src/contracts-api/Composer.ts +++ b/src/contracts-api/Composer.ts @@ -3,7 +3,11 @@ import { Contracts } from './Contracts'; import { PayableOverrides, PopulatedTransaction } from '../common/types'; import { buildTradeOverrides, isETHAddress } from './utils'; import { Logger } from '../common/logger'; -import { EncodedOrder, TradeAction } from '../common/types'; +import { + EncodedOrder, + GradientEncodedOrder, + TradeAction, +} from '../common/types'; const logger = new Logger('Composer.ts'); /** @@ -16,6 +20,12 @@ export default class Composer { this._contracts = contracts; } + private _requireGradientController(): void { + if (!this._contracts.hasGradientController) { + throw new Error('GradientController address not configured'); + } + } + /** * * @param {string} sourceToken - The address of the token to be traded. @@ -164,6 +174,33 @@ export default class Composer { ); } + public createGradientStrategy( + token0: string, + token1: string, + order0: GradientEncodedOrder, + order1: GradientEncodedOrder, + overrides?: PayableOverrides + ) { + logger.debug('createGradientStrategy called', arguments); + this._requireGradientController(); + + const customOverrides = { ...overrides }; + if (isETHAddress(token0)) { + customOverrides.value = order0.liquidity; + } else if (isETHAddress(token1)) { + customOverrides.value = order1.liquidity; + } + + logger.debug('createGradientStrategy overrides', customOverrides); + + return this._contracts.gradientController.createStrategy.populateTransaction( + token0, + token1, + [order0, order1], + customOverrides + ); + } + public updateStrategy( strategyId: bigint, token0: string, @@ -190,4 +227,46 @@ export default class Composer { customOverrides ); } + + public updateGradientStrategy( + strategyId: bigint, + token0: string, + token1: string, + currentOrders: [GradientEncodedOrder, GradientEncodedOrder], + newOrders: [GradientEncodedOrder, GradientEncodedOrder], + overrides?: PayableOverrides + ) { + this._requireGradientController(); + + const customOverrides = { ...overrides }; + if ( + isETHAddress(token0) && + newOrders[0].liquidity > currentOrders[0].liquidity + ) { + const diff = newOrders[0].liquidity - currentOrders[0].liquidity; + customOverrides.value = diff; + } else if ( + isETHAddress(token1) && + newOrders[1].liquidity > currentOrders[1].liquidity + ) { + const diff = newOrders[1].liquidity - currentOrders[1].liquidity; + customOverrides.value = diff; + } + + logger.debug('updateGradientStrategy overrides', customOverrides); + + return this._contracts.gradientController.updateStrategy.populateTransaction( + strategyId, + currentOrders, + newOrders, + customOverrides + ); + } + + public deleteGradientStrategy(id: bigint) { + this._requireGradientController(); + return this._contracts.gradientController.deleteStrategy.populateTransaction( + id + ); + } } diff --git a/src/contracts-api/Contracts.ts b/src/contracts-api/Contracts.ts index a442644..a11324e 100644 --- a/src/contracts-api/Contracts.ts +++ b/src/contracts-api/Contracts.ts @@ -9,30 +9,55 @@ import { Token__factory, CarbonBatcher, CarbonBatcher__factory, + GradientVoucher, + GradientVoucher__factory, + GradientController, + GradientController__factory, } from '../abis/types'; import { Provider } from 'ethers'; import { config as defaultConfig } from './config'; import { ContractsConfig } from './types'; +type ResolvedContractsConfig = Required< + Pick< + ContractsConfig, + | 'carbonControllerAddress' + | 'multiCallAddress' + | 'voucherAddress' + | 'carbonBatcherAddress' + > +> & + Pick< + ContractsConfig, + 'gradientControllerAddress' | 'gradientVoucherAddress' + >; + export class Contracts { private _provider: Provider; private _carbonController: CarbonController | undefined; private _multiCall: Multicall | undefined; private _voucher: Voucher | undefined; private _carbonBatcher: CarbonBatcher | undefined; - private _config = defaultConfig; + private _gradientVoucher: GradientVoucher | undefined; + private _gradientController: GradientController | undefined; + private _config: ResolvedContractsConfig; public constructor(provider: Provider, config?: ContractsConfig) { this._provider = provider; - this._config.carbonControllerAddress = - config?.carbonControllerAddress || defaultConfig.carbonControllerAddress; - this._config.multiCallAddress = - config?.multiCallAddress || defaultConfig.multiCallAddress; - this._config.voucherAddress = - config?.voucherAddress || defaultConfig.voucherAddress; - this._config.carbonBatcherAddress = - config?.carbonBatcherAddress || defaultConfig.carbonBatcherAddress; + this._config = { + carbonControllerAddress: + config?.carbonControllerAddress ?? defaultConfig.carbonControllerAddress, + multiCallAddress: + config?.multiCallAddress ?? defaultConfig.multiCallAddress, + voucherAddress: config?.voucherAddress ?? defaultConfig.voucherAddress, + carbonBatcherAddress: + config?.carbonBatcherAddress ?? defaultConfig.carbonBatcherAddress, + // Gradient contracts are opt-in per chain. If omitted, the SDK must not + // attempt to call them. + gradientControllerAddress: config?.gradientControllerAddress, + gradientVoucherAddress: config?.gradientVoucherAddress, + }; } public get carbonController(): CarbonController { @@ -45,6 +70,19 @@ export class Contracts { return this._carbonController; } + public get gradientController(): GradientController { + if (!this.hasGradientController) { + throw new Error('GradientController address not configured'); + } + if (!this._gradientController) + this._gradientController = GradientController__factory.connect( + this._config.gradientControllerAddress!, + this._provider + ); + + return this._gradientController; + } + public get carbonBatcher(): CarbonBatcher { if (!this._carbonBatcher) this._carbonBatcher = CarbonBatcher__factory.connect( @@ -75,6 +113,27 @@ export class Contracts { return this._voucher; } + public get gradientVoucher(): GradientVoucher { + if (!this.hasGradientVoucher) { + throw new Error('GradientVoucher address not configured'); + } + if (!this._gradientVoucher) + this._gradientVoucher = GradientVoucher__factory.connect( + this._config.gradientVoucherAddress!, + this._provider + ); + + return this._gradientVoucher; + } + + public get hasGradientController(): boolean { + return !!this._config.gradientControllerAddress; + } + + public get hasGradientVoucher(): boolean { + return !!this._config.gradientVoucherAddress; + } + public token(address: string): Token { return Token__factory.connect(address, this._provider); } diff --git a/src/contracts-api/Reader.ts b/src/contracts-api/Reader.ts index 53ffa8b..0cb73ee 100644 --- a/src/contracts-api/Reader.ts +++ b/src/contracts-api/Reader.ts @@ -2,6 +2,7 @@ import { StrategyStructOutput, CarbonController, } from '../abis/types/CarbonController'; +import { StrategyStructOutput as GradientStrategyStructOutput } from '../abis/types/GradientController'; import { Contracts } from './Contracts'; import { isETHAddress, @@ -13,6 +14,7 @@ import { Logger } from '../common/logger'; import { EncodedStrategy, Fetcher, + GradientEncodedStrategy, TokenPair, BlockMetadata, TradingFeeUpdate, @@ -21,6 +23,12 @@ import { } from '../common/types'; const logger = new Logger('Reader.ts'); +const compareTokens = (token0: string, token1: string): number => + token0.localeCompare(token1); + +const toPairKey = (token0: string, token1: string): string => + [token0, token1].sort(compareTokens).join('_'); + function toStrategy(res: StrategyStructOutput): EncodedStrategy { const id = res[0]; const token0 = res[2][0]; @@ -52,6 +60,47 @@ function toStrategy(res: StrategyStructOutput): EncodedStrategy { }; } +function toGradientStrategy( + res: GradientStrategyStructOutput +): GradientEncodedStrategy { + const id = res[0]; + const token0 = res[2][0]; + const token1 = res[2][1]; + const liquidity0 = res[3][0][0]; + const initialPrice0 = res[3][0][1]; + const tradingStartTime0 = res[3][0][2]; + const expiry0 = res[3][0][3]; + const multiFactor0 = res[3][0][4]; + const gradientType0 = res[3][0][5]; + const liquidity1 = res[3][1][0]; + const initialPrice1 = res[3][1][1]; + const tradingStartTime1 = res[3][1][2]; + const expiry1 = res[3][1][3]; + const multiFactor1 = res[3][1][4]; + const gradientType1 = res[3][1][5]; + return { + id, + token0, + token1, + order0: { + liquidity: liquidity0, + initialPrice: initialPrice0, + tradingStartTime: tradingStartTime0, + expiry: expiry0, + multiFactor: multiFactor0, + gradientType: gradientType0, + }, + order1: { + liquidity: liquidity1, + initialPrice: initialPrice1, + tradingStartTime: tradingStartTime1, + expiry: expiry1, + multiFactor: multiFactor1, + gradientType: gradientType1, + }, + }; +} + /** * Class that provides methods to read data from contracts. */ @@ -107,13 +156,72 @@ export default class Reader implements Fetcher { } } + public async gradientStrategy(id: bigint): Promise { + logger.debug('gradientStrategy called', id); + if (!this._contracts.hasGradientController) { + throw new Error('GradientController address not configured'); + } + try { + const res = await this._contracts.gradientController.strategy(id); + return toGradientStrategy(res); + } catch (error) { + logger.error('gradientStrategy error', error); + throw error; + } + } + + public async gradientStrategies( + ids: bigint[] + ): Promise { + logger.debug('gradientStrategies called', ids); + if (!this._contracts.hasGradientController) { + return []; + } + try { + const results = await this._multicall( + ids.map((id) => ({ + contractAddress: this._contracts.gradientController.target as string, + interface: this._contracts.gradientController.interface, + methodName: 'strategy', + methodParameters: [id], + })) + ); + logger.debug('gradientStrategies results', results); + if (!results || results.length === 0) return []; + + return results.map((strategyRes) => { + const strategy = strategyRes[0] as GradientStrategyStructOutput; + return toGradientStrategy(strategy); + }); + } catch (error) { + logger.error('gradientStrategies error', error); + throw error; + } + } + public async pairs(): Promise { logger.debug('pairs called'); try { - const pairs = await this._contracts.carbonController.pairs(); - return pairs.map( - (pair) => [pair[0].toString(), pair[1].toString()] as TokenPair - ); + const [standardPairs, gradientPairs] = await Promise.all([ + this._contracts.carbonController.pairs(), + this._contracts.hasGradientController + ? this._contracts.gradientController.pairs() + : Promise.resolve([]), + ]); + const dedupedPairs = new Map(); + + [...standardPairs, ...gradientPairs].forEach((pair) => { + const normalizedPair = [ + pair[0].toString(), + pair[1].toString(), + ] as TokenPair; + dedupedPairs.set( + toPairKey(normalizedPair[0], normalizedPair[1]), + normalizedPair + ); + }); + + return Array.from(dedupedPairs.values()); } catch (error) { logger.error('pairs error', error); throw error; @@ -153,6 +261,42 @@ export default class Reader implements Fetcher { } } + public async gradientStrategiesByPair( + token0: string, + token1: string + ): Promise { + logger.debug('gradientStrategiesByPair called', token0, token1); + if (!this._contracts.hasGradientController) { + return []; + } + try { + const allStrategies: GradientEncodedStrategy[] = []; + let startIndex = 0; + const chunkSize = 1000; + + while (true) { + const res = + (await this._contracts.gradientController.strategiesByPair( + token0, + token1, + startIndex, + startIndex + chunkSize + )) ?? []; + + allStrategies.push(...res.map((r) => toGradientStrategy(r))); + + if (res.length < chunkSize) break; + + startIndex += chunkSize; + } + + return allStrategies; + } catch (error) { + logger.error('gradientStrategiesByPair error', error); + throw error; + } + } + public async strategiesByPairs(pairs: TokenPair[]): Promise< { pair: TokenPair; @@ -238,18 +382,90 @@ export default class Reader implements Fetcher { } } - public async tokensByOwner(owner: string) { - logger.debug('tokensByOwner called', owner); - if (!owner) return []; + public async gradientStrategiesByPairs(pairs: TokenPair[]): Promise< + { + pair: TokenPair; + strategies: GradientEncodedStrategy[]; + }[] + > { + logger.debug('gradientStrategiesByPairs called', pairs); + if (!this._contracts.hasGradientController) { + return pairs.map((pair) => ({ pair, strategies: [] })); + } try { - const result = await this._contracts.voucher.tokensByOwner(owner, 0, 0); - return result.map((r) => BigInt(r)); + const chunkSize = 1000; + const results: { + pair: TokenPair; + strategies: GradientEncodedStrategy[]; + }[] = []; + const pairsNeedingMore: { pair: TokenPair; index: number }[] = []; + + const firstChunkResults = await this._multicall( + pairs.map((pair) => ({ + contractAddress: this._contracts.gradientController.target as string, + interface: this._contracts.gradientController.interface, + methodName: 'strategiesByPair', + methodParameters: [pair[0], pair[1], 0, chunkSize], + })) + ); + + if (!firstChunkResults || firstChunkResults.length === 0) return []; + + firstChunkResults.forEach((result, i) => { + const strategiesResult = (result[0] ?? []) as GradientStrategyStructOutput[]; + const currentPair = pairs[i]; + + results.push({ + pair: currentPair, + strategies: strategiesResult.map((r) => toGradientStrategy(r)), + }); + + if (strategiesResult.length === chunkSize) { + pairsNeedingMore.push({ pair: currentPair, index: i }); + } + }); + + for (const { pair, index } of pairsNeedingMore) { + let startIndex = chunkSize; + + while (true) { + const res = + (await this._contracts.gradientController.strategiesByPair( + pair[0], + pair[1], + startIndex, + startIndex + chunkSize + )) ?? []; + + results[index].strategies.push(...res.map((r) => toGradientStrategy(r))); + + if (res.length < chunkSize) break; + startIndex += chunkSize; + } + } + + return results; } catch (error) { - logger.error('tokensByOwner error', error); + logger.error('gradientStrategiesByPairs error', error); throw error; } } + public async tokensByOwner(owner: string): Promise<{ + gradientVoucherTokens: bigint[]; + voucherTokens: bigint[]; + }> { + if (!owner) return { gradientVoucherTokens: [], voucherTokens: [] }; + + const [gradientVoucherTokens, voucherTokens] = await Promise.all([ + this._contracts.hasGradientVoucher + ? this._contracts.gradientVoucher.tokensByOwner(owner, 0, 0) + : Promise.resolve([]), + this._contracts.voucher.tokensByOwner(owner, 0, 0), + ]); + return { gradientVoucherTokens, voucherTokens }; + } + public async tradingFeePPM(): Promise { logger.debug('tradingFeePPM called'); try { @@ -380,6 +596,8 @@ export default class Reader implements Fetcher { toBlock: number, maxChunkSize: number = 2000 ): Promise { + if (toBlock < fromBlock) return []; + // Calculate number of chunks needed const totalBlocks = toBlock - fromBlock + 1; const numChunks = Math.ceil(totalBlocks / maxChunkSize); @@ -394,12 +612,21 @@ export default class Reader implements Fetcher { // Fetch logs for all chunks concurrently const chunkResults = await Promise.all( chunks.map(async ({ start, end }) => { - const logs = await this._contracts.provider.getLogs({ - address: this._contracts.carbonController.target as string, - fromBlock: start, - toBlock: end, - }); - return logs; + const [standardLogs, gradientLogs] = await Promise.all([ + this._contracts.provider.getLogs({ + address: this._contracts.carbonController.target as string, + fromBlock: start, + toBlock: end, + }), + this._contracts.hasGradientController + ? this._contracts.provider.getLogs({ + address: this._contracts.gradientController.target as string, + fromBlock: start, + toBlock: end, + }) + : Promise.resolve([]), + ]); + return [...standardLogs, ...gradientLogs]; }) ); @@ -407,15 +634,31 @@ export default class Reader implements Fetcher { const allEvents = chunkResults .flat() .map((log) => { - // Get event type from topics - const parsedLog = this._contracts.carbonController.interface.parseLog({ - topics: log.topics, - data: log.data, - }); + const isGradientLog = + this._contracts.hasGradientController && + log.address?.toLowerCase() === + String(this._contracts.gradientController.target).toLowerCase(); + + let parsedLog = null; + try { + parsedLog = isGradientLog + ? this._contracts.gradientController.interface.parseLog({ + topics: log.topics, + data: log.data, + }) + : this._contracts.carbonController.interface.parseLog({ + topics: log.topics, + data: log.data, + }); + } catch { + return null; + } if (!parsedLog) return null; - const eventType = parsedLog.name as SyncedEvent['type']; + const eventType = isGradientLog + ? (`Gradient${parsedLog.name}` as SyncedEvent['type']) + : (parsedLog.name as SyncedEvent['type']); switch (eventType) { case 'StrategyCreated': @@ -445,6 +688,37 @@ export default class Reader implements Fetcher { data: eventData, } as const; } + case 'GradientStrategyCreated': + case 'GradientStrategyUpdated': + case 'GradientStrategyDeleted': { + const eventData: GradientEncodedStrategy = { + id: parsedLog.args.id, + token0: parsedLog.args.token0, + token1: parsedLog.args.token1, + order0: { + liquidity: parsedLog.args.order0.liquidity, + initialPrice: parsedLog.args.order0.initialPrice, + tradingStartTime: parsedLog.args.order0.tradingStartTime, + expiry: parsedLog.args.order0.expiry, + multiFactor: parsedLog.args.order0.multiFactor, + gradientType: parsedLog.args.order0.gradientType, + }, + order1: { + liquidity: parsedLog.args.order1.liquidity, + initialPrice: parsedLog.args.order1.initialPrice, + tradingStartTime: parsedLog.args.order1.tradingStartTime, + expiry: parsedLog.args.order1.expiry, + multiFactor: parsedLog.args.order1.multiFactor, + gradientType: parsedLog.args.order1.gradientType, + }, + }; + return { + type: eventType, + blockNumber: log.blockNumber, + logIndex: log.index, + data: eventData, + } as const; + } case 'TradingFeePPMUpdated': { const eventData: number = parsedLog.args.newFeePPM; return { diff --git a/src/contracts-api/config.ts b/src/contracts-api/config.ts index 785252f..490e8f6 100644 --- a/src/contracts-api/config.ts +++ b/src/contracts-api/config.ts @@ -5,4 +5,6 @@ export const config: Required = { multiCallAddress: '0x5ba1e12693dc8f9c48aad8770482f4739beed696', voucherAddress: '0x3660F04B79751e31128f6378eAC70807e38f554E', carbonBatcherAddress: '0x0199f3A6C4B192B9f9C3eBE31FBC535CdD4B7D4e', + gradientControllerAddress: '0x5BDdF8EdeEaE66Cc8477c9282b8c0462CD7132aa', + gradientVoucherAddress: '0x4973fa43c4c4b0Bbe4071eB3e7c900810Df143E8', }; diff --git a/src/contracts-api/types.ts b/src/contracts-api/types.ts index 41c5455..c602ccf 100644 --- a/src/contracts-api/types.ts +++ b/src/contracts-api/types.ts @@ -2,5 +2,7 @@ export type ContractsConfig = { carbonControllerAddress?: string; multiCallAddress?: string; voucherAddress?: string; + gradientControllerAddress?: string; + gradientVoucherAddress?: string; carbonBatcherAddress?: string; }; diff --git a/src/strategy-management/Toolkit.ts b/src/strategy-management/Toolkit.ts index 10fbcd8..409295d 100644 --- a/src/strategy-management/Toolkit.ts +++ b/src/strategy-management/Toolkit.ts @@ -18,6 +18,12 @@ import { OrdersMap, Filter, Action, + GradientDecodedStrategy, + GradientEncodedStrategy, + GradientEncodedStrategyBNStr, + GradientStrategy, + GradientStrategyUpdate, + GradientType, Strategy, StrategyUpdate, OrdersMapBNStr, @@ -49,17 +55,22 @@ const logger = new Logger('Toolkit.ts'); import { addFee, buildStrategyObject, + buildGradientStrategyObject, calculateOverlappingBuyBudget, calculateOverlappingSellBudget, + decodeGradientStrategy, decodeStrategy, + encodeGradientStrategy, encodeStrategy, getMinMaxPricesByDecimals, normalizeRate, + parseGradientStrategy, parseStrategy, subtractFee, } from './utils'; import { + encodedGradientStrategyStrToBN, decodeOrder, encodedStrategyStrToBN, matchActionBNToStr, @@ -345,10 +356,7 @@ export class Toolkit { const strategy = encodedStrategyStrToBN(serializedStrategy); let order; - if ( - sourceToken === strategy.token0 && - targetToken === strategy.token1 - ) { + if (sourceToken === strategy.token0 && targetToken === strategy.token1) { order = strategy.order1; } else if ( sourceToken === strategy.token1 && @@ -684,6 +692,35 @@ export class Toolkit { return strategy; } + public async getGradientStrategyById(id: string): Promise { + logger.debug('getGradientStrategyById called', arguments); + + let encodedStrategy: GradientEncodedStrategy | undefined; + + if (this._cache.isCacheInitialized()) { + encodedStrategy = this._cache.getGradientStrategyById(id); + } + + if (encodedStrategy) { + logger.debug('getGradientStrategyById fetched from cache'); + } else { + logger.debug('getGradientStrategyById fetching from chain'); + encodedStrategy = await this._api.reader.gradientStrategy(BigInt(id)); + } + + const decodedStrategy = decodeGradientStrategy(encodedStrategy); + const strategy = await parseGradientStrategy(decodedStrategy, this._decimals); + + logger.debug('getGradientStrategyById info:', { + id, + encodedStrategy, + decodedStrategy, + strategy, + }); + + return strategy; + } + /** * Gets all the strategies that belong to the given pair * @@ -734,6 +771,48 @@ export class Toolkit { return strategies; } + public async getGradientStrategiesByPair( + token0: string, + token1: string + ): Promise { + logger.debug('getGradientStrategiesByPair called', arguments); + + let encodedStrategies: GradientEncodedStrategy[] | undefined; + + if (this._cache.isCacheInitialized()) { + encodedStrategies = await this._cache.getGradientStrategiesByPair( + token0, + token1 + ); + } + + if (encodedStrategies) { + logger.debug('getGradientStrategiesByPair fetched from cache'); + } else { + logger.debug('getGradientStrategiesByPair fetching from chain'); + encodedStrategies = await this._api.reader.gradientStrategiesByPair( + token0, + token1 + ); + } + const decodedStrategies = encodedStrategies.map(decodeGradientStrategy); + const strategies = await Promise.all( + decodedStrategies.map(async (strategy) => { + return await parseGradientStrategy(strategy, this._decimals); + }) + ); + + logger.debug('getGradientStrategiesByPair info:', { + token0, + token1, + encodedStrategies, + decodedStrategies, + strategies, + }); + + return strategies; + } + /** * Gets all the strategies that belong to pairs in the given list. * If the cache is synced, it will return the strategies from the cache. @@ -828,7 +907,8 @@ export class Toolkit { public async getUserStrategies(user: string): Promise { logger.debug('getUserStrategies called', arguments); - const ids = await this._api.reader.tokensByOwner(user); + const tokens = await this._api.reader.tokensByOwner(user); + const ids = tokens.voucherTokens; let encodedStrategies: EncodedStrategy[] = []; let uncachedIds: bigint[] = ids; @@ -865,6 +945,53 @@ export class Toolkit { return strategies; } + public async getUserGradientStrategies( + user: string + ): Promise { + logger.debug('getUserGradientStrategies called', arguments); + + const tokens = await this._api.reader.tokensByOwner(user); + const ids = tokens.gradientVoucherTokens; + let encodedStrategies: GradientEncodedStrategy[] = []; + + if (this._cache.isCacheInitialized()) { + const uncachedIds: bigint[] = []; + ids.forEach((id) => { + const strategy = this._cache.getGradientStrategyById(id); + if (strategy) { + encodedStrategies.push(strategy); + } else { + uncachedIds.push(id); + } + }); + + if (uncachedIds.length > 0) { + const uncachedStrategies = + await this._api.reader.gradientStrategies(uncachedIds); + encodedStrategies = [...encodedStrategies, ...uncachedStrategies]; + } + } else { + encodedStrategies = + ids.length > 0 ? await this._api.reader.gradientStrategies(ids) : []; + } + + const decodedStrategies = encodedStrategies.map(decodeGradientStrategy); + const strategies = await Promise.all( + decodedStrategies.map(async (strategy) => { + return await parseGradientStrategy(strategy, this._decimals); + }) + ); + + logger.debug('getUserGradientStrategies info:', { + ids, + encodedStrategies, + decodedStrategies, + strategies, + }); + + return strategies; + } + /** * Returns the data needed to process a trade. * `getMatchParams` returns the data for a given source and target token pair. @@ -1338,6 +1465,61 @@ export class Toolkit { ); } + public async createBuySellGradientStrategy( + baseToken: string, + quoteToken: string, + buyPriceStart: string, + buyPriceEnd: string, + buyBudget: string, + buyGradientType: GradientType, + buyStartTime: number, + buyEndTime: number, + sellPriceStart: string, + sellPriceEnd: string, + sellBudget: string, + sellGradientType: GradientType, + sellStartTime: number, + sellEndTime: number, + overrides?: PayableOverrides + ): Promise { + logger.debug('createBuySellGradientStrategy called', arguments); + const decimals = this._decimals; + const baseDecimals = await decimals.fetchDecimals(baseToken); + const quoteDecimals = await decimals.fetchDecimals(quoteToken); + const strategy: GradientDecodedStrategy = buildGradientStrategyObject( + baseToken, + quoteToken, + baseDecimals, + quoteDecimals, + buyPriceStart, + buyPriceEnd, + buyBudget, + buyGradientType, + buyStartTime, + buyEndTime, + sellPriceStart, + sellPriceEnd, + sellBudget, + sellGradientType, + sellStartTime, + sellEndTime + ); + const encStrategy = encodeGradientStrategy(strategy); + + logger.debug('createBuySellGradientStrategy info:', { + strategy, + encStrategy, + }); + + return this._api.composer.createGradientStrategy( + encStrategy.token0, + encStrategy.token1, + encStrategy.order0, + encStrategy.order1, + overrides + ); + } + /** * Creates an unsigned transaction to create multiple strategies - similarly to `createBuySellStrategy`. * @@ -1591,6 +1773,134 @@ export class Toolkit { ); } + public async updateGradientStrategy( + strategyId: string, + encoded: GradientEncodedStrategyBNStr, + { + buyPriceStart, + buyPriceEnd, + buyBudget, + buyGradientType, + buyStartTime, + buyEndTime, + sellPriceStart, + sellPriceEnd, + sellBudget, + sellGradientType, + sellStartTime, + sellEndTime, + }: GradientStrategyUpdate, + overrides?: PayableOverrides + ): Promise { + logger.debug('updateGradientStrategy called', arguments); + + const decodedOriginal = decodeGradientStrategy( + encodedGradientStrategyStrToBN(encoded) + ); + const originalStrategy = await parseGradientStrategy( + decodedOriginal, + this._decimals + ); + + const decimals = this._decimals; + const baseDecimals = await decimals.fetchDecimals( + originalStrategy.baseToken + ); + const quoteDecimals = await decimals.fetchDecimals( + originalStrategy.quoteToken + ); + + const newStrategy: GradientDecodedStrategy = buildGradientStrategyObject( + originalStrategy.baseToken, + originalStrategy.quoteToken, + baseDecimals, + quoteDecimals, + buyPriceStart ?? originalStrategy.buyPriceStart, + buyPriceEnd ?? originalStrategy.buyPriceEnd, + buyBudget ?? originalStrategy.buyBudget, + buyGradientType ?? originalStrategy.buyGradientType, + buyStartTime ?? originalStrategy.buyStartTime, + buyEndTime ?? originalStrategy.buyEndTime, + sellPriceStart ?? originalStrategy.sellPriceStart, + sellPriceEnd ?? originalStrategy.sellPriceEnd, + sellBudget ?? originalStrategy.sellBudget, + sellGradientType ?? originalStrategy.sellGradientType, + sellStartTime ?? originalStrategy.sellStartTime, + sellEndTime ?? originalStrategy.sellEndTime + ); + const newEncodedStrategy = encodeGradientStrategy(newStrategy); + const encodedBN = encodedGradientStrategyStrToBN(encoded); + + const isBuyOrderUnchanged = + buyPriceStart === undefined && + buyPriceEnd === undefined && + buyBudget === undefined && + buyGradientType === undefined && + buyStartTime === undefined && + buyEndTime === undefined; + const isSellOrderUnchanged = + sellPriceStart === undefined && + sellPriceEnd === undefined && + sellBudget === undefined && + sellGradientType === undefined && + sellStartTime === undefined && + sellEndTime === undefined; + + if (isBuyOrderUnchanged) { + newEncodedStrategy.order1 = encodedBN.order1; + } else if ( + buyBudget !== undefined && + buyPriceStart === undefined && + buyPriceEnd === undefined && + buyGradientType === undefined && + buyStartTime === undefined && + buyEndTime === undefined + ) { + newEncodedStrategy.order1.initialPrice = encodedBN.order1.initialPrice; + newEncodedStrategy.order1.tradingStartTime = + encodedBN.order1.tradingStartTime; + newEncodedStrategy.order1.expiry = encodedBN.order1.expiry; + newEncodedStrategy.order1.multiFactor = encodedBN.order1.multiFactor; + newEncodedStrategy.order1.gradientType = encodedBN.order1.gradientType; + } + + if (isSellOrderUnchanged) { + newEncodedStrategy.order0 = encodedBN.order0; + } else if ( + sellBudget !== undefined && + sellPriceStart === undefined && + sellPriceEnd === undefined && + sellGradientType === undefined && + sellStartTime === undefined && + sellEndTime === undefined + ) { + newEncodedStrategy.order0.initialPrice = encodedBN.order0.initialPrice; + newEncodedStrategy.order0.tradingStartTime = + encodedBN.order0.tradingStartTime; + newEncodedStrategy.order0.expiry = encodedBN.order0.expiry; + newEncodedStrategy.order0.multiFactor = encodedBN.order0.multiFactor; + newEncodedStrategy.order0.gradientType = encodedBN.order0.gradientType; + } + + logger.debug('updateGradientStrategy info:', { + baseDecimals, + quoteDecimals, + decodedOriginal, + originalStrategy, + newStrategy, + newEncodedStrategy, + }); + + return this._api.composer.updateGradientStrategy( + BigInt(strategyId), + newEncodedStrategy.token0, + newEncodedStrategy.token1, + [encodedBN.order0, encodedBN.order1], + [newEncodedStrategy.order0, newEncodedStrategy.order1], + overrides + ); + } + public async deleteStrategy( strategyId: string ): Promise { @@ -1598,6 +1908,13 @@ export class Toolkit { return this._api.composer.deleteStrategy(BigInt(strategyId)); } + public async deleteGradientStrategy( + strategyId: string + ): Promise { + logger.debug('deleteGradientStrategy called', arguments); + return this._api.composer.deleteGradientStrategy(BigInt(strategyId)); + } + /** * Returns liquidity for a given rate. * diff --git a/src/strategy-management/utils.ts b/src/strategy-management/utils.ts index 3c49a5e..04ea4c3 100644 --- a/src/strategy-management/utils.ts +++ b/src/strategy-management/utils.ts @@ -9,17 +9,24 @@ import { DecodedOrder, DecodedStrategy, EncodedStrategy, + GradientDecodedOrder, + GradientDecodedStrategy, + GradientEncodedStrategy, + GradientStrategy, + GradientType, Strategy, } from '../common/types'; import { Logger } from '../common/logger'; import { calculateRequiredLiquidity, - decodeOrder, - encodeOrders, lowestPossibleRate, } from '../utils/encoders'; import { Decimals } from '../utils/decimals'; -import { encodedStrategyBigIntToStr } from '../utils'; +import { + encodedGradientStrategyBigIntToStr, + encodedStrategyBigIntToStr, +} from '../utils'; +import { getMultiFactor, getRateAtExpiry } from '../utils/gradients'; const logger = new Logger('utils.ts'); @@ -46,30 +53,8 @@ export function normalizeInvertedRate( .toFixed(); } -export const encodeStrategy = ( - strategy: DecodedStrategy -): Omit => { - const [order0, order1] = encodeOrders([strategy.order0, strategy.order1]); - return { - token0: strategy.token0, - token1: strategy.token1, - order0, - order1, - }; -}; - -export const decodeStrategy = ( - strategy: EncodedStrategy -): DecodedStrategy & { id: bigint; encoded: EncodedStrategy } => { - return { - id: strategy.id, - token0: strategy.token0, - token1: strategy.token1, - order0: decodeOrder(strategy.order0), - order1: decodeOrder(strategy.order1), - encoded: strategy, - }; -}; +export { encodeStrategy, decodeStrategy } from '../utils/encoders'; +export { encodeGradientStrategy, decodeGradientStrategy } from '../utils/encoders'; /** * Converts a DecodedStrategy object to a Strategy object. @@ -341,6 +326,255 @@ export function createOrders( return { order0, order1 }; } +function validateGradientOrderInputs( + startPrice: string, + endPrice: string, + budget: string, + startTime: number, + endTime: number +) { + if (new Decimal(startPrice).isNegative() || new Decimal(endPrice).isNegative()) { + throw new Error('prices cannot be negative'); + } + if (new Decimal(budget).isNegative()) { + throw new Error('budgets cannot be negative'); + } + if (endTime <= startTime) { + throw new Error('end time must be greater than start time'); + } +} + +export function createFromGradientBuyOrder( + baseTokenDecimals: number, + quoteTokenDecimals: number, + buyPriceStart: string, + buyPriceEnd: string, + buyBudget: string, + buyGradientType: GradientType, + buyStartTime: number, + buyEndTime: number +): GradientDecodedOrder { + validateGradientOrderInputs( + buyPriceStart, + buyPriceEnd, + buyBudget, + buyStartTime, + buyEndTime + ); + + const liquidity = parseUnits(buyBudget, quoteTokenDecimals); + const initialPrice = normalizeRate( + buyPriceStart, + quoteTokenDecimals, + baseTokenDecimals + ); + const endPrice = normalizeRate( + buyPriceEnd, + quoteTokenDecimals, + baseTokenDecimals + ); + const multiFactor = getMultiFactor( + buyGradientType, + new Decimal(initialPrice), + new Decimal(endPrice), + new Decimal(buyStartTime), + new Decimal(buyEndTime) + ); + + return { + liquidity: liquidity.toString(), + initialPrice, + tradingStartTime: buyStartTime, + expiry: buyEndTime, + multiFactor: multiFactor.toString(), + gradientType: buyGradientType, + }; +} + +export function createFromGradientSellOrder( + baseTokenDecimals: number, + quoteTokenDecimals: number, + sellPriceStart: string, + sellPriceEnd: string, + sellBudget: string, + sellGradientType: GradientType, + sellStartTime: number, + sellEndTime: number +): GradientDecodedOrder { + validateGradientOrderInputs( + sellPriceStart, + sellPriceEnd, + sellBudget, + sellStartTime, + sellEndTime + ); + + const liquidity = parseUnits(sellBudget, baseTokenDecimals); + const initialPrice = normalizeInvertedRate( + sellPriceStart, + quoteTokenDecimals, + baseTokenDecimals + ); + const endPrice = normalizeInvertedRate( + sellPriceEnd, + quoteTokenDecimals, + baseTokenDecimals + ); + const multiFactor = getMultiFactor( + sellGradientType, + new Decimal(initialPrice), + new Decimal(endPrice), + new Decimal(sellStartTime), + new Decimal(sellEndTime) + ); + + return { + liquidity: liquidity.toString(), + initialPrice, + tradingStartTime: sellStartTime, + expiry: sellEndTime, + multiFactor: multiFactor.toString(), + gradientType: sellGradientType, + }; +} + +export function createGradientOrders( + baseTokenDecimals: number, + quoteTokenDecimals: number, + buyPriceStart: string, + buyPriceEnd: string, + buyBudget: string, + buyGradientType: GradientType, + buyStartTime: number, + buyEndTime: number, + sellPriceStart: string, + sellPriceEnd: string, + sellBudget: string, + sellGradientType: GradientType, + sellStartTime: number, + sellEndTime: number +): { order0: GradientDecodedOrder; order1: GradientDecodedOrder } { + const order0 = createFromGradientSellOrder( + baseTokenDecimals, + quoteTokenDecimals, + sellPriceStart, + sellPriceEnd, + sellBudget, + sellGradientType, + sellStartTime, + sellEndTime + ); + const order1 = createFromGradientBuyOrder( + baseTokenDecimals, + quoteTokenDecimals, + buyPriceStart, + buyPriceEnd, + buyBudget, + buyGradientType, + buyStartTime, + buyEndTime + ); + return { order0, order1 }; +} + +export function buildGradientStrategyObject( + baseToken: string, + quoteToken: string, + baseDecimals: number, + quoteDecimals: number, + buyPriceStart: string, + buyPriceEnd: string, + buyBudget: string, + buyGradientType: GradientType, + buyStartTime: number, + buyEndTime: number, + sellPriceStart: string, + sellPriceEnd: string, + sellBudget: string, + sellGradientType: GradientType, + sellStartTime: number, + sellEndTime: number +): GradientDecodedStrategy { + const { order0, order1 } = createGradientOrders( + baseDecimals, + quoteDecimals, + buyPriceStart, + buyPriceEnd, + buyBudget, + buyGradientType, + buyStartTime, + buyEndTime, + sellPriceStart, + sellPriceEnd, + sellBudget, + sellGradientType, + sellStartTime, + sellEndTime + ); + + return { + token0: baseToken, + token1: quoteToken, + order0, + order1, + }; +} + +export async function parseGradientStrategy( + strategy: GradientDecodedStrategy & { + id: bigint; + encoded: GradientEncodedStrategy; + }, + decimals: Decimals +): Promise { + const { id, token0, token1, order0, order1, encoded } = strategy; + const decimals0 = await decimals.fetchDecimals(token0); + const decimals1 = await decimals.fetchDecimals(token1); + + const buyEndRate = getRateAtExpiry( + order1.gradientType, + new Decimal(order1.initialPrice), + new Decimal(order1.multiFactor), + new Decimal(order1.tradingStartTime), + new Decimal(order1.expiry) + ); + const sellEndRate = getRateAtExpiry( + order0.gradientType, + new Decimal(order0.initialPrice), + new Decimal(order0.multiFactor), + new Decimal(order0.tradingStartTime), + new Decimal(order0.expiry) + ); + + return { + type: 'gradient', + id: id.toString(), + baseToken: token0, + quoteToken: token1, + buyPriceStart: normalizeRate(order1.initialPrice, decimals0, decimals1), + buyPriceEnd: normalizeRate(buyEndRate.toString(), decimals0, decimals1), + buyBudget: formatUnits(order1.liquidity, decimals1), + buyGradientType: order1.gradientType, + buyStartTime: order1.tradingStartTime, + buyEndTime: order1.expiry, + sellPriceStart: normalizeInvertedRate( + order0.initialPrice, + decimals1, + decimals0 + ), + sellPriceEnd: normalizeInvertedRate( + sellEndRate.toString(), + decimals1, + decimals0 + ), + sellBudget: formatUnits(order0.liquidity, decimals0), + sellGradientType: order0.gradientType, + sellStartTime: order0.tradingStartTime, + sellEndTime: order0.expiry, + encoded: encodedGradientStrategyBigIntToStr(encoded), + }; +} + export const PPM_RESOLUTION = 1_000_000; export function addFee(amount: BigIntish, tradingFeePPM: number): Decimal { diff --git a/src/trade-matcher/match.ts b/src/trade-matcher/match.ts index 04b5876..6eb9abb 100644 --- a/src/trade-matcher/match.ts +++ b/src/trade-matcher/match.ts @@ -8,7 +8,7 @@ import { Quote, Rate, } from '../common/types'; -import { decodeFloat } from '../utils/encoders'; +import { decodeFloatInitialRate } from '../utils/encoders'; import { BigNumberMin } from '../utils/numerics'; import { getEncodedTradeTargetAmount as tradeTargetAmount, @@ -43,7 +43,12 @@ const rateByTargetAmount = ( }; const getParams = (order: EncodedOrder) => { - const [y, z, A, B] = [order.y, order.z, decodeFloat(order.A), decodeFloat(order.B)]; + const [y, z, A, B] = [ + order.y, + order.z, + decodeFloatInitialRate(order.A), + decodeFloatInitialRate(order.B), + ]; return [y, z, A, B]; }; @@ -54,9 +59,7 @@ const getLimit = (order: EncodedOrder): bigint => { const equalTargetAmount = (order: EncodedOrder, limit: bigint): bigint => { const [y, z, A, B] = getParams(order); - return A > 0n - ? (y * A + z * (B - limit)) / A - : y; + return A > 0n ? (y * A + z * (B - limit)) / A : y; }; const equalSourceAmount = (order: EncodedOrder, limit: bigint): bigint => { @@ -107,8 +110,8 @@ const matchFast = ( for (const quote of quotes) { const input: bigint = BigNumberMin(quote.rate.input, remainingAmount); const output: bigint = trade(input, ordersMap[quote.id.toString()]).output; - if (filter({input, output})) { - actions.push({id: quote.id, input, output}); + if (filter({ input, output })) { + actions.push({ id: quote.id, input, output }); remainingAmount = remainingAmount - input; if (remainingAmount === 0n) { break; @@ -170,10 +173,7 @@ const matchBest = ( rates = orders .slice(0, n) .map((order) => trade(equalize(order, limit), order)); - total = rates.reduce( - (sum, rate) => sum + rate.input, - 0n - ); + total = rates.reduce((sum, rate) => sum + rate.input, 0n); delta = total - amount; if (delta > 0n) { lo = limit; diff --git a/src/trade-matcher/trade.ts b/src/trade-matcher/trade.ts index f65e9d4..786833c 100644 --- a/src/trade-matcher/trade.ts +++ b/src/trade-matcher/trade.ts @@ -1,27 +1,19 @@ -import { ONE, Decimal, BigNumberMax } from '../utils/numerics'; +import { + MAX_UINT128, + MAX_UINT256, + uint128, + add, + sub, + mul, + mulDivF, + mulDivC, + minFactor, +} from './utils'; +import { ONE_48, Decimal, BigNumberMax } from '../utils/numerics'; import { EncodedOrder, DecodedOrder } from '../common/types'; -import { decodeFloat } from '../utils/encoders'; +import { decodeFloatInitialRate } from '../utils/encoders'; -const C = ONE; - -const MAX_UINT128 = (2n ** 128n) - 1n; -const MAX_UINT256 = (2n ** 256n) - 1n; - -function check(val: bigint, max: bigint): bigint { - if (val >= 0n && val <= max) { - return val; - } - throw null; -} - -const uint128 = (n: bigint): bigint => check(n, MAX_UINT128); -const add = (a: bigint, b: bigint): bigint => check(a + b, MAX_UINT256); -const sub = (a: bigint, b: bigint): bigint => check(a - b, MAX_UINT256); -const mul = (a: bigint, b: bigint): bigint => check(a * b, MAX_UINT256); -const mulDivF = (a: bigint, b: bigint, c: bigint): bigint => - check((a * b) / c, MAX_UINT256); -const mulDivC = (a: bigint, b: bigint, c: bigint): bigint => - check((a * b + c - 1n) / c, MAX_UINT256); +const C = ONE_48; // // x * (A * y + B * z) ^ 2 @@ -43,8 +35,8 @@ const getEncodedTradeBySourceAmount = ( const temp2 = add(mul(y, A), mul(z, B)); const temp3 = mul(temp2, x); - const factor1 = mulDivC(temp1, temp1, MAX_UINT256); - const factor2 = mulDivC(temp3, A, MAX_UINT256); + const factor1 = minFactor(temp1, temp1); + const factor2 = minFactor(temp3, A); const factor = BigNumberMax(factor1, factor2); const temp4 = mulDivC(temp1, temp1, factor); @@ -78,8 +70,8 @@ const getEncodedTradeByTargetAmount = ( const temp2 = add(mul(y, A), mul(z, B)); const temp3 = sub(temp2, mul(x, A)); - const factor1 = mulDivC(temp1, temp1, MAX_UINT256); - const factor2 = mulDivC(temp2, temp3, MAX_UINT256); + const factor1 = minFactor(temp1, temp1); + const factor2 = minFactor(temp2, temp3); const factor = BigNumberMax(factor1, factor2); const temp4 = mulDivC(temp1, temp1, factor); @@ -126,8 +118,8 @@ export const getEncodedTradeTargetAmount = ( const x = amount; const y = order.y; const z = order.z; - const A = decodeFloat(order.A); - const B = decodeFloat(order.B); + const A = decodeFloatInitialRate(order.A); + const B = decodeFloatInitialRate(order.B); try { return uint128(getEncodedTradeBySourceAmount(x, y, z, A, B)); } catch { @@ -142,8 +134,8 @@ export const getEncodedTradeSourceAmount = ( const x = amount; const y = order.y; const z = order.z; - const A = decodeFloat(order.A); - const B = decodeFloat(order.B); + const A = decodeFloatInitialRate(order.A); + const B = decodeFloatInitialRate(order.B); try { return uint128(getEncodedTradeByTargetAmount(x, y, z, A, B)); } catch { diff --git a/src/trade-matcher/trade_gradient.ts b/src/trade-matcher/trade_gradient.ts new file mode 100644 index 0000000..3d6c5e4 --- /dev/null +++ b/src/trade-matcher/trade_gradient.ts @@ -0,0 +1,330 @@ +import { + MAX_UINT128, + uint128, + add, + mul, + mulDivF, + mulDivC, + minFactor, +} from './utils'; +import { ONE_48, ONE_24 } from '../utils/numerics'; + +const EXP_ONE = BigInt('0x0080000000000000000000000000000000'); // 1 +const EXP_MID = BigInt('0x0400000000000000000000000000000000'); // 8 +const EXP_MAX = BigInt('0x2cb53f09f05cc627c85ddebfccfeb72758'); // ceil(ln2) * 129 +const EXP_LN2 = BigInt('0x0058b90bfbe8e7bcd5e4f1d9cc01f97b58'); // ceil(ln2) + +const R_ONE = BigInt(ONE_48); // = 2 ^ 48 +const M_ONE = BigInt(ONE_24); // = 2 ^ 24 + +const RR = R_ONE * R_ONE; // = 2 ^ 96 +const MM = M_ONE * M_ONE; // = 2 ^ 48 + +const RR_MUL_MM = RR * MM; // = 2 ^ 144 +const RR_DIV_MM = RR / MM; // = 2 ^ 48 + +const EXP_ONE_MUL_RR = EXP_ONE * RR; // = 2 ^ 223 +const EXP_ONE_DIV_RR = EXP_ONE / RR; // = 2 ^ 31 +const EXP_ONE_DIV_MM = EXP_ONE / MM; // = 2 ^ 79 + +enum GradientType { + LINEAR_INCREASE, + LINEAR_DECREASE, + LINEAR_INV_INCREASE, + LINEAR_INV_DECREASE, + EXPONENTIAL_INCREASE, + EXPONENTIAL_DECREASE, +} + +function calcTargetAmount( + gradientType: GradientType, + initialRate: bigint, + multiFactor: bigint, + timeElapsed: bigint, + sourceAmount: bigint +): bigint { + const rate = calcCurrentRate( + gradientType, + initialRate, + multiFactor, + timeElapsed + ); + return mulDivF(sourceAmount, rate[0], rate[1]); +} + +function calcSourceAmount( + gradientType: GradientType, + initialRate: bigint, + multiFactor: bigint, + timeElapsed: bigint, + targetAmount: bigint +): bigint { + const rate = calcCurrentRate( + gradientType, + initialRate, + multiFactor, + timeElapsed + ); + return mulDivC(targetAmount, rate[1], rate[0]); +} + +/** + * @dev Given the following parameters: + * r - the gradient's initial exchange rate + * m - the gradient's multiplication factor + * t - the time elapsed since strategy creation + * + * Calculate the current exchange rate for each one of the following gradients: + * +----------------+-----------+-----------------+----------------------------------------------+ + * | type | direction | formula | restriction | + * +----------------+-----------+-----------------+----------------------------------------------+ + * | linear | increase | r * (1 + m * t) | | + * | linear | decrease | r * (1 - m * t) | m * t < 1 (ensure a finite-positive rate) | + * | linear-inverse | increase | r / (1 - m * t) | m * t < 1 (ensure a finite-positive rate) | + * | linear-inverse | decrease | r / (1 + m * t) | | + * | exponential | increase | r * e ^ (m * t) | m * t < 129 * ln2 (computational limitation) | + * | exponential | decrease | r / e ^ (m * t) | m * t < 129 * ln2 (computational limitation) | + * +----------------+-----------+-----------------+----------------------------------------------+ + */ +function calcCurrentRate( + gradientType: GradientType, + initialRate: bigint, // the 48-bit-mantissa-6-bit-exponent encoding of the initial exchange rate square root + multiFactor: bigint, // the 24-bit-mantissa-5-bit-exponent encoding of the multiplication factor times 2 ^ 24 + timeElapsed: bigint /// the time elapsed since strategy creation +): [bigint, bigint] { + if (R_ONE >> (initialRate / R_ONE) === 0n) { + throw new Error('InitialRateTooHigh'); + } + + if (M_ONE >> (multiFactor / M_ONE) === 0n) { + throw new Error('MultiFactorTooHigh'); + } + + const r = initialRate % R_ONE << (initialRate / R_ONE); // = floor(sqrt(initial_rate) * 2 ^ 48) < 2 ^ 96 + const m = multiFactor % M_ONE << (multiFactor / M_ONE); // = floor(multi_factor * 2 ^ 24 * 2 ^ 24) < 2 ^ 48 + const t = timeElapsed; + + const rr = mul(r, r); // < 2 ^ 192 + const mt = mul(m, t); // < 2 ^ 80 + + if (gradientType == GradientType.LINEAR_INCREASE) { + // initial_rate * (1 + multi_factor * time_elapsed) + const temp1 = rr; /////////// < 2 ^ 192 + const temp2 = add(MM, mt); // < 2 ^ 81 + const temp3 = minFactor(temp1, temp2); + const temp4 = RR_MUL_MM; + return [mulDivF(temp1, temp2, temp3), temp4 / temp3]; // not ideal + } + + if (gradientType == GradientType.LINEAR_DECREASE) { + // initial_rate * (1 - multi_factor * time_elapsed) + const temp1 = mul(rr, sub(MM, mt)); // < 2 ^ 240 + const temp2 = RR_MUL_MM; + return [temp1, temp2]; + } + + if (gradientType == GradientType.LINEAR_INV_INCREASE) { + // initial_rate / (1 - multi_factor * time_elapsed) + const temp1 = rr; + const temp2 = sub(RR, mul(mt, RR_DIV_MM)); // < 2 ^ 128 (inner expression) + return [temp1, temp2]; + } + + if (gradientType == GradientType.LINEAR_INV_DECREASE) { + // initial_rate / (1 + multi_factor * time_elapsed) + const temp1 = rr; + const temp2 = add(RR, mul(mt, RR_DIV_MM)); // < 2 ^ 129 + return [temp1, temp2]; + } + + if (gradientType == GradientType.EXPONENTIAL_INCREASE) { + // initial_rate * e ^ (multi_factor * time_elapsed) + const temp1 = rr; //////////////////////// < 2 ^ 192 + const temp2 = exp(mul(mt, EXP_ONE_DIV_MM)); // < 2 ^ 159 (inner expression) + const temp3 = minFactor(temp1, temp2); + const temp4 = EXP_ONE_MUL_RR; + return [mulDivF(temp1, temp2, temp3), temp4 / temp3]; // not ideal + } + + if (gradientType == GradientType.EXPONENTIAL_DECREASE) { + // initial_rate / e ^ (multi_factor * time_elapsed) + const temp1 = mul(rr, EXP_ONE_DIV_RR); /////// < 2 ^ 223 + const temp2 = exp(mul(mt, EXP_ONE_DIV_MM)); // < 2 ^ 159 (inner expression) + return [temp1, temp2]; + } + + throw new Error(`Invalid gradientType ${gradientType}`); +} + +/** + * @dev Ensure a finite positive rate + */ +function sub(one: bigint, mt: bigint): bigint { + if (one <= mt) { + throw new Error('InvalidRate'); + } + return one - mt; +} + +/** + * @dev Compute e ^ (x / EXP_ONE) * EXP_ONE + * Input range: 0 <= x <= EXP_MAX - 1 + * Detailed description: + * - For x < EXP_MID, this function computes e ^ x + * - For x < EXP_MAX, this function computes e ^ mod(x, ln2) * 2 ^ div(x, ln2) + * - The latter relies on the following identity: + * e ^ x = + * e ^ x * 2 ^ k / 2 ^ k = + * e ^ x * 2 ^ k / e ^ (k * ln2) = + * e ^ x / e ^ (k * ln2) * 2 ^ k = + * e ^ (x - k * ln2) * 2 ^ k + * - Replacing k with div(x, ln2) gives the solution above + * - The value of ln2 is represented as ceil(ln2 * EXP_ONE) + */ +function exp(x: bigint): bigint { + if (x < EXP_MID) { + return _exp(x); // slightly more accurate + } + if (x < EXP_MAX) { + // e^x = e^(x mod ln2) * 2^floor(x / ln2) + return _exp(x % EXP_LN2) << (x / EXP_LN2); + } + throw new Error('ExpOverflow'); +} + +/** + * @dev Compute e ^ (x / EXP_ONE) * EXP_ONE + * Input range: 0 <= x <= EXP_ONE * 16 - 1 + * Detailed description: + * - Rewrite the input as a sum of binary exponents and a single residual r, as small as possible + * - The exponentiation of each binary exponent is given (pre-calculated) + * - The exponentiation of r is calculated via Taylor series for e^x, where x = r + * - The exponentiation of the input is calculated by multiplying the intermediate results above + * - For example: e^5.521692859 = e^(4 + 1 + 0.5 + 0.021692859) = e^4 * e^1 * e^0.5 * e^0.021692859 + */ +function _exp(x: bigint): bigint { + let res = 0n; + + let y: bigint; + let z: bigint; + + z = y = x % BigInt('0x10000000000000000000000000000000'); // get the input modulo 2^(-3) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x10e1b3be415a0000'); // add y^02 * (20! / 02!) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x05a0913f6b1e0000'); // add y^03 * (20! / 03!) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x0168244fdac78000'); // add y^04 * (20! / 04!) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x004807432bc18000'); // add y^05 * (20! / 05!) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x000c0135dca04000'); // add y^06 * (20! / 06!) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x0001b707b1cdc000'); // add y^07 * (20! / 07!) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x000036e0f639b800'); // add y^08 * (20! / 08!) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x00000618fee9f800'); // add y^09 * (20! / 09!) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x0000009c197dcc00'); // add y^10 * (20! / 10!) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x0000000e30dce400'); // add y^11 * (20! / 11!) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x000000012ebd1300'); // add y^12 * (20! / 12!) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x0000000017499f00'); // add y^13 * (20! / 13!) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x0000000001a9d480'); // add y^14 * (20! / 14!) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x00000000001c6380'); // add y^15 * (20! / 15!) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x000000000001c638'); // add y^16 * (20! / 16!) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x0000000000001ab8'); // add y^17 * (20! / 17!) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x000000000000017c'); // add y^18 * (20! / 18!) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x0000000000000014'); // add y^19 * (20! / 19!) + z = (z * y) / EXP_ONE; + res = res + z * BigInt('0x0000000000000001'); // add y^20 * (20! / 20!) + res = res / BigInt('0x21c3677c82b40000') + y + EXP_ONE; // divide by 20! and then add y^1 / 1! + y^0 / 0! + + if ((x & BigInt('0x010000000000000000000000000000000')) !== 0n) + res = + (res * BigInt('0x1c3d6a24ed82218787d624d3e5eba95f9')) / + BigInt('0x18ebef9eac820ae8682b9793ac6d1e776'); // multiply by e^2^(-3) + if ((x & BigInt('0x020000000000000000000000000000000')) !== 0n) + res = + (res * BigInt('0x18ebef9eac820ae8682b9793ac6d1e778')) / + BigInt('0x1368b2fc6f9609fe7aceb46aa619baed4'); // multiply by e^2^(-2) + if ((x & BigInt('0x040000000000000000000000000000000')) !== 0n) + res = + (res * BigInt('0x1368b2fc6f9609fe7aceb46aa619baed5')) / + BigInt('0x0bc5ab1b16779be3575bd8f0520a9f21f'); // multiply by e^2^(-1) + if ((x & BigInt('0x080000000000000000000000000000000')) !== 0n) + res = + (res * BigInt('0x0bc5ab1b16779be3575bd8f0520a9f21e')) / + BigInt('0x0454aaa8efe072e7f6ddbab84b40a55c9'); // multiply by e^2^(+0) + if ((x & BigInt('0x100000000000000000000000000000000')) !== 0n) + res = + (res * BigInt('0x0454aaa8efe072e7f6ddbab84b40a55c5')) / + BigInt('0x00960aadc109e7a3bf4578099615711ea'); // multiply by e^2^(+1) + if ((x & BigInt('0x200000000000000000000000000000000')) !== 0n) + res = + (res * BigInt('0x00960aadc109e7a3bf4578099615711d7')) / + BigInt('0x0002bf84208204f5977f9a8cf01fdce3d'); // multiply by e^2^(+2) + if ((x & BigInt('0x400000000000000000000000000000000')) !== 0n) + res = + (res * BigInt('0x0002bf84208204f5977f9a8cf01fdc307')) / + BigInt('0x0000003c6ab775dd0b95b4cbee7e65d11'); // multiply by e^2^(+3) + + return res; +} + +// TODO: get the encoded-order as input (similar to how it's done in trade.ts) +export const getEncodedTradeTargetAmount = ( + gradientType: GradientType, + initialRate: bigint, + multiFactor: bigint, + timeElapsed: bigint, + sourceAmount: bigint +): bigint => { + try { + return uint128( + calcTargetAmount( + gradientType, + initialRate, + multiFactor, + timeElapsed, + sourceAmount + ) + ); + } catch { + return BigInt(0); /* rate = zero / amount = zero */ + } +}; + +// TODO: get the encoded-order as input (similar to how it's done in trade.ts) +export const getEncodedTradeSourceAmount = ( + gradientType: GradientType, + initialRate: bigint, + multiFactor: bigint, + timeElapsed: bigint, + targetAmount: bigint +): bigint => { + try { + return uint128( + calcSourceAmount( + gradientType, + initialRate, + multiFactor, + timeElapsed, + targetAmount + ) + ); + } catch { + return MAX_UINT128; /* rate = amount / infinity = zero */ + } +}; + +export const getEncodedCurrentRate = calcCurrentRate; diff --git a/src/trade-matcher/utils.ts b/src/trade-matcher/utils.ts index 15c6f6d..ec67e4c 100644 --- a/src/trade-matcher/utils.ts +++ b/src/trade-matcher/utils.ts @@ -1,5 +1,25 @@ import { Rate } from '../common/types'; +function check(val: bigint, max: bigint) { + if (val >= 0n && val <= max) { + return val; + } + throw null; +} + +export const MAX_UINT128 = 2n ** 128n - 1n; +export const MAX_UINT256 = 2n ** 256n - 1n; + +export const uint128 = (n: bigint): bigint => check(n, MAX_UINT128); +export const add = (a: bigint, b: bigint): bigint => check(a + b, MAX_UINT256); +export const sub = (a: bigint, b: bigint): bigint => check(a - b, MAX_UINT256); +export const mul = (a: bigint, b: bigint): bigint => check(a * b, MAX_UINT256); +export const mulDivF = (a: bigint, b: bigint, c: bigint): bigint => + check((a * b) / c, MAX_UINT256); +export const mulDivC = (a: bigint, b: bigint, c: bigint): bigint => + check((a * b + c - 1n) / c, MAX_UINT256); +export const minFactor = (a: bigint, b: bigint) => mulDivC(a, b, MAX_UINT256); + export const sortByMinRate = (x: Rate, y: Rate): number => { const lhs = x.output * y.input; const rhs = y.output * x.input; diff --git a/src/utils/encoders.ts b/src/utils/encoders.ts index f98b911..8889e8b 100644 --- a/src/utils/encoders.ts +++ b/src/utils/encoders.ts @@ -1,5 +1,14 @@ -import { Decimal, BnToDec, DecToBn, ONE } from './numerics'; -import { DecodedOrder, EncodedOrder } from '../common/types'; +import { Decimal, BnToDec, DecToBn, ONE_48, ONE_24 } from './numerics'; +import { + DecodedOrder, + EncodedOrder, + DecodedStrategy, + EncodedStrategy, + GradientDecodedOrder, + GradientEncodedOrder, + GradientDecodedStrategy, + GradientEncodedStrategy, +} from '../common/types'; function bitLength(value: bigint): number { return value > 0n @@ -7,31 +16,51 @@ function bitLength(value: bigint): number { : 0; } -export const encodeRate = (value: Decimal): bigint => { - const oneDecimal = new Decimal(ONE.toString()); - const data = DecToBn(value.sqrt().mul(oneDecimal).floor()); - const length = bitLength(data / ONE); +const encodeScale = (value: Decimal, one: bigint) => { + const oneDecimal = new Decimal(one.toString()); + const data = DecToBn(value.mul(oneDecimal).floor()); + const length = bitLength(data / one); return (data >> BigInt(length)) << BigInt(length); }; -export const decodeRate = (value: Decimal): Decimal => { - const oneDecimal = new Decimal(ONE.toString()); - return value.div(oneDecimal).pow(2); +const decodeScale = (value: Decimal, one: bigint) => { + const oneDecimal = new Decimal(one.toString()); + return value.div(oneDecimal); }; -// The smallest rate that, once encoded, will not be zero. -export const lowestPossibleRate = decodeRate(new Decimal(1)); - -export const encodeFloat = (value: bigint): bigint => { - const exponent = bitLength(value / ONE); +const encodeFloat = (value: bigint, one: bigint) => { + const exponent = bitLength(value / one); const mantissa = value >> BigInt(exponent); - return (ONE * BigInt(exponent)) | mantissa; + return (one * BigInt(exponent)) | mantissa; }; -export const decodeFloat = (value: bigint): bigint => { - return value % ONE << BigInt(Number(value / ONE)); +const decodeFloat = (value: bigint, one: bigint) => { + return value % one << BigInt(Number(value / one)); }; +export const encodeScaleInitialRate = (value: Decimal) => + encodeScale(value.sqrt(), ONE_48); +export const decodeScaleInitialRate = (value: Decimal) => + decodeScale(value, ONE_48).pow(2); + +export const encodeScaleMultiFactor = (value: Decimal) => + encodeScale(value.mul(new Decimal(ONE_24.toString())), ONE_24); +export const decodeScaleMultiFactor = (value: Decimal) => + decodeScale(value, ONE_24).div(new Decimal(ONE_24.toString())); + +export const encodeFloatInitialRate = (value: bigint) => + encodeFloat(value, ONE_48); +export const decodeFloatInitialRate = (value: bigint) => + decodeFloat(value, ONE_48); + +export const encodeFloatMultiFactor = (value: bigint) => + encodeFloat(value, ONE_24); +export const decodeFloatMultiFactor = (value: bigint) => + decodeFloat(value, ONE_24); + +// The smallest rate that, once encoded, will not be zero. +export const lowestPossibleRate = decodeScaleInitialRate(new Decimal(1)); + export const encodeOrders = ([order0, order1]: [DecodedOrder, DecodedOrder]): [ EncodedOrder, EncodedOrder @@ -87,8 +116,8 @@ export const isOrderEncodable = (order: DecodedOrder): boolean => { export const areScaledRatesEqual = (x: string, y: string): boolean => { const xDec = new Decimal(x); const yDec = new Decimal(y); - const xScaled = encodeRate(xDec); - const yScaled = encodeRate(yDec); + const xScaled = encodeScaleInitialRate(xDec); + const yScaled = encodeScaleInitialRate(yDec); return xScaled === yScaled; }; @@ -99,9 +128,9 @@ export const encodeOrder = (order: DecodedOrder, z?: bigint): EncodedOrder => { const marginalRate = new Decimal(order.marginalRate); const y = DecToBn(liquidity); - const L = encodeRate(lowestRate); - const H = encodeRate(highestRate); - const M = encodeRate(marginalRate); + const L = encodeScaleInitialRate(lowestRate); + const H = encodeScaleInitialRate(highestRate); + const M = encodeScaleInitialRate(marginalRate); if (L === 0n && !(H === 0n && M === 0n)) { throw new Error( @@ -129,26 +158,112 @@ export const encodeOrder = (order: DecodedOrder, z?: bigint): EncodedOrder => { return { y, z: z !== undefined ? z : H === M || y === 0n ? y : (y * (H - L)) / (M - L), - A: encodeFloat(H - L), - B: encodeFloat(L), + A: encodeFloatInitialRate(H - L), + B: encodeFloatInitialRate(L), }; }; export const decodeOrder = (order: EncodedOrder): DecodedOrder => { const y = BnToDec(order.y); const z = BnToDec(order.z); - const A = BnToDec(decodeFloat(order.A)); - const B = BnToDec(decodeFloat(order.B)); + const A = BnToDec(decodeFloatInitialRate(order.A)); + const B = BnToDec(decodeFloatInitialRate(order.B)); return { liquidity: y.toString(), - lowestRate: decodeRate(B).toString(), - highestRate: decodeRate(B.add(A)).toString(), - marginalRate: decodeRate( + lowestRate: decodeScaleInitialRate(B).toString(), + highestRate: decodeScaleInitialRate(B.add(A)).toString(), + marginalRate: decodeScaleInitialRate( y.eq(z) ? B.add(A) : B.add(A.mul(y).div(z)) ).toString(), }; }; +export const encodeStrategy = ( + strategy: DecodedStrategy +): Omit => { + const [order0, order1] = encodeOrders([strategy.order0, strategy.order1]); + return { + token0: strategy.token0, + token1: strategy.token1, + order0, + order1, + }; +}; + +export const decodeStrategy = ( + strategy: EncodedStrategy +): DecodedStrategy & { id: bigint; encoded: EncodedStrategy } => { + return { + id: strategy.id, + token0: strategy.token0, + token1: strategy.token1, + order0: decodeOrder(strategy.order0), + order1: decodeOrder(strategy.order1), + encoded: strategy, + }; +}; + +export const encodeGradientOrder = ( + order: GradientDecodedOrder +): GradientEncodedOrder => { + return { + liquidity: BigInt(order.liquidity), + initialPrice: encodeFloatInitialRate( + encodeScaleInitialRate(new Decimal(order.initialPrice)) + ), + tradingStartTime: BigInt(order.tradingStartTime), + expiry: BigInt(order.expiry), + multiFactor: encodeFloatMultiFactor( + encodeScaleMultiFactor(new Decimal(order.multiFactor)) + ), + gradientType: BigInt(order.gradientType), + }; +}; + +export const decodeGradientOrder = ( + order: GradientEncodedOrder +): GradientDecodedOrder => { + return { + liquidity: order.liquidity.toString(), + initialPrice: decodeScaleInitialRate( + BnToDec(decodeFloatInitialRate(order.initialPrice)) + ).toString(), + tradingStartTime: Number(order.tradingStartTime), + expiry: Number(order.expiry), + multiFactor: decodeScaleMultiFactor( + BnToDec(decodeFloatMultiFactor(order.multiFactor)) + ).toString(), + gradientType: Number(order.gradientType), + }; +}; + +export const encodeGradientStrategy = ( + strategy: GradientDecodedStrategy +): Omit => { + return { + token0: strategy.token0, + token1: strategy.token1, + order0: encodeGradientOrder(strategy.order0), + order1: encodeGradientOrder(strategy.order1), + }; +}; + +export const decodeGradientStrategy = ( + strategy: GradientEncodedStrategy +): GradientDecodedStrategy & { + id: bigint; + encoded: GradientEncodedStrategy; +} => { + return { + id: strategy.id, + token0: strategy.token0, + token1: strategy.token1, + order0: decodeGradientOrder(strategy.order0), + order1: decodeGradientOrder(strategy.order1), + encoded: strategy, + }; +}; + /** * Use the capacity of the other order along with the prices of this order, * in order to calculate the capacity that this order needs to have in order for its @@ -160,9 +275,11 @@ export const calculateRequiredLiquidity = ( vagueOrder: DecodedOrder ): string => { const z: bigint = calculateCorrelatedZ(knownOrder); - const L: bigint = encodeRate(new Decimal(vagueOrder.lowestRate)); - const H: bigint = encodeRate(new Decimal(vagueOrder.highestRate)); - const M: bigint = encodeRate(new Decimal(vagueOrder.marginalRate)); + const L: bigint = encodeScaleInitialRate(new Decimal(vagueOrder.lowestRate)); + const H: bigint = encodeScaleInitialRate(new Decimal(vagueOrder.highestRate)); + const M: bigint = encodeScaleInitialRate( + new Decimal(vagueOrder.marginalRate) + ); return ((z * (M - L)) / (H - L)).toString(); }; diff --git a/src/utils/gradients.ts b/src/utils/gradients.ts new file mode 100644 index 0000000..d4a7eee --- /dev/null +++ b/src/utils/gradients.ts @@ -0,0 +1,100 @@ +import { Decimal } from './numerics'; + +const ONE = new Decimal(1); +const ZERO = new Decimal(0); + +export const STRATEGY_TYPE_SHIFT = 248n; +export const GRADIENT_STRATEGY_TYPE_MASK = 1n << 255n; +export const STRATEGY_TYPE_VALUE_MASK = (1n << STRATEGY_TYPE_SHIFT) - 1n; + +export function isGradientStrategyId(id: bigint) { + return (id & GRADIENT_STRATEGY_TYPE_MASK) !== 0n; +} + +export function stripStrategyTypeBits(id: bigint) { + return id & STRATEGY_TYPE_VALUE_MASK; +} + +export function getMultiFactor( + gradientType: number, + bgnRate: Decimal, + endRate: Decimal, + bgnTime: Decimal, + endTime: Decimal +) { + if (endTime.lte(bgnTime)) { + throw new Error('expiry must be greater than tradingStartTime'); + } + + switch (gradientType) { + case 0: + return endRate.div(bgnRate).sub(ONE).div(endTime.sub(bgnTime)); + case 1: + return ONE.sub(endRate.div(bgnRate)).div(endTime.sub(bgnTime)); + case 2: + return ONE.sub(bgnRate.div(endRate)).div(endTime.sub(bgnTime)); + case 3: + return bgnRate.div(endRate).sub(ONE).div(endTime.sub(bgnTime)); + case 4: + return endRate.div(bgnRate).ln().div(endTime.sub(bgnTime)); + case 5: + return bgnRate.div(endRate).ln().div(endTime.sub(bgnTime)); + } + throw new Error(`Invalid gradientType ${gradientType}`); +} + +export function getRateAtTime( + gradientType: number, + initialRate: Decimal, + multiFactor: Decimal, + tradingStartTime: Decimal, + currentTime: Decimal +) { + const timeElapsed = Decimal.max(currentTime.sub(tradingStartTime), ZERO); + const factor = multiFactor.mul(timeElapsed); + + let rate: Decimal; + switch (gradientType) { + case 0: + rate = initialRate.mul(ONE.add(factor)); + break; + case 1: + rate = initialRate.mul(ONE.sub(factor)); + break; + case 2: + rate = initialRate.div(ONE.sub(factor)); + break; + case 3: + rate = initialRate.div(ONE.add(factor)); + break; + case 4: + rate = initialRate.mul(factor.exp()); + break; + case 5: + rate = initialRate.div(factor.exp()); + break; + default: + throw new Error(`Invalid gradientType ${gradientType}`); + } + + if (!rate.isFinite() || rate.lte(ZERO)) { + return ZERO; + } + return rate; +} + +export function getRateAtExpiry( + gradientType: number, + initialRate: Decimal, + multiFactor: Decimal, + tradingStartTime: Decimal, + expiry: Decimal +) { + return getRateAtTime( + gradientType, + initialRate, + multiFactor, + tradingStartTime, + expiry + ); +} diff --git a/src/utils/index.ts b/src/utils/index.ts index fd1e417..ba3603c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,5 @@ export * from './decimals'; export * from './encoders'; +export * from './gradients'; export * from './serializers'; export * from './numerics'; diff --git a/src/utils/numerics.ts b/src/utils/numerics.ts index e868620..30a5e74 100644 --- a/src/utils/numerics.ts +++ b/src/utils/numerics.ts @@ -26,14 +26,13 @@ export const BigNumberMax = (a: BigIntish, b: BigIntish): bigint => { return aBN > bBN ? aBN : bBN; }; -export const ONE = 2n ** 48n; +export const ONE_48 = 2n ** 48n; +export const ONE_24 = 2n ** 24n; export const TEN = new Decimal(10); export const MAX_UINT256 = 2n ** 256n - 1n; -export const tenPow = (dec0: number, dec1: number) => { - const diff = dec0 - dec1; - return TEN.pow(diff); -}; +export const tenPow = (dec0: number, dec1: number) => + new Decimal(10).pow(dec0 - dec1); export const BnToDec = (x: bigint): Decimal => new Decimal(x.toString()); export const DecToBn = (x: Decimal): bigint => BigInt(x.toFixed()); diff --git a/src/utils/serializers.ts b/src/utils/serializers.ts index abd26dc..5cc3814 100644 --- a/src/utils/serializers.ts +++ b/src/utils/serializers.ts @@ -3,6 +3,10 @@ import { EncodedOrderBNStr, EncodedStrategy, EncodedStrategyBNStr, + GradientEncodedOrder, + GradientEncodedOrderBNStr, + GradientEncodedStrategy, + GradientEncodedStrategyBNStr, MatchAction, MatchActionBNStr, OrdersMap, @@ -45,6 +49,19 @@ export const encodedOrderStrToBN = (order: EncodedOrderBNStr): EncodedOrder => { }; }; +export const encodedGradientOrderStrToBN = ( + order: GradientEncodedOrderBNStr +): GradientEncodedOrder => { + return { + liquidity: BigInt(order.liquidity), + initialPrice: BigInt(order.initialPrice), + tradingStartTime: BigInt(order.tradingStartTime), + expiry: BigInt(order.expiry), + multiFactor: BigInt(order.multiFactor), + gradientType: BigInt(order.gradientType), + }; +}; + export const encodedStrategyBigIntToStr = ( strategy: EncodedStrategy ): EncodedStrategyBNStr => { @@ -63,6 +80,24 @@ export const encodedStrategyStrToBN = ( }; }; +export const encodedGradientStrategyBigIntToStr = ( + strategy: GradientEncodedStrategy +): GradientEncodedStrategyBNStr => { + return replaceBigIntsWithStrings(strategy); +}; + +export const encodedGradientStrategyStrToBN = ( + strategy: GradientEncodedStrategyBNStr +): GradientEncodedStrategy => { + return { + id: BigInt(strategy.id), + token0: strategy.token0, + token1: strategy.token1, + order0: encodedGradientOrderStrToBN(strategy.order0), + order1: encodedGradientOrderStrToBN(strategy.order1), + }; +}; + export const ordersMapBNToStr = (ordersMap: OrdersMap): OrdersMapBNStr => { return replaceBigIntsWithStrings(ordersMap); }; diff --git a/tests/ChainCache.spec.ts b/tests/ChainCache.spec.ts index 4915a03..8751981 100644 --- a/tests/ChainCache.spec.ts +++ b/tests/ChainCache.spec.ts @@ -3,6 +3,7 @@ import { ChainCache } from '../src/chain-cache/ChainCache'; import { EncodedOrder, EncodedStrategy, + GradientEncodedStrategy, TokenPair, TradingFeeUpdate, } from '../src/common/types'; @@ -36,6 +37,28 @@ const encodedStrategy2: EncodedStrategy = { order1: encodedOrder1, }; +const gradientEncodedStrategy1: GradientEncodedStrategy = { + id: 2n ** 255n, + token0: 'abc', + token1: 'xyz', + order0: { + liquidity: 10n, + initialPrice: 20n, + tradingStartTime: 30n, + expiry: 40n, + multiFactor: 50n, + gradientType: 1n, + }, + order1: { + liquidity: 60n, + initialPrice: 70n, + tradingStartTime: 80n, + expiry: 90n, + multiFactor: 100n, + gradientType: 4n, + }, +}; + describe('ChainCache', () => { describe('serialize and deserialize', () => { let cache: ChainCache; @@ -43,7 +66,9 @@ describe('ChainCache', () => { let deserialized: ChainCache; beforeEach(() => { cache = new ChainCache(); - cache.addPair('abc', 'xyz', [encodedStrategy1, encodedStrategy2]); + cache.addPair('abc', 'xyz', [encodedStrategy1, encodedStrategy2], [ + gradientEncodedStrategy1, + ]); cache.addPair('foo', 'bar', []); cache.applyEvents( [ @@ -77,6 +102,11 @@ describe('ChainCache', () => { it('strategy by id should match', async () => { expect(deserialized.getStrategyById('1')).to.deep.equal(encodedStrategy1); }); + it('gradient strategy by id should match', async () => { + expect( + deserialized.getGradientStrategyById(gradientEncodedStrategy1.id) + ).to.deep.equal(gradientEncodedStrategy1); + }); it('last block number should match', async () => { expect(deserialized.getLatestBlockNumber()).to.equal(7); }); @@ -192,6 +222,49 @@ describe('ChainCache', () => { cache.applyEvents([], 5); expect(affectedPairs).to.deep.equal([['abc', 'xyz']]); }); + it('should process gradient strategy events', async () => { + const cache = new ChainCache(); + cache.addPair('abc', 'xyz', [], [gradientEncodedStrategy1]); + + const updatedGradientStrategy = { + ...gradientEncodedStrategy1, + order0: { + ...gradientEncodedStrategy1.order0, + liquidity: 11n, + }, + }; + + cache.applyEvents( + [ + { + type: 'GradientStrategyUpdated', + blockNumber: 1, + logIndex: 0, + data: updatedGradientStrategy, + }, + ], + 1 + ); + + let strategies = await cache.getGradientStrategiesByPair('abc', 'xyz'); + expect(strategies).to.have.length(1); + expect(strategies?.[0]).to.deep.equal(updatedGradientStrategy); + + cache.applyEvents( + [ + { + type: 'GradientStrategyDeleted', + blockNumber: 2, + logIndex: 0, + data: updatedGradientStrategy, + }, + ], + 2 + ); + + strategies = await cache.getGradientStrategiesByPair('abc', 'xyz'); + expect(strategies).to.have.length(0); + }); it('should contain a single copy of a strategy that was updated', async () => { const cache = new ChainCache(); const encodedStrategy1_mod = { @@ -310,5 +383,13 @@ describe('ChainCache', () => { const strategies = await cache.getStrategiesByPair('abc', 'xyz'); expect(strategies).to.deep.equal([encodedStrategy1]); }); + it('getGradientStrategiesByPair calls miss handler, which adds the missing pair, allowing the call to return gradient strategies', async () => { + const cache = new ChainCache(); + cache.setCacheMissHandler(async (token0, token1) => { + cache.addPair(token0, token1, [], [gradientEncodedStrategy1]); + }); + const strategies = await cache.getGradientStrategiesByPair('abc', 'xyz'); + expect(strategies).to.deep.equal([gradientEncodedStrategy1]); + }); }); }); diff --git a/tests/ChainSync.spec.ts b/tests/ChainSync.spec.ts index 67b80f6..a34360f 100644 --- a/tests/ChainSync.spec.ts +++ b/tests/ChainSync.spec.ts @@ -2,7 +2,12 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { ChainSync } from '../src/chain-cache/ChainSync'; import { ChainCache } from '../src/chain-cache/ChainCache'; -import { EncodedStrategy, Fetcher, TokenPair } from '../src/common/types'; +import { + EncodedStrategy, + Fetcher, + GradientEncodedStrategy, + TokenPair, +} from '../src/common/types'; describe('ChainSync', () => { let chainSync: ChainSync; @@ -27,6 +32,28 @@ describe('ChainSync', () => { }, }; + const mockGradientStrategy: GradientEncodedStrategy = { + id: 2n ** 255n, + token0: '0x123', + token1: '0x456', + order0: { + liquidity: 100n, + initialPrice: 200n, + tradingStartTime: 300n, + expiry: 400n, + multiFactor: 500n, + gradientType: 1n, + }, + order1: { + liquidity: 600n, + initialPrice: 700n, + tradingStartTime: 800n, + expiry: 900n, + multiFactor: 1000n, + gradientType: 4n, + }, + }; + beforeEach(() => { chainCache = new ChainCache(); chainCache.applyEvents([], 10); @@ -57,6 +84,15 @@ describe('ChainSync', () => { ], }, ], + gradientStrategiesByPair: async (_token0: string, _token1: string) => [ + { ...mockGradientStrategy }, + ], + gradientStrategiesByPairs: async (_pairs: TokenPair[]) => [ + { + pair: ['0x123', '0x456'], + strategies: [{ ...mockGradientStrategy }], + }, + ], pairTradingFeePPM: async (_token0: string, _token1: string) => 0, tradingFeePPM: async () => 0, onTradingFeePPMUpdated: async () => {}, @@ -92,6 +128,31 @@ describe('ChainSync', () => { expect(strategies[2]).to.deep.equal(mockEncodedStrategy); }); + it('should process GradientStrategyCreated events correctly', async () => { + mockFetcher.getEvents = async () => [ + { + type: 'GradientStrategyCreated', + blockNumber: chainCache.getLatestBlockNumber() + 1, + logIndex: 0, + data: { ...mockGradientStrategy, id: 2n ** 255n + 1n }, + }, + ]; + + await chainSync.startDataSync(); + + await new Promise((resolve) => { + chainCache.on('onPairDataChanged', resolve); + }); + + const strategies = + (await chainCache.getGradientStrategiesByPair( + mockGradientStrategy.token0, + mockGradientStrategy.token1 + )) ?? []; + expect(strategies).to.have.length(2); + expect(strategies[1].id).to.equal(2n ** 255n + 1n); + }); + it('should process StrategyUpdated events correctly', async () => { // First let it read the two existing strategies mockFetcher.getEvents = async () => []; diff --git a/tests/encoders.spec.ts b/tests/encoders.spec.ts index a8d1dba..d637775 100644 --- a/tests/encoders.spec.ts +++ b/tests/encoders.spec.ts @@ -4,6 +4,14 @@ import { isOrderEncodable, areScaledRatesEqual, decodeOrder, + encodeScaleInitialRate, + decodeScaleInitialRate, + encodeFloatInitialRate, + decodeFloatInitialRate, + encodeScaleMultiFactor, + decodeScaleMultiFactor, + encodeFloatMultiFactor, + decodeFloatMultiFactor, calculateRequiredLiquidity, calculateCorrelatedZ, } from '../src/utils/encoders'; @@ -15,7 +23,7 @@ import { } from '../src/strategy-management/'; import { DecodedStrategy, EncodedStrategy } from '../src/common/types'; import sinon, { SinonStubbedInstance } from 'sinon'; -import { Decimal } from '../src/utils/numerics'; +import { BnToDec, Decimal } from '../src/utils/numerics'; import { Decimals } from '../src/utils/decimals'; import { isAlmostEqual } from './test-utils'; @@ -829,4 +837,108 @@ describe('encoders', () => { expect(Strategy.sellBudget).to.equal(expectedStrategy.sellBudget); }); }); + + describe('assertAccuracy', () => { + let decimalsStub: SinonStubbedInstance; + + beforeEach(() => { + decimalsStub = sinon.createStubInstance(Decimals); + }); + + afterEach(() => { + sinon.restore(); + }); + + function assertAccuracy( + paramName: string, + expectedValue: Decimal, + calcActualValue: (value: Decimal) => Decimal, + maxAbsoluteError: string, + maxRelativeError: string + ) { + it(`${paramName} = ${expectedValue}`, async () => { + const actualValue = calcActualValue(expectedValue); + if (!actualValue.eq(expectedValue)) { + expect(actualValue.lt(expectedValue)).to.be.equal( + true, + `\n- expectedValue = ${expectedValue.toFixed()}` + + `\n- actualValue = ${actualValue.toFixed()}` + ); + const absoluteError = actualValue.sub(expectedValue).abs(); + const relativeError = actualValue.div(expectedValue).sub(1).abs(); + expect( + absoluteError.lte(maxAbsoluteError) || + relativeError.lte(maxRelativeError) + ).to.be.equal( + true, + `\n- expectedValue = ${expectedValue.toFixed()}` + + `\n- actualValue = ${actualValue.toFixed()}` + + `\n- absoluteError = ${absoluteError.toFixed()}` + + `\n- relativeError = ${relativeError.toFixed()}` + ); + } + }); + } + + const calcInitialRate = (x: Decimal) => + decodeScaleInitialRate( + BnToDec( + decodeFloatInitialRate( + encodeFloatInitialRate(encodeScaleInitialRate(x)) + ) + ) + ); + const calcMultiFactor = (x: Decimal) => + decodeScaleMultiFactor( + BnToDec( + decodeFloatMultiFactor( + encodeFloatMultiFactor(encodeScaleMultiFactor(x)) + ) + ) + ); + + for (let a = 1; a <= 100; a++) { + const expectedValue = new Decimal(a).mul(1234.5678); + assertAccuracy( + 'initialRate', + expectedValue, + calcInitialRate, + '0', + '0.00000000000002' + ); + } + + for (let b = 1; b <= 100; b++) { + const expectedValue = new Decimal(b).mul(0.00001234); + assertAccuracy( + 'multiFactor', + expectedValue, + calcMultiFactor, + '0', + '0.0000002' + ); + } + + for (let a = -28; a <= 28; a++) { + const expectedValue = new Decimal(10).pow(a); + assertAccuracy( + 'initialRate', + expectedValue, + calcInitialRate, + '0.0000000000000005', + '0.00000000000002' + ); + } + + for (let b = -14; b <= -1; b++) { + const expectedValue = new Decimal(10).pow(b); + assertAccuracy( + 'multiFactor', + expectedValue, + calcMultiFactor, + '0.000000000000004', + '0.00000007' + ); + } + }); }); diff --git a/tests/gradient_strategy.spec.ts b/tests/gradient_strategy.spec.ts new file mode 100644 index 0000000..20ec48f --- /dev/null +++ b/tests/gradient_strategy.spec.ts @@ -0,0 +1,196 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { Toolkit } from '../src/strategy-management'; +import { + buildGradientStrategyObject, + decodeGradientStrategy, + encodeGradientStrategy, + parseGradientStrategy, +} from '../src/strategy-management/utils'; +import { ChainCache } from '../src/chain-cache'; +import { Decimals } from '../src/utils/decimals'; +import { + GradientEncodedStrategy, + GradientType, +} from '../src/common/types'; +import { encodedGradientStrategyBigIntToStr } from '../src/utils'; +import { isAlmostEqual } from './test-utils'; + +describe('gradient strategy support', () => { + it('encodes, decodes, and parses a gradient strategy', async () => { + const strategy = buildGradientStrategyObject( + 'base', + 'quote', + 18, + 6, + '1500', + '1200', + '200', + GradientType.LinearDecrease, + 1000, + 2000, + '2000', + '2400', + '1.5', + GradientType.LinearInverseDecrease, + 1100, + 2100 + ); + + const encodedWithoutId = encodeGradientStrategy(strategy); + const encoded: GradientEncodedStrategy = { + id: (1n << 255n) | 123n, + ...encodedWithoutId, + }; + const decoded = decodeGradientStrategy(encoded); + + expect(decoded.order1.gradientType).to.equal(GradientType.LinearDecrease); + expect(decoded.order1.tradingStartTime).to.equal(1000); + expect(decoded.order1.expiry).to.equal(2000); + expect(decoded.order0.gradientType).to.equal( + GradientType.LinearInverseDecrease + ); + expect(decoded.order0.tradingStartTime).to.equal(1100); + expect(decoded.order0.expiry).to.equal(2100); + expect( + ...isAlmostEqual( + decoded.order1.initialPrice, + strategy.order1.initialPrice, + '0', + '0.0000000002' + ) + ).to.be.true; + expect(...isAlmostEqual(decoded.order1.multiFactor, strategy.order1.multiFactor, '0', '0.0000002')).to.be.true; + + const decimals = sinon.createStubInstance(Decimals); + decimals.fetchDecimals.withArgs('base').resolves(18); + decimals.fetchDecimals.withArgs('quote').resolves(6); + + const parsed = await parseGradientStrategy(decoded, decimals); + + expect(parsed.type).to.equal('gradient'); + expect(parsed.id).to.equal(encoded.id.toString()); + expect(parsed.buyBudget).to.equal('200'); + expect(parsed.sellBudget).to.equal('1.5'); + expect(parsed.buyGradientType).to.equal(GradientType.LinearDecrease); + expect(parsed.sellGradientType).to.equal( + GradientType.LinearInverseDecrease + ); + expect(...isAlmostEqual(parsed.buyPriceStart, '1500', '0.00002', '0.0000000002')).to.be.true; + expect(...isAlmostEqual(parsed.buyPriceEnd, '1200', '0.00002', '0.0000000002')).to.be.true; + expect(...isAlmostEqual(parsed.sellPriceStart, '2000', '0.00002', '0.0000000002')).to.be.true; + expect(...isAlmostEqual(parsed.sellPriceEnd, '2400', '0.00002', '0.0000000002')).to.be.true; + }); + + it('creates a gradient strategy transaction with encoded gradient orders', async () => { + const apiMock = { + reader: { + getDecimalsByAddress: sinon.stub(), + }, + composer: { + createGradientStrategy: sinon.stub().resolves({}), + }, + }; + const cacheMock = sinon.createStubInstance(ChainCache); + cacheMock.isCacheInitialized.returns(false); + const decimalFetcher = async (address: string) => + address === 'base' ? 18 : 6; + + const toolkit = new Toolkit(apiMock as any, cacheMock, decimalFetcher); + await toolkit.createBuySellGradientStrategy( + 'base', + 'quote', + '1500', + '1200', + '200', + GradientType.LinearDecrease, + 1000, + 2000, + '2000', + '2400', + '1.5', + GradientType.LinearInverseDecrease, + 1100, + 2100 + ); + + const createArgs = apiMock.composer.createGradientStrategy.getCall(0).args; + expect(createArgs[0]).to.equal('base'); + expect(createArgs[1]).to.equal('quote'); + expect(createArgs[2].liquidity.toString()).to.equal('1500000000000000000'); + expect(createArgs[3].liquidity.toString()).to.equal('200000000'); + expect(Number(createArgs[2].gradientType)).to.equal( + GradientType.LinearInverseDecrease + ); + expect(Number(createArgs[3].gradientType)).to.equal( + GradientType.LinearDecrease + ); + }); + + it('preserves the raw pricing fields when only gradient budgets are updated', async () => { + const apiMock = { + reader: { + getDecimalsByAddress: sinon.stub(), + }, + composer: { + updateGradientStrategy: sinon.stub().resolves({}), + }, + }; + const cacheMock = sinon.createStubInstance(ChainCache); + cacheMock.isCacheInitialized.returns(false); + const decimalFetcher = async (address: string) => + address === 'base' ? 18 : 6; + + const toolkit = new Toolkit(apiMock as any, cacheMock, decimalFetcher); + const original = encodeGradientStrategy( + buildGradientStrategyObject( + 'base', + 'quote', + 18, + 6, + '1500', + '1200', + '200', + GradientType.LinearDecrease, + 1000, + 2000, + '2000', + '2400', + '1.5', + GradientType.LinearInverseDecrease, + 1100, + 2100 + ) + ); + const encoded = encodedGradientStrategyBigIntToStr({ + id: (1n << 255n) | 123n, + ...original, + }); + + await toolkit.updateGradientStrategy(encoded.id, encoded, { + buyBudget: '250', + sellBudget: '2', + }); + + const updateArgs = apiMock.composer.updateGradientStrategy.getCall(0).args; + const currentOrders = updateArgs[3]; + const newOrders = updateArgs[4]; + + expect(newOrders[1].initialPrice).to.equal(currentOrders[1].initialPrice); + expect(newOrders[1].tradingStartTime).to.equal( + currentOrders[1].tradingStartTime + ); + expect(newOrders[1].expiry).to.equal(currentOrders[1].expiry); + expect(newOrders[1].multiFactor).to.equal(currentOrders[1].multiFactor); + expect(newOrders[1].gradientType).to.equal(currentOrders[1].gradientType); + expect(newOrders[0].initialPrice).to.equal(currentOrders[0].initialPrice); + expect(newOrders[0].tradingStartTime).to.equal( + currentOrders[0].tradingStartTime + ); + expect(newOrders[0].expiry).to.equal(currentOrders[0].expiry); + expect(newOrders[0].multiFactor).to.equal(currentOrders[0].multiFactor); + expect(newOrders[0].gradientType).to.equal(currentOrders[0].gradientType); + expect(newOrders[1].liquidity.toString()).to.equal('250000000'); + expect(newOrders[0].liquidity.toString()).to.equal('2000000000000000000'); + }); +}); diff --git a/tests/match.spec.ts b/tests/match.spec.ts index 19039ec..1d16c86 100644 --- a/tests/match.spec.ts +++ b/tests/match.spec.ts @@ -20,10 +20,10 @@ import { // created via https://github.com/bancorprotocol/carbon-simulator/blob/main/benchmark/test_match.py // located at https://github.com/bancorprotocol/carbon-simulator/tree/main/benchmark/resources/match -import ArbitraryMatch from './data/ArbitraryMatch.json' assert { type: 'json' }; -import BigPoolMatch from './data/BigPoolMatch.json' assert { type: 'json' }; -import EthUsdcMatch from './data/EthUsdcMatch.json' assert { type: 'json' }; -import SpecialMatch from './data/SpecialMatch.json' assert { type: 'json' }; +import ArbitraryMatch from './data/ArbitraryMatch.json' with { type: 'json' }; +import BigPoolMatch from './data/BigPoolMatch.json' with { type: 'json' }; +import EthUsdcMatch from './data/EthUsdcMatch.json' with { type: 'json' }; +import SpecialMatch from './data/SpecialMatch.json' with { type: 'json' }; type TradeMethod = (amount: bigint, order: EncodedOrder) => bigint; type MatchMethod = 'matchBySourceAmount' | 'matchByTargetAmount'; diff --git a/tests/reader.spec.ts b/tests/reader.spec.ts index 071168e..8af6581 100644 --- a/tests/reader.spec.ts +++ b/tests/reader.spec.ts @@ -5,6 +5,7 @@ import { MulticallService, MultiCall } from '../src/contracts-api/utils'; import { Multicall } from '../src/abis/types'; import { EncodedStrategy, + GradientEncodedStrategy, TradingFeeUpdate, TokenPair, } from '../src/common/types'; @@ -52,10 +53,33 @@ describe('Reader', () => { }; const mockTradingFeeUpdate: TradingFeeUpdate = ['0x123', '0x456', 100]; + const mockGradientStrategy: GradientEncodedStrategy = { + id: 2n ** 255n, + token0: '0x123', + token1: '0x456', + order0: { + liquidity: 100n, + initialPrice: 200n, + tradingStartTime: 300n, + expiry: 400n, + multiFactor: 500n, + gradientType: 1n, + }, + order1: { + liquidity: 600n, + initialPrice: 700n, + tradingStartTime: 800n, + expiry: 900n, + multiFactor: 1000n, + gradientType: 4n, + }, + }; beforeEach(() => { // Create mock contracts with necessary methods mockContracts = { + hasGradientController: true, + hasGradientVoucher: true, carbonController: { target: '0x123', interface: { @@ -88,12 +112,40 @@ describe('Reader', () => { }, }, strategiesByPair: async () => [] as any, + pairs: async () => [ + ['0x123', '0x456'], + ['0xaaa', '0xbbb'], + ] as any, + } as any, + gradientController: { + target: '0x999', + interface: { + parseLog: (log: { topics: string[]; data: string }) => { + const eventType = log.topics[0]; + switch (eventType) { + case '0x456': + return { + name: 'StrategyCreated', + args: mockGradientStrategy, + }; + default: + return null; + } + }, + }, + strategiesByPair: async () => [] as any, + pairs: async () => [ + ['0x123', '0x456'], + ['0xccc', '0xddd'], + ] as any, } as any, provider: { getLogs: async ({ + address, fromBlock, toBlock, }: { + address: string; fromBlock: number; toBlock: number; }) => { @@ -101,52 +153,67 @@ describe('Reader', () => { const logs: any[] = []; for (let block = fromBlock; block <= toBlock; block++) { // Add multiple events per block with different log indices - logs.push( - { - blockNumber: block, - index: 0, - topics: ['0x123'], // StrategyCreated - data: '0x', - blockHash: '0x123', - transactionIndex: 0, - removed: false, - address: '0x123', - transactionHash: '0x456', - }, - { - blockNumber: block, - index: 1, - topics: ['0x789'], // TradingFeePPMUpdated - data: '0x', - blockHash: '0x123', - transactionIndex: 2, - removed: false, - address: '0x123', - transactionHash: '0x456', - }, - { - blockNumber: block, - index: 2, - topics: ['0x789'], // TradingFeePPMUpdated - data: '0x', - blockHash: '0x123', - transactionIndex: 2, - removed: false, - address: '0x123', - transactionHash: '0x456', - }, - { + if (address === '0x123') { + logs.push( + { + blockNumber: block, + index: 0, + topics: ['0x123'], // StrategyCreated + data: '0x', + blockHash: '0x123', + transactionIndex: 0, + removed: false, + address: '0x123', + transactionHash: '0x456', + }, + { + blockNumber: block, + index: 1, + topics: ['0x789'], // TradingFeePPMUpdated + data: '0x', + blockHash: '0x123', + transactionIndex: 2, + removed: false, + address: '0x123', + transactionHash: '0x456', + }, + { + blockNumber: block, + index: 2, + topics: ['0x789'], // TradingFeePPMUpdated + data: '0x', + blockHash: '0x123', + transactionIndex: 2, + removed: false, + address: '0x123', + transactionHash: '0x456', + }, + { + blockNumber: block, + index: 3, + topics: ['0xabc'], // PairTradingFeePPMUpdated + data: '0x', + blockHash: '0x123', + transactionIndex: 3, + removed: false, + address: '0x123', + transactionHash: '0x456', + } + ); + } + if (address === '0x999') { + logs.push({ blockNumber: block, - index: 3, - topics: ['0xabc'], // PairTradingFeePPMUpdated + index: 4, + topics: ['0x456'], // Gradient StrategyCreated data: '0x', blockHash: '0x123', - transactionIndex: 3, + transactionIndex: 4, removed: false, - address: '0x123', + address: '0x999', transactionHash: '0x456', - } - ); + }); + } } return logs; }, @@ -154,6 +221,12 @@ describe('Reader', () => { multicall: { tryAggregate: async () => [], } as unknown as Multicall, + voucher: { + tokensByOwner: async () => [1n, 2n], + } as any, + gradientVoucher: { + tokensByOwner: async () => [mockGradientStrategy.id], + } as any, } as unknown as Contracts; mockMulticallService = new MockMulticallService(); @@ -163,21 +236,21 @@ describe('Reader', () => { describe('getEvents', () => { it('should process events from a single chunk', async () => { const events = await reader.getEvents(1, 5, 10); - expect(events).to.have.length(20); // 5 blocks * 4 events per block + expect(events).to.have.length(25); // 5 blocks * (4 standard + 1 gradient) events per block expect(events[0].blockNumber).to.equal(1); expect(events[events.length - 1].blockNumber).to.equal(5); }); it('should process events from multiple chunks', async () => { const events = await reader.getEvents(1, 15, 5); // 3 chunks of 5 blocks each - expect(events).to.have.length(60); // 15 blocks * 4 events per block + expect(events).to.have.length(75); // 15 blocks * (4 standard + 1 gradient) events per block expect(events[0].blockNumber).to.equal(1); expect(events[events.length - 1].blockNumber).to.equal(15); }); it('should sort events by block number and log index', async () => { const events = await reader.getEvents(1, 3, 1); // 3 blocks, 1 block per chunk - expect(events).to.have.length(12); // 3 blocks * 4 events per block + expect(events).to.have.length(15); // 3 blocks * (4 standard + 1 gradient) events per block // Verify sorting for (let i = 1; i < events.length; i++) { @@ -193,7 +266,7 @@ describe('Reader', () => { it('should handle different event types correctly', async () => { const events = await reader.getEvents(1, 1, 1); - expect(events).to.have.length(4); // 1 block * 4 events per block + expect(events).to.have.length(5); // 1 block * (4 standard + 1 gradient) events per block // Verify event types and data expect(events[0].type).to.equal('StrategyCreated'); @@ -204,6 +277,9 @@ describe('Reader', () => { expect(events[3].type).to.equal('PairTradingFeePPMUpdated'); expect(events[3].data).to.deep.equal(mockTradingFeeUpdate); + + expect(events[4].type).to.equal('GradientStrategyCreated'); + expect(events[4].data).to.deep.equal(mockGradientStrategy); }); it('should handle empty block ranges', async () => { @@ -213,7 +289,8 @@ describe('Reader', () => { it('should handle invalid events gracefully', async () => { // Override the mock contracts to include invalid events - mockContracts.provider.getLogs = async (_filter) => { + mockContracts.provider.getLogs = async (filter) => { + if (filter.address === '0x999') return [] as any; return [ { blockNumber: 1, @@ -261,6 +338,53 @@ describe('Reader', () => { }); }); + describe('pairs', () => { + it('should return the union of standard and gradient pairs', async () => { + const pairs = await reader.pairs(); + expect(pairs).to.deep.equal([ + ['0x123', '0x456'], + ['0xaaa', '0xbbb'], + ['0xccc', '0xddd'], + ]); + }); + + it('should skip gradient pairs when gradient contracts are not configured', async () => { + (mockContracts as any).hasGradientController = false; + const pairs = await reader.pairs(); + expect(pairs).to.deep.equal([ + ['0x123', '0x456'], + ['0xaaa', '0xbbb'], + ]); + }); + }); + + describe('optional gradient contracts', () => { + it('should not query gradient strategies when the gradient controller is not configured', async () => { + (mockContracts as any).hasGradientController = false; + expect(await reader.gradientStrategiesByPair('0x123', '0x456')).to.deep.equal( + [] + ); + expect( + await reader.gradientStrategiesByPairs([['0x123', '0x456']]) + ).to.deep.equal([{ pair: ['0x123', '0x456'], strategies: [] }]); + expect(await reader.gradientStrategies([1n])).to.deep.equal([]); + }); + + it('should not query gradient voucher ownership when the gradient voucher is not configured', async () => { + (mockContracts as any).hasGradientVoucher = false; + const tokens = await reader.tokensByOwner('0xowner'); + expect(tokens.gradientVoucherTokens).to.deep.equal([]); + expect(tokens.voucherTokens).to.deep.equal([1n, 2n]); + }); + + it('should not query gradient logs when the gradient controller is not configured', async () => { + (mockContracts as any).hasGradientController = false; + const events = await reader.getEvents(1, 1, 1); + expect(events).to.have.length(4); + expect(events.some((event) => event.type === 'GradientStrategyCreated')).to.equal(false); + }); + }); + describe('strategiesByPair', () => { it('should return empty array when no strategies are found', async () => { // Override the mock contracts to return empty array diff --git a/tests/trade_gradient.spec.ts b/tests/trade_gradient.spec.ts new file mode 100644 index 0000000..9cf554e --- /dev/null +++ b/tests/trade_gradient.spec.ts @@ -0,0 +1,191 @@ +import { expect } from 'chai'; +import { getEncodedCurrentRate } from '../src/trade-matcher/trade_gradient'; +import { + encodeScaleInitialRate, + decodeScaleInitialRate, + encodeFloatInitialRate, + decodeFloatInitialRate, + encodeScaleMultiFactor, + decodeScaleMultiFactor, + encodeFloatMultiFactor, + decodeFloatMultiFactor, +} from '../src/utils/encoders'; +import { Decimal, BnToDec, DecToBn } from '../src/utils/numerics'; + +const ONE = new Decimal(1); +const TWO = new Decimal(2); + +const EXP_ONE = TWO.pow(127); +const EXP_MAX = EXP_ONE.mul(TWO.ln()).ceil().mul(129); + +const getInitialRate = (x: Decimal) => decodeScaleInitialRate(BnToDec(decodeFloatInitialRate(encodeFloatInitialRate(encodeScaleInitialRate(x))))); +const getMultiFactor = (x: Decimal) => decodeScaleMultiFactor(BnToDec(decodeFloatMultiFactor(encodeFloatMultiFactor(encodeScaleMultiFactor(x))))); + +function testConfiguration( + paramName: string, + preConfig: Decimal, + postConfig: (x: Decimal) => (Decimal), + maxAbsoluteError: string, + maxRelativeError: string +) { + it(`testConfiguration: ${paramName} = ${preConfig}`, async () => { + const expected = preConfig; + const actual = postConfig(preConfig); + if (!actual.eq(expected)) { + expect(actual.lt(expected)).to.be.equal( + true, + `\n- expected = ${expected.toFixed()}` + + `\n- actual = ${actual.toFixed()}` + ); + const absoluteError = actual.sub(expected).abs(); + const relativeError = actual.div(expected).sub(1).abs(); + expect(absoluteError.lte(maxAbsoluteError) || relativeError.lte(maxRelativeError)).to.be.equal( + true, + `\n- expected = ${expected.toFixed()}` + + `\n- actual = ${actual.toFixed()}` + + `\n- absoluteError = ${absoluteError.toFixed()}` + + `\n- relativeError = ${relativeError.toFixed()}` + ); + } + }); +} + +function expectedCurrentRate( + gradientType: number, + initialRate: Decimal, + multiFactor: Decimal, + timeElapsed: Decimal +) { + switch (gradientType) { + case 0: return initialRate.mul(ONE.add(multiFactor.mul(timeElapsed))); + case 1: return initialRate.mul(ONE.sub(multiFactor.mul(timeElapsed))); + case 2: return initialRate.div(ONE.sub(multiFactor.mul(timeElapsed))); + case 3: return initialRate.div(ONE.add(multiFactor.mul(timeElapsed))); + case 4: return initialRate.mul(multiFactor.mul(timeElapsed).exp()); + case 5: return initialRate.div(multiFactor.mul(timeElapsed).exp()); + } + throw new Error(`Invalid gradientType ${gradientType}`); +} + +function testCurrentRate( + gradientType: number, + initialRate: Decimal, + multiFactor: Decimal, + timeElapsed: Decimal, + maxError: string +) { + it(`testCurrentRate: gradientType,initialRate,multiFactor,timeElapsed = ${[gradientType, initialRate, multiFactor, timeElapsed]}`, async () => { + const rEncoded = encodeFloatInitialRate(encodeScaleInitialRate(initialRate)); + const mEncoded = encodeFloatMultiFactor(encodeScaleMultiFactor(multiFactor)); + const rDecoded = decodeScaleInitialRate(BnToDec(decodeFloatInitialRate(rEncoded))); + const mDecoded = decodeScaleMultiFactor(BnToDec(decodeFloatMultiFactor(mEncoded))); + const expected = expectedCurrentRate(gradientType, rDecoded, mDecoded, timeElapsed); + if (expected.isFinite() && expected.isPositive()) { + const retVal = getEncodedCurrentRate(gradientType, rEncoded, mEncoded, DecToBn(timeElapsed)); + const actual = BnToDec(retVal[0]).div(BnToDec(retVal[1])); + if (!actual.eq(expected)) { + const error = actual.div(expected).sub(1).abs(); + expect(error.lte(maxError)).to.be.equal( + true, + `\n- expected = ${expected.toFixed()}` + + `\n- actual = ${actual.toFixed()}` + + `\n- error = ${error.toFixed()}` + ); + } + } else { + expect(() => { + getEncodedCurrentRate(gradientType, rEncoded, mEncoded, DecToBn(timeElapsed)); + }).to.throw('InvalidRate'); + } + }); +} + +describe('trade_gradient', () => { + for (let a = 1; a <= 100; a++) { + const initialRate = new Decimal(a).mul(1234.5678); + testConfiguration('initialRate', initialRate, getInitialRate, '0', '0.00000000000002'); + } + + for (let b = 1; b <= 100; b++) { + const multiFactor = new Decimal(b).mul(0.00001234); + testConfiguration('multiFactor', multiFactor, getMultiFactor, '0', '0.0000002'); + } + + for (let a = -28; a <= 28; a++) { + const initialRate = new Decimal(10).pow(a); + testConfiguration('initialRate', initialRate, getInitialRate, '0.0000000000000005', '0.00000000000002'); + } + + for (let b = -14; b <= -1; b++) { + const multiFactor = new Decimal(10).pow(b); + testConfiguration('multiFactor', multiFactor, getMultiFactor, '0.000000000000004', '0.00000007'); + } + + for (let a = 1; a <= 10; a++) { + for (let b = 1; b <= 10; b++) { + for (let c = 1; c <= 10; c++) { + const initialRate = new Decimal(a).mul(1234.5678); + const multiFactor = new Decimal(b).mul(0.00001234); + const timeElapsed = new Decimal(c).mul(3600); + testCurrentRate(0, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(1, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(2, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(3, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(4, initialRate, multiFactor, timeElapsed, '0.00000000000000000000000000000000000002'); + testCurrentRate(5, initialRate, multiFactor, timeElapsed, '0.00000000000000000000000000000000000002'); + } + } + } + + for (let a = -27; a <= 27; a++) { + for (let b = -14; b <= -1; b++) { + for (let c = 1; c <= 10; c++) { + const initialRate = new Decimal(10).pow(a); + const multiFactor = new Decimal(10).pow(b); + const timeElapsed = Decimal.min( + TWO.pow(4).div(multiFactor).sub(1).ceil(), + TWO.pow(25).sub(1) + ).mul(c).div(10).ceil(); + testCurrentRate(0, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(1, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(2, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(3, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(4, initialRate, multiFactor, timeElapsed, '0.00000000000000000000000000000000000006'); + testCurrentRate(5, initialRate, multiFactor, timeElapsed, '0.00000000000000000000000000000000000006'); + } + } + } + + for (const a of [-27, -10, 0, 10, 27]) { + for (const b of [-14, -9, -6, -1]) { + for (const c of [1, 4, 7, 10]) { + const initialRate = new Decimal(10).pow(a); + const multiFactor = new Decimal(10).pow(b); + const timeElapsed = Decimal.min( + EXP_MAX.div(EXP_ONE).div(multiFactor).sub(1).ceil(), + TWO.pow(25).sub(1) + ).mul(c).div(10).ceil(); + testCurrentRate(0, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(1, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(2, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(3, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(4, initialRate, multiFactor, timeElapsed, '0.000000000004'); + testCurrentRate(5, initialRate, multiFactor, timeElapsed, '0.0000000000000000000000000000000000003'); + } + } + } + + for (const a of [-27, -10, 0, 10, 27]) { + for (const b of [-14, -9, -6, -1]) { + for (const c of [19, 24, 29]) { + const initialRate = new Decimal(10).pow(a); + const multiFactor = new Decimal(10).pow(b); + const timeElapsed = new Decimal(2).pow(c).sub(1); + testCurrentRate(0, initialRate, multiFactor, timeElapsed, '0.0000000000000000000000000000000000000000003'); + testCurrentRate(1, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(2, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(3, initialRate, multiFactor, timeElapsed, '0'); + } + } + } +});