diff --git a/assets/logos/bnb_chain.svg b/assets/logos/bnb_chain.svg new file mode 100644 index 0000000..7f8d0da --- /dev/null +++ b/assets/logos/bnb_chain.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/abis/pancake_swap_infinity_cl_pool_manager.abi.json b/lib/abis/pancake_swap_infinity_cl_pool_manager.abi.json new file mode 100644 index 0000000..1117a3b --- /dev/null +++ b/lib/abis/pancake_swap_infinity_cl_pool_manager.abi.json @@ -0,0 +1,36 @@ +[ + { + "inputs": [ + { + "internalType": "PoolId", + "name": "id", + "type": "bytes32" + } + ], + "name": "getSlot0", + "outputs": [ + { + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + }, + { + "internalType": "int24", + "name": "tick", + "type": "int24" + }, + { + "internalType": "uint24", + "name": "protocolFee", + "type": "uint24" + }, + { + "internalType": "uint24", + "name": "lpFee", + "type": "uint24" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/lib/abis/uniswap_v2_pool.abi.json b/lib/abis/uniswap_v2_pool.abi.json new file mode 100644 index 0000000..09dc5dc --- /dev/null +++ b/lib/abis/uniswap_v2_pool.abi.json @@ -0,0 +1,713 @@ +[ + { + "inputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "Burn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "name": "Mint", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0In", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1In", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0Out", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1Out", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "Swap", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint112", + "name": "reserve0", + "type": "uint112" + }, + { + "indexed": false, + "internalType": "uint112", + "name": "reserve1", + "type": "uint112" + } + ], + "name": "Sync", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "constant": true, + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "MINIMUM_LIQUIDITY", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "PERMIT_TYPEHASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "burn", + "outputs": [ + { + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getReserves", + "outputs": [ + { + "internalType": "uint112", + "name": "_reserve0", + "type": "uint112" + }, + { + "internalType": "uint112", + "name": "_reserve1", + "type": "uint112" + }, + { + "internalType": "uint32", + "name": "_blockTimestampLast", + "type": "uint32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_token0", + "type": "address" + }, + { + "internalType": "address", + "name": "_token1", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "kLast", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "price0CumulativeLast", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "price1CumulativeLast", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "skim", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "amount0Out", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1Out", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "swap", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "sync", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "token0", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "token1", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/lib/abis/uniswap_v2_router_02.abi.json b/lib/abis/uniswap_v2_router_02.abi.json new file mode 100644 index 0000000..c23fa60 --- /dev/null +++ b/lib/abis/uniswap_v2_router_02.abi.json @@ -0,0 +1,973 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_factory", + "type": "address" + }, + { + "internalType": "address", + "name": "_WETH", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "WETH", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountADesired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBDesired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountAMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "addLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountTokenDesired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "addLiquidityETH", + "outputs": [ + { + "internalType": "uint256", + "name": "amountToken", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveOut", + "type": "uint256" + } + ], + "name": "getAmountIn", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveOut", + "type": "uint256" + } + ], + "name": "getAmountOut", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + } + ], + "name": "getAmountsIn", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + } + ], + "name": "getAmountsOut", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveB", + "type": "uint256" + } + ], + "name": "quote", + "outputs": [ + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountAMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "removeLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "removeLiquidityETH", + "outputs": [ + { + "internalType": "uint256", + "name": "amountToken", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "removeLiquidityETHSupportingFeeOnTransferTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "approveMax", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "removeLiquidityETHWithPermit", + "outputs": [ + { + "internalType": "uint256", + "name": "amountToken", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "approveMax", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "removeLiquidityETHWithPermitSupportingFeeOnTransferTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountAMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "approveMax", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "removeLiquidityWithPermit", + "outputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapETHForExactTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactETHForTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactETHForTokensSupportingFeeOnTransferTokens", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForETH", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForETHSupportingFeeOnTransferTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForTokensSupportingFeeOnTransferTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountInMax", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapTokensForExactETH", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountInMax", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapTokensForExactTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] \ No newline at end of file diff --git a/lib/app/create/deposit/deposit_cubit.dart b/lib/app/create/deposit/deposit_cubit.dart index 0f30cd1..65fbc4c 100644 --- a/lib/app/create/deposit/deposit_cubit.dart +++ b/lib/app/create/deposit/deposit_cubit.dart @@ -41,26 +41,31 @@ class DepositCubit extends Cubit with KeysMixin, V3PoolConversorsM final ZupAnalytics _zupAnalytics; final StreamController _pooltickStreamController = StreamController.broadcast(); + final StreamController<({double reserve0, double reserve1})?> _v2PoolReservesStreamController = + StreamController.broadcast(); final StreamController _selectedYieldStreamController = StreamController.broadcast(); BigInt? _latestPoolTick; + ({double reserve0, double reserve1})? _latestV2PoolReserves; YieldDto? _selectedYield; YieldTimeFrame? _selectedYieldTimeframe; late final Stream selectedYieldStream = _selectedYieldStreamController.stream; late final Stream poolTickStream = _pooltickStreamController.stream; + late final Stream<({double reserve0, double reserve1})?> v2PoolReservesStream = + _v2PoolReservesStreamController.stream; YieldDto? get selectedYield => _selectedYield; YieldTimeFrame? get selectedYieldTimeframe => _selectedYieldTimeframe; BigInt? get latestPoolTick => _latestPoolTick; + ({double reserve0, double reserve1})? get latestV2PoolReserves => _latestV2PoolReserves; DepositSettingsDto get depositSettings => _cache.getDepositSettings(); PoolSearchSettingsDto get poolSearchSettings => _cache.getPoolSearchSettings(); void setup() async { Timer.periodic(const Duration(minutes: 1), (timer) { - if (_pooltickStreamController.isClosed) return timer.cancel(); - - if (selectedYield != null) getSelectedPoolTick(); + if (_pooltickStreamController.isClosed || _v2PoolReservesStreamController.isClosed) return timer.cancel(); + if (selectedYield != null) getSelectedPoolTickOrReserves(); }); } @@ -108,7 +113,41 @@ class DepositCubit extends Cubit with KeysMixin, V3PoolConversorsM _selectedYieldTimeframe = yieldTimeFrame; _selectedYieldStreamController.add(selectedYield); - if (selectedYield != null) await getSelectedPoolTick(); + if (selectedYield != null) await getSelectedPoolTickOrReserves(); + } + + Future getSelectedPoolTickOrReserves() async { + if (selectedYield == null) return; + + if (selectedYield!.poolType.isV2) return await getSelectedPoolV2Reserves(); + return await getSelectedPoolTick(); + } + + Future getSelectedPoolV2Reserves() async { + if (selectedYield == null) return; + + _latestV2PoolReserves = null; + _v2PoolReservesStreamController.add(null); + + final selectedYieldBeforeCall = selectedYield; + + final poolReserves = await _poolService.getV2PoolReserves(selectedYield!); + + final poolReserve0DecimalFormatted = poolReserves.reserve0.parseTokenAmount( + decimals: selectedYield!.token0NetworkDecimals, + ); + + final poolReserve1DecimalFormatted = poolReserves.reserve1.parseTokenAmount( + decimals: selectedYield!.token1NetworkDecimals, + ); + + if (selectedYieldBeforeCall != selectedYield) return await getSelectedPoolV2Reserves(); + + _v2PoolReservesStreamController.add( + (reserve0: poolReserve0DecimalFormatted, reserve1: poolReserve1DecimalFormatted), + ); + + _latestV2PoolReserves = (reserve0: poolReserve0DecimalFormatted, reserve1: poolReserve1DecimalFormatted); } Future getSelectedPoolTick() async { diff --git a/lib/app/create/deposit/deposit_page.dart b/lib/app/create/deposit/deposit_page.dart index d7c933d..7b1a8d8 100644 --- a/lib/app/create/deposit/deposit_page.dart +++ b/lib/app/create/deposit/deposit_page.dart @@ -80,6 +80,7 @@ class _DepositPageState extends State final quoteTokenAmountController = TextEditingController(); final wallet = inject(); final selectRangeSectorKey = GlobalKey(); + final v2PoolDepositSectionKey = GlobalKey(); ZupNavigator get _navigator => inject(); DepositCubit get _cubit => context.read(); @@ -119,8 +120,8 @@ class _DepositPageState extends State final price = tickToPrice( tick: _cubit.latestPoolTick!, - poolToken0Decimals: _cubit.selectedYield!.token0.decimals, - poolToken1Decimals: _cubit.selectedYield!.token1.decimals, + poolToken0Decimals: _cubit.selectedYield!.token0NetworkDecimals, + poolToken1Decimals: _cubit.selectedYield!.token1NetworkDecimals, ); return areTokensReversed ? price.priceAsQuoteToken : price.priceAsBaseToken; @@ -153,9 +154,11 @@ class _DepositPageState extends State if (yieldDto != null) { WidgetsBinding.instance.addPostFrameCallback((_) { - if (selectRangeSectorKey.currentContext != null) { + final scrollToKey = yieldDto.poolType.isV2 ? v2PoolDepositSectionKey : selectRangeSectorKey; + + if (scrollToKey.currentContext != null) { Scrollable.ensureVisible( - selectRangeSectorKey.currentContext!, + scrollToKey.currentContext!, duration: const Duration(milliseconds: 600), curve: Curves.fastEaseInToSlowEaseOut, ); @@ -178,6 +181,35 @@ class _DepositPageState extends State } void calculateDepositTokensAmount() { + if (_cubit.selectedYield?.poolType.isV2 ?? false) { + if (_cubit.latestV2PoolReserves == null || _cubit.selectedYield == null) return; + + final poolReserve0 = _cubit.latestV2PoolReserves!.reserve0; + final poolReserve1 = _cubit.latestV2PoolReserves!.reserve1; + + if (isBaseTokenAmountUserInput && baseTokenAmountController.text.isEmpty) { + quoteTokenAmountController.clear(); + return; + } + + if (!isBaseTokenAmountUserInput && quoteTokenAmountController.text.isEmpty) { + baseTokenAmountController.clear(); + return; + } + + if (isBaseTokenAmountUserInput) { + quoteTokenAmountController.text = + (poolReserve1 / poolReserve0 * (double.parse(baseTokenAmountController.text))).toString(); + + return; + } + + baseTokenAmountController.text = + (poolReserve0 / poolReserve1 * (double.parse(quoteTokenAmountController.text))).toString(); + + return; + } + if (_cubit.latestPoolTick == null || _cubit.selectedYield == null) return; if (isOutOfRange.minPrice) return quoteTokenAmountController.clear(); @@ -185,14 +217,14 @@ class _DepositPageState extends State final maxTickPrice = tickToPrice( tick: V3V4PoolConstants.maxTick, - poolToken0Decimals: _cubit.selectedYield!.token0.decimals, - poolToken1Decimals: _cubit.selectedYield!.token1.decimals, + poolToken0Decimals: _cubit.selectedYield!.token0NetworkDecimals, + poolToken1Decimals: _cubit.selectedYield!.token1NetworkDecimals, ); final minTickPrice = tickToPrice( tick: V3V4PoolConstants.minTick, - poolToken0Decimals: _cubit.selectedYield!.token0.decimals, - poolToken1Decimals: _cubit.selectedYield!.token1.decimals, + poolToken0Decimals: _cubit.selectedYield!.token0NetworkDecimals, + poolToken1Decimals: _cubit.selectedYield!.token1NetworkDecimals, ); double getMinPrice() { @@ -234,7 +266,7 @@ class _DepositPageState extends State baseTokenAmountController.text = newBaseTokenAmount; } - Future<({String title, Widget? icon, Function()? onPressed})> depositButtonState() async { + Future<({String title, Widget? icon, Function()? onPressed})> v3PoolDepositButtonState() async { final userWalletBaseTokenAmount = await _cubit.getWalletTokenAmount( baseToken.addresses[_cubit.selectedYield!.network.chainId]!, network: _cubit.selectedYield!.network, @@ -308,6 +340,65 @@ class _DepositPageState extends State ); } + Future<({String title, Widget? icon, Function()? onPressed})> v2PoolDepositButtonState() async { + if (baseTokenAmountController.text.isEmptyOrZero || quoteTokenAmountController.text.isEmptyOrZero) { + return ( + title: S.of(context).depositPageInvalidTokenAmountV2Pool( + token0Symbol: _cubit.selectedYield!.token0.symbol, + token1Symbol: _cubit.selectedYield!.token1.symbol, + ), + icon: null, + onPressed: null + ); + } + + final userWalletToken0Amount = await _cubit.getWalletTokenAmount( + _cubit.selectedYield!.token0NetworkAddress, + network: _cubit.selectedYield!.network, + ); + + final userWalletToken1Amount = await _cubit.getWalletTokenAmount( + _cubit.selectedYield!.token1NetworkAddress, + network: _cubit.selectedYield!.network, + ); + + if (userWalletToken0Amount < (double.tryParse(baseTokenAmountController.text) ?? 0)) { + return ( + title: S.of(context).depositPageInsufficientTokenBalance(tokenSymbol: baseToken.symbol), + icon: null, + onPressed: null + ); + } + + if (userWalletToken1Amount < (double.tryParse(quoteTokenAmountController.text) ?? 0)) { + return ( + title: S.of(context).depositPageInsufficientTokenBalance(tokenSymbol: quoteToken.symbol), + icon: null, + onPressed: null + ); + } + + return ( + title: S.of(context).preview, + icon: Assets.icons.scrollFill.svg(), + onPressed: () { + PreviewDepositModal( + key: const Key("preview-deposit-modal"), + yieldTimeFrame: _cubit.selectedYieldTimeframe!, + deadline: selectedDeadline, + maxSlippage: selectedSlippage, + currentYield: _cubit.selectedYield!, + isReversed: areTokensReversed, + token0DepositAmount: double.tryParse(baseTokenAmountController.text) ?? 0, + token1DepositAmount: double.tryParse(quoteTokenAmountController.text) ?? 0, + ).show( + context, + currentPoolTick: BigInt.zero, + ); + } + ); + } + @override void initState() { _cubit.setup(); @@ -399,10 +490,13 @@ class _DepositPageState extends State const SizedBox(height: 16), _buildYieldSelectionSector(yields), const SizedBox(height: 20), - if (selectedYieldSnapshot.data != null) ...[ + if (selectedYieldSnapshot.data != null && !(selectedYieldSnapshot.data!.poolType.isV2)) ...[ _buildSelectRangeSector(), const SizedBox(height: 20), _buildDepositSection(), + ] else if (selectedYieldSnapshot.data != null && + selectedYieldSnapshot.data!.poolType.isV2) ...[ + _buildDepositSectionForV2Pool() ], const SizedBox(height: 200) ], @@ -727,8 +821,8 @@ class _DepositPageState extends State "1 ${baseToken.symbol} ≈ ${() { final currentPrice = tickToPrice( tick: poolTickSnapshot.data ?? BigInt.zero, - poolToken0Decimals: _cubit.selectedYield!.token0.decimals, - poolToken1Decimals: _cubit.selectedYield!.token1.decimals, + poolToken0Decimals: _cubit.selectedYield!.token0NetworkDecimals, + poolToken1Decimals: _cubit.selectedYield!.token1NetworkDecimals, ); return areTokensReversed ? currentPrice.priceAsQuoteToken : currentPrice.priceAsBaseToken; @@ -758,8 +852,8 @@ class _DepositPageState extends State }); }, initialPrice: minPrice, - poolToken0: _cubit.selectedYield!.token0, - poolToken1: _cubit.selectedYield!.token1, + poolToken0Decimals: _cubit.selectedYield!.token0NetworkDecimals, + poolToken1Decimals: _cubit.selectedYield!.token1NetworkDecimals, isReversed: areTokensReversed, displayBaseTokenSymbol: baseToken.symbol, displayQuoteTokenSymbol: quoteToken.symbol, @@ -803,8 +897,8 @@ class _DepositPageState extends State type: RangeSelectorType.maxPrice, isInfinity: isMaxRangeInfinity, initialPrice: maxPrice, - poolToken0: _cubit.selectedYield!.token0, - poolToken1: _cubit.selectedYield!.token1, + poolToken0Decimals: _cubit.selectedYield!.token0NetworkDecimals, + poolToken1Decimals: _cubit.selectedYield!.token1NetworkDecimals, isReversed: areTokensReversed, tickSpacing: _cubit.selectedYield!.tickSpacing, state: () { @@ -838,7 +932,6 @@ class _DepositPageState extends State opacity: isRangeInvalid ? 0.2 : 1, child: StreamBuilder( stream: _cubit.poolTickStream, - initialData: _cubit.latestPoolTick, builder: (context, poolTickSnapshot) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -926,7 +1019,7 @@ class _DepositPageState extends State } return FutureBuilder( - future: depositButtonState(), + future: v3PoolDepositButtonState(), builder: (context, stateSnapshot) { return ZupPrimaryButton( alignCenter: true, @@ -949,4 +1042,97 @@ class _DepositPageState extends State }), ), ); + + Widget _buildDepositSectionForV2Pool() { + return StreamBuilder( + key: const Key("v2-pool-deposit-section"), + stream: _cubit.v2PoolReservesStream, + initialData: _cubit.latestV2PoolReserves, + builder: (context, poolReservesSnapshot) { + return Column( + key: v2PoolDepositSectionKey, + children: [ + TokenAmountInputCard( + key: const Key("v2-pool-base-token-input-card"), + token: _cubit.selectedYield!.token0, + isNative: _cubit.selectedYield!.isToken0Native, + disabledText: () { + if (!(poolReservesSnapshot.hasData) && + !(isBaseTokenAmountUserInput) && + quoteTokenAmountController.text.isNotEmpty) { + return S.of(context).loading; + } + }.call(), + onInput: (_) { + setState(() { + isBaseTokenAmountUserInput = true; + + calculateDepositTokensAmount(); + }); + }, + controller: baseTokenAmountController, + network: _cubit.selectedYield!.network, + ), + const SizedBox(height: 6), + TokenAmountInputCard( + key: const Key("v2-pool-quote-token-input-card"), + token: _cubit.selectedYield!.token1, + isNative: _cubit.selectedYield!.isToken1Native, + disabledText: () { + if (!(poolReservesSnapshot.hasData) && + isBaseTokenAmountUserInput && + baseTokenAmountController.text.isNotEmpty) { + return S.of(context).loading; + } + }.call(), + onInput: (_) { + setState(() { + isBaseTokenAmountUserInput = false; + + calculateDepositTokensAmount(); + }); + }, + controller: quoteTokenAmountController, + network: _cubit.selectedYield!.network, + ), + const SizedBox(height: 20), + StreamBuilder( + key: const Key("deposit-button"), + stream: wallet.signerStream, + initialData: wallet.signer, + builder: (context, signerSnapshot) { + if (!signerSnapshot.hasData) { + return ZupPrimaryButton( + width: double.maxFinite, + title: S.of(context).connectWallet, + icon: Assets.icons.walletBifold.svg(), + fixedIcon: true, + alignCenter: true, + hoverElevation: 0, + backgroundColor: ZupColors.brand7, + foregroundColor: ZupColors.brand, + onPressed: () => ConnectModal().show(context), + ); + } + + return FutureBuilder( + future: v2PoolDepositButtonState(), + builder: (context, stateSnapshot) { + return ZupPrimaryButton( + alignCenter: true, + title: stateSnapshot.data?.title ?? "Loading...", + icon: stateSnapshot.data?.icon, + isLoading: stateSnapshot.connectionState == ConnectionState.waiting, + fixedIcon: true, + onPressed: stateSnapshot.data?.onPressed, + width: double.maxFinite, + ); + }, + ); + }, + ), + ], + ); + }); + } } diff --git a/lib/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal.dart b/lib/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal.dart index 475849e..db4d3db 100644 --- a/lib/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal.dart +++ b/lib/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal.dart @@ -31,8 +31,8 @@ class PreviewDepositModal extends StatefulWidget with DeviceInfoMixin { super.key, required this.currentYield, required this.isReversed, - required this.minPrice, - required this.maxPrice, + this.minPrice, + this.maxPrice, required this.token0DepositAmount, required this.token1DepositAmount, required this.deadline, @@ -43,22 +43,20 @@ class PreviewDepositModal extends StatefulWidget with DeviceInfoMixin { final YieldDto currentYield; final YieldTimeFrame yieldTimeFrame; final bool isReversed; - final ({double price, bool isInfinity}) minPrice; - final ({double price, bool isInfinity}) maxPrice; + final ({double price, bool isInfinity})? minPrice; + final ({double price, bool isInfinity})? maxPrice; final double token0DepositAmount; final double token1DepositAmount; final Duration deadline; final Slippage maxSlippage; - final double paddingSize = 20; - - show(BuildContext context, {required BigInt currentPoolTick}) { + show(BuildContext context, {BigInt? currentPoolTick}) { return ZupModal.show( context, showAsBottomSheet: isMobileSize(context), title: S.of(context).previewDepositModalTitle, - size: const Size(450, 650), - padding: EdgeInsets.only(left: paddingSize).copyWith(top: 5), + size: Size(450, currentYield.poolType.isV2 ? 505 : 650), + padding: const EdgeInsets.only(left: 20).copyWith(top: 5), content: BlocProvider( create: (context) => PreviewDepositModalCubit( zupAnalytics: inject(), @@ -118,17 +116,17 @@ class _PreviewDepositModalState extends State with V3PoolCo double get quoteTokenAmount => isReversedLocal ? widget.token0DepositAmount : widget.token1DepositAmount; PreviewDepositModalCubit get cubit => context.read(); BigInt get token0DepositAmount => - widget.token0DepositAmount.parseTokenAmount(decimals: widget.currentYield.token0.decimals); + widget.token0DepositAmount.parseTokenAmount(decimals: widget.currentYield.token0NetworkDecimals); BigInt get token1DepositAmount => - widget.token1DepositAmount.parseTokenAmount(decimals: widget.currentYield.token1.decimals); + widget.token1DepositAmount.parseTokenAmount(decimals: widget.currentYield.token1NetworkDecimals); double get currentPrice { final currentTick = cubit.latestPoolTick; final price = tickToPrice( tick: currentTick, - poolToken0Decimals: widget.currentYield.token0.decimals, - poolToken1Decimals: widget.currentYield.token1.decimals, + poolToken0Decimals: widget.currentYield.token0NetworkDecimals, + poolToken1Decimals: widget.currentYield.token1NetworkDecimals, ); return isReversedLocal ? price.priceAsQuoteToken : price.priceAsBaseToken; @@ -136,20 +134,20 @@ class _PreviewDepositModalState extends State with V3PoolCo double get minPrice { BigInt tick() { - if (widget.isReversed != isReversedLocal && widget.maxPrice.isInfinity) return V3V4PoolConstants.minTick; + if (widget.isReversed != isReversedLocal && widget.maxPrice!.isInfinity) return V3V4PoolConstants.minTick; return priceToTick( - price: (widget.isReversed == !isReversedLocal) ? widget.maxPrice.price : widget.minPrice.price, - poolToken0Decimals: widget.currentYield.token0.decimals, - poolToken1Decimals: widget.currentYield.token1.decimals, + price: (widget.isReversed == !isReversedLocal) ? widget.maxPrice!.price : widget.minPrice!.price, + poolToken0Decimals: widget.currentYield.token0NetworkDecimals, + poolToken1Decimals: widget.currentYield.token1NetworkDecimals, isReversed: widget.isReversed, ); } ({double priceAsBaseToken, double priceAsQuoteToken}) price() => tickToPrice( tick: tick(), - poolToken0Decimals: widget.currentYield.token0.decimals, - poolToken1Decimals: widget.currentYield.token1.decimals, + poolToken0Decimals: widget.currentYield.token0NetworkDecimals, + poolToken1Decimals: widget.currentYield.token1NetworkDecimals, ); return isReversedLocal ? price().priceAsQuoteToken : price().priceAsBaseToken; @@ -157,20 +155,20 @@ class _PreviewDepositModalState extends State with V3PoolCo double get maxPrice { BigInt tick() { - if (widget.isReversed != isReversedLocal && widget.minPrice.isInfinity) return V3V4PoolConstants.minTick; + if (widget.isReversed != isReversedLocal && widget.minPrice!.isInfinity) return V3V4PoolConstants.minTick; return priceToTick( - price: (widget.isReversed == !isReversedLocal) ? widget.minPrice.price : widget.maxPrice.price, - poolToken0Decimals: widget.currentYield.token0.decimals, - poolToken1Decimals: widget.currentYield.token1.decimals, + price: (widget.isReversed == !isReversedLocal) ? widget.minPrice!.price : widget.maxPrice!.price, + poolToken0Decimals: widget.currentYield.token0NetworkDecimals, + poolToken1Decimals: widget.currentYield.token1NetworkDecimals, isReversed: widget.isReversed, ); } ({double priceAsBaseToken, double priceAsQuoteToken}) price() => tickToPrice( tick: tick(), - poolToken0Decimals: widget.currentYield.token0.decimals, - poolToken1Decimals: widget.currentYield.token1.decimals, + poolToken0Decimals: widget.currentYield.token0NetworkDecimals, + poolToken1Decimals: widget.currentYield.token1NetworkDecimals, ); return isReversedLocal ? price().priceAsQuoteToken : price().priceAsBaseToken; @@ -189,8 +187,8 @@ class _PreviewDepositModalState extends State with V3PoolCo } ({bool minPrice, bool maxPrice, bool any}) get isOutOfRange { - final isMinPriceOutOfRange = !widget.minPrice.isInfinity && (minPrice) > currentPrice; - final isMaxPriceOutOfRanfe = !widget.maxPrice.isInfinity && (maxPrice) < currentPrice; + final isMinPriceOutOfRange = !widget.minPrice!.isInfinity && (minPrice) > currentPrice; + final isMaxPriceOutOfRanfe = !widget.maxPrice!.isInfinity && (maxPrice) < currentPrice; return ( minPrice: isMinPriceOutOfRange, @@ -260,10 +258,10 @@ class _PreviewDepositModalState extends State with V3PoolCo slippage: widget.maxSlippage, token0Amount: token0DepositAmount, token1Amount: token1DepositAmount, - minPrice: widget.minPrice.price, - maxPrice: widget.maxPrice.price, - isMinPriceInfinity: widget.minPrice.isInfinity, - isMaxPriceInfinity: widget.maxPrice.isInfinity, + minPrice: widget.minPrice?.price, + maxPrice: widget.maxPrice?.price, + isMinPriceInfinity: widget.minPrice?.isInfinity, + isMaxPriceInfinity: widget.maxPrice?.isInfinity, isReversed: widget.isReversed, ), ); @@ -419,56 +417,59 @@ class _PreviewDepositModalState extends State with V3PoolCo "${baseToken.symbol}/${quoteToken.symbol}", style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500), ), - const SizedBox(width: 10), - StreamBuilder( - stream: cubit.poolTickStream, - builder: (context, tickSnapshot) { - return ZupTag( - title: isOutOfRange.any - ? S.of(context).previewDepositModalOutOfRange - : S.of(context).previewDepositModalInRange, - color: isOutOfRange.any ? ZupColors.orange : ZupColors.green, - ); - }), - const Spacer(), + if (!widget.currentYield.poolType.isV2) ...[ + const SizedBox(width: 10), + StreamBuilder( + stream: cubit.poolTickStream, + builder: (context, tickSnapshot) { + return ZupTag( + title: isOutOfRange.any + ? S.of(context).previewDepositModalOutOfRange + : S.of(context).previewDepositModalInRange, + color: isOutOfRange.any ? ZupColors.orange : ZupColors.green, + ); + }), + const Spacer(), + ] ], ), const SizedBox(height: 16), - CupertinoSlidingSegmentedControl( - groupValue: isReversedLocal, - children: { - false: MouseRegion( - key: const Key("unreverse-tokens"), - cursor: SystemMouseCursors.click, - child: IgnorePointer( - child: SizedBox( - height: 15, - child: Text( - "${widget.currentYield.token0.symbol} / ${widget.currentYield.token1.symbol}", - style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600), + if (!widget.currentYield.poolType.isV2) + CupertinoSlidingSegmentedControl( + groupValue: isReversedLocal, + children: { + false: MouseRegion( + key: const Key("unreverse-tokens"), + cursor: SystemMouseCursors.click, + child: IgnorePointer( + child: SizedBox( + height: 15, + child: Text( + "${widget.currentYield.token0.symbol} / ${widget.currentYield.token1.symbol}", + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600), + ), ), ), ), - ), - true: MouseRegion( - key: const Key("reverse-tokens"), - cursor: SystemMouseCursors.click, - child: IgnorePointer( - child: SizedBox( - height: 16, - child: Text( - "${widget.currentYield.token1.symbol} / ${widget.currentYield.token0.symbol}", - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, + true: MouseRegion( + key: const Key("reverse-tokens"), + cursor: SystemMouseCursors.click, + child: IgnorePointer( + child: SizedBox( + height: 16, + child: Text( + "${widget.currentYield.token1.symbol} / ${widget.currentYield.token0.symbol}", + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + ), ), ), ), ), - ), - }, - onValueChanged: (value) => setState(() => isReversedLocal = value ?? false), - ), + }, + onValueChanged: (value) => setState(() => isReversedLocal = value ?? false), + ), const SizedBox(height: 10), const ZupDivider(), const SizedBox(height: 10), @@ -566,14 +567,16 @@ class _PreviewDepositModalState extends State with V3PoolCo const SizedBox(height: 10), const ZupDivider(), const SizedBox(height: 10), - Row( - children: [ - Expanded(child: rangeInfoCard(isMinPrice: true)), - const SizedBox(width: 10), - Expanded(child: rangeInfoCard(isMinPrice: false)), - ], - ), - const Spacer(), + if (!widget.currentYield.poolType.isV2) ...[ + Row( + children: [ + Expanded(child: rangeInfoCard(isMinPrice: true)), + const SizedBox(width: 10), + Expanded(child: rangeInfoCard(isMinPrice: false)), + ], + ), + const Spacer(), + ], const SizedBox(height: 10), ZupPrimaryButton( key: const Key("deposit-button"), @@ -617,7 +620,7 @@ class _PreviewDepositModalState extends State with V3PoolCo Text( () { if (isMinPrice) { - return widget.minPrice.isInfinity + return widget.minPrice!.isInfinity ? "0" : minPrice.maybeFormatCompactCurrency( isUSD: false, @@ -627,7 +630,7 @@ class _PreviewDepositModalState extends State with V3PoolCo ); } - return widget.maxPrice.isInfinity + return widget.maxPrice!.isInfinity ? "∞" : maxPrice.maybeFormatCompactCurrency( isUSD: false, diff --git a/lib/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal_cubit.dart b/lib/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal_cubit.dart index 24bca8c..47874ca 100644 --- a/lib/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal_cubit.dart +++ b/lib/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal_cubit.dart @@ -26,7 +26,7 @@ part "preview_deposit_modal_state.dart"; class PreviewDepositModalCubit extends Cubit with V3PoolConversorsMixin, DeviceInfoMixin { PreviewDepositModalCubit({ - required BigInt initialPoolTick, + BigInt? initialPoolTick, required PoolService poolService, required YieldDto currentYield, required Erc20 erc20, @@ -39,7 +39,7 @@ class PreviewDepositModalCubit extends Cubit with V3Po _poolRepository = poolService, _erc20 = erc20, _wallet = wallet, - _latestPoolTick = initialPoolTick, + _latestPoolTick = initialPoolTick ?? BigInt.zero, _navigatorKey = navigatorKey, _zupAnalytics = zupAnalytics, _permit2 = permit2, @@ -156,11 +156,11 @@ class PreviewDepositModalCubit extends Cubit with V3Po Future deposit({ required BigInt token0Amount, required BigInt token1Amount, - required double minPrice, - required double maxPrice, - required bool isMinPriceInfinity, - required bool isMaxPriceInfinity, - required bool isReversed, + double? minPrice, + double? maxPrice, + bool? isMinPriceInfinity, + bool? isMaxPriceInfinity, + bool? isReversed, required Slippage slippage, required Duration deadline, }) async { @@ -170,13 +170,13 @@ class PreviewDepositModalCubit extends Cubit with V3Po BigInt tickLower() { BigInt convertPriceToTickLower() { - if (isMinPriceInfinity && !isReversed) return V3V4PoolConstants.minTick; - if (isReversed && isMaxPriceInfinity) return V3V4PoolConstants.minTick; + if (isMinPriceInfinity! && !(isReversed)!) return V3V4PoolConstants.minTick; + if (isReversed! && isMaxPriceInfinity!) return V3V4PoolConstants.minTick; return priceToTick( - price: isReversed ? maxPrice : minPrice, - poolToken0Decimals: _yield.token0.decimals, - poolToken1Decimals: _yield.token1.decimals, + price: isReversed ? maxPrice! : minPrice!, + poolToken0Decimals: _yield.token0NetworkDecimals, + poolToken1Decimals: _yield.token1NetworkDecimals, isReversed: isReversed, ); } @@ -189,13 +189,13 @@ class PreviewDepositModalCubit extends Cubit with V3Po BigInt tickUpper() { BigInt convertPriceToTickUpper() { - if (isMaxPriceInfinity && !isReversed) return V3V4PoolConstants.maxTick; - if (isReversed && isMinPriceInfinity) return V3V4PoolConstants.maxTick; + if (isMaxPriceInfinity! && !isReversed!) return V3V4PoolConstants.maxTick; + if (isReversed! && isMinPriceInfinity!) return V3V4PoolConstants.maxTick; return priceToTick( - price: isReversed ? minPrice : maxPrice, - poolToken0Decimals: _yield.token0.decimals, - poolToken1Decimals: _yield.token1.decimals, + price: isReversed ? minPrice! : maxPrice!, + poolToken0Decimals: _yield.token0NetworkDecimals, + poolToken1Decimals: _yield.token1NetworkDecimals, isReversed: isReversed, ); } @@ -213,6 +213,18 @@ class PreviewDepositModalCubit extends Cubit with V3Po final recipient = await _wallet.signer!.address; final TransactionResponse tx = await () async { + if (_yield.poolType.isV2) { + return await _poolRepository.sendV2PoolDepositTransaction( + _yield, + _wallet.signer!, + amount0: amount0Desired, + amount1: amount1Desired, + amount0Min: amount0Min, + amount1Min: amount1Min, + deadline: deadline, + ); + } + if (_yield.poolType.isV3) { return await _poolRepository.sendV3PoolDepositTransaction( _yield, @@ -253,8 +265,8 @@ class PreviewDepositModalCubit extends Cubit with V3Po emit(PreviewDepositModalState.depositSuccess(txId: tx.hash)); _zupAnalytics.logDeposit( depositedYield: _yield, - amount0: amount0Desired.parseTokenAmount(decimals: _yield.token0.decimals), - amount1: amount1Desired.parseTokenAmount(decimals: _yield.token1.decimals), + amount0: amount0Desired.parseTokenAmount(decimals: _yield.token0NetworkDecimals), + amount1: amount1Desired.parseTokenAmount(decimals: _yield.token1NetworkDecimals), walletAddress: recipient, ); } catch (e) { diff --git a/lib/app/create/deposit/widgets/range_selector.dart b/lib/app/create/deposit/widgets/range_selector.dart index de702e0..364f433 100644 --- a/lib/app/create/deposit/widgets/range_selector.dart +++ b/lib/app/create/deposit/widgets/range_selector.dart @@ -1,6 +1,5 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; -import 'package:zup_app/core/dtos/token_dto.dart'; import 'package:zup_app/core/extensions/num_extension.dart'; import 'package:zup_app/core/mixins/v3_pool_conversors_mixin.dart'; import 'package:zup_app/core/token_amount_input_formatter.dart'; @@ -45,8 +44,8 @@ class RangeSelectorState { class RangeSelector extends StatefulWidget { const RangeSelector({ super.key, - required this.poolToken0, - required this.poolToken1, + required this.poolToken0Decimals, + required this.poolToken1Decimals, required this.displayBaseTokenSymbol, required this.displayQuoteTokenSymbol, required this.isReversed, @@ -58,8 +57,8 @@ class RangeSelector extends StatefulWidget { this.state = const RangeSelectorState(type: RangeSelectorStateType.regular), }); - final TokenDto poolToken0; - final TokenDto poolToken1; + final int poolToken0Decimals; + final int poolToken1Decimals; final String displayBaseTokenSymbol; final String displayQuoteTokenSymbol; final bool isReversed; @@ -102,8 +101,8 @@ class _RangeSelectorState extends State with V3PoolConversorsMixi double getAdjustedPrice(double price) { final adjustedPrice = priceToClosestValidPrice( price: price, - poolToken0Decimals: widget.poolToken0.decimals, - poolToken1Decimals: widget.poolToken1.decimals, + poolToken0Decimals: widget.poolToken0Decimals, + poolToken1Decimals: widget.poolToken1Decimals, tickSpacing: widget.tickSpacing, isReversed: widget.isReversed, ); @@ -117,13 +116,16 @@ class _RangeSelectorState extends State with V3PoolConversorsMixi if (currentPrice == 0 && !increasing) return; if ((currentPrice == 0 || widget.isInfinity) && increasing) { - final minimumPrice = tickToPrice( - tick: BigInt.from(widget.tickSpacing), - poolToken0Decimals: widget.poolToken0.decimals, - poolToken1Decimals: widget.poolToken1.decimals, + final minimumPrice = priceToClosestValidPrice( + price: 0.000000000000000001, + poolToken0Decimals: widget.poolToken0Decimals, + poolToken1Decimals: widget.poolToken1Decimals, + tickSpacing: widget.tickSpacing, + isReversed: widget.isReversed, ); - userTypedValue = minimumPrice.priceAsBaseToken.toString(); + userTypedValue = minimumPrice.price.toString(); + return adjustTypedAmountAndCallback(); } @@ -131,8 +133,8 @@ class _RangeSelectorState extends State with V3PoolConversorsMixi final BigInt currentTick = tickToClosestValidTick( tick: priceToTick( price: currentPrice, - poolToken0Decimals: widget.poolToken0.decimals, - poolToken1Decimals: widget.poolToken1.decimals, + poolToken0Decimals: widget.poolToken0Decimals, + poolToken1Decimals: widget.poolToken1Decimals, isReversed: widget.isReversed, ), tickSpacing: widget.tickSpacing, @@ -149,8 +151,8 @@ class _RangeSelectorState extends State with V3PoolConversorsMixi double nextPrice() { final nextPrice = tickToPrice( tick: nextTick(), - poolToken0Decimals: widget.poolToken0.decimals, - poolToken1Decimals: widget.poolToken1.decimals, + poolToken0Decimals: widget.poolToken0Decimals, + poolToken1Decimals: widget.poolToken1Decimals, ); if (widget.isReversed) return nextPrice.priceAsQuoteToken; @@ -165,8 +167,8 @@ class _RangeSelectorState extends State with V3PoolConversorsMixi if (widget.initialPrice != null) { final adjustedInitialPrice = priceToClosestValidPrice( price: widget.initialPrice ?? 0, - poolToken0Decimals: widget.poolToken0.decimals, - poolToken1Decimals: widget.poolToken1.decimals, + poolToken0Decimals: widget.poolToken0Decimals, + poolToken1Decimals: widget.poolToken1Decimals, tickSpacing: widget.tickSpacing, isReversed: widget.isReversed, ); diff --git a/lib/app/create/widgets/create_page_settings_dropdown.dart b/lib/app/create/widgets/create_page_settings_dropdown.dart index 8692fc7..cda1056 100644 --- a/lib/app/create/widgets/create_page_settings_dropdown.dart +++ b/lib/app/create/widgets/create_page_settings_dropdown.dart @@ -180,36 +180,59 @@ class _CreatePageSettingsDropdownState extends State ], ), const SizedBox(height: 5), - Row( - children: [ - const Text("V4", style: TextStyle(fontWeight: FontWeight.w500, fontSize: 15)), - const SizedBox(width: 5), - ZupSwitch( - key: const Key("pool-types-allowed-v4-switch"), - value: cache.getPoolSearchSettings().allowV4Search, - onChanged: (value) async { - await cache.savePoolSearchSettings( - settings: cache.getPoolSearchSettings().copyWith(allowV4Search: value), - ); + SizedBox( + width: 200, + child: Wrap( + alignment: WrapAlignment.start, + runAlignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + const Text("V4", style: TextStyle(fontWeight: FontWeight.w500, fontSize: 15)), + const SizedBox(width: 5), + ZupSwitch( + key: const Key("pool-types-allowed-v4-switch"), + value: cache.getPoolSearchSettings().allowV4Search, + onChanged: (value) async { + await cache.savePoolSearchSettings( + settings: cache.getPoolSearchSettings().copyWith(allowV4Search: value), + ); - setState(() {}); - }, - ), - const SizedBox(width: 12), - const Text("V3", style: TextStyle(fontWeight: FontWeight.w500)), - const SizedBox(width: 5), - ZupSwitch( - key: const Key("pool-types-allowed-v3-switch"), - value: cache.getPoolSearchSettings().allowV3Search, - onChanged: (value) async { - await cache.savePoolSearchSettings( - settings: cache.getPoolSearchSettings().copyWith(allowV3Search: value), - ); + setState(() {}); + }, + ), + const SizedBox(width: 12), + const Text("V3", style: TextStyle(fontWeight: FontWeight.w500)), + const SizedBox(width: 5), + ZupSwitch( + key: const Key("pool-types-allowed-v3-switch"), + value: cache.getPoolSearchSettings().allowV3Search, + onChanged: (value) async { + await cache.savePoolSearchSettings( + settings: cache.getPoolSearchSettings().copyWith(allowV3Search: value), + ); - setState(() {}); - }, - ), - ], + setState(() {}); + }, + ), + Row( + children: [ + const Text("V2", style: TextStyle(fontWeight: FontWeight.w500)), + const SizedBox(width: 5), + ZupSwitch( + key: const Key("pool-types-allowed-v2-switch"), + value: cache.getPoolSearchSettings().allowV2Search, + onChanged: (value) async { + await cache.savePoolSearchSettings( + settings: cache.getPoolSearchSettings().copyWith(allowV2Search: value), + ); + + setState(() {}); + }, + ), + ], + ) + ], + ), ), ], ), diff --git a/lib/core/dtos/pool_search_settings_dto.dart b/lib/core/dtos/pool_search_settings_dto.dart index 9565b98..bd63e09 100644 --- a/lib/core/dtos/pool_search_settings_dto.dart +++ b/lib/core/dtos/pool_search_settings_dto.dart @@ -12,6 +12,7 @@ class PoolSearchSettingsDto with _$PoolSearchSettingsDto { @Default(PoolSearchSettingsDto.defaultMinLiquidityUSD) @JsonKey(name: 'min_liquidity_usd') num minLiquidityUSD, @Default(true) bool allowV4Search, @Default(true) bool allowV3Search, + @Default(true) bool allowV2Search, }) = _PoolSearchSettingsDto; const PoolSearchSettingsDto._(); diff --git a/lib/core/dtos/protocol_dto.dart b/lib/core/dtos/protocol_dto.dart index 78c7f44..d184d81 100644 --- a/lib/core/dtos/protocol_dto.dart +++ b/lib/core/dtos/protocol_dto.dart @@ -1,4 +1,5 @@ import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:zup_app/core/enums/protocol_id.dart'; part 'protocol_dto.freezed.dart'; part 'protocol_dto.g.dart'; @@ -9,6 +10,7 @@ class ProtocolDto with _$ProtocolDto { @JsonSerializable(explicitToJson: true) const factory ProtocolDto({ + @Default(ProtocolId.unknown) @JsonKey(unknownEnumValue: ProtocolId.unknown) ProtocolId id, @Default("") String name, @Default("") String url, @Default("") String logo, diff --git a/lib/core/dtos/token_dto.dart b/lib/core/dtos/token_dto.dart index e69ddef..2c20e2a 100644 --- a/lib/core/dtos/token_dto.dart +++ b/lib/core/dtos/token_dto.dart @@ -13,7 +13,7 @@ class TokenDto with _$TokenDto { @Default("") String name, @Default("") String logoUrl, @Default({}) Map addresses, - @Default(0) int decimals, + @Default({}) Map decimals, }) = _TokenDto; factory TokenDto.fromJson(Map json) => _$TokenDtoFromJson(json); @@ -23,6 +23,13 @@ class TokenDto with _$TokenDto { factory TokenDto.fixture() => TokenDto( symbol: 'WETH', name: 'Wrapped Ether', + decimals: Map.fromEntries( + AppNetworks.values.where((network) => !network.isAllNetworks).map( + (network) { + return MapEntry(network.chainId, 18); + }, + ), + ), addresses: Map.fromEntries( AppNetworks.values.where((network) => !network.isAllNetworks).map( (network) { diff --git a/lib/core/dtos/yield_dto.dart b/lib/core/dtos/yield_dto.dart index 0ddd6e7..e405b20 100644 --- a/lib/core/dtos/yield_dto.dart +++ b/lib/core/dtos/yield_dto.dart @@ -41,7 +41,6 @@ class YieldDto with _$YieldDto { required TokenDto token1, required String poolAddress, required String positionManagerAddress, - required int tickSpacing, required ProtocolDto protocol, required int feeTier, required num yield24h, @@ -49,6 +48,7 @@ class YieldDto with _$YieldDto { required num yield90d, required int chainId, required PoolType poolType, + @Default(0) int tickSpacing, @Default(0) num totalValueLockedUSD, @Default(EthereumConstants.zeroAddress) @JsonKey(name: "hooksAddress") String v4Hooks, @JsonKey(name: "poolManagerAddress") String? v4PoolManager, @@ -61,6 +61,12 @@ class YieldDto with _$YieldDto { bool get isToken0Native => token0.addresses[network.chainId] == EthereumConstants.zeroAddress; bool get isToken1Native => token1.addresses[network.chainId] == EthereumConstants.zeroAddress; + int get token0NetworkDecimals => token0.decimals[network.chainId]!; + int get token1NetworkDecimals => token1.decimals[network.chainId]!; + + String get token0NetworkAddress => token0.addresses[network.chainId]!; + String get token1NetworkAddress => token1.addresses[network.chainId]!; + factory YieldDto.fromJson(Map json) => _$YieldDtoFromJson(json); factory YieldDto.fixture() => YieldDto( @@ -73,7 +79,9 @@ class YieldDto with _$YieldDto { poolType: PoolType.v3, token0: TokenDto.fixture().copyWith( symbol: "USDC", - decimals: 6, + decimals: { + AppNetworks.sepolia.chainId: 6, + }, addresses: { AppNetworks.sepolia.chainId: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", }, @@ -82,7 +90,9 @@ class YieldDto with _$YieldDto { ), token1: TokenDto.fixture().copyWith( symbol: "WETH", - decimals: 18, + decimals: { + AppNetworks.sepolia.chainId: 18, + }, addresses: { AppNetworks.sepolia.chainId: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", }, diff --git a/lib/core/dtos/yields_dto.dart b/lib/core/dtos/yields_dto.dart index 15a54a6..2f160f6 100644 --- a/lib/core/dtos/yields_dto.dart +++ b/lib/core/dtos/yields_dto.dart @@ -37,7 +37,7 @@ class YieldsDto with _$YieldsDto { poolType: PoolType.v3, token0: TokenDto( addresses: {11155111: "0x02a3e7E0480B668bD46b42852C58363F93e3bA5C"}, - decimals: 6, + decimals: {11155111: 6}, logoUrl: "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/scroll/assets/0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4/logo.png", name: "USDC", @@ -45,7 +45,7 @@ class YieldsDto with _$YieldsDto { ), token1: TokenDto( addresses: {11155111: "0x5300000000000000000000000000000000000004"}, - decimals: 18, + decimals: {11155111: 18}, logoUrl: "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/scroll/assets/0x5300000000000000000000000000000000000004/logo.png", name: "Wrapped Ether", diff --git a/lib/core/enums/networks.dart b/lib/core/enums/networks.dart index bc1f10d..171b3f5 100644 --- a/lib/core/enums/networks.dart +++ b/lib/core/enums/networks.dart @@ -2,13 +2,13 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:web3kit/web3kit.dart'; -import 'package:zup_app/core/dtos/token_dto.dart'; import 'package:zup_app/gen/assets.gen.dart'; enum AppNetworks { allNetworks, mainnet, base, + bnb, unichain, scroll, sepolia; @@ -47,6 +47,7 @@ enum AppNetworks { allNetworks => false, base => false, unichain => false, + bnb => false }; String get label => switch (this) { @@ -56,6 +57,7 @@ enum AppNetworks { allNetworks => "All Networks", base => "Base", unichain => "Unichain", + bnb => "BNB Chain", }; Widget get icon => switch (this) { @@ -65,6 +67,7 @@ enum AppNetworks { base => Assets.logos.base.svg(), unichain => Assets.logos.unichain.svg(), allNetworks => Assets.icons.all.svg(), + bnb => Assets.logos.bnbChain.svg() }; ChainInfo get chainInfo => switch (this) { @@ -100,10 +103,17 @@ enum AppNetworks { unichain => ChainInfo( hexChainId: "0x82", chainName: label, - blockExplorerUrls: const ["https://uniscan.xyz/"], + blockExplorerUrls: const ["https://uniscan.xyz"], nativeCurrency: NativeCurrencies.eth.currencyInfo, rpcUrls: [rpcUrl], ), + bnb => ChainInfo( + hexChainId: "0x38", + chainName: label, + blockExplorerUrls: const ["https://bscscan.com"], + nativeCurrency: NativeCurrencies.bnb.currencyInfo, + rpcUrls: [rpcUrl], + ), }; String get wrappedNativeTokenAddress => switch (this) { @@ -112,48 +122,8 @@ enum AppNetworks { mainnet => "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", scroll => "0x5300000000000000000000000000000000000004", base => "0x4200000000000000000000000000000000000006", - unichain => "0x4200000000000000000000000000000000000006" - }; - - TokenDto get wrappedNative => switch (this) { - allNetworks => throw UnimplementedError("allNetworks is not a valid network"), - sepolia => TokenDto( - addresses: {chainId: wrappedNativeTokenAddress}, - name: "Wrapped Ether", - decimals: NativeCurrencies.eth.currencyInfo.decimals, - symbol: "WETH", - logoUrl: "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/info/logo.png", - ), - mainnet => TokenDto( - addresses: {chainId: wrappedNativeTokenAddress}, - decimals: NativeCurrencies.eth.currencyInfo.decimals, - name: "Wrapped Ether", - symbol: "WETH", - logoUrl: "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/info/logo.png", - ), - scroll => TokenDto( - addresses: {chainId: wrappedNativeTokenAddress}, - decimals: NativeCurrencies.eth.currencyInfo.decimals, - name: "Wrapped Ether", - symbol: "WETH", - logoUrl: - "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/scroll/assets/0x5300000000000000000000000000000000000004/logo.png", - ), - base => TokenDto( - addresses: {chainId: wrappedNativeTokenAddress}, - decimals: NativeCurrencies.eth.currencyInfo.decimals, - name: "Wrapped Ether", - symbol: "WETH", - logoUrl: - "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/base/assets/0x4200000000000000000000000000000000000006/logo.png", - ), - unichain => TokenDto( - addresses: {chainId: wrappedNativeTokenAddress}, - decimals: NativeCurrencies.eth.currencyInfo.decimals, - name: "Wrapped Ether", - symbol: "WETH", - logoUrl: "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/unichain/logo.png", - ), + unichain => "0x4200000000000000000000000000000000000006", + bnb => "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", }; String get rpcUrl => switch (this) { @@ -163,6 +133,7 @@ enum AppNetworks { scroll => "https://scroll-rpc.publicnode.com", base => "https://base-rpc.publicnode.com", unichain => "https://unichain-rpc.publicnode.com", + bnb => "https://bsc-rpc.publicnode.com" }; Future openTx(String txHash) async { diff --git a/lib/core/enums/pool_type.dart b/lib/core/enums/pool_type.dart index b8945e3..4133305 100644 --- a/lib/core/enums/pool_type.dart +++ b/lib/core/enums/pool_type.dart @@ -1,6 +1,8 @@ import 'package:freezed_annotation/freezed_annotation.dart'; enum PoolType { + @JsonValue("V2") + v2, @JsonValue("V3") v3, @JsonValue("V4") @@ -8,9 +10,11 @@ enum PoolType { bool get isV3 => this == PoolType.v3; bool get isV4 => this == PoolType.v4; + bool get isV2 => this == PoolType.v2; String get label => switch (this) { PoolType.v3 => "V3", PoolType.v4 => "V4", + PoolType.v2 => "V2", }; } diff --git a/lib/core/enums/protocol_id.dart b/lib/core/enums/protocol_id.dart new file mode 100644 index 0000000..e955e37 --- /dev/null +++ b/lib/core/enums/protocol_id.dart @@ -0,0 +1,9 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +enum ProtocolId { + @JsonValue("pancake-v4-cl") + pancakeSwapInfinityCL, + unknown; + + bool get isPancakeSwapInfinityCL => this == ProtocolId.pancakeSwapInfinityCL; +} diff --git a/lib/core/injections.dart b/lib/core/injections.dart index e6decbc..a2ffaf4 100644 --- a/lib/core/injections.dart +++ b/lib/core/injections.dart @@ -7,7 +7,10 @@ import 'package:lottie/lottie.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:web3kit/web3kit.dart'; import 'package:zup_app/abis/erc_20.abi.g.dart'; +import 'package:zup_app/abis/pancake_swap_infinity_cl_pool_manager.abi.g.dart'; import 'package:zup_app/abis/uniswap_permit2.abi.g.dart'; +import 'package:zup_app/abis/uniswap_v2_pool.abi.g.dart'; +import 'package:zup_app/abis/uniswap_v2_router_02.abi.g.dart'; import 'package:zup_app/abis/uniswap_v3_pool.abi.g.dart'; import 'package:zup_app/abis/uniswap_v3_position_manager.abi.g.dart'; import 'package:zup_app/abis/uniswap_v4_position_manager.abi.g.dart'; @@ -115,14 +118,24 @@ Future setupInjections() async { inject.registerLazySingleton(() => EthereumAbiCoder()); + inject.registerLazySingleton( + () => PancakeSwapInfinityClPoolManager(), + ); + + inject.registerLazySingleton(() => UniswapV2Pool()); + + inject.registerLazySingleton(() => UniswapV2Router02()); + inject.registerLazySingleton( () => PoolService( - inject(), - inject(), - inject(), - inject(), - inject(), - ), + inject(), + inject(), + inject(), + inject(), + inject(), + inject(), + inject(), + inject()), ); inject.registerLazySingleton( diff --git a/lib/core/pool_service.dart b/lib/core/pool_service.dart index 8a85d1a..732785d 100644 --- a/lib/core/pool_service.dart +++ b/lib/core/pool_service.dart @@ -1,6 +1,9 @@ import 'package:clock/clock.dart'; import 'package:web3kit/core/dtos/transaction_response.dart'; import 'package:web3kit/web3kit.dart'; +import 'package:zup_app/abis/pancake_swap_infinity_cl_pool_manager.abi.g.dart'; +import 'package:zup_app/abis/uniswap_v2_pool.abi.g.dart'; +import 'package:zup_app/abis/uniswap_v2_router_02.abi.g.dart'; import 'package:zup_app/abis/uniswap_v3_pool.abi.g.dart'; import 'package:zup_app/abis/uniswap_v3_position_manager.abi.g.dart'; import 'package:zup_app/abis/uniswap_v4_position_manager.abi.g.dart'; @@ -12,19 +15,45 @@ import 'package:zup_app/core/v4_pool_constants.dart'; class PoolService with V4PoolLiquidityCalculationsMixin { final UniswapV4StateView _uniswapV4StateView; final UniswapV3Pool _uniswapV3Pool; + final UniswapV2Pool _uniswapV2Pool; final UniswapV3PositionManager _uniswapV3PositionManager; final UniswapV4PositionManager _uniswapV4PositionManager; + final UniswapV2Router02 _uniswapV2Router02; final EthereumAbiCoder _ethereumAbiCoder; + final PancakeSwapInfinityClPoolManager _pancakeSwapInfinityClPoolManager; PoolService( this._uniswapV4StateView, this._uniswapV3Pool, + this._uniswapV2Pool, this._uniswapV3PositionManager, this._uniswapV4PositionManager, + this._uniswapV2Router02, this._ethereumAbiCoder, + this._pancakeSwapInfinityClPoolManager, ); + Future<({BigInt reserve0, BigInt reserve1})> getV2PoolReserves(YieldDto forYield) async { + final poolContract = _uniswapV2Pool.fromRpcProvider( + contractAddress: forYield.poolAddress, + rpcUrl: forYield.network.rpcUrl, + ); + + final reserves = await poolContract.getReserves(); + + return (reserve0: reserves.reserve0, reserve1: reserves.reserve1); + } + Future getPoolTick(YieldDto forYield) async { + if (forYield.protocol.id.isPancakeSwapInfinityCL) { + final pancakeSwapInfinityCLPoolManagerContract = _pancakeSwapInfinityClPoolManager.fromRpcProvider( + contractAddress: forYield.v4PoolManager!, + rpcUrl: forYield.network.rpcUrl, + ); + + return (await pancakeSwapInfinityCLPoolManagerContract.getSlot0(id: forYield.poolAddress)).tick; + } + if (forYield.poolType.isV4) { final stateView = _uniswapV4StateView.fromRpcProvider( contractAddress: forYield.v4StateView!, @@ -34,12 +63,55 @@ class PoolService with V4PoolLiquidityCalculationsMixin { return (await stateView.getSlot0(poolId: forYield.poolAddress)).tick; } - final uniswapV3Pool = _uniswapV3Pool.fromRpcProvider( - contractAddress: forYield.poolAddress, - rpcUrl: forYield.network.rpcUrl, + if (forYield.poolType.isV3) { + final uniswapV3Pool = _uniswapV3Pool.fromRpcProvider( + contractAddress: forYield.poolAddress, + rpcUrl: forYield.network.rpcUrl, + ); + + return (await uniswapV3Pool.slot0()).tick; + } + + throw Exception('Cannot get pool tick for ${forYield.poolType} pool type'); + } + + Future sendV2PoolDepositTransaction( + YieldDto depositOnYield, + Signer signer, { + required BigInt amount0, + required BigInt amount1, + required BigInt amount0Min, + required BigInt amount1Min, + required Duration deadline, + }) async { + final v2RouterContract = _uniswapV2Router02.fromSigner( + contractAddress: depositOnYield.positionManagerAddress, + signer: signer, ); - return (await uniswapV3Pool.slot0()).tick; + if (depositOnYield.isToken0Native || depositOnYield.isToken1Native) { + return v2RouterContract.addLiquidityETH( + token: + depositOnYield.isToken0Native ? depositOnYield.token1NetworkAddress : depositOnYield.token0NetworkAddress, + amountTokenDesired: depositOnYield.isToken0Native ? amount1 : amount0, + amountTokenMin: depositOnYield.isToken0Native ? amount1Min : amount0Min, + amountETHMin: depositOnYield.isToken0Native ? amount0Min : amount1Min, + to: await signer.address, + deadline: BigInt.from(clock.now().add(deadline).millisecondsSinceEpoch), + ethValue: depositOnYield.isToken0Native ? amount0 : amount1, + ); + } + + return await v2RouterContract.addLiquidity( + tokenA: depositOnYield.token0NetworkAddress, + tokenB: depositOnYield.token1NetworkAddress, + amountADesired: amount0, + amountBDesired: amount1, + amountAMin: amount0Min, + amountBMin: amount1Min, + to: await signer.address, + deadline: BigInt.from(clock.now().add(deadline).millisecondsSinceEpoch), + ); } Future sendV3PoolDepositTransaction( diff --git a/lib/core/repositories/yield_repository.dart b/lib/core/repositories/yield_repository.dart index cde644d..cdc18d6 100644 --- a/lib/core/repositories/yield_repository.dart +++ b/lib/core/repositories/yield_repository.dart @@ -23,6 +23,7 @@ class YieldRepository { "allowedPoolTypes": [ if (searchSettings.allowV3Search) "V3", if (searchSettings.allowV4Search) "V4", + if (searchSettings.allowV2Search) "V2", ], } }); diff --git a/lib/l10n/en.arb b/lib/l10n/en.arb index bdea6df..ecfcfbc 100644 --- a/lib/l10n/en.arb +++ b/lib/l10n/en.arb @@ -210,6 +210,17 @@ } } }, + "depositPageInvalidTokenAmountV2Pool": "Enter a valid amount for {token0Symbol} or {token1Symbol}", + "@depositPageInvalidTokenAmountV2Pool": { + "placeholders": { + "token0Symbol": { + "type": "String" + }, + "token1Symbol": { + "type": "String" + } + } + }, "depositPageDeposit": "Deposit", "connectYourWallet": "Connect your wallet", "tokenSelectorModalTitle": "Select a token", diff --git a/lib/l10n/gen/app_localizations.dart b/lib/l10n/gen/app_localizations.dart index e0f6eac..e6d2d5c 100644 --- a/lib/l10n/gen/app_localizations.dart +++ b/lib/l10n/gen/app_localizations.dart @@ -634,6 +634,13 @@ abstract class S { /// **'Please enter an amount for {tokenSymbol}'** String depositPagePleaseEnterAmountForToken({required String tokenSymbol}); + /// No description provided for @depositPageInvalidTokenAmountV2Pool. + /// + /// In en, this message translates to: + /// **'Enter a valid amount for {token0Symbol} or {token1Symbol}'** + String depositPageInvalidTokenAmountV2Pool( + {required String token0Symbol, required String token1Symbol}); + /// No description provided for @depositPageDeposit. /// /// In en, this message translates to: diff --git a/lib/l10n/gen/app_localizations_en.dart b/lib/l10n/gen/app_localizations_en.dart index b4ec1ad..ea3b456 100644 --- a/lib/l10n/gen/app_localizations_en.dart +++ b/lib/l10n/gen/app_localizations_en.dart @@ -337,6 +337,12 @@ class SEn extends S { return 'Please enter an amount for $tokenSymbol'; } + @override + String depositPageInvalidTokenAmountV2Pool( + {required String token0Symbol, required String token1Symbol}) { + return 'Enter a valid amount for $token0Symbol or $token1Symbol'; + } + @override String get depositPageDeposit => 'Deposit'; diff --git a/pubspec.lock b/pubspec.lock index e842ad6..29b70c3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1293,7 +1293,7 @@ packages: description: path: "." ref: main - resolved-ref: "42dfe9f12673e21d2bc15623a6f58a1094969f2c" + resolved-ref: "96c38241bbd55f27820c0fbe0e62ed53f2febed7" url: "https://github.com/Zup-Protocol/web3kit.git" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index 132d90b..3452363 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,13 +53,13 @@ dependencies: firebase_analytics: ^11.4.5 envied: ^1.1.1 -dependency_overrides: - # zup_core: - # path: ../zup-core - # web3kit: - # path: ../web3kit - # zup_ui_kit: - # path: ../zup-ui-kit +# dependency_overrides: +# zup_core: +# path: ../zup-core +# web3kit: +# path: ../web3kit +# zup_ui_kit: +# path: ../zup-ui-kit dev_dependencies: flutter_test: diff --git a/test/app/create/create_page_select_token_stage_test.dart b/test/app/create/create_page_select_token_stage_test.dart index 238a8c0..6860563 100644 --- a/test/app/create/create_page_select_token_stage_test.dart +++ b/test/app/create/create_page_select_token_stage_test.dart @@ -84,7 +84,7 @@ void main() { "When selecting the B token with the same address as A token, it should change the A token to null, and the B token to the selected token", goldenFileName: "create_page_select_tokens_stage_change_b_token_to_same_token_as_a", (tester) async { const selectedNetwork = AppNetworks.sepolia; - final token0 = selectedNetwork.wrappedNative; + final token0 = TokenDto.fixture(); when(() => tokensRepository.getPopularTokens(any())).thenAnswer( (_) async => [token0], @@ -104,7 +104,7 @@ void main() { "When selecting the A token with the same address as B token, it should change the B token to null and the A token to the selected token", goldenFileName: "create_page_select_tokens_stage_change_a_token_to_same_token_as_b", (tester) async { const selectedNetwork = AppNetworks.sepolia; - final token0 = selectedNetwork.wrappedNative; + final token0 = TokenDto.fixture(); when(() => appCubit.currentChainId).thenReturn(selectedNetwork.chainId); when(() => tokensRepository.getPopularTokens(any())).thenAnswer( diff --git a/test/app/create/deposit/deposit_cubit_test.dart b/test/app/create/deposit/deposit_cubit_test.dart index d3d5284..f3e513b 100644 --- a/test/app/create/deposit/deposit_cubit_test.dart +++ b/test/app/create/deposit/deposit_cubit_test.dart @@ -12,6 +12,7 @@ import 'package:zup_app/core/dtos/pool_search_settings_dto.dart'; import 'package:zup_app/core/dtos/yield_dto.dart'; import 'package:zup_app/core/dtos/yields_dto.dart'; import 'package:zup_app/core/enums/networks.dart'; +import 'package:zup_app/core/enums/pool_type.dart'; import 'package:zup_app/core/pool_service.dart'; import 'package:zup_app/core/repositories/yield_repository.dart'; import 'package:zup_app/core/slippage.dart'; @@ -632,4 +633,338 @@ void main() { expect(sut.selectedYieldTimeframe, selectedYieldTimeFrame); }); + + test( + """When calling 'selectYield' and the yield is v2 pool, + it should get the pool reserves from the pool service + and update the variable with the formatted values to double""", + () async { + final expectedReserves = (reserve0: BigInt.from(21578), reserve1: BigInt.from(45678)); + when(() => poolService.getV2PoolReserves(any())).thenAnswer((_) async => expectedReserves); + + final selectedYield = YieldDto.fixture().copyWith(poolType: PoolType.v2); + await sut.selectYield(selectedYield, YieldTimeFrame.day); + + expect( + sut.latestV2PoolReserves!.reserve0, + expectedReserves.reserve0.parseTokenAmount(decimals: selectedYield.token0NetworkDecimals), + ); + + expect( + sut.latestV2PoolReserves!.reserve1, + expectedReserves.reserve1.parseTokenAmount(decimals: selectedYield.token1NetworkDecimals), + ); + + verify(() => poolService.getV2PoolReserves(selectedYield)).called(1); + }, + ); + + test( + """When calling 'selectYield' and the yield is v3 pool, + it should get the pool tick from the pool service + and update the variable with the received values""", + () async { + final expectedTick = BigInt.from(215781527812761); + when(() => poolService.getPoolTick(any())).thenAnswer((_) async => expectedTick); + + final selectedYield = YieldDto.fixture().copyWith(poolType: PoolType.v3); + await sut.selectYield(selectedYield, YieldTimeFrame.day); + + expect( + sut.latestPoolTick!, + expectedTick, + ); + + verify(() => poolService.getPoolTick(selectedYield)).called(1); + }, + ); + + test( + """When calling 'selectYield' and the yield is v4 pool, + it should get the pool tick from the pool service + and update the variable with the received values""", + () async { + final expectedTick = BigInt.from(1716161616161); + when(() => poolService.getPoolTick(any())).thenAnswer((_) async => expectedTick); + + final selectedYield = YieldDto.fixture().copyWith(poolType: PoolType.v4); + await sut.selectYield(selectedYield, YieldTimeFrame.month); + + expect( + sut.latestPoolTick!, + expectedTick, + ); + + verify(() => poolService.getPoolTick(selectedYield)).called(1); + }, + ); + + test( + """When calling 'getSelectedPoolTickOrReserves' and the yield is v2 pool, + it should get the pool reserves from the pool service + and update the variable with the formatted values to double""", + () async { + final expectedReserves = (reserve0: BigInt.from(9781278), reserve1: BigInt.from(11111655666)); + when(() => poolService.getV2PoolReserves(any())).thenAnswer((_) async => expectedReserves); + + final selectedYield = YieldDto.fixture().copyWith(poolType: PoolType.v2); + await sut.selectYield(selectedYield, YieldTimeFrame.day); + + await sut.getSelectedPoolTickOrReserves(); + + expect( + sut.latestV2PoolReserves!.reserve0, + expectedReserves.reserve0.parseTokenAmount(decimals: selectedYield.token0NetworkDecimals), + ); + + expect( + sut.latestV2PoolReserves!.reserve1, + expectedReserves.reserve1.parseTokenAmount(decimals: selectedYield.token1NetworkDecimals), + ); + + verify(() => poolService.getV2PoolReserves(selectedYield)) + .called(2); // 2 because it's lso called when the yield is selected + }, + ); + + test( + """When calling 'getSelectedPoolTickOrReserves' and the yield is v4 pool, + it should get the pool tick from the pool service + and update the variable with the received values""", + () async { + final expectedTick = BigInt.from(1716161616161); + when(() => poolService.getPoolTick(any())).thenAnswer((_) async => BigInt.zero); + + final selectedYield = YieldDto.fixture().copyWith(poolType: PoolType.v4); + await sut.selectYield(selectedYield, YieldTimeFrame.month); + + when(() => poolService.getPoolTick(any())).thenAnswer((_) async => expectedTick); + + await sut.getSelectedPoolTickOrReserves(); + + expect(sut.latestPoolTick!, expectedTick); + + verify(() => poolService.getPoolTick(selectedYield)) + .called(2); // two because it's lso called when the yield is selected + }, + ); + + test( + """When calling 'getSelectedPoolTickOrReserves' and the yield is v3 pool, + it should get the pool tick from the pool service + and update the variable with the received values""", + () async { + final expectedTick = BigInt.from(98987162781); + when(() => poolService.getPoolTick(any())).thenAnswer((_) async => BigInt.zero); + + final selectedYield = YieldDto.fixture().copyWith(poolType: PoolType.v3); + await sut.selectYield(selectedYield, YieldTimeFrame.month); + + when(() => poolService.getPoolTick(any())).thenAnswer((_) async => expectedTick); + await sut.getSelectedPoolTickOrReserves(); + + expect(sut.latestPoolTick!, expectedTick); + + verify(() => poolService.getPoolTick(selectedYield)) + .called(2); // two because it's lso called when the yield is selected + }, + ); + + test( + """When calling 'getSelectedPoolTickOrReserves' and there is no yield selected, + it should not call to get the pool tick or reserves""", + () async { + await sut.getSelectedPoolTickOrReserves(); + + verifyNever(() => poolService.getPoolTick(any())); + verifyNever(() => poolService.getV2PoolReserves(any())); + }, + ); + + test( + """When calling 'getSelectedPoolV2Reserves' and there is no yield selected, + it should not call to get the pool reserves""", + () async { + await sut.getSelectedPoolV2Reserves(); + + verifyNever(() => poolService.getV2PoolReserves(any())); + }, + ); + + test( + """When calling 'getSelectedPoolV2Reserves' it should emit a + new pool reserve in the stream with the formatted values""", + () async { + ({double reserve0, double reserve1})? receivedPoolReserves; + final expectedReserves = (reserve0: BigInt.from(9781278), reserve1: BigInt.from(11111655666)); + + when(() => poolService.getV2PoolReserves(any())).thenAnswer((_) async => expectedReserves); + + final selectedYield = YieldDto.fixture().copyWith(poolType: PoolType.v2); + await sut.selectYield(selectedYield, YieldTimeFrame.day); + + sut.v2PoolReservesStream.listen((poolReserves) { + receivedPoolReserves = poolReserves; + }); + + await sut.getSelectedPoolV2Reserves(); + + await Future.delayed(Duration.zero); + + expect( + receivedPoolReserves!.reserve0, + expectedReserves.reserve0.parseTokenAmount(decimals: selectedYield.token0NetworkDecimals), + ); + + expect( + receivedPoolReserves!.reserve1, + expectedReserves.reserve1.parseTokenAmount(decimals: selectedYield.token1NetworkDecimals), + ); + }, + ); + + test( + """When calling 'getSelectedPoolV2Reserves' it should + update the latest pool reserve variable with the + received values formatted""", + () async { + final expectedReserves = (reserve0: BigInt.from(12516), reserve1: BigInt.from(999991929127)); + + when(() => poolService.getV2PoolReserves(any())).thenAnswer((_) async => expectedReserves); + + final selectedYield = YieldDto.fixture().copyWith(poolType: PoolType.v2); + await sut.selectYield(selectedYield, YieldTimeFrame.day); + + await sut.getSelectedPoolV2Reserves(); + + await Future.delayed(Duration.zero); + + expect( + sut.latestV2PoolReserves!.reserve0, + expectedReserves.reserve0.parseTokenAmount(decimals: selectedYield.token0NetworkDecimals), + ); + + expect( + sut.latestV2PoolReserves!.reserve1, + expectedReserves.reserve1.parseTokenAmount(decimals: selectedYield.token1NetworkDecimals), + ); + }, + ); + + test( + """When calling 'getSelectedPoolV2Reserves' and selecting + another yield while the call is being made, it should get + again the pool reserves for the new selected yield""", + () async { + final selectedYield1 = YieldDto.fixture().copyWith(poolAddress: "0x1", poolType: PoolType.v2); + final selectedYield2 = YieldDto.fixture().copyWith(poolAddress: "0x2", poolType: PoolType.v2); + + final expectedReserves = (reserve0: BigInt.from(12516), reserve1: BigInt.from(999991929127)); + + when(() => poolService.getV2PoolReserves(selectedYield1)).thenAnswer( + (_) async => (reserve0: BigInt.zero, reserve1: BigInt.zero), + ); + + when(() => poolService.getV2PoolReserves(selectedYield2)).thenAnswer((_) async => expectedReserves); + + await sut.selectYield(selectedYield1, YieldTimeFrame.day); + + when(() => poolService.getV2PoolReserves(selectedYield1)).thenAnswer((_) async { + await sut.selectYield(selectedYield2, YieldTimeFrame.day); + + return (reserve0: BigInt.zero, reserve1: BigInt.zero); + }); + + await sut.getSelectedPoolV2Reserves(); + + expect( + sut.latestV2PoolReserves!.reserve0, + expectedReserves.reserve0.parseTokenAmount(decimals: selectedYield2.token0NetworkDecimals), + ); + + expect( + sut.latestV2PoolReserves!.reserve1, + expectedReserves.reserve1.parseTokenAmount(decimals: selectedYield2.token1NetworkDecimals), + ); + + verify(() => poolService.getV2PoolReserves(selectedYield2)) + .called(2); // two because of the selection call and the getSelectedPoolV2Reserves + }, + ); + + test( + """When calling 'getSelectedPoolV2Reserves' and there is already a previous + value in the latest pool reserve, it should make it null before the call""", + () async { + bool testRun = false; + + when(() => poolService.getV2PoolReserves(any())).thenAnswer( + (_) async => (reserve0: BigInt.zero, reserve1: BigInt.zero), + ); + + final selectedYield = YieldDto.fixture().copyWith(poolType: PoolType.v2); + await sut.selectYield(selectedYield, YieldTimeFrame.day); + + when(() => poolService.getV2PoolReserves(any())).thenAnswer( + (_) async { + testRun = true; + expect(sut.latestV2PoolReserves, null); + return (reserve0: BigInt.zero, reserve1: BigInt.zero); + }, + ); + + await sut.getSelectedPoolTickOrReserves(); + + expect(testRun, true); // checker that the call was made and the previous value was null + }, + ); + + test( + """When calling 'getSelectedPoolV2Reserves' it should emit a null reserve in the stream before the call""", + () async { + when(() => poolService.getV2PoolReserves(any())) + .thenAnswer((_) async => (reserve0: BigInt.zero, reserve1: BigInt.zero)); + + final selectedYield = YieldDto.fixture().copyWith(poolType: PoolType.v2); + await sut.selectYield(selectedYield, YieldTimeFrame.day); + + expectLater(sut.v2PoolReservesStream, emits(null)); + + await sut.getSelectedPoolTickOrReserves(); + }, + ); + + test( + """When every 1 minute is passed, it should refresh the pool + reserves in the latest pool reserve variable""", + () async { + const minutesPassed = 5; + when(() => poolService.getV2PoolReserves(any())).thenAnswer( + (_) async => (reserve0: BigInt.zero, reserve1: BigInt.zero), + ); + + final selectedYield = YieldDto.fixture().copyWith(poolType: PoolType.v2); + + fakeAsync((time) async { + final expectedNewReserves = (reserve0: BigInt.from(12516), reserve1: BigInt.from(999991929127)); + await sut.selectYield(selectedYield, YieldTimeFrame.day); + + when(() => poolService.getV2PoolReserves(any())).thenAnswer((_) async => expectedNewReserves); + + time.elapse(const Duration(minutes: minutesPassed)); + + expect( + sut.latestV2PoolReserves!.reserve0, + expectedNewReserves.reserve0.parseTokenAmount(decimals: selectedYield.token0NetworkDecimals), + ); + + expect( + sut.latestV2PoolReserves!.reserve1, + expectedNewReserves.reserve1.parseTokenAmount(decimals: selectedYield.token1NetworkDecimals), + ); + + verify(() => poolService.getV2PoolReserves(selectedYield)).called(minutesPassed + 1); // 1 of the initial call + }); + }, + ); } diff --git a/test/app/create/deposit/deposit_page_test.dart b/test/app/create/deposit/deposit_page_test.dart index 8ff97b5..40dbfda 100644 --- a/test/app/create/deposit/deposit_page_test.dart +++ b/test/app/create/deposit/deposit_page_test.dart @@ -19,10 +19,12 @@ import 'package:zup_app/app/create/deposit/widgets/preview_deposit_modal/preview import 'package:zup_app/core/cache.dart'; import 'package:zup_app/core/dtos/deposit_settings_dto.dart'; import 'package:zup_app/core/dtos/pool_search_settings_dto.dart'; +import 'package:zup_app/core/dtos/protocol_dto.dart'; import 'package:zup_app/core/dtos/token_price_dto.dart'; import 'package:zup_app/core/dtos/yield_dto.dart'; import 'package:zup_app/core/dtos/yields_dto.dart'; import 'package:zup_app/core/enums/networks.dart'; +import 'package:zup_app/core/enums/pool_type.dart'; import 'package:zup_app/core/enums/zup_navigator_paths.dart'; import 'package:zup_app/core/injections.dart'; import 'package:zup_app/core/pool_service.dart'; @@ -2243,4 +2245,361 @@ void main() { verifyNever(() => appCubit.updateAppNetwork(any())); }, ); + + zGoldenTest( + "When selecting a V2 pool yield, it should automatically scroll to the deposit section", + goldenFileName: "deposit_page_select_v2_pool_scroll_to_deposit_section", + (tester) async { + await tester.runAsync(() async { + when(() => cubit.depositSettings).thenReturn(DepositSettingsDto()); + when(() => cubit.v2PoolReservesStream).thenAnswer((_) => const Stream.empty()); + when(() => cubit.latestV2PoolReserves).thenReturn((reserve0: 0, reserve1: 0)); + final selectedYield = YieldDto.fixture().copyWith( + poolType: PoolType.v2, + protocol: ProtocolDto.fixture().copyWith(name: "V2 PPOOL"), + ); + + final yields = YieldsDto.fixture().copyWith(pools: [selectedYield]); + + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.state).thenReturn(DepositState.success(yields)); + when(() => cubit.selectYield(any(), any())).thenAnswer((_) async => () {}); + + await tester.pumpDeviceBuilder(await goldenBuilder(isMobile: true)); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key("yield-card-30d"))); + await tester.pumpAndSettle(); + }); + }, + ); + + zGoldenTest( + """When inputting an amount in the base token amount input and the yield is v2, it should calculate + the quote token amount correctly based on the pool reserves""", + goldenFileName: "deposit_page_calculate_quote_token_amount", + (tester) async { + await tester.runAsync(() async { + const ({double reserve0, double reserve1}) poolReserves = (reserve0: 1, reserve1: 2); + + when(() => cubit.v2PoolReservesStream).thenAnswer((_) => const Stream.empty()); + when(() => cubit.latestV2PoolReserves).thenReturn(poolReserves); + + final selectedYield = YieldDto.fixture().copyWith( + poolType: PoolType.v2, + protocol: ProtocolDto.fixture().copyWith(name: "V2 PPOOL"), + ); + + final yields = YieldsDto.fixture().copyWith(pools: [selectedYield]); + + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.state).thenReturn(DepositState.success(yields)); + when(() => cubit.selectYield(any(), any())).thenAnswer((_) async => () {}); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("v2-pool-base-token-input-card")), "1"); + await tester.pumpAndSettle(); + }); + }, + ); + + zGoldenTest( + """When inputting an amount in the quote token amount input and the yield is v2, it should calculate + the base token amount correctly based on the pool reserves""", + goldenFileName: "deposit_page_calculate_base_token_amount", + (tester) async { + await tester.runAsync(() async { + const ({double reserve0, double reserve1}) poolReserves = (reserve0: 1, reserve1: 2); + + when(() => cubit.v2PoolReservesStream).thenAnswer((_) => const Stream.empty()); + when(() => cubit.latestV2PoolReserves).thenReturn(poolReserves); + + final selectedYield = YieldDto.fixture().copyWith( + poolType: PoolType.v2, + protocol: ProtocolDto.fixture().copyWith(name: "V2 PPOOL"), + ); + + final yields = YieldsDto.fixture().copyWith(pools: [selectedYield]); + + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.state).thenReturn(DepositState.success(yields)); + when(() => cubit.selectYield(any(), any())).thenAnswer((_) async => () {}); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("v2-pool-quote-token-input-card")), "1"); + await tester.pumpAndSettle(); + }); + }, + ); + + zGoldenTest( + """When inputting an amount in the quote token amount input with the yield being v2, + and then deleting the text, it should also clear the amount in the base token amount input""", + goldenFileName: "deposit_page_clear_base_token_amount", + (tester) async { + await tester.runAsync(() async { + const ({double reserve0, double reserve1}) poolReserves = (reserve0: 1, reserve1: 2); + + when(() => cubit.v2PoolReservesStream).thenAnswer((_) => const Stream.empty()); + when(() => cubit.latestV2PoolReserves).thenReturn(poolReserves); + + final selectedYield = YieldDto.fixture().copyWith( + poolType: PoolType.v2, + protocol: ProtocolDto.fixture().copyWith(name: "V2 PPOOL"), + ); + + final yields = YieldsDto.fixture().copyWith(pools: [selectedYield]); + + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.state).thenReturn(DepositState.success(yields)); + when(() => cubit.selectYield(any(), any())).thenAnswer((_) async => () {}); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("v2-pool-quote-token-input-card")), "1"); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("v2-pool-quote-token-input-card")), ""); + await tester.pumpAndSettle(); + }); + }, + ); + + zGoldenTest( + """When inputting an amount in the base token amount input with the yield being v2, + and then deleting the text, it should also clear the amount in the quote token amount input""", + goldenFileName: "deposit_page_clear_quote_token_amount", + (tester) async { + await tester.runAsync(() async { + const ({double reserve0, double reserve1}) poolReserves = (reserve0: 1, reserve1: 2); + + when(() => cubit.v2PoolReservesStream).thenAnswer((_) => const Stream.empty()); + when(() => cubit.latestV2PoolReserves).thenReturn(poolReserves); + + final selectedYield = YieldDto.fixture().copyWith( + poolType: PoolType.v2, + protocol: ProtocolDto.fixture().copyWith(name: "V2 PPOOL"), + ); + + final yields = YieldsDto.fixture().copyWith(pools: [selectedYield]); + + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.state).thenReturn(DepositState.success(yields)); + when(() => cubit.selectYield(any(), any())).thenAnswer((_) async => () {}); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("v2-pool-base-token-input-card")), "1"); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("v2-pool-base-token-input-card")), ""); + await tester.pumpAndSettle(); + }); + }, + ); + + zGoldenTest( + """When a wallet is connected, a v2 yield is selected, and there is no deposit amount typed, + the deposit button should be disabled with text to input the amount""", + goldenFileName: "deposit_page_deposit_v2_pool_button_disabled_input_amount", + (tester) async { + await tester.runAsync(() async { + const ({double reserve0, double reserve1}) poolReserves = (reserve0: 1, reserve1: 2); + final signer = SignerMock(); + final selectedYield = YieldDto.fixture().copyWith( + poolType: PoolType.v2, + protocol: ProtocolDto.fixture().copyWith(name: "V2 PPOOL"), + ); + final yields = YieldsDto.fixture().copyWith(pools: [selectedYield]); + + when(() => cubit.v2PoolReservesStream).thenAnswer((_) => const Stream.empty()); + when(() => cubit.latestV2PoolReserves).thenReturn(poolReserves); + when(() => wallet.signer).thenReturn(signer); + when(() => wallet.signerStream).thenAnswer((_) => Stream.value(signer)); + when(() => signer.address).thenAnswer((_) => Future.value("0x99E3CfADCD8Feecb5DdF91f88998cFfB3145F78c")); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.state).thenReturn(DepositState.success(yields)); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + }); + }, + ); + + zGoldenTest( + """When a wallet is connected, a v2 yield is selected, and the typed amount in the + base token input is less than the available in the wallet, the deposit button should + be disabled with the text indicating that the wallet balance is too low""", + goldenFileName: "deposit_page_deposit_v2_pool_button_disabled_wallet_balance_too_low_base_token", + (tester) async { + await tester.runAsync(() async { + const ({double reserve0, double reserve1}) poolReserves = (reserve0: 1, reserve1: 2); + final signer = SignerMock(); + final selectedYield = YieldDto.fixture().copyWith( + poolType: PoolType.v2, + protocol: ProtocolDto.fixture().copyWith(name: "V2 PPOOL"), + ); + final yields = YieldsDto.fixture().copyWith(pools: [selectedYield]); + const availableWalletAmount = 0.1; + + when(() => cubit.v2PoolReservesStream).thenAnswer((_) => const Stream.empty()); + when(() => cubit.latestV2PoolReserves).thenReturn(poolReserves); + when(() => wallet.signer).thenReturn(signer); + when(() => wallet.signerStream).thenAnswer((_) => Stream.value(signer)); + when(() => signer.address).thenAnswer((_) => Future.value("0x99E3CfADCD8Feecb5DdF91f88998cFfB3145F78c")); + when(() => cubit.getWalletTokenAmount(selectedYield.token1NetworkAddress, network: any(named: "network"))) + .thenAnswer((_) async => 172678152164213); + when(() => cubit.getWalletTokenAmount(selectedYield.token0NetworkAddress, network: any(named: "network"))) + .thenAnswer((_) async => availableWalletAmount); + when(() => wallet.nativeOrTokenBalance(selectedYield.token0NetworkAddress, rpcUrl: any(named: "rpcUrl"))) + .thenAnswer((_) async => availableWalletAmount); + when(() => wallet.nativeOrTokenBalance(selectedYield.token1NetworkAddress, rpcUrl: any(named: "rpcUrl"))) + .thenAnswer((_) async => 172678152164213); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.state).thenReturn(DepositState.success(yields)); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + await tester.enterText( + find.byKey(const Key("v2-pool-base-token-input-card")), + (availableWalletAmount + 0.5).toString(), + ); + await tester.pumpAndSettle(); + }); + }, + ); + + zGoldenTest( + """When a wallet is connected, a v2 yield is selected, and the typed amount in the + quote token input is less than the available in the wallet, the deposit button should + be disabled with the text indicating that the wallet balance is too low""", + goldenFileName: "deposit_page_deposit_v2_pool_button_disabled_wallet_balance_too_low_quote_token", + (tester) async { + await tester.runAsync(() async { + const ({double reserve0, double reserve1}) poolReserves = (reserve0: 1, reserve1: 2); + final signer = SignerMock(); + final selectedYield = YieldDto.fixture().copyWith( + poolType: PoolType.v2, + protocol: ProtocolDto.fixture().copyWith(name: "V2 PPOOL"), + ); + final yields = YieldsDto.fixture().copyWith(pools: [selectedYield]); + const availableWalletAmount = 0.1; + + when(() => cubit.v2PoolReservesStream).thenAnswer((_) => const Stream.empty()); + when(() => cubit.latestV2PoolReserves).thenReturn(poolReserves); + when(() => wallet.signer).thenReturn(signer); + when(() => wallet.signerStream).thenAnswer((_) => Stream.value(signer)); + when(() => signer.address).thenAnswer((_) => Future.value("0x99E3CfADCD8Feecb5DdF91f88998cFfB3145F78c")); + when(() => cubit.getWalletTokenAmount(selectedYield.token0NetworkAddress, network: any(named: "network"))) + .thenAnswer((_) async => 172678152164213); + when(() => cubit.getWalletTokenAmount(selectedYield.token1NetworkAddress, network: any(named: "network"))) + .thenAnswer((_) async => availableWalletAmount); + when(() => wallet.nativeOrTokenBalance(selectedYield.token1NetworkAddress, rpcUrl: any(named: "rpcUrl"))) + .thenAnswer((_) async => availableWalletAmount); + when(() => wallet.nativeOrTokenBalance(selectedYield.token0NetworkAddress, rpcUrl: any(named: "rpcUrl"))) + .thenAnswer((_) async => 172678152164213); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.state).thenReturn(DepositState.success(yields)); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + await tester.enterText( + find.byKey(const Key("v2-pool-quote-token-input-card")), + (availableWalletAmount + 0.5).toString(), + ); + await tester.pumpAndSettle(); + }); + }, + ); + + zGoldenTest( + """When a wallet is connected, a v2 yield is selected, and the amounts to deposit + are available in the wallet, the deposit button should be enabled to preview the deposit""", + goldenFileName: "deposit_page_deposit_v2_pool_button_enabled_preview", + (tester) async { + await tester.runAsync(() async { + const ({double reserve0, double reserve1}) poolReserves = (reserve0: 1, reserve1: 2); + final signer = SignerMock(); + final selectedYield = YieldDto.fixture().copyWith( + poolType: PoolType.v2, + protocol: ProtocolDto.fixture().copyWith(name: "V2 PPOOL"), + ); + final yields = YieldsDto.fixture().copyWith(pools: [selectedYield]); + + when(() => cubit.v2PoolReservesStream).thenAnswer((_) => const Stream.empty()); + when(() => cubit.latestV2PoolReserves).thenReturn(poolReserves); + when(() => wallet.signer).thenReturn(signer); + when(() => wallet.signerStream).thenAnswer((_) => Stream.value(signer)); + when(() => signer.address).thenAnswer((_) => Future.value("0x99E3CfADCD8Feecb5DdF91f88998cFfB3145F78c")); + when(() => cubit.getWalletTokenAmount(any(), network: any(named: "network"))) + .thenAnswer((_) async => 172678152164213); + when(() => wallet.nativeOrTokenBalance(any(), rpcUrl: any(named: "rpcUrl"))) + .thenAnswer((_) async => 172678152164213); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.state).thenReturn(DepositState.success(yields)); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("v2-pool-quote-token-input-card")), 0.1.toString()); + await tester.pumpAndSettle(); + }); + }, + ); + + zGoldenTest( + """When clicking the preview deposit button in the v2 pool, the preview deposit + modal should be shown""", + goldenFileName: "deposit_page_deposit_v2_pool_button_preview_tap", + (tester) async { + await tester.runAsync(() async { + const ({double reserve0, double reserve1}) poolReserves = (reserve0: 1, reserve1: 2); + final signer = SignerMock(); + final selectedYield = YieldDto.fixture().copyWith( + poolType: PoolType.v2, + protocol: ProtocolDto.fixture().copyWith(name: "V2 PPOOL"), + ); + final yields = YieldsDto.fixture().copyWith(pools: [selectedYield]); + + when(() => cubit.v2PoolReservesStream).thenAnswer((_) => const Stream.empty()); + when(() => cubit.latestV2PoolReserves).thenReturn(poolReserves); + when(() => wallet.signer).thenReturn(signer); + when(() => wallet.signerStream).thenAnswer((_) => Stream.value(signer)); + when(() => signer.address).thenAnswer((_) => Future.value("0x99E3CfADCD8Feecb5DdF91f88998cFfB3145F78c")); + when(() => cubit.getWalletTokenAmount(any(), network: any(named: "network"))) + .thenAnswer((_) async => 172678152164213); + when(() => wallet.nativeOrTokenBalance(any(), rpcUrl: any(named: "rpcUrl"))) + .thenAnswer((_) async => 172678152164213); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.state).thenReturn(DepositState.success(yields)); + + await tester.pumpDeviceBuilder(await goldenBuilder(), wrapper: GoldenConfig.localizationsWrapper()); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("v2-pool-quote-token-input-card")), 0.1.toString()); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key("deposit-button"))); + await tester.pumpAndSettle(); + }); + }, + ); } diff --git a/test/app/create/deposit/goldens/deposit_page_calculate_base_token_amount.png b/test/app/create/deposit/goldens/deposit_page_calculate_base_token_amount.png new file mode 100644 index 0000000..fd2120f Binary files /dev/null and b/test/app/create/deposit/goldens/deposit_page_calculate_base_token_amount.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_calculate_quote_token_amount.png b/test/app/create/deposit/goldens/deposit_page_calculate_quote_token_amount.png new file mode 100644 index 0000000..5571409 Binary files /dev/null and b/test/app/create/deposit/goldens/deposit_page_calculate_quote_token_amount.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_clear_base_token_amount.png b/test/app/create/deposit/goldens/deposit_page_clear_base_token_amount.png new file mode 100644 index 0000000..c187d0e Binary files /dev/null and b/test/app/create/deposit/goldens/deposit_page_clear_base_token_amount.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_clear_quote_token_amount.png b/test/app/create/deposit/goldens/deposit_page_clear_quote_token_amount.png new file mode 100644 index 0000000..d674c7b Binary files /dev/null and b/test/app/create/deposit/goldens/deposit_page_clear_quote_token_amount.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_deposit_v2_pool_button_disabled_input_amount.png b/test/app/create/deposit/goldens/deposit_page_deposit_v2_pool_button_disabled_input_amount.png new file mode 100644 index 0000000..a791203 Binary files /dev/null and b/test/app/create/deposit/goldens/deposit_page_deposit_v2_pool_button_disabled_input_amount.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_deposit_v2_pool_button_disabled_wallet_balance_too_low_base_token.png b/test/app/create/deposit/goldens/deposit_page_deposit_v2_pool_button_disabled_wallet_balance_too_low_base_token.png new file mode 100644 index 0000000..a1abd39 Binary files /dev/null and b/test/app/create/deposit/goldens/deposit_page_deposit_v2_pool_button_disabled_wallet_balance_too_low_base_token.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_deposit_v2_pool_button_disabled_wallet_balance_too_low_quote_token.png b/test/app/create/deposit/goldens/deposit_page_deposit_v2_pool_button_disabled_wallet_balance_too_low_quote_token.png new file mode 100644 index 0000000..f06e0ba Binary files /dev/null and b/test/app/create/deposit/goldens/deposit_page_deposit_v2_pool_button_disabled_wallet_balance_too_low_quote_token.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_deposit_v2_pool_button_enabled_preview.png b/test/app/create/deposit/goldens/deposit_page_deposit_v2_pool_button_enabled_preview.png new file mode 100644 index 0000000..704cc35 Binary files /dev/null and b/test/app/create/deposit/goldens/deposit_page_deposit_v2_pool_button_enabled_preview.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_deposit_v2_pool_button_preview_tap.png b/test/app/create/deposit/goldens/deposit_page_deposit_v2_pool_button_preview_tap.png new file mode 100644 index 0000000..6be2b66 Binary files /dev/null and b/test/app/create/deposit/goldens/deposit_page_deposit_v2_pool_button_preview_tap.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_select_v2_pool_scroll_to_deposit_section.png b/test/app/create/deposit/goldens/deposit_page_select_v2_pool_scroll_to_deposit_section.png new file mode 100644 index 0000000..8e39d03 Binary files /dev/null and b/test/app/create/deposit/goldens/deposit_page_select_v2_pool_scroll_to_deposit_section.png differ diff --git a/test/app/create/deposit/widgets/goldens/range_selector_is_infinity_increase_price.png b/test/app/create/deposit/widgets/goldens/range_selector_is_infinity_increase_price.png index ad2038b..5aa5a7c 100644 Binary files a/test/app/create/deposit/widgets/goldens/range_selector_is_infinity_increase_price.png and b/test/app/create/deposit/widgets/goldens/range_selector_is_infinity_increase_price.png differ diff --git a/test/app/create/deposit/widgets/goldens/range_selector_is_infinity_increase_price_reversed.png b/test/app/create/deposit/widgets/goldens/range_selector_is_infinity_increase_price_reversed.png index 5c9e4af..5aa5a7c 100644 Binary files a/test/app/create/deposit/widgets/goldens/range_selector_is_infinity_increase_price_reversed.png and b/test/app/create/deposit/widgets/goldens/range_selector_is_infinity_increase_price_reversed.png differ diff --git a/test/app/create/deposit/widgets/preview_deposit_modal/goldens/preview_deposit_modal_v2_pool_type.png b/test/app/create/deposit/widgets/preview_deposit_modal/goldens/preview_deposit_modal_v2_pool_type.png new file mode 100644 index 0000000..eea5ec3 Binary files /dev/null and b/test/app/create/deposit/widgets/preview_deposit_modal/goldens/preview_deposit_modal_v2_pool_type.png differ diff --git a/test/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal_cubit_test.dart b/test/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal_cubit_test.dart index 464a145..63b47d5 100644 --- a/test/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal_cubit_test.dart +++ b/test/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal_cubit_test.dart @@ -1004,8 +1004,8 @@ void main() { tickLower: V3PoolConversorsMixinWrapper().tickToClosestValidTick( tick: V3PoolConversorsMixinWrapper().priceToTick( price: minPrice, - poolToken0Decimals: currentYield.token0.decimals, - poolToken1Decimals: currentYield.token1.decimals, + poolToken0Decimals: currentYield.token0NetworkDecimals, + poolToken1Decimals: currentYield.token1NetworkDecimals, isReversed: false, ), tickSpacing: currentYield.tickSpacing, @@ -1052,8 +1052,8 @@ void main() { tickLower: V3PoolConversorsMixinWrapper().tickToClosestValidTick( tick: V3PoolConversorsMixinWrapper().priceToTick( price: maxPrice, - poolToken0Decimals: currentYield.token0.decimals, - poolToken1Decimals: currentYield.token1.decimals, + poolToken0Decimals: currentYield.token0NetworkDecimals, + poolToken1Decimals: currentYield.token1NetworkDecimals, isReversed: isReversed, ), tickSpacing: currentYield.tickSpacing, @@ -1177,8 +1177,8 @@ void main() { tickUpper: V3PoolConversorsMixinWrapper().tickToClosestValidTick( tick: V3PoolConversorsMixinWrapper().priceToTick( price: maxPrice, - poolToken0Decimals: currentYield.token0.decimals, - poolToken1Decimals: currentYield.token1.decimals, + poolToken0Decimals: currentYield.token0NetworkDecimals, + poolToken1Decimals: currentYield.token1NetworkDecimals, isReversed: false, ), tickSpacing: currentYield.tickSpacing, @@ -1224,8 +1224,8 @@ void main() { tickUpper: V3PoolConversorsMixinWrapper().tickToClosestValidTick( tick: V3PoolConversorsMixinWrapper().priceToTick( price: minPrice, - poolToken0Decimals: currentYield.token0.decimals, - poolToken1Decimals: currentYield.token1.decimals, + poolToken0Decimals: currentYield.token0NetworkDecimals, + poolToken1Decimals: currentYield.token1NetworkDecimals, isReversed: isReversed, ), tickSpacing: currentYield.tickSpacing, @@ -1702,8 +1702,8 @@ void main() { verify(() => zupAnalytics.logDeposit( depositedYield: currentYield, - amount0: token0amount.parseTokenAmount(decimals: currentYield.token0.decimals), - amount1: token1amount.parseTokenAmount(decimals: currentYield.token1.decimals), + amount0: token0amount.parseTokenAmount(decimals: currentYield.token0NetworkDecimals), + amount1: token1amount.parseTokenAmount(decimals: currentYield.token1NetworkDecimals), walletAddress: userAddress, )).called(1); }, @@ -1936,16 +1936,16 @@ void main() { final tickLower = V3PoolConversorsMixinWrapper().tickToClosestValidTick( tick: V3PoolConversorsMixinWrapper().priceToTick( price: minPrice, - poolToken0Decimals: currentYield0.token0.decimals, - poolToken1Decimals: currentYield0.token1.decimals, + poolToken0Decimals: currentYield0.token0NetworkDecimals, + poolToken1Decimals: currentYield0.token1NetworkDecimals, ), tickSpacing: currentYield0.tickSpacing); final tickUpper = V3PoolConversorsMixinWrapper().tickToClosestValidTick( tick: V3PoolConversorsMixinWrapper().priceToTick( price: maxPrice, - poolToken0Decimals: currentYield0.token0.decimals, - poolToken1Decimals: currentYield0.token1.decimals, + poolToken0Decimals: currentYield0.token0NetworkDecimals, + poolToken1Decimals: currentYield0.token1NetworkDecimals, ), tickSpacing: currentYield0.tickSpacing); @@ -1989,4 +1989,43 @@ void main() { ), ).called(1); }); + + test( + """When calling `deposit` and the deposit pool type is v2, + it should call the pool service to deposit on v2 with the + correct params""", + () async { + const slippage = Slippage.halfPercent; + final amount0 = BigInt.from(1261821789); + final amount1 = BigInt.from(1261821789); + const deadline = Duration(minutes: 30); + + final currentYield0 = currentYield.copyWith(poolType: PoolType.v2); + final sut0 = PreviewDepositModalCubit( + initialPoolTick: initialPoolTick, + poolService: poolService, + currentYield: currentYield0, + erc20: erc20, + wallet: wallet, + uniswapPositionManager: uniswapPositionManager, + permit2: permit2, + navigatorKey: GlobalKey(), + zupAnalytics: zupAnalytics, + ); + + await sut0.deposit(token0Amount: amount0, token1Amount: amount1, slippage: slippage, deadline: deadline); + + verify( + () => poolService.sendV2PoolDepositTransaction( + currentYield0, + signer, + amount0: amount0, + amount1: amount1, + amount0Min: slippage.calculateMinTokenAmountFromSlippage(amount0), + amount1Min: slippage.calculateMinTokenAmountFromSlippage(amount1), + deadline: deadline, + ), + ).called(1); + }, + ); } diff --git a/test/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal_test.dart b/test/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal_test.dart index 0961758..30a4104 100644 --- a/test/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal_test.dart +++ b/test/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal_test.dart @@ -17,6 +17,7 @@ import 'package:zup_app/app/create/deposit/widgets/preview_deposit_modal/preview import 'package:zup_app/core/dtos/token_dto.dart'; import 'package:zup_app/core/dtos/yield_dto.dart'; import 'package:zup_app/core/enums/networks.dart'; +import 'package:zup_app/core/enums/pool_type.dart'; import 'package:zup_app/core/injections.dart'; import 'package:zup_app/core/pool_service.dart'; import 'package:zup_app/core/slippage.dart'; @@ -307,8 +308,8 @@ void main() { final currentPriceAsTick = V3PoolConversorsMixinWrapper().priceToTick( price: currentPrice, - poolToken0Decimals: currentYield.token0.decimals, - poolToken1Decimals: currentYield.token1.decimals, + poolToken0Decimals: currentYield.token0NetworkDecimals, + poolToken1Decimals: currentYield.token1NetworkDecimals, ); when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); @@ -333,8 +334,8 @@ void main() { final currentPriceAsTick = V3PoolConversorsMixinWrapper().priceToTick( price: currentPrice, - poolToken0Decimals: currentYield.token0.decimals, - poolToken1Decimals: currentYield.token1.decimals, + poolToken0Decimals: currentYield.token0NetworkDecimals, + poolToken1Decimals: currentYield.token1NetworkDecimals, ); when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); @@ -362,8 +363,8 @@ void main() { final currentPriceAsTick = V3PoolConversorsMixinWrapper().priceToTick( price: currentPrice, - poolToken0Decimals: currentYield.token0.decimals, - poolToken1Decimals: currentYield.token1.decimals, + poolToken0Decimals: currentYield.token0NetworkDecimals, + poolToken1Decimals: currentYield.token1NetworkDecimals, ); when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); @@ -539,7 +540,7 @@ void main() { verify( () => cubit.approveToken( currentYield.token0, - depositAmount.parseTokenAmount(decimals: currentYield.token0.decimals), + depositAmount.parseTokenAmount(decimals: currentYield.token0NetworkDecimals), ), ).called(1); }, @@ -556,14 +557,14 @@ void main() { when(() => cubit.state).thenReturn( PreviewDepositModalState.initial( - token0Allowance: depositAmount.parseTokenAmount(decimals: currentYield.token0.decimals), + token0Allowance: depositAmount.parseTokenAmount(decimals: currentYield.token0NetworkDecimals), token1Allowance: token1Allowance, ), ); when(() => cubit.stream).thenAnswer((_) { return Stream.value(PreviewDepositModalState.initial( - token0Allowance: depositAmount.parseTokenAmount(decimals: currentYield.token0.decimals), + token0Allowance: depositAmount.parseTokenAmount(decimals: currentYield.token0NetworkDecimals), token1Allowance: token1Allowance, )); }); @@ -591,14 +592,14 @@ void main() { when(() => cubit.state).thenReturn( PreviewDepositModalState.initial( - token0Allowance: depositAmount.parseTokenAmount(decimals: currentYield.token0.decimals), + token0Allowance: depositAmount.parseTokenAmount(decimals: currentYield.token0NetworkDecimals), token1Allowance: token1Allowance, ), ); when(() => cubit.stream).thenAnswer((_) { return Stream.value(PreviewDepositModalState.initial( - token0Allowance: depositAmount.parseTokenAmount(decimals: currentYield.token0.decimals), + token0Allowance: depositAmount.parseTokenAmount(decimals: currentYield.token0NetworkDecimals), token1Allowance: token1Allowance, )); }); @@ -615,7 +616,7 @@ void main() { verify( () => cubit.approveToken( currentYield.token1, - depositAmount.parseTokenAmount(decimals: currentYield.token1.decimals), + depositAmount.parseTokenAmount(decimals: currentYield.token1NetworkDecimals), ), ).called(1); }, @@ -627,8 +628,8 @@ void main() { in the deposit state""", goldenFileName: "preview_deposit_modal_deposit_state", (tester) async { - final token0Allowance = 400.parseTokenAmount(decimals: currentYield.token0.decimals); - final token1Allowance = 1200.parseTokenAmount(decimals: currentYield.token1.decimals); + final token0Allowance = 400.parseTokenAmount(decimals: currentYield.token0NetworkDecimals); + final token1Allowance = 1200.parseTokenAmount(decimals: currentYield.token1NetworkDecimals); const deposit0Amount = 100.2; const deposit1Amount = 110.2; @@ -661,8 +662,8 @@ void main() { in the deposit state. Once the deposit button is clicked, it should call the deposit function in the cubit passing the correct params (got from the constructor)""", (tester) async { - final token0Allowance = 400.parseTokenAmount(decimals: currentYield.token0.decimals); - final token1Allowance = 1200.parseTokenAmount(decimals: currentYield.token1.decimals); + final token0Allowance = 400.parseTokenAmount(decimals: currentYield.token0NetworkDecimals); + final token1Allowance = 1200.parseTokenAmount(decimals: currentYield.token1NetworkDecimals); const deposit0Amount = 100.2; const deposit1Amount = 110.2; @@ -714,8 +715,8 @@ void main() { maxPrice: maxPrice, minPrice: minPrice, slippage: slippage, - token0Amount: deposit0Amount.parseTokenAmount(decimals: currentYield.token0.decimals), - token1Amount: deposit1Amount.parseTokenAmount(decimals: currentYield.token1.decimals), + token0Amount: deposit0Amount.parseTokenAmount(decimals: currentYield.token0NetworkDecimals), + token1Amount: deposit1Amount.parseTokenAmount(decimals: currentYield.token1NetworkDecimals), ), ).called(1); }, @@ -728,8 +729,8 @@ void main() { when(() => cubit.latestPoolTick).thenReturn( V3PoolConversorsMixinWrapper().priceToTick( price: 0.01, // It should be shown in the card (or very close to it) - poolToken0Decimals: currentYield.token0.decimals, - poolToken1Decimals: currentYield.token1.decimals, + poolToken0Decimals: currentYield.token0NetworkDecimals, + poolToken1Decimals: currentYield.token1NetworkDecimals, isReversed: false, ), ); @@ -746,8 +747,8 @@ void main() { when(() => cubit.latestPoolTick).thenReturn( V3PoolConversorsMixinWrapper().priceToTick( price: 1200, // It should be shown in the card (or very close to it) - poolToken0Decimals: currentYield.token0.decimals, - poolToken1Decimals: currentYield.token1.decimals, + poolToken0Decimals: currentYield.token0NetworkDecimals, + poolToken1Decimals: currentYield.token1NetworkDecimals, isReversed: true, ), ); @@ -766,8 +767,8 @@ void main() { const newPrice = 0.02632; // It should be shown in the card (or very close to it) final newPriceAsTick = V3PoolConversorsMixinWrapper().priceToTick( price: newPrice, - poolToken0Decimals: currentYield.token0.decimals, - poolToken1Decimals: currentYield.token1.decimals, + poolToken0Decimals: currentYield.token0NetworkDecimals, + poolToken1Decimals: currentYield.token1NetworkDecimals, isReversed: false, ); @@ -1017,4 +1018,24 @@ void main() { await tester.pumpAndSettle(); }, ); + + zGoldenTest( + "When the pool type is v2, the modal should be adapted to show v2 deposit information", + goldenFileName: "preview_deposit_modal_v2_pool_type", + (tester) async { + when(() => cubit.state).thenReturn( + PreviewDepositModalState.initial( + token0Allowance: BigInt.from(0), + token1Allowance: BigInt.from(0), + ), + ); + + await tester.pumpDeviceBuilder( + await goldenBuilder(customYield: YieldDto.fixture().copyWith(poolType: PoolType.v2)), + wrapper: GoldenConfig.localizationsWrapper(scaffoldMessengerKey: scaffoldMessengerKey), + ); + + await tester.pumpAndSettle(); + }, + ); } diff --git a/test/app/create/deposit/widgets/range_selector_test.dart b/test/app/create/deposit/widgets/range_selector_test.dart index 00d56cb..2464733 100644 --- a/test/app/create/deposit/widgets/range_selector_test.dart +++ b/test/app/create/deposit/widgets/range_selector_test.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:golden_toolkit/golden_toolkit.dart'; import 'package:zup_app/app/create/deposit/widgets/range_selector.dart'; -import 'package:zup_app/core/dtos/token_dto.dart'; import '../../../../golden_config.dart'; @@ -11,8 +10,8 @@ void main() { Key? key, bool isReversed = false, Function(double price)? onPriceChanged, - TokenDto? poolToken0, - TokenDto? poolToken1, + int? poolToken0Decimals, + int? poolToken1Decimals, int tickSpacing = 10, RangeSelectorType type = RangeSelectorType.minPrice, double? initialPrice, @@ -32,8 +31,8 @@ void main() { displayQuoteTokenSymbol: "Token B", isReversed: isReversed, onPriceChanged: onPriceChanged ?? (_) {}, - poolToken0: poolToken0 ?? TokenDto.fixture().copyWith(symbol: "Token A"), - poolToken1: poolToken1 ?? TokenDto.fixture().copyWith(symbol: "Token B"), + poolToken0Decimals: poolToken0Decimals ?? 18, + poolToken1Decimals: poolToken0Decimals ?? 18, tickSpacing: tickSpacing, type: type, initialPrice: initialPrice, @@ -58,11 +57,7 @@ void main() { zGoldenTest("When the `isReversed` param is true, it should reverse the tokens in the widget", goldenFileName: "range_selector_reversed", (tester) async { return tester.pumpDeviceBuilder( - await goldenBuilder( - isReversed: true, - poolToken0: TokenDto.fixture().copyWith(symbol: "Token 0"), - poolToken1: TokenDto.fixture().copyWith(symbol: "Token 1"), - ), + await goldenBuilder(isReversed: true), ); }); @@ -304,13 +299,13 @@ void main() { "When the price is infinity, and click to increase, the price should increase to the minimum price based on the tick spacing", goldenFileName: "range_selector_is_infinity_increase_price", (tester) async { - const expectedIncreasedPrice = 1.0010004501200209e-12; + const expectedIncreasedPrice = 9.996040641477102e-19; double actualIncreasedPrice = 0; await tester.pumpDeviceBuilder(await goldenBuilder( isInfinity: true, - poolToken0: TokenDto.fixture().copyWith(decimals: 6), - poolToken1: TokenDto.fixture().copyWith(decimals: 18), + poolToken0Decimals: 6, + poolToken1Decimals: 18, onPriceChanged: (price) { actualIncreasedPrice = price; }, @@ -328,14 +323,14 @@ void main() { the price should increase to the minimum price based on the tick spacing""", goldenFileName: "range_selector_is_infinity_increase_price_reversed", (tester) async { - const expectedIncreasedPrice = 1.0008055719626048e-12; + const expectedIncreasedPrice = 9.996040641477102e-19; double actualIncreasedPrice = 0; await tester.pumpDeviceBuilder(await goldenBuilder( isInfinity: true, isReversed: true, - poolToken0: TokenDto.fixture().copyWith(decimals: 6), - poolToken1: TokenDto.fixture().copyWith(decimals: 18), + poolToken0Decimals: 6, + poolToken1Decimals: 18, onPriceChanged: (price) { actualIncreasedPrice = price; }, @@ -359,8 +354,8 @@ void main() { await tester.pumpDeviceBuilder(await goldenBuilder( isInfinity: true, isReversed: true, - poolToken0: TokenDto.fixture().copyWith(decimals: 6), - poolToken1: TokenDto.fixture().copyWith(decimals: 18), + poolToken0Decimals: 6, + poolToken1Decimals: 18, onPriceChanged: (price) { actualIncreasedPrice = price; }, @@ -382,8 +377,8 @@ void main() { await tester.pumpDeviceBuilder(await goldenBuilder( isInfinity: true, - poolToken0: TokenDto.fixture().copyWith(decimals: 6), - poolToken1: TokenDto.fixture().copyWith(decimals: 18), + poolToken0Decimals: 6, + poolToken1Decimals: 18, onPriceChanged: (price) { actualIncreasedPrice = price; }, diff --git a/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_open.png b/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_open.png index ebcd14b..6b9b139 100644 Binary files a/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_open.png and b/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_open.png differ diff --git a/test/app/create/widgets/create_page_settings_dropdown_test.dart b/test/app/create/widgets/create_page_settings_dropdown_test.dart index ebfe786..4f5e258 100644 --- a/test/app/create/widgets/create_page_settings_dropdown_test.dart +++ b/test/app/create/widgets/create_page_settings_dropdown_test.dart @@ -280,4 +280,72 @@ void main() { ).called(1); }, ); + + zGoldenTest("When clicking to disable the V2 switch it should update the UI", + goldenFileName: "create_page_settings_dropdown_v2_pool_type_disable", (tester) async { + final initialSettings = PoolSearchSettingsDto(); + when(() => cache.getPoolSearchSettings()).thenReturn(initialSettings); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + when(() => cache.getPoolSearchSettings()).thenReturn( + initialSettings.copyWith(allowV2Search: false), + ); + + await tester.tap(find.byKey(const Key("pool-types-allowed-v2-switch"))); + await tester.pumpAndSettle(); + }); + zGoldenTest("When clicking to enable the V2 switch it should update the UI", + goldenFileName: "create_page_settings_dropdown_v2_pool_type_enable", (tester) async { + final initialSettings = PoolSearchSettingsDto(allowV2Search: false, allowV3Search: false, allowV4Search: false); + + when(() => cache.getPoolSearchSettings()).thenReturn(initialSettings); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + when(() => cache.getPoolSearchSettings()).thenReturn( + initialSettings.copyWith(allowV2Search: true), + ); + + await tester.tap(find.byKey(const Key("pool-types-allowed-v2-switch"))); + await tester.pumpAndSettle(); + }); + + zGoldenTest( + "When clicking to disable the V2 switch it should call the cache to update the settings only for the v2 switch", + (tester) async { + final initialSettings = PoolSearchSettingsDto(allowV2Search: true); + + when(() => cache.getPoolSearchSettings()).thenReturn(initialSettings); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key("pool-types-allowed-v2-switch"))); + await tester.pumpAndSettle(); + + verify( + () => cache.savePoolSearchSettings(settings: initialSettings.copyWith(allowV2Search: false)), + ).called(1); + }); + + zGoldenTest( + "When clicking to enable the V2 switch it should call the cache to update the settings only for the v2 switch", + (tester) async { + final initialSettings = PoolSearchSettingsDto(allowV2Search: false); + + when(() => cache.getPoolSearchSettings()).thenReturn(initialSettings); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key("pool-types-allowed-v2-switch"))); + await tester.pumpAndSettle(); + + verify( + () => cache.savePoolSearchSettings(settings: initialSettings.copyWith(allowV2Search: true)), + ).called(1); + }); } diff --git a/test/app/create/widgets/goldens/create_page_setting_dropdown_min_liquidity_non_numeric.png b/test/app/create/widgets/goldens/create_page_setting_dropdown_min_liquidity_non_numeric.png index f399b79..0776b14 100644 Binary files a/test/app/create/widgets/goldens/create_page_setting_dropdown_min_liquidity_non_numeric.png and b/test/app/create/widgets/goldens/create_page_setting_dropdown_min_liquidity_non_numeric.png differ diff --git a/test/app/create/widgets/goldens/create_page_settings_dropdown.png b/test/app/create/widgets/goldens/create_page_settings_dropdown.png index 8d2ed69..a00b1b4 100644 Binary files a/test/app/create/widgets/goldens/create_page_settings_dropdown.png and b/test/app/create/widgets/goldens/create_page_settings_dropdown.png differ diff --git a/test/app/create/widgets/goldens/create_page_settings_dropdown_low_min_tvl_warning.png b/test/app/create/widgets/goldens/create_page_settings_dropdown_low_min_tvl_warning.png index 88cf67b..8ebce06 100644 Binary files a/test/app/create/widgets/goldens/create_page_settings_dropdown_low_min_tvl_warning.png and b/test/app/create/widgets/goldens/create_page_settings_dropdown_low_min_tvl_warning.png differ diff --git a/test/app/create/widgets/goldens/create_page_settings_dropdown_min_liquidity_tooltip.png b/test/app/create/widgets/goldens/create_page_settings_dropdown_min_liquidity_tooltip.png index 63d3ca9..0587391 100644 Binary files a/test/app/create/widgets/goldens/create_page_settings_dropdown_min_liquidity_tooltip.png and b/test/app/create/widgets/goldens/create_page_settings_dropdown_min_liquidity_tooltip.png differ diff --git a/test/app/create/widgets/goldens/create_page_settings_dropdown_min_liquidity_warning_field.png b/test/app/create/widgets/goldens/create_page_settings_dropdown_min_liquidity_warning_field.png index fcc6d03..a112446 100644 Binary files a/test/app/create/widgets/goldens/create_page_settings_dropdown_min_liquidity_warning_field.png and b/test/app/create/widgets/goldens/create_page_settings_dropdown_min_liquidity_warning_field.png differ diff --git a/test/app/create/widgets/goldens/create_page_settings_dropdown_pool_types_tooltip_hover.png b/test/app/create/widgets/goldens/create_page_settings_dropdown_pool_types_tooltip_hover.png index 501f6fe..58ab69b 100644 Binary files a/test/app/create/widgets/goldens/create_page_settings_dropdown_pool_types_tooltip_hover.png and b/test/app/create/widgets/goldens/create_page_settings_dropdown_pool_types_tooltip_hover.png differ diff --git a/test/app/create/widgets/goldens/create_page_settings_dropdown_v2_pool_type_disable.png b/test/app/create/widgets/goldens/create_page_settings_dropdown_v2_pool_type_disable.png new file mode 100644 index 0000000..ac97e97 Binary files /dev/null and b/test/app/create/widgets/goldens/create_page_settings_dropdown_v2_pool_type_disable.png differ diff --git a/test/app/create/widgets/goldens/create_page_settings_dropdown_v2_pool_type_enable.png b/test/app/create/widgets/goldens/create_page_settings_dropdown_v2_pool_type_enable.png new file mode 100644 index 0000000..ec5eb19 Binary files /dev/null and b/test/app/create/widgets/goldens/create_page_settings_dropdown_v2_pool_type_enable.png differ diff --git a/test/app/create/widgets/goldens/create_page_settings_dropdown_v3_pool_type_disabled.png b/test/app/create/widgets/goldens/create_page_settings_dropdown_v3_pool_type_disabled.png index 268ed13..52084d6 100644 Binary files a/test/app/create/widgets/goldens/create_page_settings_dropdown_v3_pool_type_disabled.png and b/test/app/create/widgets/goldens/create_page_settings_dropdown_v3_pool_type_disabled.png differ diff --git a/test/app/create/widgets/goldens/create_page_settings_dropdown_v3_pool_type_enable.png b/test/app/create/widgets/goldens/create_page_settings_dropdown_v3_pool_type_enable.png index c619409..0dab400 100644 Binary files a/test/app/create/widgets/goldens/create_page_settings_dropdown_v3_pool_type_enable.png and b/test/app/create/widgets/goldens/create_page_settings_dropdown_v3_pool_type_enable.png differ diff --git a/test/app/create/widgets/goldens/create_page_settings_dropdown_v4_pool_type_disabled.png b/test/app/create/widgets/goldens/create_page_settings_dropdown_v4_pool_type_disabled.png index 303eebf..e4c2400 100644 Binary files a/test/app/create/widgets/goldens/create_page_settings_dropdown_v4_pool_type_disabled.png and b/test/app/create/widgets/goldens/create_page_settings_dropdown_v4_pool_type_disabled.png differ diff --git a/test/app/create/widgets/goldens/create_page_settings_dropdown_v4_pool_type_enable.png b/test/app/create/widgets/goldens/create_page_settings_dropdown_v4_pool_type_enable.png index 650fd94..e296b64 100644 Binary files a/test/app/create/widgets/goldens/create_page_settings_dropdown_v4_pool_type_enable.png and b/test/app/create/widgets/goldens/create_page_settings_dropdown_v4_pool_type_enable.png differ diff --git a/test/core/enums/goldens/bnb_network_icon.png b/test/core/enums/goldens/bnb_network_icon.png new file mode 100644 index 0000000..198aeeb Binary files /dev/null and b/test/core/enums/goldens/bnb_network_icon.png differ diff --git a/test/core/enums/networks_test.dart b/test/core/enums/networks_test.dart index a51c3ed..82d2e44 100644 --- a/test/core/enums/networks_test.dart +++ b/test/core/enums/networks_test.dart @@ -3,7 +3,6 @@ import 'package:golden_toolkit/golden_toolkit.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; import 'package:web3kit/core/dtos/chain_info.dart'; import 'package:web3kit/core/enums/native_currencies.dart'; -import 'package:zup_app/core/dtos/token_dto.dart'; import 'package:zup_app/core/enums/networks.dart'; import '../../golden_config.dart'; @@ -24,6 +23,7 @@ void main() { expect(AppNetworks.fromValue("allNetworks"), AppNetworks.allNetworks, reason: "All networks should match"); expect(AppNetworks.fromValue("base"), AppNetworks.base, reason: "Base should match"); expect(AppNetworks.fromValue("unichain"), AppNetworks.unichain, reason: "Unichain should match"); + expect(AppNetworks.fromValue("bnb"), AppNetworks.bnb, reason: "BNB should match"); }); test("Label extension should match for all networks", () { @@ -33,6 +33,7 @@ void main() { expect(AppNetworks.allNetworks.label, "All Networks", reason: "All Networks Label should match"); expect(AppNetworks.base.label, "Base", reason: "Base Label should match"); expect(AppNetworks.unichain.label, "Unichain", reason: "Unichain Label should match"); + expect(AppNetworks.bnb.label, "BNB Chain", reason: "BNB Chain Label should match"); }); test("`testnets` method should return all testnets in the enum, excluding the 'all networks'", () { @@ -48,6 +49,7 @@ void main() { AppNetworks.scroll, AppNetworks.base, AppNetworks.unichain, + AppNetworks.bnb ]), ); }); @@ -72,6 +74,10 @@ void main() { expect(AppNetworks.unichain.isTestnet, false); }); + test("`isTestnet` method should return false for bnb", () { + expect(AppNetworks.bnb.isTestnet, false); + }); + test("Chain info extension should match for all networks", () { expect( AppNetworks.sepolia.chainInfo, @@ -125,12 +131,24 @@ void main() { ChainInfo( hexChainId: "0x82", chainName: "Unichain", - blockExplorerUrls: const ["https://uniscan.xyz/"], + blockExplorerUrls: const ["https://uniscan.xyz"], nativeCurrency: NativeCurrencies.eth.currencyInfo, rpcUrls: const ["https://unichain-rpc.publicnode.com"], ), reason: "Unichain ChainInfo should match", ); + + expect( + AppNetworks.bnb.chainInfo, + ChainInfo( + hexChainId: "0x38", + chainName: "BNB Chain", + blockExplorerUrls: const ["https://bscscan.com"], + nativeCurrency: NativeCurrencies.bnb.currencyInfo, + rpcUrls: const ["https://bsc-rpc.publicnode.com"], + ), + reason: "BNB Chain ChainInfo should match", + ); }); test("wrapped native token address should match for all networks", () { @@ -163,79 +181,11 @@ void main() { "0x4200000000000000000000000000000000000006", reason: "Unichain wrapped native token address should match", ); - }); - test("wrapped native token should match for all networks", () { expect( - AppNetworks.sepolia.wrappedNative, - TokenDto( - addresses: { - AppNetworks.sepolia.chainId: "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14", - }, - name: "Wrapped Ether", - decimals: 18, - symbol: "WETH", - logoUrl: "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/info/logo.png", - ), - reason: "Sepolia default token should match", - ); - - expect( - AppNetworks.mainnet.wrappedNative, - TokenDto( - addresses: { - AppNetworks.mainnet.chainId: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - }, - name: "Wrapped Ether", - decimals: 18, - symbol: "WETH", - logoUrl: "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/info/logo.png", - ), - reason: "Ethereum default token should match", - ); - - expect( - AppNetworks.scroll.wrappedNative, - TokenDto( - addresses: { - AppNetworks.scroll.chainId: "0x5300000000000000000000000000000000000004", - }, - name: "Wrapped Ether", - decimals: 18, - symbol: "WETH", - logoUrl: - "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/scroll/assets/0x5300000000000000000000000000000000000004/logo.png", - ), - reason: "Scroll default token should match", - ); - - expect( - AppNetworks.base.wrappedNative, - TokenDto( - addresses: { - AppNetworks.base.chainId: "0x4200000000000000000000000000000000000006", - }, - name: "Wrapped Ether", - decimals: 18, - symbol: "WETH", - logoUrl: - "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/base/assets/0x4200000000000000000000000000000000000006/logo.png", - ), - reason: "Base default token should match", - ); - - expect( - AppNetworks.unichain.wrappedNative, - TokenDto( - addresses: { - AppNetworks.unichain.chainId: "0x4200000000000000000000000000000000000006", - }, - name: "Wrapped Ether", - decimals: 18, - symbol: "WETH", - logoUrl: "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/unichain/logo.png", - ), - reason: "Unichain default token should match", + AppNetworks.bnb.wrappedNativeTokenAddress, + "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", + reason: "BNB wrapped native token address should match", ); }); @@ -269,6 +219,12 @@ void main() { "https://unichain-rpc.publicnode.com", reason: "Unichain rpc url should match", ); + + expect( + AppNetworks.bnb.rpcUrl, + "https://bsc-rpc.publicnode.com", + reason: "BNB rpc url should match", + ); }); test("openTx should open the correct url for each network", () async { @@ -350,4 +306,11 @@ void main() { device: GoldenDevice.square, )); }); + + zGoldenTest("BNB network icon should match", goldenFileName: "bnb_network_icon", (tester) async { + await tester.pumpDeviceBuilder(await goldenDeviceBuilder( + AppNetworks.bnb.icon, + device: GoldenDevice.square, + )); + }); } diff --git a/test/core/enums/pool_type_test.dart b/test/core/enums/pool_type_test.dart index ddcfa80..b19fb17 100644 --- a/test/core/enums/pool_type_test.dart +++ b/test/core/enums/pool_type_test.dart @@ -18,8 +18,17 @@ void main() { expect(PoolType.v3.isV4, false); }); + test('When calling `isV2` and the pool is indeed v2, it should return true', () { + expect(PoolType.v2.isV2, true); + }); + + test('When calling `isV2` and the pool is not v2, it should return false', () { + expect(PoolType.v3.isV2, false); + }); + test('label should return correct string', () { expect(PoolType.v3.label, "V3"); expect(PoolType.v4.label, "V4"); + expect(PoolType.v2.label, "V2"); }); } diff --git a/test/core/enums/protocol_id_test.dart b/test/core/enums/protocol_id_test.dart new file mode 100644 index 0000000..b2d22a2 --- /dev/null +++ b/test/core/enums/protocol_id_test.dart @@ -0,0 +1,18 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:zup_app/core/enums/protocol_id.dart'; + +void main() { + test( + "When calling `isPancakeSwapInfinityCL` and the protocol is indeed pancakeSwapInfinityCL, it should return true", + () { + expect(ProtocolId.pancakeSwapInfinityCL.isPancakeSwapInfinityCL, true); + }, + ); + + test( + "When calling `isPancakeSwapInfinityCL` and the protocol is not pancakeSwapInfinityCL, it should return false", + () { + expect(ProtocolId.unknown.isPancakeSwapInfinityCL, false); + }, + ); +} diff --git a/test/core/pool_service_test.dart b/test/core/pool_service_test.dart index c19d037..4a875af 100644 --- a/test/core/pool_service_test.dart +++ b/test/core/pool_service_test.dart @@ -4,16 +4,22 @@ import 'package:mocktail/mocktail.dart'; import 'package:web3kit/core/dtos/transaction_receipt.dart'; import 'package:web3kit/core/dtos/transaction_response.dart'; import 'package:web3kit/web3kit.dart'; +import 'package:zup_app/abis/pancake_swap_infinity_cl_pool_manager.abi.g.dart'; +import 'package:zup_app/abis/uniswap_v2_pool.abi.g.dart'; +import 'package:zup_app/abis/uniswap_v2_router_02.abi.g.dart'; import 'package:zup_app/abis/uniswap_v3_pool.abi.g.dart'; import 'package:zup_app/abis/uniswap_v3_position_manager.abi.g.dart'; import 'package:zup_app/abis/uniswap_v4_position_manager.abi.g.dart'; import 'package:zup_app/abis/uniswap_v4_state_view.abi.g.dart'; +import 'package:zup_app/core/dtos/protocol_dto.dart'; import 'package:zup_app/core/dtos/token_dto.dart'; import 'package:zup_app/core/dtos/yield_dto.dart'; import 'package:zup_app/core/enums/networks.dart'; import 'package:zup_app/core/enums/pool_type.dart'; +import 'package:zup_app/core/enums/protocol_id.dart'; import 'package:zup_app/core/mixins/v4_pool_liquidity_calculations_mixin.dart'; import 'package:zup_app/core/pool_service.dart'; +import 'package:zup_app/core/slippage.dart'; import 'package:zup_app/core/v4_pool_constants.dart'; import '../mocks.dart'; @@ -24,8 +30,11 @@ void main() { late PoolService sut; late UniswapV4StateView stateView; late UniswapV3Pool uniswapV3Pool; + late UniswapV2Pool uniswapV2Pool; + late UniswapV2Router02 uniswapV2Router02; late UniswapV3PositionManager positionManagerV3; late UniswapV4PositionManager positionManagerV4; + late PancakeSwapInfinityClPoolManager pancakeSwapInfinityCLPoolManager; late Signer signer; late YieldDto currentYield; late TransactionResponse transactionResponse; @@ -34,6 +43,9 @@ void main() { late UniswapV3PoolImpl uniswapV3PoolImpl; late UniswapV3PositionManagerImpl positionManagerV3Impl; late UniswapV4PositionManagerImpl positionManagerV4Impl; + late PancakeSwapInfinityClPoolManagerImpl pancakeSwapInfinityCLPoolManagerImpl; + late UniswapV2PoolImpl uniswapV2PoolImpl; + late UniswapV2Router02Impl uniswapV2Router02Impl; late EthereumAbiCoder ethereumAbiCoder; setUp(() { @@ -56,11 +68,17 @@ void main() { transactionResponse = TransactionResponseMock(); stateView = UniswapV4StateViewMock(); uniswapV3Pool = UniswapV3PoolMock(); + uniswapV2Pool = UniswapV2PoolMock(); + uniswapV2Router02 = UniswapV2Router02Mock(); positionManagerV3 = UniswapV3PositionManagerMock(); positionManagerV4 = UniswapV4PositionManagerMock(); + pancakeSwapInfinityCLPoolManager = PancakeSwapInfinityCLPoolManagerMock(); ethereumAbiCoder = EthereumAbiCoderMock(); signer = SignerMock(); + pancakeSwapInfinityCLPoolManagerImpl = PancakeSwapInfinityCLPoolManagerImplMock(); + uniswapV2PoolImpl = UniswapV2PoolImplMock(); + uniswapV2Router02Impl = UniswapV2Router02ImplMock(); stateViewImpl = UniswapV4StateViewImplMock(); uniswapV3PoolImpl = UniswapV3PoolImplMock(); positionManagerV3Impl = UniswapV3PositionManagerImplMock(); @@ -68,7 +86,25 @@ void main() { currentYield = YieldDto.fixture(); - sut = PoolService(stateView, uniswapV3Pool, positionManagerV3, positionManagerV4, ethereumAbiCoder); + sut = PoolService( + stateView, + uniswapV3Pool, + uniswapV2Pool, + positionManagerV3, + positionManagerV4, + uniswapV2Router02, + ethereumAbiCoder, + pancakeSwapInfinityCLPoolManager, + ); + + when(() => + uniswapV2Pool.fromRpcProvider(contractAddress: any(named: "contractAddress"), rpcUrl: any(named: "rpcUrl"))) + .thenReturn(uniswapV2PoolImpl); + + when(() => uniswapV2Router02.fromRpcProvider( + contractAddress: any(named: "contractAddress"), + rpcUrl: any(named: "rpcUrl"), + )).thenReturn(uniswapV2Router02Impl); when(() => stateView.fromRpcProvider(contractAddress: any(named: "contractAddress"), rpcUrl: any(named: "rpcUrl"))) .thenReturn(stateViewImpl); @@ -98,6 +134,9 @@ void main() { when(() => transactionResponse.waitConfirmation()).thenAnswer((_) async => TransactionReceipt(hash: "0x123")); when(() => transactionResponse.hash).thenReturn("0x123"); + when(() => + uniswapV2Pool.fromRpcProvider(contractAddress: any(named: "contractAddress"), rpcUrl: any(named: "rpcUrl"))) + .thenReturn(uniswapV2PoolImpl); }); test( @@ -110,6 +149,12 @@ void main() { sqrtPriceX96: BigInt.from(0), tick: expectedTick, )); + when(() => pancakeSwapInfinityCLPoolManagerImpl.getSlot0(id: any(named: "id"))).thenAnswer((_) async => ( + lpFee: BigInt.from(0), + protocolFee: BigInt.from(0), + sqrtPriceX96: BigInt.from(0), + tick: expectedTick, + )); final currentYield0 = currentYield.copyWith(poolType: PoolType.v4, v4StateView: "0x123"); final result = await sut.getPoolTick(currentYield0); @@ -254,7 +299,7 @@ void main() { recipient: recipient, tickLower: tickLower, tickUpper: tickUpper, - token0: network.wrappedNative.addresses[network.chainId]!, + token0: network.wrappedNativeTokenAddress, token1: token1Address, )), ).called(1); @@ -1331,4 +1376,257 @@ void main() { ).called(1); }, ); + + test( + """"When calling `getPoolTick` and the yield protocol is pancakeswap infinity cl, + it should use the pancakeswap inifity cl pool manager to get the tick""", + () async { + final expectedTick = BigInt.from(318675); + + when(() => pancakeSwapInfinityCLPoolManager.fromRpcProvider( + contractAddress: any(named: "contractAddress"), rpcUrl: any(named: "rpcUrl"))).thenReturn( + pancakeSwapInfinityCLPoolManagerImpl, + ); + + when(() => pancakeSwapInfinityCLPoolManagerImpl.getSlot0(id: any(named: "id"))).thenAnswer((_) async => ( + sqrtPriceX96: BigInt.from(0), + tick: expectedTick, + protocolFee: BigInt.from(0), + lpFee: BigInt.from(0), + )); + + final yield0 = currentYield.copyWith( + protocol: ProtocolDto.fixture().copyWith(id: ProtocolId.pancakeSwapInfinityCL), + v4PoolManager: "0x0000001", + ); + + final receivedPoolTick = await sut.getPoolTick(yield0); + expect(receivedPoolTick, expectedTick); + + verify(() => pancakeSwapInfinityCLPoolManagerImpl.getSlot0(id: yield0.poolAddress)).called(1); + }, + ); + + test( + "When calling 'getV2PoolReserves' it should call the pool contract to get it and return the result", + () async { + final expectedReserves = (reserve0: BigInt.from(861287), reserve1: BigInt.from(98687)); + + when(() => uniswapV2PoolImpl.getReserves()).thenAnswer( + (_) async => ( + reserve0: expectedReserves.reserve0, + reserve1: expectedReserves.reserve1, + blockTimestampLast: BigInt.from(0) + ), + ); + + final receivedReserves = await sut.getV2PoolReserves(currentYield); + expect(receivedReserves, expectedReserves); + }, + ); + + test( + "When calling `sendV2PoolDepositTransaction` it should connect the passed signer to the v2 router contract", + () async { + when(() => uniswapV2Router02.fromSigner( + contractAddress: any(named: "contractAddress"), + signer: any(named: "signer"))).thenReturn(uniswapV2Router02Impl); + when( + () => uniswapV2Router02Impl.addLiquidity( + tokenA: any(named: "tokenA"), + tokenB: any(named: "tokenB"), + amountADesired: any(named: "amountADesired"), + amountBDesired: any(named: "amountBDesired"), + amountAMin: any(named: "amountAMin"), + amountBMin: any(named: "amountBMin"), + to: any(named: "to"), + deadline: any(named: "deadline")), + ).thenAnswer((_) async => transactionResponse); + + await sut.sendV2PoolDepositTransaction( + currentYield, + signer, + amount0: BigInt.from(0), + amount1: BigInt.from(0), + amount0Min: BigInt.from(0), + amount1Min: BigInt.from(0), + deadline: Duration.zero, + ); + + verify( + () => uniswapV2Router02.fromSigner(contractAddress: currentYield.positionManagerAddress, signer: signer), + ).called(1); + }, + ); + + test( + """When calling `sendV2PoolDepositTransaction` and the pool token0 is native, + it should use the `addLiquidityETH` method with the correct params""", + () async { + withClock(Clock.fixed(DateTime(2027)), () async { + when(() => uniswapV2Router02.fromSigner( + contractAddress: any(named: "contractAddress"), + signer: any(named: "signer"))).thenReturn(uniswapV2Router02Impl); + + when( + () => uniswapV2Router02Impl.addLiquidityETH( + token: any(named: "token"), + amountETHMin: any(named: "amountETHMin"), + amountTokenDesired: any(named: "amountTokenDesired"), + amountTokenMin: any(named: "amountTokenMin"), + ethValue: any(named: "ethValue"), + to: any(named: "to"), + deadline: any(named: "deadline")), + ).thenAnswer((_) async => transactionResponse); + + final amount0 = BigInt.from(12718929112712516); + final amount1 = BigInt.from(88627236); + final amount0Min = Slippage.halfPercent.calculateMinTokenAmountFromSlippage(amount0); + final amount1Min = Slippage.halfPercent.calculateMinTokenAmountFromSlippage(amount1); + const deadline = Duration(minutes: 3211); + final walletSigner = signer; + final signerAddress = await signer.address; + + await sut.sendV2PoolDepositTransaction( + currentYield.copyWith( + token0: TokenDto.fixture().copyWith( + addresses: {currentYield.chainId: EthereumConstants.zeroAddress}, + ), + ), + walletSigner, + amount0: amount0, + amount1: amount1, + amount0Min: amount0Min, + amount1Min: amount1Min, + deadline: deadline, + ); + + verify( + () => uniswapV2Router02Impl.addLiquidityETH( + token: currentYield.token1NetworkAddress, + amountTokenDesired: amount1, + amountTokenMin: amount1Min, + amountETHMin: amount0Min, + to: signerAddress, + deadline: BigInt.from(clock.now().add(deadline).millisecondsSinceEpoch), + ethValue: amount0, + ), + ).called(1); + }); + }, + ); + + test( + """When calling `sendV2PoolDepositTransaction` and the pool token1 is native, + it should use the `addLiquidityETH` method with the correct params""", + () async { + withClock(Clock.fixed(DateTime(1998)), () async { + when(() => uniswapV2Router02.fromSigner( + contractAddress: any(named: "contractAddress"), + signer: any(named: "signer"))).thenReturn(uniswapV2Router02Impl); + + when( + () => uniswapV2Router02Impl.addLiquidityETH( + token: any(named: "token"), + amountETHMin: any(named: "amountETHMin"), + amountTokenDesired: any(named: "amountTokenDesired"), + amountTokenMin: any(named: "amountTokenMin"), + ethValue: any(named: "ethValue"), + to: any(named: "to"), + deadline: any(named: "deadline")), + ).thenAnswer((_) async => transactionResponse); + + final amount0 = BigInt.from(12718929112712516); + final amount1 = BigInt.from(88627236); + final amount0Min = Slippage.halfPercent.calculateMinTokenAmountFromSlippage(amount0); + final amount1Min = Slippage.halfPercent.calculateMinTokenAmountFromSlippage(amount1); + const deadline = Duration(minutes: 3211); + final walletSigner = signer; + final signerAddress = await signer.address; + + await sut.sendV2PoolDepositTransaction( + currentYield.copyWith( + token1: TokenDto.fixture().copyWith( + addresses: {currentYield.chainId: EthereumConstants.zeroAddress}, + ), + ), + walletSigner, + amount0: amount0, + amount1: amount1, + amount0Min: amount0Min, + amount1Min: amount1Min, + deadline: deadline, + ); + + verify( + () => uniswapV2Router02Impl.addLiquidityETH( + token: currentYield.token0NetworkAddress, + amountTokenDesired: amount0, + amountTokenMin: amount0Min, + amountETHMin: amount1Min, + to: signerAddress, + deadline: BigInt.from(clock.now().add(deadline).millisecondsSinceEpoch), + ethValue: amount1, + ), + ).called(1); + }); + }, + ); + + test( + """When calling `sendV2PoolDepositTransaction` it should correctly + call the correct function in the router to deposit with the correct + params""", + () async { + withClock(Clock.fixed(DateTime(1500)), () async { + when(() => uniswapV2Router02.fromSigner( + contractAddress: any(named: "contractAddress"), + signer: any(named: "signer"))).thenReturn(uniswapV2Router02Impl); + + when( + () => uniswapV2Router02Impl.addLiquidity( + amountADesired: any(named: "amountADesired"), + to: any(named: "to"), + deadline: any(named: "deadline"), + amountAMin: any(named: "amountAMin"), + amountBDesired: any(named: "amountBDesired"), + amountBMin: any(named: "amountBMin"), + tokenA: any(named: "tokenA"), + tokenB: any(named: "tokenB"), + ), + ).thenAnswer((_) async => transactionResponse); + + final amount0 = BigInt.from(12718929112712516); + final amount1 = BigInt.from(88627236); + final amount0Min = Slippage.halfPercent.calculateMinTokenAmountFromSlippage(amount0); + final amount1Min = Slippage.halfPercent.calculateMinTokenAmountFromSlippage(amount1); + const deadline = Duration(minutes: 3211); + final walletSigner = signer; + final signerAddress = await signer.address; + + await sut.sendV2PoolDepositTransaction( + currentYield, + walletSigner, + amount0: amount0, + amount1: amount1, + amount0Min: amount0Min, + amount1Min: amount1Min, + deadline: deadline, + ); + + verify( + () => uniswapV2Router02Impl.addLiquidity( + amountADesired: amount0, + amountAMin: amount0Min, + amountBDesired: amount1, + amountBMin: amount1Min, + tokenA: currentYield.token0NetworkAddress, + tokenB: currentYield.token1NetworkAddress, + to: signerAddress, + deadline: BigInt.from(clock.now().add(deadline).millisecondsSinceEpoch), + ), + ).called(1); + }); + }, + ); } diff --git a/test/core/repositories/yield_repository_test.dart b/test/core/repositories/yield_repository_test.dart index 2962c19..c4c8980 100644 --- a/test/core/repositories/yield_repository_test.dart +++ b/test/core/repositories/yield_repository_test.dart @@ -51,6 +51,7 @@ void main() { "allowedPoolTypes": [ "V3", "V4", + "V2", ], } }), @@ -89,6 +90,7 @@ void main() { "minTvlUsd": searchSettings.minLiquidityUSD, "allowedPoolTypes": [ "V4", + "V2", ], } }), @@ -125,7 +127,43 @@ void main() { }, data: { "filters": { "minTvlUsd": searchSettings.minLiquidityUSD, - "allowedPoolTypes": ["V3"], + "allowedPoolTypes": ["V3", "V2"], + } + }), + ).called(1); + }); + + test("When the V2 Pool is disabled in the search settings, it should not be included in the request", () async { + final yields = YieldsDto.fixture(); + + when(() => dio.post(any(), queryParameters: any(named: "queryParameters"), data: any(named: "data"))).thenAnswer( + (_) async => Response( + data: {"bestYields": yields.toJson()}, + statusCode: 200, + requestOptions: RequestOptions(), + ), + ); + + const token0Address = "0x123"; + const token1Address = "0x456"; + const network = AppNetworks.sepolia; + final searchSettings = PoolSearchSettingsDto.fixture().copyWith(allowV2Search: false); + + await sut.getSingleNetworkYield( + token0Address: token0Address, + token1Address: token1Address, + network: network, + searchSettings: searchSettings, + ); + + verify( + () => dio.post("/pools/search/${network.chainId}", queryParameters: { + "token0Address": token0Address, + "token1Address": token1Address + }, data: { + "filters": { + "minTvlUsd": searchSettings.minLiquidityUSD, + "allowedPoolTypes": ["V3", "V4"], } }), ).called(1); diff --git a/test/mocks.dart b/test/mocks.dart index f167e89..e0a7927 100644 --- a/test/mocks.dart +++ b/test/mocks.dart @@ -12,7 +12,10 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. import 'package:web3kit/core/dtos/transaction_response.dart'; import 'package:web3kit/web3kit.dart'; import 'package:zup_app/abis/erc_20.abi.g.dart'; +import 'package:zup_app/abis/pancake_swap_infinity_cl_pool_manager.abi.g.dart'; import 'package:zup_app/abis/uniswap_permit2.abi.g.dart'; +import 'package:zup_app/abis/uniswap_v2_pool.abi.g.dart'; +import 'package:zup_app/abis/uniswap_v2_router_02.abi.g.dart'; import 'package:zup_app/abis/uniswap_v3_pool.abi.g.dart'; import 'package:zup_app/abis/uniswap_v3_position_manager.abi.g.dart'; import 'package:zup_app/abis/uniswap_v4_position_manager.abi.g.dart'; @@ -87,10 +90,22 @@ class UniswapV4PositionManagerMock extends Mock implements UniswapV4PositionMana class UniswapV4PositionManagerImplMock extends Mock implements UniswapV4PositionManagerImpl {} +class PancakeSwapInfinityCLPoolManagerMock extends Mock implements PancakeSwapInfinityClPoolManager {} + +class PancakeSwapInfinityCLPoolManagerImplMock extends Mock implements PancakeSwapInfinityClPoolManagerImpl {} + class UniswapV3PoolImplMock extends Mock implements UniswapV3PoolImpl {} class UniswapV3PoolMock extends Mock implements UniswapV3Pool {} +class UniswapV2PoolMock extends Mock implements UniswapV2Pool {} + +class UniswapV2PoolImplMock extends Mock implements UniswapV2PoolImpl {} + +class UniswapV2Router02Mock extends Mock implements UniswapV2Router02 {} + +class UniswapV2Router02ImplMock extends Mock implements UniswapV2Router02Impl {} + class WalletMock extends Mock implements Wallet {} class YieldRepositoryMock extends Mock implements YieldRepository {} diff --git a/test/widgets/token_selector_modal/token_selector_modal_cubit_test.dart b/test/widgets/token_selector_modal/token_selector_modal_cubit_test.dart index 3f2868a..0ac65fb 100644 --- a/test/widgets/token_selector_modal/token_selector_modal_cubit_test.dart +++ b/test/widgets/token_selector_modal/token_selector_modal_cubit_test.dart @@ -279,8 +279,8 @@ void main() { test("""When calling 'searchToken' and all the tokens in the list returned does not have name and symbol, it should emit the search not found state""", () async { final returnedList = [ - const TokenDto(name: "", symbol: "", decimals: 0, logoUrl: "", addresses: {}), - const TokenDto(name: "", symbol: "", decimals: 0, logoUrl: "", addresses: {}), + TokenDto.fixture().copyWith(name: "", symbol: "", logoUrl: "", addresses: {}), + TokenDto.fixture().copyWith(name: "", symbol: "", decimals: {}, logoUrl: "", addresses: {}), ]; when(() => tokensRepository.searchToken(any(), any())).thenAnswer((_) async => returnedList); @@ -294,8 +294,8 @@ void main() { it should emit the search sucesss state, without the tokens without name and symbol""", () async { final namedToken = TokenDto.fixture(); final returnedList = [ - const TokenDto(name: "", symbol: "", decimals: 0, logoUrl: "", addresses: {}), - const TokenDto(name: "", symbol: "", decimals: 0, logoUrl: "", addresses: {}), + TokenDto.fixture().copyWith(name: "", symbol: "", logoUrl: "", addresses: {}), + TokenDto.fixture().copyWith(name: "", symbol: "", logoUrl: "", addresses: {}), namedToken, ];