diff --git a/.gitignore b/.gitignore index 71a12ed..17206bd 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,6 @@ ENV/ # mkdocs documentation /site +/.vs/slnx.sqlite +/.vs/ProjectSettings.json +/.vs diff --git a/uniswap/__init__.py b/uniswap/__init__.py index a1612f6..8a48cdf 100644 --- a/uniswap/__init__.py +++ b/uniswap/__init__.py @@ -1,5 +1,6 @@ from . import exceptions from .cli import main from .uniswap import Uniswap, _str_to_addr +from .uniswap4 import Uniswap4Core -__all__ = ["Uniswap", "exceptions", "_str_to_addr", "main"] +__all__ = ["Uniswap", "Uniswap4Core", "exceptions", "_str_to_addr", "main"] \ No newline at end of file diff --git a/uniswap/assets/uniswap-v4/poolmanager.abi b/uniswap/assets/uniswap-v4/poolmanager.abi new file mode 100644 index 0000000..3c09a94 --- /dev/null +++ b/uniswap/assets/uniswap-v4/poolmanager.abi @@ -0,0 +1,1289 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "controllerGasLimit", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "AlreadyUnlocked", + "type": "error" + }, + { + "inputs": [], + "name": "CurrenciesOutOfOrderOrEqual", + "type": "error" + }, + { + "inputs": [], + "name": "CurrencyNotSettled", + "type": "error" + }, + { + "inputs": [], + "name": "DelegateCallNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidCaller", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidProtocolFee", + "type": "error" + }, + { + "inputs": [], + "name": "ManagerLocked", + "type": "error" + }, + { + "inputs": [], + "name": "NonZeroNativeValue", + "type": "error" + }, + { + "inputs": [], + "name": "PoolNotInitialized", + "type": "error" + }, + { + "inputs": [], + "name": "ProtocolFeeCannotBeFetched", + "type": "error" + }, + { + "inputs": [], + "name": "SwapAmountCannotBeZero", + "type": "error" + }, + { + "inputs": [], + "name": "TickSpacingTooLarge", + "type": "error" + }, + { + "inputs": [], + "name": "TickSpacingTooSmall", + "type": "error" + }, + { + "inputs": [], + "name": "UnauthorizedDynamicLPFeeUpdate", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "PoolId", + "name": "id", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "indexed": true, + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "indexed": false, + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "indexed": false, + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "name": "Initialize", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "PoolId", + "name": "id", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "indexed": false, + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "indexed": false, + "internalType": "int256", + "name": "liquidityDelta", + "type": "int256" + } + ], + "name": "ModifyLiquidity", + "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": "OperatorSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "protocolFeeController", + "type": "address" + } + ], + "name": "ProtocolFeeControllerUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "PoolId", + "name": "id", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint24", + "name": "protocolFee", + "type": "uint24" + } + ], + "name": "ProtocolFeeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "PoolId", + "name": "id", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "int128", + "name": "amount0", + "type": "int128" + }, + { + "indexed": false, + "internalType": "int128", + "name": "amount1", + "type": "int128" + }, + { + "indexed": false, + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "int24", + "name": "tick", + "type": "int24" + }, + { + "indexed": false, + "internalType": "uint24", + "name": "fee", + "type": "uint24" + } + ], + "name": "Swap", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "MAX_TICK_SPACING", + "outputs": [ + { + "internalType": "int24", + "name": "", + "type": "int24" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MIN_TICK_SPACING", + "outputs": [ + { + "internalType": "int24", + "name": "", + "type": "int24" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "collectProtocolFees", + "outputs": [ + { + "internalType": "uint256", + "name": "amountCollected", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "key", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "name": "donate", + "outputs": [ + { + "internalType": "BalanceDelta", + "name": "delta", + "type": "int256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "slot", + "type": "bytes32" + } + ], + "name": "extsload", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "startSlot", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "nSlots", + "type": "uint256" + } + ], + "name": "extsload", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "slots", + "type": "bytes32[]" + } + ], + "name": "extsload", + "outputs": [ + { + "internalType": "bytes32[]", + "name": "", + "type": "bytes32[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "slots", + "type": "bytes32[]" + } + ], + "name": "exttload", + "outputs": [ + { + "internalType": "bytes32[]", + "name": "", + "type": "bytes32[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "slot", + "type": "bytes32" + } + ], + "name": "exttload", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "key", + "type": "tuple" + }, + { + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "name": "initialize", + "outputs": [ + { + "internalType": "int24", + "name": "tick", + "type": "int24" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isOperator", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "key", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "internalType": "int256", + "name": "liquidityDelta", + "type": "int256" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + } + ], + "internalType": "struct IPoolManager.ModifyLiquidityParams", + "name": "params", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "name": "modifyLiquidity", + "outputs": [ + { + "internalType": "BalanceDelta", + "name": "callerDelta", + "type": "int256" + }, + { + "internalType": "BalanceDelta", + "name": "feesAccrued", + "type": "int256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "protocolFeeController", + "outputs": [ + { + "internalType": "contract IProtocolFeeController", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Currency", + "name": "currency", + "type": "address" + } + ], + "name": "protocolFeesAccrued", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setOperator", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "key", + "type": "tuple" + }, + { + "internalType": "uint24", + "name": "newProtocolFee", + "type": "uint24" + } + ], + "name": "setProtocolFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IProtocolFeeController", + "name": "controller", + "type": "address" + } + ], + "name": "setProtocolFeeController", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Currency", + "name": "currency", + "type": "address" + } + ], + "name": "settle", + "outputs": [ + { + "internalType": "uint256", + "name": "paid", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "key", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bool", + "name": "zeroForOne", + "type": "bool" + }, + { + "internalType": "int256", + "name": "amountSpecified", + "type": "int256" + }, + { + "internalType": "uint160", + "name": "sqrtPriceLimitX96", + "type": "uint160" + } + ], + "internalType": "struct IPoolManager.SwapParams", + "name": "params", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "name": "swap", + "outputs": [ + { + "internalType": "BalanceDelta", + "name": "swapDelta", + "type": "int256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Currency", + "name": "currency", + "type": "address" + } + ], + "name": "sync", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Currency", + "name": "currency", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "take", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "unlock", + "outputs": [ + { + "internalType": "bytes", + "name": "result", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "key", + "type": "tuple" + }, + { + "internalType": "uint24", + "name": "newDynamicLPFee", + "type": "uint24" + } + ], + "name": "updateDynamicLPFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/uniswap/assets/uniswap-v4/quoter.abi b/uniswap/assets/uniswap-v4/quoter.abi new file mode 100644 index 0000000..e906997 --- /dev/null +++ b/uniswap/assets/uniswap-v4/quoter.abi @@ -0,0 +1,710 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_poolManager", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "InsufficientAmountOut", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidLockCaller", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidQuoteBatchParams", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidUnlockCallbackSender", + "type": "error" + }, + { + "inputs": [], + "name": "LockFailure", + "type": "error" + }, + { + "inputs": [], + "name": "NotSelf", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "revertData", + "type": "bytes" + } + ], + "name": "UnexpectedRevertBytes", + "type": "error" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "exactCurrency", + "type": "address" + }, + { + "components": [ + { + "internalType": "Currency", + "name": "intermediateCurrency", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "internalType": "struct PathKey[]", + "name": "path", + "type": "tuple[]" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint128", + "name": "exactAmount", + "type": "uint128" + } + ], + "internalType": "struct IQuoter.QuoteExactParams", + "name": "params", + "type": "tuple" + } + ], + "name": "_quoteExactInput", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "poolKey", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "zeroForOne", + "type": "bool" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint128", + "name": "exactAmount", + "type": "uint128" + }, + { + "internalType": "uint160", + "name": "sqrtPriceLimitX96", + "type": "uint160" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "internalType": "struct IQuoter.QuoteExactSingleParams", + "name": "params", + "type": "tuple" + } + ], + "name": "_quoteExactInputSingle", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "exactCurrency", + "type": "address" + }, + { + "components": [ + { + "internalType": "Currency", + "name": "intermediateCurrency", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "internalType": "struct PathKey[]", + "name": "path", + "type": "tuple[]" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint128", + "name": "exactAmount", + "type": "uint128" + } + ], + "internalType": "struct IQuoter.QuoteExactParams", + "name": "params", + "type": "tuple" + } + ], + "name": "_quoteExactOutput", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "poolKey", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "zeroForOne", + "type": "bool" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint128", + "name": "exactAmount", + "type": "uint128" + }, + { + "internalType": "uint160", + "name": "sqrtPriceLimitX96", + "type": "uint160" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "internalType": "struct IQuoter.QuoteExactSingleParams", + "name": "params", + "type": "tuple" + } + ], + "name": "_quoteExactOutputSingle", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "manager", + "outputs": [ + { + "internalType": "contract IPoolManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "exactCurrency", + "type": "address" + }, + { + "components": [ + { + "internalType": "Currency", + "name": "intermediateCurrency", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "internalType": "struct PathKey[]", + "name": "path", + "type": "tuple[]" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint128", + "name": "exactAmount", + "type": "uint128" + } + ], + "internalType": "struct IQuoter.QuoteExactParams", + "name": "params", + "type": "tuple" + } + ], + "name": "quoteExactInput", + "outputs": [ + { + "internalType": "int128[]", + "name": "deltaAmounts", + "type": "int128[]" + }, + { + "internalType": "uint160[]", + "name": "sqrtPriceX96AfterList", + "type": "uint160[]" + }, + { + "internalType": "uint32[]", + "name": "initializedTicksLoadedList", + "type": "uint32[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "poolKey", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "zeroForOne", + "type": "bool" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint128", + "name": "exactAmount", + "type": "uint128" + }, + { + "internalType": "uint160", + "name": "sqrtPriceLimitX96", + "type": "uint160" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "internalType": "struct IQuoter.QuoteExactSingleParams", + "name": "params", + "type": "tuple" + } + ], + "name": "quoteExactInputSingle", + "outputs": [ + { + "internalType": "int128[]", + "name": "deltaAmounts", + "type": "int128[]" + }, + { + "internalType": "uint160", + "name": "sqrtPriceX96After", + "type": "uint160" + }, + { + "internalType": "uint32", + "name": "initializedTicksLoaded", + "type": "uint32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "exactCurrency", + "type": "address" + }, + { + "components": [ + { + "internalType": "Currency", + "name": "intermediateCurrency", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "internalType": "struct PathKey[]", + "name": "path", + "type": "tuple[]" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint128", + "name": "exactAmount", + "type": "uint128" + } + ], + "internalType": "struct IQuoter.QuoteExactParams", + "name": "params", + "type": "tuple" + } + ], + "name": "quoteExactOutput", + "outputs": [ + { + "internalType": "int128[]", + "name": "deltaAmounts", + "type": "int128[]" + }, + { + "internalType": "uint160[]", + "name": "sqrtPriceX96AfterList", + "type": "uint160[]" + }, + { + "internalType": "uint32[]", + "name": "initializedTicksLoadedList", + "type": "uint32[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "poolKey", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "zeroForOne", + "type": "bool" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint128", + "name": "exactAmount", + "type": "uint128" + }, + { + "internalType": "uint160", + "name": "sqrtPriceLimitX96", + "type": "uint160" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "internalType": "struct IQuoter.QuoteExactSingleParams", + "name": "params", + "type": "tuple" + } + ], + "name": "quoteExactOutputSingle", + "outputs": [ + { + "internalType": "int128[]", + "name": "deltaAmounts", + "type": "int128[]" + }, + { + "internalType": "uint160", + "name": "sqrtPriceX96After", + "type": "uint160" + }, + { + "internalType": "uint32", + "name": "initializedTicksLoaded", + "type": "uint32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "unlockCallback", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/uniswap/constants.py b/uniswap/constants.py index b30e9ab..6f75799 100644 --- a/uniswap/constants.py +++ b/uniswap/constants.py @@ -12,6 +12,7 @@ ) ETH_ADDRESS = "0x0000000000000000000000000000000000000000" +NOHOOK_ADDRESS = "0x0000000000000000000000000000000000000000" WETH9_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" # see: https://chainid.network/chains/ @@ -27,8 +28,10 @@ 137: "polygon", 100: "xdai", 250: "fantom", + 17000: "holesky", 42161: "arbitrum", 421611: "arbitrum_testnet", + 11155111: "sepolia", 1666600000: "harmony_mainnet", 1666700000: "harmony_testnet", 11155111: "sepolia" @@ -74,6 +77,27 @@ "sepolia": "0xC532a74256D3Db42D0Bf7a0400fEFDbad7694008", } +# TODO: replace with actual addresses after official deployment +_poolmanager_contract_addresses = { + #"mainnet": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", + "ropsten": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", + "rinkeby": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", + "görli": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", + #"xdai": "0xA818b4F111Ccac7AA31D0BCc0806d64F2E0737D7", + #"binance": "0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73", + "binance_testnet": "0x6725F303b657a9451d8BA641348b6761A6CC7a17", +} + +# TODO: replace with actual addresses after official deployment +_quoter_contract_addresses = { + #"mainnet": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", + "ropsten": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", + "rinkeby": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", + "görli": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", + #"xdai": "0xA818b4F111Ccac7AA31D0BCc0806d64F2E0737D7", + #"binance": "0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73", + "binance_testnet": "0x6725F303b657a9451d8BA641348b6761A6CC7a17", +} MAX_UINT_128 = (2**128) - 1 # Source: https://github.com/Uniswap/v3-core/blob/v1.0.0/contracts/libraries/TickMath.sol#L8-L11 diff --git a/uniswap/types.py b/uniswap/types.py index d55ec98..e131b7a 100644 --- a/uniswap/types.py +++ b/uniswap/types.py @@ -1,5 +1,48 @@ from typing import Union +from dataclasses import dataclass from eth_typing.evm import Address, ChecksumAddress +from typing import List, Tuple AddressLike = Union[Address, ChecksumAddress] + +@dataclass +class UniswapV4_slot0: + sqrtPriceX96: int + tick: int + protocolFee: int + + def __repr__(self) -> str: + return f"Slot0 value (sqrtPriceX96: {self.sqrtPriceX96}; tick: {self.tick}; protocolFee: {self.protocolFee!r})" + +@dataclass +class UniswapV4_position_info: + liquidity: int + feeGrowthInside0LastX128: int + feeGrowthInside1LastX128: int + + def __repr__(self) -> str: + return f"Position info (liquidity: {self.liquidity}; feeGrowthInside0LastX128: {self.feeGrowthInside0LastX128}; feeGrowthInside1LastX128: {self.feeGrowthInside1LastX128!r})" + +@dataclass +class UniswapV4_tick_info: + liquidityGross : int + liquidityNet : int + feeGrowthOutside0X128 : int + feeGrowthOutside1X128 : int + + def __repr__(self) -> str: + return f"Tick info (liquidityGross: {self.liquidityGross}; liquidityNet: {self.liquidityNet}; feeGrowthOutside0X128: {self.feeGrowthOutside0X128}; feeGrowthOutside1X128: {self.feeGrowthOutside1X128!r})" + +@dataclass +class UniswapV4_path_key: + # The lower currency of the pool, sorted numerically + currency0 : str + # The higher currency of the pool, sorted numerically + currency1 : str + # The pool swap fee, capped at 1_000_000. If the first bit is 1, the pool has a dynamic fee and must be exactly equal to 0x800000 + fee : int + # Ticks that involve positions must be a multiple of tick spacing + tickSpacing : int + # The hooks of the pool + hooks : str diff --git a/uniswap/uniswap4.py b/uniswap/uniswap4.py new file mode 100644 index 0000000..d777a89 --- /dev/null +++ b/uniswap/uniswap4.py @@ -0,0 +1,746 @@ +import os +import time +import logging +import functools +import dataclasses +from typing import List, Any, Optional, Union, Tuple, Dict + +from web3 import Web3 +from web3.contract import Contract +from web3.contract.contract import ContractFunction +from web3.exceptions import BadFunctionCallOutput, ContractLogicError +from web3.types import ( + TxParams, + Wei, + Address, + ChecksumAddress, + Nonce, + HexBytes, +) +from .types import AddressLike, UniswapV4_slot0, UniswapV4_position_info, UniswapV4_tick_info, UniswapV4_path_key +from .token import ERC20Token +from .exceptions import InvalidToken, InsufficientBalance +from .util import ( + _str_to_addr, + _addr_to_str, + _validate_address, + _load_contract, + _load_contract_erc20, + is_same_address, +) +from .decorators import supports, check_approval +from .constants import ( + _netid_to_name, + _poolmanager_contract_addresses, + _quoter_contract_addresses, + ETH_ADDRESS, + NOHOOK_ADDRESS, +) + +logger = logging.getLogger(__name__) + + +class Uniswap4Core: + """ + Wrapper around Uniswap v4 contracts. + """ + + def __init__( + self, + address: Union[AddressLike, str, None], + private_key: Optional[str], + provider: Optional[str] = None, + web3: Optional[Web3] = None, + default_slippage: float = 0.01, + poolmanager_contract_addr: Optional[str] = None, + quoter_contract_addr: Optional[str] = None, + ) -> None: + """ + :param address: The public address of the ETH wallet to use. + :param private_key: The private key of the ETH wallet to use. + :param provider: Can be optionally set to a Web3 provider URI. If none set, will fall back to the PROVIDER environment variable, or web3 if set. + :param web3: Can be optionally set to a custom Web3 instance. + :param poolmanager_contract_addr: Can be optionally set to override the address of the PoolManager contract. + """ + self.address: AddressLike = _str_to_addr( + address or "0x0000000000000000000000000000000000000000" + ) + self.private_key = ( + private_key + or "0x0000000000000000000000000000000000000000000000000000000000000000" + ) + + if web3: + self.w3 = web3 + else: + # Initialize web3. Extra provider for testing. + self.provider = provider or os.environ["PROVIDER"] + self.w3 = Web3( + Web3.HTTPProvider(self.provider, request_kwargs={"timeout": 60}) + ) + + netid = int(self.w3.net.version) + if netid in _netid_to_name: + self.network = _netid_to_name[netid] + else: + raise Exception(f"Unknown netid: {netid}") + logger.info(f"Using {self.w3} ('{self.network}')") + + self.last_nonce: Nonce = self.w3.eth.get_transaction_count(self.address) + + max_approval_hex = f"0x{64 * 'f'}" + self.max_approval_int = int(max_approval_hex, 16) + max_approval_check_hex = f"0x{15 * '0'}{49 * 'f'}" + self.max_approval_check_int = int(max_approval_check_hex, 16) + + if poolmanager_contract_addr is None: + poolmanager_contract_addr = _poolmanager_contract_addresses[self.network] + self.poolmanager_contract_addr: AddressLike = _str_to_addr(poolmanager_contract_addr) + + if quoter_contract_addr is None: + quoter_contract_addr = _quoter_contract_addresses[self.network] + self.quoter_contract_addr: AddressLike = _str_to_addr(quoter_contract_addr) + + self.router = _load_contract( + self.w3, + abi_name="uniswap-v4/poolmanager", + address=self.poolmanager_contract_addr, + ) + + self.quoter = _load_contract( + self.w3, + abi_name="uniswap-v4/quoter", + address=self.quoter_contract_addr, + ) + + if hasattr(self, "poolmanager_contract"): + logger.info(f"Using pool manager contract: {self.router}") + + # ------ Contract calls ------------------------------------------------------------ + + # ------ Quoter methods -------------------------------------------------------------------- + + def get_quote_exact_input_single( + self, + currency0: AddressLike, # input token + currency1: AddressLike, # output token + qty: int, + fee: int, + tick_spacing: int, + hook_data: bytes, + sqrt_price_limit_x96: int = 0, + zero_for_one: bool = True, + hooks: Union[AddressLike, str, None] = NOHOOK_ADDRESS, + ) -> Any: + """ + :if `zero_to_one` is true: given `qty` amount of the input `token0`, returns the maximum output amount of output `token1`. + :if `zero_to_one` is false: returns the minimum amount of `token0` required to buy `qty` amount of `token1`. + """ + + if currency0 == currency1: + raise ValueError + + pool_key = { + "currency0": currency0, + "currency1": currency1, + "fee": fee, + "tickSpacing": tick_spacing, + "hooks": hooks, + } + + quote_params = { + "poolKey": pool_key, + "zeroForOne": zero_for_one, + "recipient": self.address, + "exactAmount": qty, + "sqrtPriceLimitX96": sqrt_price_limit_x96, + "hookData" : hook_data, + } + + values = self.quoter.functions.quoteExactInputSingle(quote_params) + #[0]returns deltaAmounts: Delta amounts resulted from the swap + #[1]returns sqrtPriceX96After: The sqrt price of the pool after the swap + #[2]returns initializedTicksLoaded: The number of initialized ticks that the swap loaded + return values + + def get_quote_exact_output_single( + self, + currency0: AddressLike, # input token + currency1: AddressLike, # output token + qty: int, + fee: int, + tick_spacing: int, + hook_data: bytes, + sqrt_price_limit_x96: int = 0, + zero_for_one: bool = True, + hooks: Union[AddressLike, str, None] = NOHOOK_ADDRESS, + ) -> Any: + """ + :if `zero_to_one` is true: given `qty` amount of the input `token0`, returns the maximum output amount of output `token1`. + :if `zero_to_one` is false: returns the minimum amount of `token0` required to buy `qty` amount of `token1`. + """ + + if currency0 == currency1: + raise ValueError + + pool_key = { + "currency0": currency0, + "currency1": currency1, + "fee": fee, + "tickSpacing": tick_spacing, + "hooks": hooks, + } + + quote_params = { + "poolKey": pool_key, + "zeroForOne": zero_for_one, + "recipient": self.address, + "exactAmount": qty, + "sqrtPriceLimitX96": sqrt_price_limit_x96, + "hookData" : hook_data, + } + + values = self.quoter.functions.quoteExactOutputSingle(quote_params) + #[0]returns deltaAmounts: Delta amounts resulted from the swap + #[1]returns sqrtPriceX96After: The sqrt price of the pool after the swap + #[2]returns initializedTicksLoaded: The number of initialized ticks that the swap loaded + return values + + def get_quote_exact_input( + self, + currency: AddressLike, # input token + qty: int, + path : List[UniswapV4_path_key], + ) -> Any: + """ + :path is a swap route + """ + + quote_path = [dataclasses.astuple(item) for item in path] + quote_params = { + "exactCurrency": currency, + "path": quote_path, + "recipient": self.address, + "exactAmount": qty, + } + + values = self.quoter.functions.quoteExactInput(quote_params) + #[0] returns deltaAmounts: Delta amounts along the path resulted from the swap + #[1] returns sqrtPriceX96AfterList: List of the sqrt price after the swap for each pool in the path + #[2] returns initializedTicksLoadedList: List of the initialized ticks that the swap loaded for each pool in the path + return values + + def get_quote_exact_output( + self, + currency: AddressLike, # input token + qty: int, + path : List[UniswapV4_path_key], + ) -> Any: + """ + :path is a swap route + """ + + quote_path = [dataclasses.astuple(item) for item in path] + quote_params = { + "exactCurrency": currency, + "path": quote_path, + "recipient": self.address, + "exactAmount": qty, + } + + values = self.quoter.functions.quoteExactOutput(quote_params) + #[0] returns deltaAmounts: Delta amounts along the path resulted from the swap + #[1] returns sqrtPriceX96AfterList: List of the sqrt price after the swap for each pool in the path + #[2] returns initializedTicksLoadedList: List of the initialized ticks that the swap loaded for each pool in the path + return values + + # ------ Pool manager READ methods -------------------------------------------------------------------- + def get_slot0( + self, + currency0: AddressLike, # input token + currency1: AddressLike, # output token + fee: int, + tick_spacing: int, + hooks: Union[AddressLike, str, None] = NOHOOK_ADDRESS, + ) -> UniswapV4_slot0: + """ + :Get the current value in slot0 of the given pool + """ + + pool_id = self.get_pool_id(currency0, currency1, fee, tick_spacing, hooks) + slot0 = UniswapV4_slot0(*self.router.functions.getSlot0(pool_id).call()) + return slot0 + + def get_liquidity( + self, + currency0: AddressLike, # input token + currency1: AddressLike, # output token + fee: int, + tick_spacing: int, + hooks: Union[AddressLike, str, None] = NOHOOK_ADDRESS, + ) -> int: + """ + :Get the current value of liquidity of the given pool + """ + pool_id = self.get_pool_id(currency0, currency1, fee, tick_spacing, hooks) + liquidity = int(self.router.functions.getLiquidity(pool_id).call()) + return liquidity + + def get_liquidity_for_position( + self, + currency0: AddressLike, # input token + currency1: AddressLike, # output token + fee: int, + tick_spacing: int, + owner: AddressLike, + tick_lower: int, + tick_upper: int, + hooks: Union[AddressLike, str, None] = NOHOOK_ADDRESS, + ) -> int: + """ + :Get the current value of liquidity for the specified pool and position + """ + pool_id = self.get_pool_id(currency0, currency1, fee, tick_spacing, hooks) + liquidity = int(self.router.functions.getLiquidity(pool_id,owner,tick_lower,tick_upper).call()) + return liquidity + + def get_position( + self, + currency0: AddressLike, # input token + currency1: AddressLike, # output token + fee: int, + tick_spacing: int, + owner: AddressLike, # output token + tick_lower: int, + tick_upper: int, + hooks: Union[AddressLike, str, None] = NOHOOK_ADDRESS, + ) -> UniswapV4_position_info: + """ + :Get the current value of liquidity for the specified pool and position + """ + pool_id = self.get_pool_id(currency0, currency1, fee, tick_spacing, hooks) + liquidity = UniswapV4_position_info(*self.router.functions.getPosition(pool_id,owner,tick_lower,tick_upper).call()) + return liquidity + + def get_pool_tick_info( + self, + currency0: AddressLike, # input token + currency1: AddressLike, # output token + fee: int, + tick_spacing: int, + tick: int, + hooks: Union[AddressLike, str, None] = NOHOOK_ADDRESS, + ) -> UniswapV4_tick_info: + """ + :Get the current value of liquidity for the specified pool and position + """ + pool_id = self.get_pool_id(currency0, currency1, fee, tick_spacing, hooks) + tick_info = UniswapV4_tick_info(*self.router.functions.getPoolTickInfo(pool_id,tick).call()) + return tick_info + + def get_pool_bitmap_info( + self, + currency0: AddressLike, # input token + currency1: AddressLike, # output token + fee: int, + tick_spacing: int, + word: int, + hooks: Union[AddressLike, str, None] = NOHOOK_ADDRESS, + ) -> int: + """ + :Get the current value of liquidity for the specified pool and position + """ + pool_id = self.get_pool_id(currency0, currency1, fee, tick_spacing, hooks) + bitmap_info = int(self.router.functions.getPoolBitmapInfo(pool_id, word).call()) + return bitmap_info + + def currency_delta( + self, + locker: AddressLike, # input token + currency0: AddressLike, # output token + ) -> int: + """ + :Get the current value of liquidity for the specified pool and position + """ + currency_delta = int(self.router.functions.currencyDelta(locker, currency0).call()) + return currency_delta + + def reserves_of( + self, + currency0: AddressLike, # input token + ) -> int: + """ + :Get the current value in slot0 of the given pool + """ + + reserves = int(self.router.functions.reservesOf().call()) + return reserves + + # ------ Pool manager WRITE methods ---------------------------------------------------------------- + def swap( + self, + currency0: ERC20Token, + currency1: ERC20Token, + qty: int, + fee: int, + tick_spacing: int, + hook_data : bytes, + sqrt_price_limit_x96: int = 0, + zero_for_one: bool = True, + hooks: Union[AddressLike, str, None] = NOHOOK_ADDRESS, + gas: Optional[Wei] = None, + max_fee: Optional[Wei] = None, + priority_fee: Optional[Wei] = None, + ) -> HexBytes: + """ + :Swap against the given pool + : + :`currency0`:The lower currency of the pool, sorted numerically + :`currency1`:The higher currency of the pool, sorted numerically + :`fee`: The pool swap fee, capped at 1_000_000. The upper 4 bits determine if the hook sets any fees. + :`tickSpacing`: Ticks that involve positions must be a multiple of tick spacing + :`hooks`: The hooks of the pool + :if `zero_for_one` is true: make a trade by defining the qty of the input token. + :if `zero_for_one` is false: make a trade by defining the qty of the output token. + """ + if currency0 == currency1: + raise ValueError + + pool_key = { + "currency0": currency0.address, + "currency1": currency1.address, + "fee": fee, + "tickSpacing": tick_spacing, + "hooks": hooks, + } + + swap_params = { + "zeroForOne": zero_for_one, + "amountSpecified": qty, + "sqrtPriceLimitX96": sqrt_price_limit_x96, + } + + return self._build_and_send_tx( + self.router.functions.swap( + { + "key": pool_key, + "params": swap_params, + } + ), + self._get_tx_params(gas = gas, max_fee = max_fee, priority_fee = priority_fee), + ) + + def initialize( + self, + currency0: ERC20Token, + currency1: ERC20Token, + qty: int, + fee: int, + tick_spacing: int, + sqrt_price_limit_x96: int, + hook_data : bytes, + hooks: Union[AddressLike, str, None] = NOHOOK_ADDRESS, + gas: Optional[Wei] = None, + max_fee: Optional[Wei] = None, + priority_fee: Optional[Wei] = None, + ) -> HexBytes: + """ + :Initialize the state for a given pool key + : + :`currency0`:The lower currency of the pool, sorted numerically + :`currency1`:The higher currency of the pool, sorted numerically + :`fee`: The pool swap fee, capped at 1_000_000. The upper 4 bits determine if the hook sets any fees. + :`tickSpacing`: Ticks that involve positions must be a multiple of tick spacing + :`hooks`: The hooks of the pool + """ + if currency0 == currency1: + raise ValueError + + pool_key = { + "currency0": currency0.address, + "currency1": currency1.address, + "fee": fee, + "tickSpacing": tick_spacing, + "hooks": hooks, + } + + return self._build_and_send_tx( + self.router.functions.initialize( + { + "key": pool_key, + "sqrtPriceX96": sqrt_price_limit_x96, + "hookData": hook_data, + } + ), + self._get_tx_params(gas = gas, max_fee = max_fee, priority_fee = priority_fee), + ) + + def donate( + self, + currency0: ERC20Token, + currency1: ERC20Token, + qty1: int, + qty2: int, + fee: int, + tick_spacing: int, + sqrt_price_limit_x96: int, + hook_data : bytes, + hooks: Union[AddressLike, str, None] = NOHOOK_ADDRESS, + gas: Optional[Wei] = None, + max_fee: Optional[Wei] = None, + priority_fee: Optional[Wei] = None, + ) -> HexBytes: + """ + :Donate the given currency amounts to the pool with the given pool key + : + :`currency0`:The lower currency of the pool, sorted numerically + :`currency1`:The higher currency of the pool, sorted numerically + :`fee`: The pool swap fee, capped at 1_000_000. The upper 4 bits determine if the hook sets any fees. + :`tickSpacing`: Ticks that involve positions must be a multiple of tick spacing + :`hooks`: The hooks of the pool + """ + if currency0 == currency1: + raise ValueError + + pool_key = { + "currency0": currency0.address, + "currency1": currency1.address, + "fee": fee, + "tickSpacing": tick_spacing, + "hooks": hooks, + } + + return self._build_and_send_tx( + self.router.functions.donate( + { + "key": pool_key, + "amount0": qty1, + "amount1": qty2, + "hookData": hook_data, + } + ), + self._get_tx_params(gas = gas, max_fee = max_fee, priority_fee = priority_fee), + ) + + def modify_liquidity( + self, + currency0: ERC20Token, + currency1: ERC20Token, + qty: int, + fee: int, + tick_spacing: int, + tick_upper: int, + tick_lower: int, + salt : int, + hook_data : bytes, + hooks: Union[AddressLike, str, None] = NOHOOK_ADDRESS, + gas: Optional[Wei] = None, + max_fee: Optional[Wei] = None, + priority_fee: Optional[Wei] = None, + ) -> HexBytes: + """ + :Modify the liquidity for the given pool + :Poke by calling with a zero liquidityDelta + : + :`currency0`:The lower currency of the pool, sorted numerically + :`currency1`:The higher currency of the pool, sorted numerically + :`fee`: The pool swap fee, capped at 1_000_000. The upper 4 bits determine if the hook sets any fees. + :`tickSpacing`: Ticks that involve positions must be a multiple of tick spacing + :`hooks`: The hooks of the pool + """ + if currency0 == currency1: + raise ValueError + + pool_key = { + "currency0": currency0.address, + "currency1": currency1.address, + "fee": fee, + "tickSpacing": tick_spacing, + "hooks": hooks, + } + + modify_liquidity_params = { + "tickLower": tick_lower, + "tickUpper": tick_upper, + "liquidityDelta": qty, + "salt": salt, + } + + return self._build_and_send_tx( + self.router.functions.modifyLiquidity( + { + "key": pool_key, + "params": modify_liquidity_params, + "hookData": hook_data, + } + ), + self._get_tx_params(value=Wei(qty), gas = gas, max_fee = max_fee, priority_fee = priority_fee), + ) + + def settle( + self, + currency0: Union[AddressLike, str, None], + qty: int, + gas: Optional[Wei] = None, + max_fee: Optional[Wei] = None, + priority_fee: Optional[Wei] = None, + ) -> HexBytes: + """ + :Called by the user to pay what is owed + """ + + return self._build_and_send_tx( + self.router.functions.settle( + { + "currency ": currency0, + } + ), + self._get_tx_params(value=Wei(qty), gas = gas, max_fee = max_fee, priority_fee = priority_fee), + ) + + def take( + self, + currency0: Union[AddressLike, str, None], + to: Union[AddressLike, str, None], + qty: int, + gas: Optional[Wei] = None, + max_fee: Optional[Wei] = None, + priority_fee: Optional[Wei] = None, + ) -> HexBytes: + """ + :Called by the user to net out some value owed to the user + :Can also be used as a mechanism for _free_ flash loans + """ + + return self._build_and_send_tx( + self.router.functions.take( + { + "currency ": currency0, + "to ": to, + "amount ": qty, + } + ), + self._get_tx_params(gas = gas, max_fee = max_fee, priority_fee = priority_fee), + ) + + def mint( + self, + currency0: Union[AddressLike, str, None], + id: int, + qty: int, + gas: Optional[Wei] = None, + max_fee: Optional[Wei] = None, + priority_fee: Optional[Wei] = None, + ) -> HexBytes: + """ + :Called by the user to net out some value owed to the user + :Can also be used as a mechanism for _free_ flash loans + """ + + return self._build_and_send_tx( + self.router.functions.mint( + { + "currency ": currency0, + "id ": id, + "amount ": qty, + } + ), + self._get_tx_params(gas = gas, max_fee = max_fee, priority_fee = priority_fee), + ) + + def burn( + self, + currency0: Union[AddressLike, str, None], + id: int, + qty: int, + gas: Optional[Wei] = None, + max_fee: Optional[Wei] = None, + priority_fee: Optional[Wei] = None, + ) -> HexBytes: + """ + :Called by the user to net out some value owed to the user + :Can also be used as a mechanism for _free_ flash loans + """ + + return self._build_and_send_tx( + self.router.functions.burn( + { + "currency ": currency0, + "id ": id, + "amount ": qty, + } + ), + self._get_tx_params(gas = gas, max_fee = max_fee, priority_fee = priority_fee), + ) + + # ------ Approval Utils ------------------------------------------------------------ + def approve(self, token: AddressLike, max_approval: Optional[int] = None) -> None: + """Give an exchange/router max approval of a token.""" + max_approval = self.max_approval_int if not max_approval else max_approval + contract_addr = _addr_to_str(self.poolmanager_contract_addr) + function = _load_contract_erc20(self.w3, token).functions.approve( + contract_addr, max_approval + ) + logger.warning(f"Approving {_addr_to_str(token)}...") + tx = self._build_and_send_tx(function) + self.w3.eth.wait_for_transaction_receipt(tx, timeout=6000) + + # Add extra sleep to let tx propogate correctly + time.sleep(1) + + # ------ Tx Utils ------------------------------------------------------------------ + def _deadline(self) -> int: + """Get a predefined deadline. 10min by default.""" + return int(time.time()) + 10 * 60 + + def _build_and_send_tx( + self, function: ContractFunction, tx_params: Optional[TxParams] = None + ) -> HexBytes: + """Build and send a transaction.""" + if not tx_params: + tx_params = self._get_tx_params() + transaction = function.build_transaction(tx_params) + # Uniswap3 uses 20% margin for transactions + transaction["gas"] = Wei(int(self.w3.eth.estimate_gas(transaction) * 1.2)) + signed_txn = self.w3.eth.account.sign_transaction( + transaction, private_key=self.private_key + ) + # TODO: This needs to get more complicated if we want to support replacing a transaction + # FIXME: This does not play nice if transactions are sent from other places using the same wallet. + try: + return self.w3.eth.send_raw_transaction(signed_txn.rawTransaction) + finally: + logger.debug(f"nonce: {tx_params['nonce']}") + self.last_nonce = Nonce(tx_params["nonce"] + 1) + + def _get_tx_params(self, value: Wei = Wei(0), gas: Optional[Wei] = None, max_fee: Optional[Wei] = None, priority_fee: Optional[Wei] = None) -> TxParams: + """Get generic transaction parameters.""" + params: TxParams = { + "from": _addr_to_str(self.address), + "value": value, + "nonce": max( + self.last_nonce, self.w3.eth.get_transaction_count(self.address) + ), + } + + if gas: + params["gas"] = gas + if max_fee: + params["maxFeePerGas"] = max_fee + if priority_fee: + params["maxPriorityFeePerGas"] = priority_fee + + return params + + # ------ Helpers ------------------------------------------------------------ + + def get_pool_id(self, currency0: Union[AddressLike, str, None], currency1: Union[AddressLike, str, None], fee : int, tickSpacing : int, hooks : Union[AddressLike, str, None] = NOHOOK_ADDRESS) -> bytes: + currency0 = str(currency0) + currency1 = str(currency1) + if int(currency0, 16) > int(currency1, 16): + currency0 , currency1 = currency1 , currency0 + pool_id = bytes(self.w3.solidity_keccak(["address", "address", "int24", "int24", "address"], [(currency0, currency1, fee, tickSpacing, hooks)])) + return pool_id + + + diff --git a/uniswap/util.py b/uniswap/util.py index 888aa39..406c521 100644 --- a/uniswap/util.py +++ b/uniswap/util.py @@ -91,6 +91,13 @@ def _encode_path(token_in: AddressLike, route: List[Tuple[int, AddressLike]]) -> raise NotImplementedError +# Adapted from: https://github.com/Uniswap/v3-sdk/blob/main/src/utils/encodeSqrtRatioX96.ts +def decode_sqrt_ratioX96(sqrtPriceX96: int) -> float: + Q96 = 2**96 + ratio = sqrtPriceX96 / Q96 + price = ratio**2 + return price + # Adapted from: https://github.com/Uniswap/v3-sdk/blob/main/src/utils/encodeSqrtRatioX96.ts def encode_sqrt_ratioX96(amount_0: int, amount_1: int) -> int: numerator = amount_1 << 192