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
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: