diff --git a/.fvmrc b/.fvmrc index 4cac08f..3135e2b 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.29.2" + "flutter": "3.32.2" } \ No newline at end of file diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 2a75e03..de75e55 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -30,7 +30,7 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: "stable" - flutter-version: 3.29.2 + flutter-version: 3.32.2 - name: Install dependencies run: flutter pub get && dart run build_runner build --delete-conflicting-outputs && flutter gen-l10n && dart run routefly && dart run web3kit:generate_abis - name: Run tests diff --git a/.vscode/launch.json b/.vscode/launch.json index 1638f1f..ad61127 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,6 +10,9 @@ "flutterMode": "release", "toolArgs": [ "--dart-define=env=local", + ], + "args": [ + "--web-experimental-hot-reload" ] }, { @@ -21,6 +24,9 @@ "flutterMode": "debug", "toolArgs": [ "--dart-define=env=local", + ], + "args": [ + "--web-experimental-hot-reload" ] }, { @@ -31,6 +37,9 @@ "program": "./lib/main.dart", "toolArgs": [ "--dart-define=env=local", + ], + "args": [ + "--web-experimental-hot-reload" ] }, ] diff --git a/.vscode/settings.json b/.vscode/settings.json index e4ebc86..de056e2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { - "dart.flutterSdkPath": ".fvm/versions/3.29.2", + "dart.flutterSdkPath": ".fvm/versions/3.32.2", "dart.lineLength": 120 } \ No newline at end of file diff --git a/assets/icons/text.document.svg b/assets/icons/text.document.svg new file mode 100644 index 0000000..21695be --- /dev/null +++ b/assets/icons/text.document.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/assets/logos/unichain.svg b/assets/logos/unichain.svg new file mode 100644 index 0000000..d1015bd --- /dev/null +++ b/assets/logos/unichain.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/abis/uniswap_permit2.abi.json b/lib/abis/uniswap_permit2.abi.json new file mode 100644 index 0000000..d1380fa --- /dev/null +++ b/lib/abis/uniswap_permit2.abi.json @@ -0,0 +1,69 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint160", + "name": "amount", + "type": "uint160" + }, + { + "internalType": "uint48", + "name": "expiration", + "type": "uint48" + }, + { + "internalType": "uint48", + "name": "nonce", + "type": "uint48" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint160", + "name": "amount", + "type": "uint160" + }, + { + "internalType": "uint48", + "name": "expiration", + "type": "uint48" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/lib/abis/uniswap_position_manager.abi.json b/lib/abis/uniswap_v3_position_manager.abi.json similarity index 100% rename from lib/abis/uniswap_position_manager.abi.json rename to lib/abis/uniswap_v3_position_manager.abi.json diff --git a/lib/abis/uniswap_v4_position_manager.abi.json b/lib/abis/uniswap_v4_position_manager.abi.json new file mode 100644 index 0000000..5562e4c --- /dev/null +++ b/lib/abis/uniswap_v4_position_manager.abi.json @@ -0,0 +1,20 @@ +[ + { + "type": "function", + "name": "modifyLiquidities", + "inputs": [ + { + "name": "unlockData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "payable" + } +] \ No newline at end of file diff --git a/lib/abis/uniswap_v4_state_view.abi.json b/lib/abis/uniswap_v4_state_view.abi.json new file mode 100644 index 0000000..fe9cd4f --- /dev/null +++ b/lib/abis/uniswap_v4_state_view.abi.json @@ -0,0 +1,365 @@ +[ + { + "inputs": [ + { + "internalType": "contract IPoolManager", + "name": "_poolManager", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + } + ], + "name": "getFeeGrowthGlobals", + "outputs": [ + { + "internalType": "uint256", + "name": "feeGrowthGlobal0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthGlobal1", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + } + ], + "name": "getFeeGrowthInside", + "outputs": [ + { + "internalType": "uint256", + "name": "feeGrowthInside0X128", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthInside1X128", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + } + ], + "name": "getLiquidity", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "positionId", + "type": "bytes32" + } + ], + "name": "getPositionInfo", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "feeGrowthInside0LastX128", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthInside1LastX128", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + } + ], + "name": "getPositionInfo", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "feeGrowthInside0LastX128", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthInside1LastX128", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "positionId", + "type": "bytes32" + } + ], + "name": "getPositionLiquidity", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "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" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "int16", + "name": "tick", + "type": "int16" + } + ], + "name": "getTickBitmap", + "outputs": [ + { + "internalType": "uint256", + "name": "tickBitmap", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "int24", + "name": "tick", + "type": "int24" + } + ], + "name": "getTickFeeGrowthOutside", + "outputs": [ + { + "internalType": "uint256", + "name": "feeGrowthOutside0X128", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthOutside1X128", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "int24", + "name": "tick", + "type": "int24" + } + ], + "name": "getTickInfo", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidityGross", + "type": "uint128" + }, + { + "internalType": "int128", + "name": "liquidityNet", + "type": "int128" + }, + { + "internalType": "uint256", + "name": "feeGrowthOutside0X128", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthOutside1X128", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "int24", + "name": "tick", + "type": "int24" + } + ], + "name": "getTickLiquidity", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidityGross", + "type": "uint128" + }, + { + "internalType": "int128", + "name": "liquidityNet", + "type": "int128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "poolManager", + "outputs": [ + { + "internalType": "contract IPoolManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/lib/app/create/create_page_select_tokens_stage.dart b/lib/app/create/create_page_select_tokens_stage.dart index 1ca78bf..5989e8e 100644 --- a/lib/app/create/create_page_select_tokens_stage.dart +++ b/lib/app/create/create_page_select_tokens_stage.dart @@ -127,27 +127,35 @@ class _CreatePageState extends State with DeviceInf ), const Spacer(), StatefulBuilder(builder: (context, localSetState) { - return Badge( - alignment: const Alignment(0.8, -0.8), - smallSize: cache.getPoolSearchSettings().isDefault ? 0 : 6, - child: ZupIconButton( - key: const Key("pool-search-settings-button"), - backgroundColor: Colors.transparent, - icon: Assets.icons.gear.svg(height: 18), - padding: const EdgeInsets.all(10), - iconColor: ZupColors.brand, - onPressed: (buttonContext) => CreatePageSettingsDropdown.show( - buttonContext, - onClose: () { - if (mounted) WidgetsBinding.instance.addPostFrameCallback((_) => localSetState(() {})); - }, + return Row( + children: [ + Badge( + alignment: const Alignment(1.05, -1.05), + smallSize: cache.getPoolSearchSettings().isDefault ? 0 : 6, + backgroundColor: ZupColors.orange, + child: ZupPillButton( + key: const Key("pool-search-settings-button"), + onPressed: (buttonContext) => CreatePageSettingsDropdown.show( + buttonContext, + onClose: () { + if (mounted) { + WidgetsBinding.instance.addPostFrameCallback((_) => localSetState(() {})); + } + }, + ), + foregroundColor: ZupColors.gray, + backgroundColor: ZupColors.gray6, + title: "Search settings", + icon: Assets.icons.gear.svg( + height: 18, colorFilter: const ColorFilter.mode(ZupColors.white, BlendMode.srcIn)), + ), ), - ), + ], ); }) ], ), - const SizedBox(height: 5), + const SizedBox(height: 12), TokenSelectorButton( key: const Key("token-a-selector"), controller: token0SelectorController, diff --git a/lib/app/create/deposit/deposit_cubit.dart b/lib/app/create/deposit/deposit_cubit.dart index b83ec28..0f30cd1 100644 --- a/lib/app/create/deposit/deposit_cubit.dart +++ b/lib/app/create/deposit/deposit_cubit.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:web3kit/web3kit.dart'; -import 'package:zup_app/abis/uniswap_v3_pool.abi.g.dart'; import 'package:zup_app/app/app_cubit/app_cubit.dart'; import 'package:zup_app/core/cache.dart'; import 'package:zup_app/core/dtos/deposit_settings_dto.dart'; @@ -13,6 +12,7 @@ import 'package:zup_app/core/dtos/yields_dto.dart'; import 'package:zup_app/core/enums/networks.dart'; import 'package:zup_app/core/mixins/keys_mixin.dart'; import 'package:zup_app/core/mixins/v3_pool_conversors_mixin.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'; import 'package:zup_app/core/zup_analytics.dart'; @@ -26,16 +26,16 @@ class DepositCubit extends Cubit with KeysMixin, V3PoolConversorsM this._yieldRepository, this._zupSingletonCache, this._wallet, - this._uniswapV3Pool, this._cache, this._appCubit, this._zupAnalytics, + this._poolService, ) : super(const DepositState.initial()); final YieldRepository _yieldRepository; final ZupSingletonCache _zupSingletonCache; final Wallet _wallet; - final UniswapV3Pool _uniswapV3Pool; + final PoolService _poolService; final Cache _cache; final AppCubit _appCubit; final ZupAnalytics _zupAnalytics; @@ -81,14 +81,14 @@ class DepositCubit extends Cubit with KeysMixin, V3PoolConversorsM ? await _yieldRepository.getAllNetworksYield( token0InternalId: token0AddressOrId, token1InternalId: token1AddressOrId, - minTvlUsd: ignoreMinLiquidity ? 0 : poolSearchSettings.minLiquidityUSD, + searchSettings: ignoreMinLiquidity ? poolSearchSettings.copyWith(minLiquidityUSD: 0) : poolSearchSettings, testnetMode: _appCubit.isTestnetMode, ) : await _yieldRepository.getSingleNetworkYield( token0Address: token0AddressOrId, token1Address: token1AddressOrId, network: _appCubit.selectedNetwork, - minTvlUsd: ignoreMinLiquidity ? 0 : poolSearchSettings.minLiquidityUSD, + searchSettings: ignoreMinLiquidity ? poolSearchSettings.copyWith(minLiquidityUSD: 0) : poolSearchSettings, ); if (yields.isEmpty) { @@ -118,17 +118,12 @@ class DepositCubit extends Cubit with KeysMixin, V3PoolConversorsM _pooltickStreamController.add(null); final selectedYieldBeforeCall = selectedYield; + BigInt tick = await _poolService.getPoolTick(selectedYieldBeforeCall!); - final uniswapV3Pool = _uniswapV3Pool.fromRpcProvider( - contractAddress: selectedYieldBeforeCall!.poolAddress, - rpcUrl: selectedYieldBeforeCall.network.rpcUrl, - ); - - final slot0 = await uniswapV3Pool.slot0(); if (selectedYieldBeforeCall != selectedYield) return await getSelectedPoolTick(); - _pooltickStreamController.add(slot0.tick); - _latestPoolTick = slot0.tick; + _pooltickStreamController.add(tick); + _latestPoolTick = tick; } Future getWalletTokenAmount(String tokenAddress, {required AppNetworks network}) async { diff --git a/lib/app/create/deposit/deposit_page.dart b/lib/app/create/deposit/deposit_page.dart index 87f7204..d7c933d 100644 --- a/lib/app/create/deposit/deposit_page.dart +++ b/lib/app/create/deposit/deposit_page.dart @@ -6,7 +6,6 @@ import 'package:intl/intl.dart'; import 'package:lottie/lottie.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:web3kit/web3kit.dart'; -import 'package:zup_app/abis/uniswap_v3_pool.abi.g.dart'; import 'package:zup_app/app/app_cubit/app_cubit.dart'; import 'package:zup_app/app/create/deposit/deposit_cubit.dart'; import 'package:zup_app/app/create/deposit/widgets/deposit_settings_dropdown_child.dart'; @@ -26,9 +25,10 @@ import 'package:zup_app/core/extensions/widget_extension.dart'; import 'package:zup_app/core/injections.dart'; import 'package:zup_app/core/mixins/v3_pool_conversors_mixin.dart'; import 'package:zup_app/core/mixins/v3_pool_liquidity_calculations_mixin.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'; -import 'package:zup_app/core/v3_pool_constants.dart'; +import 'package:zup_app/core/v3_v4_pool_constants.dart'; import 'package:zup_app/core/zup_analytics.dart'; import 'package:zup_app/core/zup_navigator.dart'; import 'package:zup_app/gen/assets.gen.dart'; @@ -47,10 +47,10 @@ Route routeBuilder(BuildContext context, RouteSettings settings) { inject(), inject(), inject(), - inject(), inject(), inject(), inject(), + inject(), ), child: const DepositPage(), ), @@ -87,31 +87,11 @@ class _DepositPageState extends State String get token0Address => _navigator.getParam(ZupNavigatorPaths.deposit.routeParamsName?.param0 ?? "") ?? ""; String get token1Address => _navigator.getParam(ZupNavigatorPaths.deposit.routeParamsName?.param1 ?? "") ?? ""; TokenDto get baseToken { - return areTokensReversed - ? _cubit.selectedYield!.maybeNativeToken1(permitNative: depositWithNativeToken) - : _cubit.selectedYield!.maybeNativeToken0(permitNative: depositWithNativeToken); + return areTokensReversed ? _cubit.selectedYield!.token1 : _cubit.selectedYield!.token0; } TokenDto get quoteToken { - return areTokensReversed - ? _cubit.selectedYield!.maybeNativeToken0(permitNative: depositWithNativeToken) - : _cubit.selectedYield!.maybeNativeToken1(permitNative: depositWithNativeToken); - } - - bool get isBaseTokenNative { - final currentChain = _cubit.selectedYield!.network.chainId; - - return (_cubit.selectedYield!.network.wrappedNative.addresses[currentChain]! - .lowercasedEquals(baseToken.addresses[currentChain]!) && - depositWithNativeToken); - } - - bool get isQuoteTokenNative { - final currentChain = _cubit.selectedYield!.network.chainId; - - return (_cubit.selectedYield!.network.wrappedNative.addresses[currentChain]! - .lowercasedEquals(quoteToken.addresses[currentChain]!) && - depositWithNativeToken); + return areTokensReversed ? _cubit.selectedYield!.token0 : _cubit.selectedYield!.token1; } bool areTokensReversed = false; @@ -121,8 +101,6 @@ class _DepositPageState extends State double minPrice = 0; double maxPrice = 0; - late bool depositWithNativeToken = true; - late Slippage selectedSlippage = _cubit.depositSettings.slippage; late Duration selectedDeadline = _cubit.depositSettings.deadline; @@ -206,13 +184,13 @@ class _DepositPageState extends State if (isOutOfRange.maxPrice) return baseTokenAmountController.clear(); final maxTickPrice = tickToPrice( - tick: V3PoolConstants.maxTick, + tick: V3V4PoolConstants.maxTick, poolToken0Decimals: _cubit.selectedYield!.token0.decimals, poolToken1Decimals: _cubit.selectedYield!.token1.decimals, ); final minTickPrice = tickToPrice( - tick: V3PoolConstants.minTick, + tick: V3V4PoolConstants.minTick, poolToken0Decimals: _cubit.selectedYield!.token0.decimals, poolToken1Decimals: _cubit.selectedYield!.token1.decimals, ); @@ -258,12 +236,12 @@ class _DepositPageState extends State Future<({String title, Widget? icon, Function()? onPressed})> depositButtonState() async { final userWalletBaseTokenAmount = await _cubit.getWalletTokenAmount( - isBaseTokenNative ? EthereumConstants.zeroAddress : baseToken.addresses[_cubit.selectedYield!.network.chainId]!, + baseToken.addresses[_cubit.selectedYield!.network.chainId]!, network: _cubit.selectedYield!.network, ); final userWalletQuoteTokenAmount = await _cubit.getWalletTokenAmount( - isQuoteTokenNative ? EthereumConstants.zeroAddress : quoteToken.addresses[_cubit.selectedYield!.network.chainId]!, + quoteToken.addresses[_cubit.selectedYield!.network.chainId]!, network: _cubit.selectedYield!.network, ); @@ -308,7 +286,6 @@ class _DepositPageState extends State PreviewDepositModal( key: const Key("preview-deposit-modal"), yieldTimeFrame: _cubit.selectedYieldTimeframe!, - depositWithNativeToken: depositWithNativeToken, deadline: selectedDeadline, maxSlippage: selectedSlippage, currentYield: _cubit.selectedYield!, @@ -697,7 +674,7 @@ class _DepositPageState extends State child: IgnorePointer( ignoring: true, child: Text( - "${_cubit.selectedYield?.maybeNativeToken0(permitNative: depositWithNativeToken).symbol} / ${_cubit.selectedYield?.maybeNativeToken1(permitNative: depositWithNativeToken).symbol}", + "${_cubit.selectedYield?.token0.symbol} / ${_cubit.selectedYield?.token1.symbol}", style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500), ), ), @@ -708,7 +685,7 @@ class _DepositPageState extends State child: IgnorePointer( ignoring: true, child: Text( - "${_cubit.selectedYield?.maybeNativeToken1(permitNative: depositWithNativeToken).symbol} / ${_cubit.selectedYield?.maybeNativeToken0(permitNative: depositWithNativeToken).symbol}", + "${_cubit.selectedYield?.token1.symbol} / ${_cubit.selectedYield?.token0.symbol}", style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500), )), ), @@ -866,36 +843,14 @@ class _DepositPageState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (baseToken.addresses[_cubit.selectedYield!.network.chainId]!.lowercasedEquals(_cubit - .selectedYield!.network.wrappedNative.addresses[_cubit.selectedYield!.network.chainId]!) || - quoteToken.addresses[_cubit.selectedYield!.network.chainId]!.lowercasedEquals( - _cubit.selectedYield!.network.wrappedNative.addresses[_cubit.selectedYield!.network.chainId]!, - )) - Row( - children: [ - _sectionTitle(S.of(context).depositPageDepositSectionTitle), - const Spacer(), - Text( - S.of(context).depositPageDepositWithNativeToken( - tokenSymbol: _cubit.selectedYield?.network.chainInfo.nativeCurrency!.symbol ?? "", - ), - style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500), - ), - const SizedBox(width: 10), - ZupSwitch( - key: const Key("deposit-with-native-token-switch"), - value: depositWithNativeToken, - onChanged: (value) => setState(() => depositWithNativeToken = value), - ), - ], - ), + _sectionTitle(S.of(context).depositPageDepositSectionTitle), const SizedBox(height: 12), TokenAmountInputCard( key: const Key("base-token-input-card"), token: baseToken, - isNative: depositWithNativeToken && - baseToken.addresses[_cubit.selectedYield!.network.chainId]!.lowercasedEquals(_cubit - .selectedYield!.network.wrappedNative.addresses[_cubit.selectedYield!.network.chainId]!), + isNative: baseToken.addresses[_cubit.selectedYield!.network.chainId]!.lowercasedEquals( + EthereumConstants.zeroAddress, + ), onRefreshBalance: () => setState(() {}), disabledText: () { if (!isBaseTokenNeeded) { @@ -922,9 +877,9 @@ class _DepositPageState extends State TokenAmountInputCard( key: const Key("quote-token-input-card"), token: quoteToken, - isNative: depositWithNativeToken && - quoteToken.addresses[_cubit.selectedYield!.network.chainId]!.lowercasedEquals(_cubit - .selectedYield!.network.wrappedNative.addresses[_cubit.selectedYield!.network.chainId]!), + isNative: quoteToken.addresses[_cubit.selectedYield!.network.chainId]!.lowercasedEquals( + EthereumConstants.zeroAddress, + ), onRefreshBalance: () => setState(() {}), disabledText: () { if (!isQuoteTokenNeeded) { @@ -948,39 +903,46 @@ class _DepositPageState extends State 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: depositButtonState(), - 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, + Row( + children: [ + Expanded( + child: 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: depositButtonState(), + 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/deposit_success_modal.dart b/lib/app/create/deposit/widgets/deposit_success_modal.dart index 66823dd..7cb5396 100644 --- a/lib/app/create/deposit/widgets/deposit_success_modal.dart +++ b/lib/app/create/deposit/widgets/deposit_success_modal.dart @@ -10,20 +10,18 @@ import 'package:zup_app/widgets/zup_cached_image.dart'; import 'package:zup_ui_kit/zup_ui_kit.dart'; class DepositSuccessModal extends StatefulWidget { - const DepositSuccessModal({super.key, required this.depositedYield, required this.depositedWithNative}); + const DepositSuccessModal({super.key, required this.depositedYield}); final YieldDto depositedYield; - final bool depositedWithNative; static Future show( BuildContext context, { required YieldDto depositedYield, required showAsBottomSheet, - required bool depositedWithNative, }) async => ZupModal.show( context, - content: DepositSuccessModal(depositedYield: depositedYield, depositedWithNative: depositedWithNative), + content: DepositSuccessModal(depositedYield: depositedYield), padding: const EdgeInsets.all(20), showAsBottomSheet: showAsBottomSheet, dismissible: true, @@ -61,11 +59,11 @@ class _DepositSuccessModalState extends State { children: [ ZupMergedWidgets( firstWidget: TokenAvatar( - asset: widget.depositedYield.maybeNativeToken0(permitNative: widget.depositedWithNative), + asset: widget.depositedYield.token0, size: 70, ), secondWidget: TokenAvatar( - asset: widget.depositedYield.maybeNativeToken1(permitNative: widget.depositedWithNative), + asset: widget.depositedYield.token1, size: 70, ), spacing: 0, @@ -103,8 +101,7 @@ class _DepositSuccessModalState extends State { text: TextSpan(style: const TextStyle(color: ZupColors.gray, fontSize: 14), children: [ TextSpan(text: "${S.of(context).depositSuccessModalDescriptionPart1} "), TextSpan( - text: - "${widget.depositedYield.maybeNativeToken0(permitNative: widget.depositedWithNative).symbol}/${widget.depositedYield.maybeNativeToken1(permitNative: widget.depositedWithNative).symbol}", + text: "${widget.depositedYield.token0.symbol}/${widget.depositedYield.token1.symbol}", style: const TextStyle( fontWeight: FontWeight.w700, color: ZupColors.black, 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 f7855a8..fca2d4b 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 @@ -5,8 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web3kit/web3kit.dart'; import 'package:zup_app/abis/erc_20.abi.g.dart'; -import 'package:zup_app/abis/uniswap_position_manager.abi.g.dart'; -import 'package:zup_app/abis/uniswap_v3_pool.abi.g.dart'; +import 'package:zup_app/abis/uniswap_permit2.abi.g.dart'; +import 'package:zup_app/abis/uniswap_v3_position_manager.abi.g.dart'; import 'package:zup_app/app/create/deposit/widgets/deposit_success_modal.dart'; import 'package:zup_app/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal_cubit.dart'; import 'package:zup_app/core/dtos/token_dto.dart'; @@ -14,9 +14,11 @@ import 'package:zup_app/core/dtos/yield_dto.dart'; import 'package:zup_app/core/extensions/num_extension.dart'; import 'package:zup_app/core/injections.dart'; import 'package:zup_app/core/mixins/v3_pool_conversors_mixin.dart'; +import 'package:zup_app/core/pool_service.dart'; import 'package:zup_app/core/slippage.dart'; -import 'package:zup_app/core/v3_pool_constants.dart'; +import 'package:zup_app/core/v3_v4_pool_constants.dart'; import 'package:zup_app/core/zup_analytics.dart'; +import 'package:zup_app/core/zup_links.dart'; import 'package:zup_app/core/zup_navigator.dart'; import 'package:zup_app/gen/assets.gen.dart'; import 'package:zup_app/l10n/gen/app_localizations.dart'; @@ -36,7 +38,6 @@ class PreviewDepositModal extends StatefulWidget with DeviceInfoMixin { required this.token1DepositAmount, required this.deadline, required this.maxSlippage, - required this.depositWithNativeToken, required this.yieldTimeFrame, }); @@ -49,7 +50,6 @@ class PreviewDepositModal extends StatefulWidget with DeviceInfoMixin { final double token1DepositAmount; final Duration deadline; final Slippage maxSlippage; - final bool depositWithNativeToken; final double paddingSize = 20; @@ -64,17 +64,16 @@ class PreviewDepositModal extends StatefulWidget with DeviceInfoMixin { create: (context) => PreviewDepositModalCubit( zupAnalytics: inject(), currentYield: currentYield, - uniswapPositionManager: inject(), + uniswapPositionManager: inject(), erc20: inject(), wallet: inject(), - uniswapV3Pool: inject(), + poolService: inject(), + permit2: inject(), initialPoolTick: currentPoolTick, - depositWithNative: depositWithNativeToken, navigatorKey: inject>(), ), child: PreviewDepositModal( deadline: deadline, - depositWithNativeToken: depositWithNativeToken, maxSlippage: maxSlippage, token0DepositAmount: token0DepositAmount, token1DepositAmount: token1DepositAmount, @@ -95,6 +94,7 @@ class PreviewDepositModal extends StatefulWidget with DeviceInfoMixin { class _PreviewDepositModalState extends State with V3PoolConversorsMixin, DeviceInfoMixin { final zupCachedImage = inject(); final navigator = inject(); + final zupLinks = inject(); final ScrollController appScrollController = inject( instanceName: InjectInstanceNames.appScrollController, @@ -102,18 +102,18 @@ class _PreviewDepositModalState extends State with V3PoolCo TokenDto get baseToken { if (isReversedLocal) { - return widget.currentYield.maybeNativeToken1(permitNative: widget.depositWithNativeToken); + return widget.currentYield.token1; } - return widget.currentYield.maybeNativeToken0(permitNative: widget.depositWithNativeToken); + return widget.currentYield.token0; } TokenDto get quoteToken { if (isReversedLocal) { - return widget.currentYield.maybeNativeToken0(permitNative: widget.depositWithNativeToken); + return widget.currentYield.token0; } - return widget.currentYield.maybeNativeToken1(permitNative: widget.depositWithNativeToken); + return widget.currentYield.token1; } double get baseTokenAmount => isReversedLocal ? widget.token1DepositAmount : widget.token0DepositAmount; @@ -138,7 +138,7 @@ class _PreviewDepositModalState extends State with V3PoolCo double get minPrice { BigInt tick() { - if (widget.isReversed != isReversedLocal && widget.maxPrice.isInfinity) return V3PoolConstants.minTick; + if (widget.isReversed != isReversedLocal && widget.maxPrice.isInfinity) return V3V4PoolConstants.minTick; return priceToTick( price: (widget.isReversed == !isReversedLocal) ? widget.maxPrice.price : widget.minPrice.price, @@ -159,7 +159,7 @@ class _PreviewDepositModalState extends State with V3PoolCo double get maxPrice { BigInt tick() { - if (widget.isReversed != isReversedLocal && widget.minPrice.isInfinity) return V3PoolConstants.minTick; + if (widget.isReversed != isReversedLocal && widget.minPrice.isInfinity) return V3V4PoolConstants.minTick; return priceToTick( price: (widget.isReversed == !isReversedLocal) ? widget.minPrice.price : widget.maxPrice.price, @@ -231,7 +231,7 @@ class _PreviewDepositModalState extends State with V3PoolCo isLoading: true, ), initial: (token0Allowance, token1Allowance) { - if (!(widget.depositWithNativeToken && widget.currentYield.isToken0WrappedNative)) { + if (!widget.currentYield.isToken0Native) { if (token0Allowance < token0DepositAmount) { return ( title: S.of(context).previewDepositModalApproveToken(tokenSymbol: widget.currentYield.token0.symbol), @@ -242,7 +242,7 @@ class _PreviewDepositModalState extends State with V3PoolCo } } - if (!(widget.depositWithNativeToken && widget.currentYield.isToken1WrappedNative)) { + if (!widget.currentYield.isToken1Native) { if (token1Allowance < token1DepositAmount) { return ( title: S.of(context).previewDepositModalApproveToken(tokenSymbol: widget.currentYield.token1.symbol), @@ -363,7 +363,6 @@ class _PreviewDepositModalState extends State with V3PoolCo context, depositedYield: widget.currentYield, showAsBottomSheet: isMobileSize(context), - depositedWithNative: widget.depositWithNativeToken, ); }, slippageCheckError: () { @@ -385,9 +384,7 @@ class _PreviewDepositModalState extends State with V3PoolCo context, helperButton: ( title: S.of(context).previewDepositModalTransactionErrorSnackBarHelperButtonTitle, - onButtonTap: () { - // TODO: Add contact us info - } + onButtonTap: () => zupLinks.launchZupContactUs() ), message: S.of(context).previewDepositModalTransactionErrorSnackBarMessage, type: ZupSnackBarType.error, @@ -447,7 +444,7 @@ class _PreviewDepositModalState extends State with V3PoolCo child: SizedBox( height: 15, child: Text( - "${widget.currentYield.maybeNativeToken0(permitNative: widget.depositWithNativeToken).symbol} / ${widget.currentYield.maybeNativeToken1(permitNative: widget.depositWithNativeToken).symbol}", + "${widget.currentYield.token0.symbol} / ${widget.currentYield.token1.symbol}", style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600), ), ), @@ -460,7 +457,7 @@ class _PreviewDepositModalState extends State with V3PoolCo child: SizedBox( height: 16, child: Text( - "${widget.currentYield.maybeNativeToken1(permitNative: widget.depositWithNativeToken).symbol} / ${widget.currentYield.maybeNativeToken0(permitNative: widget.depositWithNativeToken).symbol}", + "${widget.currentYield.token1.symbol} / ${widget.currentYield.token0.symbol}", style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, 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 42b308e..24bca8c 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 @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -8,14 +7,15 @@ import 'package:web3kit/core/dtos/transaction_response.dart'; import 'package:web3kit/core/exceptions/ethers_exceptions.dart'; import 'package:web3kit/web3kit.dart'; import 'package:zup_app/abis/erc_20.abi.g.dart'; -import 'package:zup_app/abis/uniswap_position_manager.abi.g.dart'; -import 'package:zup_app/abis/uniswap_v3_pool.abi.g.dart'; +import 'package:zup_app/abis/uniswap_permit2.abi.g.dart'; +import 'package:zup_app/abis/uniswap_v3_position_manager.abi.g.dart'; import 'package:zup_app/app/create/deposit/widgets/deposit_success_modal.dart'; import 'package:zup_app/core/dtos/token_dto.dart'; import 'package:zup_app/core/dtos/yield_dto.dart'; import 'package:zup_app/core/mixins/v3_pool_conversors_mixin.dart'; +import 'package:zup_app/core/pool_service.dart'; import 'package:zup_app/core/slippage.dart'; -import 'package:zup_app/core/v3_pool_constants.dart'; +import 'package:zup_app/core/v3_v4_pool_constants.dart'; import 'package:zup_app/core/zup_analytics.dart'; import 'package:zup_app/l10n/gen/app_localizations.dart'; import 'package:zup_core/mixins/device_info_mixin.dart'; @@ -27,33 +27,32 @@ part "preview_deposit_modal_state.dart"; class PreviewDepositModalCubit extends Cubit with V3PoolConversorsMixin, DeviceInfoMixin { PreviewDepositModalCubit({ required BigInt initialPoolTick, - required UniswapV3Pool uniswapV3Pool, + required PoolService poolService, required YieldDto currentYield, required Erc20 erc20, required Wallet wallet, - required UniswapPositionManager uniswapPositionManager, + required UniswapV3PositionManager uniswapPositionManager, + required UniswapPermit2 permit2, required GlobalKey navigatorKey, - required bool depositWithNative, required ZupAnalytics zupAnalytics, }) : _yield = currentYield, - _uniswapV3Pool = uniswapV3Pool, + _poolRepository = poolService, _erc20 = erc20, _wallet = wallet, - _uniswapPositionManager = uniswapPositionManager, _latestPoolTick = initialPoolTick, _navigatorKey = navigatorKey, - _depositWithNative = depositWithNative, _zupAnalytics = zupAnalytics, + _permit2 = permit2, super(const PreviewDepositModalState.loading()); - final UniswapV3Pool _uniswapV3Pool; + final PoolService _poolRepository; final Erc20 _erc20; final YieldDto _yield; final Wallet _wallet; - final UniswapPositionManager _uniswapPositionManager; final GlobalKey _navigatorKey; - final bool _depositWithNative; + final ZupAnalytics _zupAnalytics; + final UniswapPermit2 _permit2; final StreamController _poolTickStreamController = StreamController.broadcast(); @@ -87,6 +86,7 @@ class PreviewDepositModalCubit extends Cubit with V3Po Future approveToken(TokenDto token, BigInt value) async { try { + final spender = _yield.poolType.isV4 ? _yield.permit2! : _yield.positionManagerAddress; final tokenAddressInNetwork = token.addresses[_yield.network.chainId]!; emit(PreviewDepositModalState.approvingToken(token.symbol)); @@ -96,7 +96,10 @@ class PreviewDepositModalCubit extends Cubit with V3Po contractAddress: tokenAddressInNetwork, signer: _wallet.signer!, ); - final tx = await contract.approve(spender: _yield.positionManagerAddress, value: value); + + final tx = await contract.approve(spender: spender, value: value); + + if (_yield.poolType.isV4) checkOrApprovePermit2ForV4Pool(value, token); emit(PreviewDepositModalState.waitingTransaction(txId: tx.hash, type: WaitingTransactionType.approve)); @@ -123,6 +126,33 @@ class PreviewDepositModalCubit extends Cubit with V3Po } } + Future checkOrApprovePermit2ForV4Pool(BigInt approveValue, TokenDto token) async { + final tokenAddressInNetwork = token.addresses[_yield.network.chainId]!; + + final permit2Contract = _permit2.fromSigner( + contractAddress: _yield.permit2!, + signer: _wallet.signer!, + ); + + final permit2CurrentAllowance = await permit2Contract.allowance( + await _wallet.signer!.address, + tokenAddressInNetwork, + _yield.positionManagerAddress, + ); + + if (permit2CurrentAllowance.amount <= approveValue || + permit2CurrentAllowance.expiration < BigInt.from(DateTime.now().millisecondsSinceEpoch / 1000)) { + final tx = await permit2Contract.approve( + token: tokenAddressInNetwork, + spender: _yield.positionManagerAddress, + amount: EthereumConstants.uint160Max, + expiration: EthereumConstants.uint48Max, + ); + + await tx.waitConfirmation(); + } + } + Future deposit({ required BigInt token0Amount, required BigInt token1Amount, @@ -138,15 +168,10 @@ class PreviewDepositModalCubit extends Cubit with V3Po emit(const PreviewDepositModalState.depositing()); await _maybeSwitchNetwork(); - final positionManagerContract = _uniswapPositionManager.fromSigner( - contractAddress: _yield.positionManagerAddress, - signer: _wallet.signer!, - ); - BigInt tickLower() { BigInt convertPriceToTickLower() { - if (isMinPriceInfinity && !isReversed) return V3PoolConstants.minTick; - if (isReversed && isMaxPriceInfinity) return V3PoolConstants.minTick; + if (isMinPriceInfinity && !isReversed) return V3V4PoolConstants.minTick; + if (isReversed && isMaxPriceInfinity) return V3V4PoolConstants.minTick; return priceToTick( price: isReversed ? maxPrice : minPrice, @@ -164,8 +189,8 @@ class PreviewDepositModalCubit extends Cubit with V3Po BigInt tickUpper() { BigInt convertPriceToTickUpper() { - if (isMaxPriceInfinity && !isReversed) return V3PoolConstants.maxTick; - if (isReversed && isMinPriceInfinity) return V3PoolConstants.maxTick; + if (isMaxPriceInfinity && !isReversed) return V3V4PoolConstants.maxTick; + if (isReversed && isMinPriceInfinity) return V3V4PoolConstants.maxTick; return priceToTick( price: isReversed ? minPrice : maxPrice, @@ -183,60 +208,41 @@ class PreviewDepositModalCubit extends Cubit with V3Po final amount0Desired = token0Amount; final amount1Desired = token1Amount; - final amount0Min = slippage.calculateTokenAmountFromSlippage(amount0Desired); - final amount1Min = slippage.calculateTokenAmountFromSlippage(amount1Desired); + final amount0Min = slippage.calculateMinTokenAmountFromSlippage(amount0Desired); + final amount1Min = slippage.calculateMinTokenAmountFromSlippage(amount1Desired); final recipient = await _wallet.signer!.address; final TransactionResponse tx = await () async { - if (_depositWithNative) { - final mintCalldata = _uniswapPositionManager.getMintCalldata( - params: ( - amount0Desired: amount0Desired, - amount1Desired: amount1Desired, - deadline: BigInt.from(clock.now().add(deadline).millisecondsSinceEpoch), - amount0Min: amount0Min, - amount1Min: amount1Min, - recipient: recipient, - tickLower: tickLower(), - tickUpper: tickUpper(), - fee: BigInt.from(_yield.feeTier), - token0: _yield.token0.addresses[_yield.network.chainId]!, - token1: _yield.token1.addresses[_yield.network.chainId]!, - ), - ); - - return await positionManagerContract.multicall( - data: [ - mintCalldata, - if (_depositWithNative) _uniswapPositionManager.getRefundETHCalldata(), - ], - ethValue: () { - if (_depositWithNative && _yield.isToken0WrappedNative) { - return amount0Desired; - } - - if (_depositWithNative && _yield.isToken1WrappedNative) { - return amount1Desired; - } - - return BigInt.zero; - }.call()); - } - - return await positionManagerContract.mint( - params: ( + if (_yield.poolType.isV3) { + return await _poolRepository.sendV3PoolDepositTransaction( + _yield, + _wallet.signer!, amount0Desired: amount0Desired, amount1Desired: amount1Desired, - deadline: BigInt.from(clock.now().add(deadline).millisecondsSinceEpoch), + deadline: deadline, amount0Min: amount0Min, amount1Min: amount1Min, recipient: recipient, tickLower: tickLower(), tickUpper: tickUpper(), - fee: BigInt.from(_yield.feeTier), - token0: _yield.token0.addresses[_yield.network.chainId]!, - token1: _yield.token1.addresses[_yield.network.chainId]!, - ), + ); + } + + await checkOrApprovePermit2ForV4Pool(amount0Desired, _yield.token0); + await checkOrApprovePermit2ForV4Pool(amount1Desired, _yield.token1); + + return await _poolRepository.sendV4PoolDepositTransaction( + _yield, + _wallet.signer!, + deadline: deadline, + tickLower: tickLower(), + tickUpper: tickUpper(), + amount0toDeposit: amount0Desired, + amount1ToDeposit: amount1Desired, + maxAmount0ToDeposit: slippage.calculateMaxTokenAmountFromSlippage(amount0Desired), + maxAmount1ToDeposit: slippage.calculateMaxTokenAmountFromSlippage(amount1Desired), + recipient: recipient, + currentPoolTick: _latestPoolTick, ); }.call(); @@ -285,43 +291,42 @@ class PreviewDepositModalCubit extends Cubit with V3Po Future _getTokensAllowance({bool canThrow = false}) async { try { - final token0contract = _erc20.fromRpcProvider( - contractAddress: _yield.token0.addresses[_yield.network.chainId]!, - rpcUrl: _yield.network.rpcUrl, - ); + final spender = _yield.poolType.isV4 ? _yield.permit2! : _yield.positionManagerAddress; + final owner = await _wallet.signer!.address; - final token1contract = _erc20.fromRpcProvider( - contractAddress: _yield.token1.addresses[_yield.network.chainId]!, - rpcUrl: _yield.network.rpcUrl, - ); + if (!_yield.isToken0Native) { + final token0contract = _erc20.fromRpcProvider( + contractAddress: _yield.token0.addresses[_yield.network.chainId]!, + rpcUrl: _yield.network.rpcUrl, + ); - final token0Allowance = await token0contract.allowance( - owner: await _wallet.signer!.address, - spender: _yield.positionManagerAddress, - ); + _token0Allowance = await token0contract.allowance( + owner: owner, + spender: spender, + ); + } - final token1Allowance = await token1contract.allowance( - owner: await _wallet.signer!.address, - spender: _yield.positionManagerAddress, - ); + if (!_yield.isToken1Native) { + final token1contract = _erc20.fromRpcProvider( + contractAddress: _yield.token1.addresses[_yield.network.chainId]!, + rpcUrl: _yield.network.rpcUrl, + ); - _token0Allowance = token0Allowance; - _token1Allowance = token1Allowance; + _token1Allowance = await token1contract.allowance( + owner: owner, + spender: spender, + ); + } } catch (e) { if (canThrow) rethrow; } } void _updateTick() { - final uniswapV3PoolContract = _uniswapV3Pool.fromRpcProvider( - contractAddress: _yield.poolAddress, - rpcUrl: _yield.network.rpcUrl, - ); - try { - uniswapV3PoolContract.slot0().then((slot0) { - _poolTickStreamController.add(slot0.tick); - _latestPoolTick = slot0.tick; + _poolRepository.getPoolTick(_yield).then((tick) { + _latestPoolTick = tick; + _poolTickStreamController.add(tick); }); } catch (_) { // DO NOTHING @@ -338,8 +343,8 @@ class PreviewDepositModalCubit extends Cubit with V3Po WaitingTransactionType.deposit => ZupSnackBar( context, message: "${S.of(context).previewDepositModalCubitDepositingSnackBarMessage( - token0Symbol: _yield.maybeNativeToken0(permitNative: _depositWithNative).symbol, - token1Symbol: _yield.maybeNativeToken1(permitNative: _depositWithNative).symbol, + token0Symbol: _yield.token0.symbol, + token1Symbol: _yield.token1.symbol, )} ", customIcon: const ZupCircularLoadingIndicator(size: 20), type: ZupSnackBarType.info, @@ -391,7 +396,6 @@ class PreviewDepositModalCubit extends Cubit with V3Po context, depositedYield: _yield, showAsBottomSheet: isMobileSize(context), - depositedWithNative: _depositWithNative, ); }, orElse: () async { diff --git a/lib/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card.dart b/lib/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card.dart index b1dfd40..15054e4 100644 --- a/lib/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card.dart +++ b/lib/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web3kit/web3kit.dart'; +import 'package:zup_app/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_cubit.dart'; import 'package:zup_app/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_user_balance_cubit.dart'; import 'package:zup_app/core/dtos/token_dto.dart'; import 'package:zup_app/core/enums/networks.dart'; import 'package:zup_app/core/extensions/num_extension.dart'; import 'package:zup_app/core/extensions/widget_extension.dart'; import 'package:zup_app/core/injections.dart'; +import 'package:zup_app/core/repositories/tokens_repository.dart'; import 'package:zup_app/core/token_amount_input_formatter.dart'; import 'package:zup_app/gen/assets.gen.dart'; import 'package:zup_app/widgets/position_token.dart'; @@ -42,6 +44,10 @@ class _TokenAmountInputCardState extends State with Single Wallet get wallet => inject(); ZupSingletonCache get zupSingletonCache => inject(); + final TokensRepository _tokensRepository = inject(); + final ZupHolder _zupHolder = inject(); + + TokenAmountInputCardCubit? cubit; late TokenAmountCardUserBalanceCubit userBalanceCubit = TokenAmountCardUserBalanceCubit( wallet, @@ -55,19 +61,22 @@ class _TokenAmountInputCardState extends State with Single @override void initState() { + cubit = TokenAmountInputCardCubit(_tokensRepository, zupSingletonCache, _zupHolder); refreshBalanceAnimationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 100)); WidgetsBinding.instance.addPostFrameCallback((tester) { - if (wallet.signer != null) userBalanceCubit.getUserTokenAmount(isNative: widget.isNative); + userBalanceCubit.updateNativeTokenAndFetch(isNative: widget.isNative); }); + super.initState(); } @override - void didUpdateWidget(covariant TokenAmountInputCard oldWidget) { - if (widget.isNative != oldWidget.isNative) { + void didUpdateWidget(TokenAmountInputCard oldWidget) { + if (widget.isNative != oldWidget.isNative && + (widget.token.addresses[widget.network.chainId] ?? "").lowercasedEquals(EthereumConstants.zeroAddress)) { WidgetsBinding.instance - .addPostFrameCallback((_) => userBalanceCubit.getUserTokenAmount(isNative: widget.isNative)); + .addPostFrameCallback((_) => userBalanceCubit.updateNativeTokenAndFetch(isNative: widget.isNative)); return super.didUpdateWidget(oldWidget); } @@ -199,12 +208,31 @@ class _TokenAmountInputCardState extends State with Single Row( children: [ Padding( - padding: EdgeInsets.only(left: paddingValue), - child: const Text( - r"$-", - style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: ZupColors.gray), - ), - ), + padding: EdgeInsets.only(left: paddingValue), + child: StreamBuilder( + stream: (() async* { + yield await cubit!.getTokenPrice(token: widget.token, network: widget.network); + + await for (final _ in Stream.periodic(const Duration(seconds: 30))) { + yield await cubit!.getTokenPrice(token: widget.token, network: widget.network); + } + })(), + builder: (context, snapshot) { + if (!snapshot.hasData) return const ZupCircularLoadingIndicator(size: 10); + + return Text( + widget.controller.value.text.isEmpty + ? "\$-" + : ((double.tryParse(widget.controller.value.text) ?? 0) * snapshot.data!) + .formatCurrency(), + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: ZupColors.gray, + ), + ); + }, + )), const Spacer(), BlocProvider.value( value: userBalanceCubit, diff --git a/lib/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_cubit.dart b/lib/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_cubit.dart new file mode 100644 index 0000000..d97b867 --- /dev/null +++ b/lib/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_cubit.dart @@ -0,0 +1,33 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:zup_app/core/dtos/token_dto.dart'; +import 'package:zup_app/core/enums/networks.dart'; +import 'package:zup_app/core/mixins/keys_mixin.dart'; +import 'package:zup_app/core/repositories/tokens_repository.dart'; +import 'package:zup_core/zup_core.dart'; + +part 'token_amount_input_card_cubit.freezed.dart'; +part 'token_amount_input_card_state.dart'; + +class TokenAmountInputCardCubit extends Cubit with KeysMixin { + TokenAmountInputCardCubit(this._tokensRepository, this._zupSingletonCache, this._zupHolder) + : super(const TokenAmountInputCardState.initial()); + + final TokensRepository _tokensRepository; + final ZupSingletonCache _zupSingletonCache; + final ZupHolder _zupHolder; + + Future getTokenPrice({required TokenDto token, required AppNetworks network}) async { + try { + final tokenAddress = token.addresses[network.chainId]!; + + return await _zupHolder.hold(() async => await _zupSingletonCache.run( + () async => (await _tokensRepository.getTokenPrice(tokenAddress, network)).usdPrice, + expiration: const Duration(minutes: 1), + key: tokenPriceCacheKey(tokenAddress: tokenAddress, network: network), + )); + } catch (_) { + return 0; + } + } +} diff --git a/lib/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_state.dart b/lib/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_state.dart new file mode 100644 index 0000000..6207e96 --- /dev/null +++ b/lib/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_state.dart @@ -0,0 +1,6 @@ +part of 'token_amount_input_card_cubit.dart'; + +@freezed +class TokenAmountInputCardState with _$TokenAmountInputCardState { + const factory TokenAmountInputCardState.initial() = _Initial; +} diff --git a/lib/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_user_balance_cubit.dart b/lib/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_user_balance_cubit.dart index 7bc347b..e982f11 100644 --- a/lib/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_user_balance_cubit.dart +++ b/lib/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_user_balance_cubit.dart @@ -25,6 +25,7 @@ class TokenAmountCardUserBalanceCubit extends Cubit? _signerStreamSubscription; double userBalance = 0; @@ -32,17 +33,25 @@ class TokenAmountCardUserBalanceCubit extends Cubit updateTokenAndNetwork(String tokenAddress, AppNetworks network, {required bool asNativeToken}) async { _tokenAddress = tokenAddress; _network = network; + _isNative = asNativeToken; if (_wallet.signer != null) await getUserTokenAmount(isNative: asNativeToken); } + Future updateNativeTokenAndFetch({required bool isNative}) async { + _isNative = isNative; + if (isNative) _tokenAddress = EthereumConstants.zeroAddress; + + if (_wallet.signer != null) await getUserTokenAmount(isNative: _isNative); + } + Future getUserTokenAmount({bool ignoreCache = false, bool isNative = false}) async { try { emit(const TokenAmountCardUserBalanceState.loadingUserBalance()); diff --git a/lib/app/create/widgets/create_page_settings_dropdown.dart b/lib/app/create/widgets/create_page_settings_dropdown.dart index d919b21..8692fc7 100644 --- a/lib/app/create/widgets/create_page_settings_dropdown.dart +++ b/lib/app/create/widgets/create_page_settings_dropdown.dart @@ -162,6 +162,55 @@ class _CreatePageSettingsDropdownState extends State ) : const SizedBox.shrink(), ), + const SizedBox(height: 10), + Row( + children: [ + Text( + S.of(context).createPageSettingsDropdownAllowedPoolTypes, + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15), + ), + const SizedBox(width: 8), + ZupTooltip( + key: const Key("pool-types-allowed-tooltip"), + message: S.of(context).createPageSettingsDropdownAllowedPoolTypesDescription, + child: Assets.icons.infoCircle.svg( + colorFilter: const ColorFilter.mode(ZupColors.gray, BlendMode.srcIn), + ), + ), + ], + ), + 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), + ); + + 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(() {}); + }, + ), + ], + ), ], ), ); diff --git a/lib/core/dtos/pool_search_settings_dto.dart b/lib/core/dtos/pool_search_settings_dto.dart index 029b52a..9565b98 100644 --- a/lib/core/dtos/pool_search_settings_dto.dart +++ b/lib/core/dtos/pool_search_settings_dto.dart @@ -10,6 +10,8 @@ class PoolSearchSettingsDto with _$PoolSearchSettingsDto { @JsonSerializable(explicitToJson: true) factory PoolSearchSettingsDto({ @Default(PoolSearchSettingsDto.defaultMinLiquidityUSD) @JsonKey(name: 'min_liquidity_usd') num minLiquidityUSD, + @Default(true) bool allowV4Search, + @Default(true) bool allowV3Search, }) = _PoolSearchSettingsDto; const PoolSearchSettingsDto._(); diff --git a/lib/core/dtos/token_dto.dart b/lib/core/dtos/token_dto.dart index 0c0c15f..e69ddef 100644 --- a/lib/core/dtos/token_dto.dart +++ b/lib/core/dtos/token_dto.dart @@ -23,7 +23,13 @@ class TokenDto with _$TokenDto { factory TokenDto.fixture() => TokenDto( symbol: 'WETH', name: 'Wrapped Ether', - addresses: {AppNetworks.sepolia.chainId: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"}, + addresses: Map.fromEntries( + AppNetworks.values.where((network) => !network.isAllNetworks).map( + (network) { + return MapEntry(network.chainId, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); + }, + ), + ), logoUrl: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', ); diff --git a/lib/core/dtos/token_price_dto.dart b/lib/core/dtos/token_price_dto.dart new file mode 100644 index 0000000..920427c --- /dev/null +++ b/lib/core/dtos/token_price_dto.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'token_price_dto.freezed.dart'; +part 'token_price_dto.g.dart'; + +@freezed +class TokenPriceDto with _$TokenPriceDto { + @JsonSerializable(explicitToJson: true) + factory TokenPriceDto({ + @Default(0) num usdPrice, + @Default("") String address, + }) = _TokenPriceDto; + + factory TokenPriceDto.fromJson(Map json) => _$TokenPriceDtoFromJson(json); + + factory TokenPriceDto.fixture() => TokenPriceDto(usdPrice: 1328.112); +} diff --git a/lib/core/dtos/yield_dto.dart b/lib/core/dtos/yield_dto.dart index eb58a19..0ddd6e7 100644 --- a/lib/core/dtos/yield_dto.dart +++ b/lib/core/dtos/yield_dto.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:web3kit/core/ethereum_constants.dart'; import 'package:zup_app/core/dtos/protocol_dto.dart'; import 'package:zup_app/core/dtos/token_dto.dart'; import 'package:zup_app/core/enums/networks.dart'; +import 'package:zup_app/core/enums/pool_type.dart'; import 'package:zup_app/l10n/gen/app_localizations.dart'; -import 'package:zup_core/zup_core.dart'; part 'yield_dto.freezed.dart'; part 'yield_dto.g.dart'; @@ -47,46 +48,18 @@ class YieldDto with _$YieldDto { required num yield30d, required num yield90d, required int chainId, + required PoolType poolType, @Default(0) num totalValueLockedUSD, + @Default(EthereumConstants.zeroAddress) @JsonKey(name: "hooksAddress") String v4Hooks, + @JsonKey(name: "poolManagerAddress") String? v4PoolManager, + @JsonKey(name: "stateViewAddress") String? v4StateView, + @JsonKey(name: "permit2Address") String? permit2, }) = _YieldDto; AppNetworks get network => AppNetworks.fromChainId(chainId)!; - TokenDto maybeNativeToken0({required bool permitNative}) { - if (permitNative && - (token0.addresses[network.chainId]!).lowercasedEquals(network.wrappedNative.addresses[network.chainId]!)) { - return TokenDto( - addresses: {network.chainId: network.wrappedNative.addresses[network.chainId]!}, - decimals: network.chainInfo.nativeCurrency!.decimals, - logoUrl: network.chainInfo.nativeCurrency!.logoUrl, - symbol: network.chainInfo.nativeCurrency!.symbol, - name: network.chainInfo.nativeCurrency!.name, - ); - } - - return token0; - } - - TokenDto maybeNativeToken1({required bool permitNative}) { - if (permitNative && - token1.addresses[network.chainId]!.lowercasedEquals(network.wrappedNative.addresses[network.chainId]!)) { - return TokenDto( - addresses: {network.chainId: network.wrappedNative.addresses[network.chainId]!}, - decimals: network.chainInfo.nativeCurrency!.decimals, - logoUrl: network.chainInfo.nativeCurrency!.logoUrl, - symbol: network.chainInfo.nativeCurrency!.symbol, - name: network.chainInfo.nativeCurrency!.name, - ); - } - - return token1; - } - - bool get isToken0WrappedNative => - token0.addresses[network.chainId]!.lowercasedEquals(network.wrappedNativeTokenAddress); - - bool get isToken1WrappedNative => - token1.addresses[network.chainId]!.lowercasedEquals(network.wrappedNativeTokenAddress); + bool get isToken0Native => token0.addresses[network.chainId] == EthereumConstants.zeroAddress; + bool get isToken1Native => token1.addresses[network.chainId] == EthereumConstants.zeroAddress; factory YieldDto.fromJson(Map json) => _$YieldDtoFromJson(json); @@ -97,6 +70,7 @@ class YieldDto with _$YieldDto { yield90d: 32.2, positionManagerAddress: "0x5Df2f0aFb5b5bB2Df9D1e9C7b6f5f0DD5f9eD5e0", poolAddress: "0x5Df2f0aFb5b5bB2Df9D1e9C7b6f5f0DD5f9eD5e0", + poolType: PoolType.v3, token0: TokenDto.fixture().copyWith( symbol: "USDC", decimals: 6, diff --git a/lib/core/dtos/yields_dto.dart b/lib/core/dtos/yields_dto.dart index 291f72c..15a54a6 100644 --- a/lib/core/dtos/yields_dto.dart +++ b/lib/core/dtos/yields_dto.dart @@ -2,6 +2,7 @@ import 'package:freezed_annotation/freezed_annotation.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/pool_type.dart'; part 'yields_dto.freezed.dart'; part 'yields_dto.g.dart'; @@ -33,6 +34,7 @@ class YieldsDto with _$YieldsDto { pools: [ YieldDto( positionManagerAddress: "0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4", + poolType: PoolType.v3, token0: TokenDto( addresses: {11155111: "0x02a3e7E0480B668bD46b42852C58363F93e3bA5C"}, decimals: 6, diff --git a/lib/core/enums/networks.dart b/lib/core/enums/networks.dart index d68d8e1..a29915a 100644 --- a/lib/core/enums/networks.dart +++ b/lib/core/enums/networks.dart @@ -8,6 +8,8 @@ import 'package:zup_app/gen/assets.gen.dart'; enum AppNetworks { allNetworks, mainnet, + // base, + unichain, scroll, sepolia; @@ -43,6 +45,8 @@ enum AppNetworks { scroll => false, sepolia => true, allNetworks => false, + // base => false, + unichain => false, }; String get label => switch (this) { @@ -50,12 +54,16 @@ enum AppNetworks { mainnet => "Ethereum", scroll => "Scroll", allNetworks => "All Networks", + // base => "Base", + unichain => "Unichain", }; Widget get icon => switch (this) { sepolia => Assets.logos.ethereum.svg(), mainnet => Assets.logos.ethereum.svg(), scroll => Assets.logos.scroll.svg(), + // base => Assets.logos.base.svg(), + unichain => Assets.logos.unichain.svg(), allNetworks => Assets.icons.all.svg(), }; @@ -82,6 +90,20 @@ enum AppNetworks { nativeCurrency: NativeCurrencies.eth.currencyInfo, rpcUrls: [rpcUrl], ), + // base => ChainInfo( + // hexChainId: "0x2105", + // chainName: label, + // blockExplorerUrls: const ["https://basescan.org"], + // nativeCurrency: NativeCurrencies.eth.currencyInfo, + // rpcUrls: [rpcUrl], + // ), + unichain => ChainInfo( + hexChainId: "0x82", + chainName: label, + blockExplorerUrls: const ["https://uniscan.xyz/"], + nativeCurrency: NativeCurrencies.eth.currencyInfo, + rpcUrls: [rpcUrl], + ), }; String get wrappedNativeTokenAddress => switch (this) { @@ -89,6 +111,8 @@ enum AppNetworks { sepolia => "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14", mainnet => "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", scroll => "0x5300000000000000000000000000000000000004", + // base => "0x4200000000000000000000000000000000000006", + unichain => "0x4200000000000000000000000000000000000006" }; TokenDto get wrappedNative => switch (this) { @@ -115,6 +139,21 @@ enum AppNetworks { 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", + ), }; String get rpcUrl => switch (this) { @@ -122,28 +161,8 @@ enum AppNetworks { sepolia => "https://ethereum-sepolia-rpc.publicnode.com", mainnet => "https://ethereum-rpc.publicnode.com", scroll => "https://scroll-rpc.publicnode.com", - }; - - TokenDto get nativeCurrencyTokenDto => switch (this) { - allNetworks => throw UnimplementedError("allNetworks is not a valid network"), - sepolia => TokenDto( - name: NativeCurrencies.eth.currencyInfo.name, - decimals: NativeCurrencies.eth.currencyInfo.decimals, - symbol: NativeCurrencies.eth.currencyInfo.symbol, - logoUrl: NativeCurrencies.eth.currencyInfo.logoUrl, - ), - mainnet => TokenDto( - name: NativeCurrencies.eth.currencyInfo.name, - decimals: NativeCurrencies.eth.currencyInfo.decimals, - symbol: NativeCurrencies.eth.currencyInfo.symbol, - logoUrl: NativeCurrencies.eth.currencyInfo.logoUrl, - ), - scroll => TokenDto( - name: NativeCurrencies.eth.currencyInfo.name, - decimals: NativeCurrencies.eth.currencyInfo.decimals, - symbol: NativeCurrencies.eth.currencyInfo.symbol, - logoUrl: NativeCurrencies.eth.currencyInfo.logoUrl, - ), + // base => "https://base-rpc.publicnode.com", + unichain => "https://unichain-rpc.publicnode.com", }; Future openTx(String txHash) async { diff --git a/lib/core/enums/pool_type.dart b/lib/core/enums/pool_type.dart new file mode 100644 index 0000000..b8945e3 --- /dev/null +++ b/lib/core/enums/pool_type.dart @@ -0,0 +1,16 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +enum PoolType { + @JsonValue("V3") + v3, + @JsonValue("V4") + v4; + + bool get isV3 => this == PoolType.v3; + bool get isV4 => this == PoolType.v4; + + String get label => switch (this) { + PoolType.v3 => "V3", + PoolType.v4 => "V4", + }; +} diff --git a/lib/core/extensions/bigint_extension.dart b/lib/core/extensions/bigint_extension.dart index f5ceb0d..a4cfc49 100644 --- a/lib/core/extensions/bigint_extension.dart +++ b/lib/core/extensions/bigint_extension.dart @@ -1,6 +1,6 @@ -import 'package:zup_app/core/v3_pool_constants.dart'; +import 'package:zup_app/core/v3_v4_pool_constants.dart'; extension BigIntExtension on BigInt { - bool get isMinTick => this == V3PoolConstants.minTick; - bool get isMaxTick => this == V3PoolConstants.maxTick; + bool get isMinTick => this == V3V4PoolConstants.minTick; + bool get isMaxTick => this == V3V4PoolConstants.maxTick; } diff --git a/lib/core/extensions/num_extension.dart b/lib/core/extensions/num_extension.dart index a86d15a..f5b30cf 100644 --- a/lib/core/extensions/num_extension.dart +++ b/lib/core/extensions/num_extension.dart @@ -39,7 +39,7 @@ extension NumExtension on num { if (decimals > maxDecimals && (this > maxDecimalsNumber)) decimalsDigits = maxDecimals; if (useLessThan && this < maxDecimalsNumber) return toAmount(useLessThan: true, maxFixedDigits: maxDecimals); - if (this < 0.1) return Decimal.parse(toString()).toString(); + if (this < 0.1) return "${(isUSD ? "\$" : "")}${Decimal.parse(toString()).toString()}"; return NumberFormat.simpleCurrency( decimalDigits: decimalsDigits, diff --git a/lib/core/injections.dart b/lib/core/injections.dart index 460f76a..e6decbc 100644 --- a/lib/core/injections.dart +++ b/lib/core/injections.dart @@ -7,12 +7,16 @@ 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/uniswap_position_manager.abi.g.dart'; +import 'package:zup_app/abis/uniswap_permit2.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/app/app_cubit/app_cubit.dart'; import 'package:zup_app/core/cache.dart'; import 'package:zup_app/core/debouncer.dart'; import 'package:zup_app/core/enums/app_environment.dart'; +import 'package:zup_app/core/pool_service.dart'; import 'package:zup_app/core/repositories/positions_repository.dart'; import 'package:zup_app/core/repositories/tokens_repository.dart'; import 'package:zup_app/core/repositories/yield_repository.dart'; @@ -37,6 +41,7 @@ abstract class InjectInstanceNames { static final lottieSearching = Assets.lotties.seaching.path; static const zupAPIDio = 'zup_api_dio'; static const confettiController10s = 'confetti_controller_10s'; + static const zupHolderFactory = 'zup_holder_factory'; } Future setupInjections() async { @@ -73,9 +78,10 @@ Future setupInjections() async { inject.registerLazySingleton(() => FirebaseAnalytics.instance); inject.registerLazySingleton(() => ZupAnalytics(inject())); inject.registerLazySingleton(() => ZupHolder()); + inject.registerFactory(() => ZupHolder(), instanceName: InjectInstanceNames.zupHolderFactory); inject.registerLazySingleton(() => Erc20()); inject.registerLazySingleton>(() => GlobalKey()); - inject.registerLazySingleton(() => UniswapPositionManager()); + inject.registerLazySingleton(() => UniswapV3PositionManager()); inject.registerLazySingleton(() => ZupSingletonCache.shared); inject.registerFactory(() => ZupLinks()); @@ -103,6 +109,29 @@ Future setupInjections() async { () => Assets.lotties.seaching.lottie(), instanceName: InjectInstanceNames.lottieSearching, ); + inject.registerLazySingleton( + () => UniswapV4StateView(), + ); + + inject.registerLazySingleton(() => EthereumAbiCoder()); + + inject.registerLazySingleton( + () => PoolService( + inject(), + inject(), + inject(), + inject(), + inject(), + ), + ); + + inject.registerLazySingleton( + () => UniswapPermit2(), + ); + + inject.registerLazySingleton( + () => UniswapV4PositionManager(), + ); // WARNING: this should always be factory following the instructions inject.registerFactory( diff --git a/lib/core/mixins/keys_mixin.dart b/lib/core/mixins/keys_mixin.dart index 0093135..35d41c6 100644 --- a/lib/core/mixins/keys_mixin.dart +++ b/lib/core/mixins/keys_mixin.dart @@ -8,4 +8,8 @@ mixin KeysMixin { String poolTickCacheKey({required AppNetworks network, required String poolAddress}) { return 'poolTick-$poolAddress-${network.name}'; } + + String tokenPriceCacheKey({required String tokenAddress, required AppNetworks network}) { + return 'tokenPrice-$tokenAddress-${network.name}'; + } } diff --git a/lib/core/mixins/v3_pool_conversors_mixin.dart b/lib/core/mixins/v3_pool_conversors_mixin.dart index 03a88e5..e45c6b0 100644 --- a/lib/core/mixins/v3_pool_conversors_mixin.dart +++ b/lib/core/mixins/v3_pool_conversors_mixin.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:zup_app/core/v3_pool_constants.dart'; +import 'package:zup_app/core/v3_v4_pool_constants.dart'; mixin V3PoolConversorsMixin { ({double priceAsQuoteToken, double priceAsBaseToken}) tickToPrice({ @@ -22,11 +22,11 @@ mixin V3PoolConversorsMixin { final highestValidTickDistanceFromTick = (highestValidTick - tick).abs(); if (lowestValidTickDistanceFromTick < highestValidTickDistanceFromTick && - lowestValidTick >= V3PoolConstants.minTick) { + lowestValidTick >= V3V4PoolConstants.minTick) { return lowestValidTick; } - return highestValidTick > V3PoolConstants.maxTick ? lowestValidTick : highestValidTick; + return highestValidTick > V3V4PoolConstants.maxTick ? lowestValidTick : highestValidTick; } BigInt priceToTick({ diff --git a/lib/core/mixins/v4_pool_liquidity_calculations_mixin.dart b/lib/core/mixins/v4_pool_liquidity_calculations_mixin.dart new file mode 100644 index 0000000..4f1def4 --- /dev/null +++ b/lib/core/mixins/v4_pool_liquidity_calculations_mixin.dart @@ -0,0 +1,84 @@ +mixin V4PoolLiquidityCalculationsMixin { + final _q96 = BigInt.parse("0x1000000000000000000000000"); + + BigInt getLiquidityForAmount0(BigInt sqrtPriceAX96, BigInt sqrtPriceBX96, BigInt amount0) { + if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); + + BigInt intermediate = ((sqrtPriceAX96 * sqrtPriceBX96) ~/ _q96); + return (amount0 * intermediate) ~/ (sqrtPriceBX96 - sqrtPriceAX96); + } + + BigInt getLiquidityForAmount1(BigInt sqrtPriceAX96, BigInt sqrtPriceBX96, BigInt amount1) { + if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); + return (amount1 * _q96) ~/ (sqrtPriceBX96 - sqrtPriceAX96); + } + + BigInt getLiquidityForAmounts( + BigInt sqrtPriceX96, + BigInt sqrtPriceAX96, + BigInt sqrtPriceBX96, + BigInt amount0, + BigInt amount1, + ) { + if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); + + if (sqrtPriceX96 <= sqrtPriceAX96) { + return getLiquidityForAmount0(sqrtPriceAX96, sqrtPriceBX96, amount0); + } else if (sqrtPriceX96 < sqrtPriceBX96) { + BigInt liquidity0 = getLiquidityForAmount0(sqrtPriceX96, sqrtPriceBX96, amount0); + BigInt liquidity1 = getLiquidityForAmount1(sqrtPriceAX96, sqrtPriceX96, amount1); + + return liquidity0 < liquidity1 ? liquidity0 : liquidity1; + } else { + return getLiquidityForAmount1(sqrtPriceAX96, sqrtPriceBX96, amount1); + } + } + + BigInt getSqrtPriceAtTick(BigInt tick) { + const int maxTick = 887272; + final List mulConstants = [ + BigInt.parse('fff97272373d413259a46990580e213a', radix: 16), + BigInt.parse('fff2e50f5f656932ef12357cf3c7fdcc', radix: 16), + BigInt.parse('ffe5caca7e10e4e61c3624eaa0941cd0', radix: 16), + BigInt.parse('ffcb9843d60f6159c9db58835c926644', radix: 16), + BigInt.parse('ff973b41fa98c081472e6896dfb254c0', radix: 16), + BigInt.parse('ff2ea16466c96a3843ec78b326b52861', radix: 16), + BigInt.parse('fe5dee046a99a2a811c461f1969c3053', radix: 16), + BigInt.parse('fcbe86c7900a88aedcffc83b479aa3a4', radix: 16), + BigInt.parse('f987a7253ac413176f2b074cf7815e54', radix: 16), + BigInt.parse('f3392b0822b70005940c7a398e4b70f3', radix: 16), + BigInt.parse('e7159475a2c29b7443b29c7fa6e889d9', radix: 16), + BigInt.parse('d097f3bdfd2022b8845ad8f792aa5825', radix: 16), + BigInt.parse('a9f746462d870fdf8a65dc1f90e061e5', radix: 16), + BigInt.parse('70d869a156d2a1b890bb3df62baf32f7', radix: 16), + BigInt.parse('31be135f97d08fd981231505542fcfa6', radix: 16), + BigInt.parse('9aa508b5b7a84e1c677de54f3e99bc9', radix: 16), + BigInt.parse('5d6af8dedb81196699c329225ee604', radix: 16), + BigInt.parse('2216e584f5fa1ea926041bedfe98', radix: 16), + BigInt.parse('48a170391f7dc42444e8fa2', radix: 16), + ]; + + BigInt absTick = tick.isNegative ? -tick : tick; + if (absTick > BigInt.from(maxTick)) throw Exception('Tick out of range'); + + BigInt price = + (absTick.toInt() & 0x1) != 0 ? BigInt.parse('fffcb933bd6fad37aa2d162d1a594001', radix: 16) : BigInt.one << 128; + + for (int i = 0; i < mulConstants.length; i++) { + if ((absTick.toInt() & (1 << (i + 1))) != 0) { + price = (price * mulConstants[i]) >> 128; + } + } + + if (tick > BigInt.zero) { + BigInt maxUint256 = (BigInt.one << 256) - BigInt.one; + price = maxUint256 ~/ price; + } + + BigInt maxUint32 = (BigInt.one << 32) - BigInt.one; + price = (price + maxUint32) >> 32; + + BigInt mask160 = (BigInt.one << 160) - BigInt.one; + return price & mask160; + } +} diff --git a/lib/core/pool_service.dart b/lib/core/pool_service.dart new file mode 100644 index 0000000..8a85d1a --- /dev/null +++ b/lib/core/pool_service.dart @@ -0,0 +1,223 @@ +import 'package:clock/clock.dart'; +import 'package:web3kit/core/dtos/transaction_response.dart'; +import 'package:web3kit/web3kit.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/yield_dto.dart'; +import 'package:zup_app/core/mixins/v4_pool_liquidity_calculations_mixin.dart'; +import 'package:zup_app/core/v4_pool_constants.dart'; + +class PoolService with V4PoolLiquidityCalculationsMixin { + final UniswapV4StateView _uniswapV4StateView; + final UniswapV3Pool _uniswapV3Pool; + final UniswapV3PositionManager _uniswapV3PositionManager; + final UniswapV4PositionManager _uniswapV4PositionManager; + final EthereumAbiCoder _ethereumAbiCoder; + + PoolService( + this._uniswapV4StateView, + this._uniswapV3Pool, + this._uniswapV3PositionManager, + this._uniswapV4PositionManager, + this._ethereumAbiCoder, + ); + + Future getPoolTick(YieldDto forYield) async { + if (forYield.poolType.isV4) { + final stateView = _uniswapV4StateView.fromRpcProvider( + contractAddress: forYield.v4StateView!, + rpcUrl: forYield.network.rpcUrl, + ); + + return (await stateView.getSlot0(poolId: forYield.poolAddress)).tick; + } + + final uniswapV3Pool = _uniswapV3Pool.fromRpcProvider( + contractAddress: forYield.poolAddress, + rpcUrl: forYield.network.rpcUrl, + ); + + return (await uniswapV3Pool.slot0()).tick; + } + + Future sendV3PoolDepositTransaction( + YieldDto depositOnYield, + Signer signer, { + required BigInt amount0Desired, + required BigInt amount1Desired, + required Duration deadline, + required BigInt amount0Min, + required BigInt amount1Min, + required String recipient, + required BigInt tickLower, + required BigInt tickUpper, + }) async { + final v3PositionManagerContract = _uniswapV3PositionManager.fromSigner( + contractAddress: depositOnYield.positionManagerAddress, + signer: signer, + ); + + final TransactionResponse tx = await () async { + if (depositOnYield.isToken1Native || depositOnYield.isToken0Native) { + final mintCalldata = _uniswapV3PositionManager.getMintCalldata( + params: ( + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + deadline: BigInt.from(clock.now().add(deadline).millisecondsSinceEpoch), + amount0Min: amount0Min, + amount1Min: amount1Min, + recipient: recipient, + tickLower: tickLower, + tickUpper: tickUpper, + fee: BigInt.from(depositOnYield.feeTier), + token0: depositOnYield.token0.addresses[depositOnYield.network.chainId]! == EthereumConstants.zeroAddress + ? depositOnYield.network.wrappedNativeTokenAddress + : depositOnYield.token0.addresses[depositOnYield.network.chainId]!, + token1: depositOnYield.token1.addresses[depositOnYield.network.chainId]! == EthereumConstants.zeroAddress + ? depositOnYield.network.wrappedNativeTokenAddress + : depositOnYield.token1.addresses[depositOnYield.network.chainId]!, + ), + ); + + return await v3PositionManagerContract.multicall( + data: [ + mintCalldata, + _uniswapV3PositionManager.getRefundETHCalldata(), + ], + ethValue: () { + if (depositOnYield.isToken0Native) { + return amount0Desired; + } + + return amount1Desired; + }.call()); + } + + return await v3PositionManagerContract.mint( + params: ( + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + deadline: BigInt.from(clock.now().add(deadline).millisecondsSinceEpoch), + amount0Min: amount0Min, + amount1Min: amount1Min, + recipient: recipient, + tickLower: tickLower, + tickUpper: tickUpper, + fee: BigInt.from(depositOnYield.feeTier), + token0: depositOnYield.token0.addresses[depositOnYield.network.chainId]!, + token1: depositOnYield.token1.addresses[depositOnYield.network.chainId]!, + ), + ); + }.call(); + + return tx; + } + + Future sendV4PoolDepositTransaction( + YieldDto depositOnYield, + Signer signer, { + required Duration deadline, + required BigInt tickLower, + required BigInt tickUpper, + required BigInt amount0toDeposit, + required BigInt amount1ToDeposit, + required BigInt maxAmount0ToDeposit, + required BigInt maxAmount1ToDeposit, + required String recipient, + required BigInt currentPoolTick, + }) async { + final isNativeDeposit = depositOnYield.isToken0Native || depositOnYield.isToken1Native; + + final actions = _ethereumAbiCoder.encodePacked([ + "uint8", + "uint8", + if (isNativeDeposit) "uint8", + ], [ + V4PoolConstants.mintPositionActionValue, + V4PoolConstants.settlePairActionValue, + if (isNativeDeposit) V4PoolConstants.sweepActionValue, + ]); + + final mintPositionActionParams = _ethereumAbiCoder.encode([ + "tuple(address,address,int32,int24,address)", + "int24", + "int24", + "uint256", + "uint128", + "uint128", + "address", + "bytes" + ], [ + [ + depositOnYield.token0.addresses[depositOnYield.network.chainId]!, + depositOnYield.token1.addresses[depositOnYield.network.chainId]!, + BigInt.from(depositOnYield.feeTier), + BigInt.from(depositOnYield.tickSpacing), + depositOnYield.v4Hooks, + ], + tickLower, + tickUpper, + getLiquidityForAmounts( + getSqrtPriceAtTick(currentPoolTick), + getSqrtPriceAtTick(tickLower), + getSqrtPriceAtTick(tickUpper), + amount0toDeposit, + amount1ToDeposit, + ), + maxAmount0ToDeposit, + maxAmount1ToDeposit, + recipient, + EthereumConstants.emptyBytes, + ]); + + final settlePairActionParams = _ethereumAbiCoder.encode([ + "address", + "address" + ], [ + depositOnYield.token0.addresses[depositOnYield.network.chainId]!, + depositOnYield.token1.addresses[depositOnYield.network.chainId]!, + ]); + + final sweepActionParams = isNativeDeposit + ? _ethereumAbiCoder.encode([ + "address", + "address" + ], [ + EthereumConstants.zeroAddress, + recipient, + ]) + : null; + + final uniswapV4PositionManagerContract = _uniswapV4PositionManager.fromSigner( + contractAddress: depositOnYield.positionManagerAddress, + signer: signer, + ); + + final params = [ + mintPositionActionParams, + settlePairActionParams, + if (isNativeDeposit) sweepActionParams, + ]; + + final unlockData = _ethereumAbiCoder.encode([ + "bytes", + "bytes[]" + ], [ + actions, + params, + ]); + + return await uniswapV4PositionManagerContract.modifyLiquidities( + deadline: BigInt.from(clock.now().add(deadline).millisecondsSinceEpoch), + unlockData: unlockData, + ethValue: () { + if (!isNativeDeposit) return null; + if (depositOnYield.isToken0Native) return amount0toDeposit; + + return amount1ToDeposit; + }.call(), + ); + } +} diff --git a/lib/core/repositories/tokens_repository.dart b/lib/core/repositories/tokens_repository.dart index c9c119c..455af32 100644 --- a/lib/core/repositories/tokens_repository.dart +++ b/lib/core/repositories/tokens_repository.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:zup_app/core/dtos/token_dto.dart'; +import 'package:zup_app/core/dtos/token_price_dto.dart'; import 'package:zup_app/core/enums/networks.dart'; class TokensRepository { @@ -35,4 +36,13 @@ class TokensRepository { _searchTokenLastCancelToken = null; return (response.data as List).map((token) => TokenDto.fromJson(token)).toList(); } + + Future getTokenPrice(String address, AppNetworks network) async { + final response = await _zupAPIDio.get("/tokens/price", queryParameters: { + "address": address, + "chainId": network.chainId, + }); + + return TokenPriceDto.fromJson(response.data); + } } diff --git a/lib/core/repositories/yield_repository.dart b/lib/core/repositories/yield_repository.dart index b856ebc..cde644d 100644 --- a/lib/core/repositories/yield_repository.dart +++ b/lib/core/repositories/yield_repository.dart @@ -1,5 +1,5 @@ import 'package:dio/dio.dart'; -import 'package:web3kit/core/ethereum_constants.dart'; +import 'package:zup_app/core/dtos/pool_search_settings_dto.dart'; import 'package:zup_app/core/dtos/yields_dto.dart'; import 'package:zup_app/core/enums/networks.dart'; @@ -12,31 +12,42 @@ class YieldRepository { required String token0Address, required String token1Address, required AppNetworks network, - num? minTvlUsd, + required PoolSearchSettingsDto searchSettings, }) async { - final response = await _zupAPIDio.get("/pools/search/${network.chainId}", queryParameters: { - "token0Address": token0Address != EthereumConstants.zeroAddress - ? token0Address - : network.wrappedNative.addresses[network.chainId]!, - "token1Address": token1Address != EthereumConstants.zeroAddress - ? token1Address - : network.wrappedNative.addresses[network.chainId]!, - if (minTvlUsd != null) "minTvlUsd": minTvlUsd + final response = await _zupAPIDio.post("/pools/search/${network.chainId}", queryParameters: { + "token0Address": token0Address, + "token1Address": token1Address, + }, data: { + "filters": { + "minTvlUsd": searchSettings.minLiquidityUSD, + "allowedPoolTypes": [ + if (searchSettings.allowV3Search) "V3", + if (searchSettings.allowV4Search) "V4", + ], + } }); return YieldsDto.fromJson(response.data); } - Future getAllNetworksYield( - {required String token0InternalId, - required String token1InternalId, - num? minTvlUsd, - bool testnetMode = false}) async { - final response = await _zupAPIDio.get("/pools/search/all", queryParameters: { + Future getAllNetworksYield({ + required String token0InternalId, + required String token1InternalId, + required PoolSearchSettingsDto searchSettings, + bool testnetMode = false, + }) async { + final response = await _zupAPIDio.post("/pools/search/all", queryParameters: { "token0Id": token0InternalId, "token1Id": token1InternalId, - "testnetMode": testnetMode, - if (minTvlUsd != null) "minTvlUsd": minTvlUsd, + }, data: { + "filters": { + "minTvlUsd": searchSettings.minLiquidityUSD, + "testnetMode": testnetMode, + "allowedPoolTypes": [ + if (searchSettings.allowV3Search) "V3", + if (searchSettings.allowV4Search) "V4", + ], + } }); return YieldsDto.fromJson(response.data); diff --git a/lib/core/slippage.dart b/lib/core/slippage.dart index 37c159d..d1748ff 100644 --- a/lib/core/slippage.dart +++ b/lib/core/slippage.dart @@ -35,10 +35,14 @@ class Slippage extends Equatable { return ZupColors.brand; } - BigInt calculateTokenAmountFromSlippage(BigInt amount) { + BigInt calculateMinTokenAmountFromSlippage(BigInt amount) { return amount * (BigInt.from(10000) - BigInt.from(valueBasisPoints)) ~/ BigInt.from(10000); } + BigInt calculateMaxTokenAmountFromSlippage(BigInt amount) { + return amount * (BigInt.from(10000) + BigInt.from(valueBasisPoints)) ~/ BigInt.from(10000); + } + bool get isCustom => this != zeroPointOnePercent && this != halfPercent && this != onePercent; int get valueBasisPoints => (value * 100).toInt(); diff --git a/lib/core/v3_pool_constants.dart b/lib/core/v3_v4_pool_constants.dart similarity index 73% rename from lib/core/v3_pool_constants.dart rename to lib/core/v3_v4_pool_constants.dart index 22ce80b..9f6a3ec 100644 --- a/lib/core/v3_pool_constants.dart +++ b/lib/core/v3_v4_pool_constants.dart @@ -1,4 +1,4 @@ -abstract class V3PoolConstants { +abstract class V3V4PoolConstants { static final BigInt minTick = BigInt.from(-887272); static final BigInt maxTick = -minTick; } diff --git a/lib/core/v4_pool_constants.dart b/lib/core/v4_pool_constants.dart new file mode 100644 index 0000000..75ad585 --- /dev/null +++ b/lib/core/v4_pool_constants.dart @@ -0,0 +1,5 @@ +abstract class V4PoolConstants { + static const mintPositionActionValue = 0x02; + static const settlePairActionValue = 0x0d; + static const sweepActionValue = 0x14; +} diff --git a/lib/l10n/en.arb b/lib/l10n/en.arb index fae6f3d..bdea6df 100644 --- a/lib/l10n/en.arb +++ b/lib/l10n/en.arb @@ -338,6 +338,8 @@ } }, "createPageSettingsDropdownMinimumLiquidity": "Minimum Pool Liquidity", + "createPageSettingsDropdownAllowedPoolTypes": "Allowed Pool Types", + "createPageSettingsDropdownAllowedPoolTypesDescription": "Filter the types of liquidity pools to include in your search", "createPageSettingsDropdownMinimumLiquidityExplanation": "Filter pools by minimum liquidity. We’ll exclude pools with less liquidity than specified, as low Liquidity can lead to misleading yields. This helps you find more reliable opportunities", "createPageSettingsDropdownMiniumLiquidityLowWarning": "Low minimum TVL can lead to misleading yields.", "appSettingsDropdownTestnetMode": "Testnet Mode", diff --git a/lib/l10n/gen/app_localizations.dart b/lib/l10n/gen/app_localizations.dart index 7186596..e0f6eac 100644 --- a/lib/l10n/gen/app_localizations.dart +++ b/lib/l10n/gen/app_localizations.dart @@ -965,6 +965,18 @@ abstract class S { /// **'Minimum Pool Liquidity'** String get createPageSettingsDropdownMinimumLiquidity; + /// No description provided for @createPageSettingsDropdownAllowedPoolTypes. + /// + /// In en, this message translates to: + /// **'Allowed Pool Types'** + String get createPageSettingsDropdownAllowedPoolTypes; + + /// No description provided for @createPageSettingsDropdownAllowedPoolTypesDescription. + /// + /// In en, this message translates to: + /// **'Filter the types of liquidity pools to include in your search'** + String get createPageSettingsDropdownAllowedPoolTypesDescription; + /// No description provided for @createPageSettingsDropdownMinimumLiquidityExplanation. /// /// 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 1a6d63f..b4ec1ad 100644 --- a/lib/l10n/gen/app_localizations_en.dart +++ b/lib/l10n/gen/app_localizations_en.dart @@ -543,6 +543,13 @@ class SEn extends S { String get createPageSettingsDropdownMinimumLiquidity => 'Minimum Pool Liquidity'; + @override + String get createPageSettingsDropdownAllowedPoolTypes => 'Allowed Pool Types'; + + @override + String get createPageSettingsDropdownAllowedPoolTypesDescription => + 'Filter the types of liquidity pools to include in your search'; + @override String get createPageSettingsDropdownMinimumLiquidityExplanation => 'Filter pools by minimum liquidity. We’ll exclude pools with less liquidity than specified, as low Liquidity can lead to misleading yields. This helps you find more reliable opportunities'; diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 2e9a2db..52a7343 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -6,11 +6,11 @@ PODS: - FirebaseAnalytics (~> 11.10.0) - Firebase/CoreOnly (11.10.0): - FirebaseCore (~> 11.10.0) - - firebase_analytics (11.4.5): + - firebase_analytics (11.4.6): - Firebase/Analytics (= 11.10.0) - firebase_core - FlutterMacOS - - firebase_core (3.13.0): + - firebase_core (3.13.1): - Firebase/CoreOnly (~> 11.10.0) - FlutterMacOS - FirebaseAnalytics (11.10.0): @@ -102,7 +102,7 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqflite (0.0.3): + - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - url_launcher_macos (0.0.1): @@ -114,7 +114,7 @@ DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) + - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) SPEC REPOS: @@ -140,15 +140,15 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin - sqflite: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin + sqflite_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2 - firebase_analytics: 934289da4d473663d5866f1563f0cae2a38ebd82 - firebase_core: bb06473757206589a00a36920cbf0f33646e19cc + firebase_analytics: 42038ac430b2701716daba1fe06325c2e922aaa0 + firebase_core: 2a91d5383968c70446b882d94e2fa54d11a28661 FirebaseAnalytics: 4e42333f02cf78ed93703a5c36f36dd518aebdef FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7 FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679 @@ -160,8 +160,8 @@ SPEC CHECKSUMS: path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec - url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 diff --git a/pubspec.lock b/pubspec.lock index 482fc2c..e842ad6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -58,10 +58,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" bloc: dependency: transitive description: @@ -266,10 +266,10 @@ packages: dependency: "direct main" description: name: decimal - sha256: "4140a688f9e443e2f4de3a1162387bf25e1ac6d51e24c9da263f245210f41440" + sha256: "28239b8b929c1bd8618702e6dbc96e2618cf99770bbe9cb040d6cf56a11e4ec3" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.2.1" dio: dependency: "direct main" description: @@ -314,10 +314,10 @@ packages: dependency: "direct dev" description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -590,10 +590,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: @@ -630,10 +630,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -999,10 +999,10 @@ packages: dependency: "direct main" description: name: skeletonizer - sha256: "0dcacc51c144af4edaf37672072156f49e47036becbc394d7c51850c5c1e884b" + sha256: a9ddf63900947f4c0648372b6e9987bc2b028db9db843376db6767224d166c31 url: "https://pub.dev" source: hosted - version: "1.4.3" + version: "2.0.1" sky_engine: dependency: transitive description: flutter @@ -1268,10 +1268,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" watcher: dependency: transitive description: @@ -1293,7 +1293,7 @@ packages: description: path: "." ref: main - resolved-ref: efecab377221d2984aaaa7329dfb5cebcccf58f2 + resolved-ref: "42dfe9f12673e21d2bc15623a6f58a1094969f2c" url: "https://github.com/Zup-Protocol/web3kit.git" source: git version: "0.0.1" @@ -1357,4 +1357,4 @@ packages: version: "0.0.1" sdks: dart: ">=3.7.0 <4.0.0" - flutter: ">=3.29.2" + flutter: ">=3.32.2" diff --git a/pubspec.yaml b/pubspec.yaml index e6c1770..132d90b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ publish_to: none environment: sdk: ^3.5.2 - flutter: 3.29.2 + flutter: 3.32.2 dependencies: build_runner: ^2.4.12 @@ -38,10 +38,10 @@ dependencies: json_annotation: ^4.9.0 url_launcher: ^6.3.0 cached_network_image: ^3.4.1 - skeletonizer: ^1.4.2 + skeletonizer: ^2.0.1 shared_preferences: ^2.3.2 dio: ^5.7.0 - intl: ^0.19.0 + intl: ^0.20.2 lottie: ^3.1.2 equatable: ^2.0.5 decimal: ^3.0.2 diff --git a/test/app/create/deposit/deposit_cubit_test.dart b/test/app/create/deposit/deposit_cubit_test.dart index e2768d3..d3d5284 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/pool_service.dart'; import 'package:zup_app/core/repositories/yield_repository.dart'; import 'package:zup_app/core/slippage.dart'; import 'package:zup_app/core/zup_analytics.dart'; @@ -29,12 +30,15 @@ void main() { late Cache cache; late AppCubit appCubit; late ZupAnalytics zupAnalytics; - + late PoolService poolService; final poolTick = BigInt.from(31276567121); setUp(() { registerFallbackValue(DepositSettingsDto.fixture()); registerFallbackValue(AppNetworks.sepolia); + registerFallbackValue(PoolSearchSettingsDto.fixture()); + registerFallbackValue(YieldDto.fixture()); + poolService = PoolServiceMock(); yieldRepository = YieldRepositoryMock(); zupSingletonCache = ZupSingletonCache.shared; @@ -49,10 +53,10 @@ void main() { yieldRepository, zupSingletonCache, wallet, - uniswapV3Pool, cache, appCubit, zupAnalytics, + poolService, ); when(() => appCubit.isTestnetMode).thenReturn(false); @@ -60,7 +64,7 @@ void main() { when(() => yieldRepository.getAllNetworksYield( token0InternalId: any(named: "token0InternalId"), token1InternalId: any(named: "token1InternalId"), - minTvlUsd: any(named: "minTvlUsd"), + searchSettings: any(named: "searchSettings"), testnetMode: any(named: "testnetMode"))).thenAnswer((_) async => YieldsDto.fixture()); when(() => appCubit.selectedNetwork).thenAnswer((_) => AppNetworks.sepolia); @@ -87,6 +91,7 @@ void main() { unlocked: true )); + when(() => poolService.getPoolTick(any())).thenAnswer((_) async => poolTick); when(() => cache.getPoolSearchSettings()).thenReturn(PoolSearchSettingsDto(minLiquidityUSD: 129816)); }); @@ -180,7 +185,7 @@ void main() { when(() => yieldRepository.getSingleNetworkYield( token0Address: any(named: "token0Address"), token1Address: any(named: "token1Address"), - minTvlUsd: any(named: "minTvlUsd"), + searchSettings: any(named: "searchSettings"), network: any(named: "network"))).thenAnswer( (_) async => YieldsDto.fixture(), ); @@ -193,7 +198,7 @@ void main() { verify(() => yieldRepository.getSingleNetworkYield( token0Address: token0Address, token1Address: token1Address, - minTvlUsd: any(named: "minTvlUsd"), + searchSettings: any(named: "searchSettings"), network: any(named: "network"), )).called(1); }); @@ -206,7 +211,7 @@ void main() { when(() => yieldRepository.getSingleNetworkYield( token0Address: any(named: "token0Address"), token1Address: any(named: "token1Address"), - minTvlUsd: any(named: "minTvlUsd"), + searchSettings: any(named: "searchSettings"), network: any(named: "network"))).thenAnswer( (_) async => const YieldsDto(pools: [], minLiquidityUSD: minLiquidityUSD), ); @@ -227,7 +232,7 @@ void main() { when(() => yieldRepository.getSingleNetworkYield( token0Address: any(named: "token0Address"), token1Address: any(named: "token1Address"), - minTvlUsd: any(named: "minTvlUsd"), + searchSettings: any(named: "searchSettings"), network: any(named: "network"))).thenAnswer((_) async => pools); expectLater(sut.stream, emitsInOrder([const DepositState.loading(), DepositState.success(pools)])); @@ -239,7 +244,7 @@ void main() { when(() => yieldRepository.getSingleNetworkYield( token0Address: any(named: "token0Address"), token1Address: any(named: "token1Address"), - minTvlUsd: any(named: "minTvlUsd"), + searchSettings: any(named: "searchSettings"), network: any(named: "network"))).thenThrow(Exception()); expectLater(sut.stream, emitsInOrder([const DepositState.loading(), const DepositState.error()])); @@ -268,7 +273,7 @@ void main() { await sut.selectYield(selectedYield, YieldTimeFrame.day); - verify(() => uniswapV3PoolImpl.slot0()).called(1); + verify(() => poolService.getPoolTick(selectedYield)).called(1); }); test("When calling `selectYield` but the yield is null, it should not get the pool tick", () async { @@ -291,35 +296,20 @@ void main() { await sut.getSelectedPoolTick(); }); - test("""When calling `getSelectedPoolTick` it should create the - UniswapV3Pool contract from RPC, with the address of the selected - pool""", () async { - final selectedYield = YieldDto.fixture(); - - await sut.selectYield(YieldDto.fixture(), YieldTimeFrame.day); - await sut.getSelectedPoolTick(); - - verify( - () => uniswapV3Pool.fromRpcProvider( - contractAddress: selectedYield.poolAddress, - rpcUrl: selectedYield.network.rpcUrl, - ), - ).called(2); // 2 because of the `selectYield` and the `getSelectedPoolTick` - }); - - test("When calling `getSelectedPoolTick` it should use the slot0 from the UniswapV3Pool contract", () async { - await sut.selectYield(YieldDto.fixture(), YieldTimeFrame.day); + test("When calling `getSelectedPoolTick` it should use the pool service to get it", () async { + final yieldDto = YieldDto.fixture(); + await sut.selectYield(yieldDto, YieldTimeFrame.day); await sut.getSelectedPoolTick(); - verify(() => uniswapV3PoolImpl.slot0()).called(2); // 2 because of the `selectYield` and the `getSelectedPoolTick` + verify(() => poolService.getPoolTick(yieldDto)) + .called(2); // 2 because of the `selectYield` and the `getSelectedPoolTick` }); test("""" When calling `getSelectedPoolTick` for a selected pool, but when the call to the contract completes, the selected pool is not the same as the one passed to the call, it shoul re-call - `getSelectedPoolTick` to get the correct pool tick -""", () async { + `getSelectedPoolTick` to get the correct pool tick""", () async { final expectedYieldBTick = BigInt.from(326287637265372111); const yieldAPoolAddress = "0x3263782637263"; @@ -328,37 +318,20 @@ void main() { final yieldA = YieldDto.fixture().copyWith(poolAddress: yieldAPoolAddress); final yieldB = YieldDto.fixture().copyWith(poolAddress: yieldBPoolAddress); - when(() => uniswapV3PoolImpl.slot0()).thenAnswer((_) async { - when(() => uniswapV3PoolImpl.slot0()).thenAnswer((_) async { - return ( - feeProtocol: BigInt.zero, - observationCardinality: BigInt.zero, - observationCardinalityNext: BigInt.zero, - observationIndex: BigInt.zero, - sqrtPriceX96: BigInt.zero, - tick: expectedYieldBTick, - unlocked: true - ); + when(() => poolService.getPoolTick(any())).thenAnswer((_) async { + when(() => poolService.getPoolTick(any())).thenAnswer((_) async { + return expectedYieldBTick; }); await sut.selectYield(yieldB, YieldTimeFrame.day); - return ( - feeProtocol: BigInt.zero, - observationCardinality: BigInt.zero, - observationCardinalityNext: BigInt.zero, - observationIndex: BigInt.zero, - sqrtPriceX96: BigInt.zero, - tick: poolTick, - unlocked: true - ); + return poolTick; }); await sut.selectYield(yieldA, YieldTimeFrame.day); // assuming that select yield will call `getSelectedPoolTick` - verify( - () => uniswapV3Pool.fromRpcProvider(contractAddress: yieldBPoolAddress, rpcUrl: yieldB.network.rpcUrl), - ).called(2); // 2 because of the check in the `getSelectedPoolTick` that will re-call, and the selection + verify(() => poolService.getPoolTick(yieldB)) + .called(2); // 2 because of the check in the `getSelectedPoolTick` that will re-call, and the selection expect(sut.latestPoolTick, expectedYieldBTick); }); @@ -366,15 +339,7 @@ void main() { test("When calling `getSelectedPoolTick` it should emit the pool tick got", () async { final expectedPoolTick = BigInt.from(97866745634534392); - when(() => uniswapV3PoolImpl.slot0()).thenAnswer((_) async => ( - feeProtocol: BigInt.zero, - observationCardinality: BigInt.zero, - observationCardinalityNext: BigInt.zero, - observationIndex: BigInt.zero, - sqrtPriceX96: BigInt.zero, - tick: expectedPoolTick, - unlocked: true - )); + when(() => poolService.getPoolTick(any())).thenAnswer((_) async => expectedPoolTick); expectLater(sut.poolTickStream, emitsInOrder([null, expectedPoolTick])); @@ -385,15 +350,7 @@ void main() { test("When calling `getSelectedPoolTick` it should save the pool tick in the cubit", () async { final expectedPoolTick = BigInt.from(97866745634534392); - when(() => uniswapV3PoolImpl.slot0()).thenAnswer((_) async => ( - feeProtocol: BigInt.zero, - observationCardinality: BigInt.zero, - observationCardinalityNext: BigInt.zero, - observationIndex: BigInt.zero, - sqrtPriceX96: BigInt.zero, - tick: expectedPoolTick, - unlocked: true - )); + when(() => poolService.getPoolTick(any())).thenAnswer((_) async => expectedPoolTick); await sut.selectYield( YieldDto.fixture(), YieldTimeFrame.day); // assuming that select yield will call `getSelectedPoolTick` @@ -404,15 +361,7 @@ void main() { test("When calling `getSelectedPoolTick` it should save the same tick as the emitted ", () async { final expectedPoolTick = BigInt.from(97866745634534392); - when(() => uniswapV3PoolImpl.slot0()).thenAnswer((_) async => ( - feeProtocol: BigInt.zero, - observationCardinality: BigInt.zero, - observationCardinalityNext: BigInt.zero, - observationIndex: BigInt.zero, - sqrtPriceX96: BigInt.zero, - tick: expectedPoolTick, - unlocked: true - )); + when(() => poolService.getPoolTick(any())).thenAnswer((_) async => expectedPoolTick); expectLater(sut.poolTickStream, emitsInOrder([null, expectedPoolTick])); await sut.selectYield( @@ -588,7 +537,7 @@ void main() { token0Address: any(named: "token0Address"), token1Address: any(named: "token1Address"), network: any(named: "network"), - minTvlUsd: any(named: "minTvlUsd"), + searchSettings: any(named: "searchSettings"), )).thenAnswer((_) async => YieldsDto.fixture()); await sut.getBestPools(token0AddressOrId: "0x", token1AddressOrId: "0x", ignoreMinLiquidity: true); @@ -597,7 +546,26 @@ void main() { token0Address: any(named: "token0Address"), token1Address: any(named: "token1Address"), network: any(named: "network"), - minTvlUsd: 0, + searchSettings: PoolSearchSettingsDto.fixture().copyWith(minLiquidityUSD: 0), + )).called(1); + }); + + test("""When calling 'getBestPools' with the current network as all networks and the param + 'ignoreMinLiquidity' true, it should pass the minLiquidityUSD as 0 to the repository""", () async { + when(() => appCubit.selectedNetwork).thenReturn(AppNetworks.allNetworks); + when(() => cache.getPoolSearchSettings()).thenReturn(PoolSearchSettingsDto(minLiquidityUSD: 129816)); + when(() => yieldRepository.getAllNetworksYield( + token0InternalId: any(named: "token0InternalId"), + token1InternalId: any(named: "token1InternalId"), + searchSettings: any(named: "searchSettings"), + )).thenAnswer((_) async => YieldsDto.fixture()); + + await sut.getBestPools(token0AddressOrId: "0x", token1AddressOrId: "0x", ignoreMinLiquidity: true); + + verify(() => yieldRepository.getAllNetworksYield( + token0InternalId: any(named: "token0InternalId"), + token1InternalId: any(named: "token1InternalId"), + searchSettings: PoolSearchSettingsDto.fixture().copyWith(minLiquidityUSD: 0), )).called(1); }); @@ -610,7 +578,7 @@ void main() { token0Address: any(named: "token0Address"), token1Address: any(named: "token1Address"), network: any(named: "network"), - minTvlUsd: any(named: "minTvlUsd"), + searchSettings: any(named: "searchSettings"), )).thenAnswer((_) async => YieldsDto.fixture()); await sut.getBestPools(token0AddressOrId: "0x", token1AddressOrId: "0x", ignoreMinLiquidity: false); @@ -619,7 +587,7 @@ void main() { token0Address: any(named: "token0Address"), token1Address: any(named: "token1Address"), network: any(named: "network"), - minTvlUsd: minLiquiditySaved, + searchSettings: PoolSearchSettingsDto.fixture().copyWith(minLiquidityUSD: minLiquiditySaved), )).called(1); }); @@ -652,7 +620,7 @@ void main() { verify(() => yieldRepository.getAllNetworksYield( token0InternalId: token0Address, token1InternalId: token1Address, - minTvlUsd: any(named: "minTvlUsd"), + searchSettings: any(named: "searchSettings"), )).called(1); }); diff --git a/test/app/create/deposit/deposit_page_test.dart b/test/app/create/deposit/deposit_page_test.dart index 5dd6e3a..ba41504 100644 --- a/test/app/create/deposit/deposit_page_test.dart +++ b/test/app/create/deposit/deposit_page_test.dart @@ -9,8 +9,9 @@ import 'package:mocktail/mocktail.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; import 'package:web3kit/web3kit.dart'; import 'package:zup_app/abis/erc_20.abi.g.dart'; -import 'package:zup_app/abis/uniswap_position_manager.abi.g.dart'; +import 'package:zup_app/abis/uniswap_permit2.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/app/app_cubit/app_cubit.dart'; import 'package:zup_app/app/create/deposit/deposit_cubit.dart'; import 'package:zup_app/app/create/deposit/deposit_page.dart'; @@ -18,14 +19,17 @@ 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/token_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/zup_navigator_paths.dart'; import 'package:zup_app/core/injections.dart'; +import 'package:zup_app/core/pool_service.dart'; +import 'package:zup_app/core/repositories/tokens_repository.dart'; import 'package:zup_app/core/slippage.dart'; import 'package:zup_app/core/zup_analytics.dart'; +import 'package:zup_app/core/zup_links.dart'; import 'package:zup_app/core/zup_navigator.dart'; import 'package:zup_app/gen/assets.gen.dart'; import 'package:zup_app/widgets/zup_cached_image.dart'; @@ -42,6 +46,10 @@ void main() { late Cache cache; late UniswapV3Pool uniswapV3pool; late Erc20 erc20; + late TokensRepository tokensRepository; + late ZupHolder zupHolder; + late PoolService poolService; + late UniswapPermit2 permit2; setUp(() async { await Web3Kit.initializeForTest(); @@ -55,6 +63,10 @@ void main() { uniswapV3pool = UniswapV3PoolMock(); erc20 = Erc20Mock(); cache = CacheMock(); + tokensRepository = TokensRepositoryMock(); + zupHolder = ZupHolder(); + poolService = PoolServiceMock(); + permit2 = UniswapPermit2Mock(); registerFallbackValue(BuildContextMock()); registerFallbackValue(AppNetworks.sepolia); @@ -89,6 +101,7 @@ void main() { inject.registerFactory(() => cache); inject.registerFactory(() => ZupAnalyticsMock()); + inject.registerFactory(() => ZupLinksMock()); inject.registerFactory>(() => GlobalKey()); inject.registerFactory(() => navigator); inject.registerFactory(() => wallet); @@ -98,8 +111,13 @@ void main() { inject.registerFactory>(() => GlobalKey()); inject.registerFactory(() => uniswapV3pool); inject.registerFactory(() => erc20); - inject.registerFactory(() => UniswapPositionManagerMock()); + inject.registerFactory(() => UniswapV3PositionManagerMock()); + inject.registerFactory(() => tokensRepository); + inject.registerFactory(() => zupHolder); + inject.registerFactory(() => poolService); + inject.registerFactory(() => permit2); + when(() => tokensRepository.getTokenPrice(any(), any())).thenAnswer((_) async => TokenPriceDto.fixture()); when(() => cubit.stream).thenAnswer((_) => const Stream.empty()); when(() => cubit.state).thenAnswer((_) => const DepositState.initial()); when(() => cubit.getBestPools( @@ -380,25 +398,27 @@ void main() { "When the running device is mobile, the range section should be adapted to it", goldenFileName: "deposit_page_range_section_mobile", (tester) async { - final selectedYield = YieldDto.fixture(); - final yields = YieldsDto.fixture(); + await tester.runAsync(() async { + final selectedYield = YieldDto.fixture(); + final yields = YieldsDto.fixture(); - when(() => cubit.depositSettings).thenReturn(DepositSettingsDto( - deadlineMinutes: 10, - maxSlippage: DepositSettingsDto.defaultMaxSlippage, - )); + when(() => cubit.depositSettings).thenReturn(DepositSettingsDto( + deadlineMinutes: 10, + maxSlippage: DepositSettingsDto.defaultMaxSlippage, + )); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(yields)); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(yields)); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - await tester.pumpDeviceBuilder(await goldenBuilder(isMobile: true)); - await tester.pumpAndSettle(); - await tester.drag(find.byKey(const Key("full-range-button")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder(isMobile: true)); + await tester.pumpAndSettle(); + await tester.drag(find.byKey(const Key("full-range-button")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); + }); }, ); @@ -446,15 +466,17 @@ void main() { zGoldenTest("When the selected yield stream in the cubit emits a yield, it should select the yield", goldenFileName: "deposit_page_selected_yield_stream", (tester) async { - final yields = YieldsDto.fixture(); - final selectedYield = yields.best24hYield; + await tester.runAsync(() async { + final yields = YieldsDto.fixture(); + final selectedYield = yields.best24hYield; - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(yields)); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(yields)); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + }); }); zGoldenTest("When selecting a yield, it should call select yield in the cubit", (tester) async { @@ -471,120 +493,135 @@ void main() { zGoldenTest("When selecting a yield, it should scroll down to the range section", goldenFileName: "deposit_page_select_yield_scroll", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(null); - when(() => cubit.selectYield(any(), any())).thenAnswer((_) async {}); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.selectedYield).thenReturn(selectedYield); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(null); + when(() => cubit.selectYield(any(), any())).thenAnswer((_) async {}); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.selectedYield).thenReturn(selectedYield); - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.tap(find.byKey(const Key("yield-card-30d"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("yield-card-30d"))); + await tester.pumpAndSettle(); - verify(() => cubit.selectYield(any(), any())).called(1); + verify(() => cubit.selectYield(any(), any())).called(1); + }); }); zGoldenTest( "When clicking the segmented control to switch the base token to quote token, it should reverse the tokens", goldenFileName: "deposit_page_reverse_tokens", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); + await tester.pumpAndSettle(); + }); }); zGoldenTest( "When clicking the segmented control to switch back to base token, after reversing the tokens, it should reverse again", goldenFileName: "deposit_page_reverse_tokens_back", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); + await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key("reverse-tokens-not-reversed"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("reverse-tokens-not-reversed"))); + await tester.pumpAndSettle(); + }); }); zGoldenTest( "When clicking the segmented control to switch back to base token, after reversing the tokens, it should reverse again", goldenFileName: "deposit_page_reverse_tokens_back", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); + await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key("reverse-tokens-not-reversed"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("reverse-tokens-not-reversed"))); + await tester.pumpAndSettle(); + }); }); zGoldenTest("""When emitting an event to the tick stream, it should calculate the price of the selected yield assets""", goldenFileName: "deposit_page_calculate_price", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(BigInt.from(174072))); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(BigInt.from(174072))); - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + }); }); zGoldenTest( "When reversing the tokens, it should calculate the price based on the reversed tokens, from a given tick in the cubit", goldenFileName: "deposit_page_calculate_price_reversed", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(BigInt.from(174072))); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(BigInt.from(174072))); - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); + await tester.pumpAndSettle(); + }); }); zGoldenTest( "When typing a min price more than the current price, it should show an alert saying that is out of range", goldenFileName: "deposit_page_min_price_out_of_range", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.enterText(find.byKey(const Key("min-price-selector")), "1000"); - FocusManager.instance.primaryFocus?.unfocus(); + await tester.enterText(find.byKey(const Key("min-price-selector")), "1000"); + FocusManager.instance.primaryFocus?.unfocus(); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); + }); }, ); @@ -595,22 +632,24 @@ void main() { is not out of range, it should not show the alert""", goldenFileName: "deposit_page_min_price_out_of_range_reversed_in_range", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.enterText(find.byKey(const Key("min-price-selector")), "1000"); - FocusManager.instance.primaryFocus?.unfocus(); + await tester.enterText(find.byKey(const Key("min-price-selector")), "1000"); + FocusManager.instance.primaryFocus?.unfocus(); - await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); + await tester.pumpAndSettle(); + }); }, ); @@ -621,6 +660,30 @@ void main() { is is still out of range, it should keep showing the alert""", goldenFileName: "deposit_page_min_price_out_of_range_reversed", (tester) async { + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); + + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + + await tester.enterText(find.byKey(const Key("min-price-selector")), "90000000000"); + FocusManager.instance.primaryFocus?.unfocus(); + + await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); + await tester.pumpAndSettle(); + }); + }, + ); + + zGoldenTest("When typing a max price less than the min price, it should show an error message", + goldenFileName: "deposit_page_max_price_less_than_min_price", (tester) async { + await tester.runAsync(() async { final selectedYield = YieldsDto.fixture().best24hYield; final currentPriceAsTick = BigInt.from(174072); @@ -632,191 +695,193 @@ void main() { await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.enterText(find.byKey(const Key("min-price-selector")), "90000000000"); + await tester.enterText(find.byKey(const Key("min-price-selector")), "1200"); FocusManager.instance.primaryFocus?.unfocus(); - await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); await tester.pumpAndSettle(); - }, - ); - - zGoldenTest("When typing a max price less than the min price, it should show an error message", - goldenFileName: "deposit_page_max_price_less_than_min_price", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); - - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); - - await tester.enterText(find.byKey(const Key("min-price-selector")), "1200"); - FocusManager.instance.primaryFocus?.unfocus(); + await tester.enterText(find.byKey(const Key("max-price-selector")), "1000"); + FocusManager.instance.primaryFocus?.unfocus(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "1000"); - FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); + }); }); zGoldenTest("""When typing a max price lower than the current price but higher than min price, it shouw show a alert of out of range""", goldenFileName: "deposit_page_max_price_out_of_range", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.enterText(find.byKey(const Key("min-price-selector")), "0.000000001"); - FocusManager.instance.primaryFocus?.unfocus(); + await tester.enterText(find.byKey(const Key("min-price-selector")), "0.000000001"); + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "0.0000001"); - FocusManager.instance.primaryFocus?.unfocus(); + await tester.enterText(find.byKey(const Key("max-price-selector")), "0.0000001"); + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); + }); }); zGoldenTest("When typing 0 in the max price, it should set it to infinity max price", goldenFileName: "deposit_page_max_price_set_to_infinity", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.enterText(find.byKey(const Key("min-price-selector")), "1"); - FocusManager.instance.primaryFocus?.unfocus(); + await tester.enterText(find.byKey(const Key("min-price-selector")), "1"); + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "2"); - FocusManager.instance.primaryFocus?.unfocus(); + await tester.enterText(find.byKey(const Key("max-price-selector")), "2"); + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), ""); - await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); - FocusManager.instance.primaryFocus?.unfocus(); + await tester.enterText(find.byKey(const Key("max-price-selector")), ""); + await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); + FocusManager.instance.primaryFocus?.unfocus(); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); + }); }); zGoldenTest("When typing a min price, but then selecting the full range button, it should set it to 0", goldenFileName: "deposit_page_min_price_set_to_full_range", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.enterText(find.byKey(const Key("min-price-selector")), "1"); - FocusManager.instance.primaryFocus?.unfocus(); + await tester.enterText(find.byKey(const Key("min-price-selector")), "1"); + FocusManager.instance.primaryFocus?.unfocus(); - await tester.tap(find.byKey(const Key("full-range-button"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("full-range-button"))); + await tester.pumpAndSettle(); + }); }); zGoldenTest("When typing a max price, but then selecting the full range button, it should set it to infinity", goldenFileName: "deposit_page_max_price_set_to_full_range", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.enterText(find.byKey(const Key("max-price-selector")), "1"); - FocusManager.instance.primaryFocus?.unfocus(); + await tester.enterText(find.byKey(const Key("max-price-selector")), "1"); + FocusManager.instance.primaryFocus?.unfocus(); - await tester.tap(find.byKey(const Key("full-range-button"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("full-range-button"))); + await tester.pumpAndSettle(); + }); }); zGoldenTest("""When typing a min and max price and then clicking the full range button, it should set the min price to 0 and the max price to infinity""", goldenFileName: "deposit_page_min_and_max_price_set_to_full_range", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); - - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - await tester.enterText(find.byKey(const Key("min-price-selector")), "1"); - FocusManager.instance.primaryFocus?.unfocus(); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.enterText(find.byKey(const Key("max-price-selector")), "2"); - FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.tap(find.byKey(const Key("full-range-button"))); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("min-price-selector")), "1"); + FocusManager.instance.primaryFocus?.unfocus(); + + await tester.enterText(find.byKey(const Key("max-price-selector")), "2"); + FocusManager.instance.primaryFocus?.unfocus(); + + await tester.tap(find.byKey(const Key("full-range-button"))); + await tester.pumpAndSettle(); + }); }); zGoldenTest( "When there's a invalid range, the deposit section should be disabled (with opacity) and cannot be clicked or typed", goldenFileName: "deposit_page_invalid_range_deposit_section", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.enterText(find.byKey(const Key("min-price-selector")), "2"); - FocusManager.instance.primaryFocus?.unfocus(); + await tester.enterText(find.byKey(const Key("min-price-selector")), "2"); + FocusManager.instance.primaryFocus?.unfocus(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "1"); - FocusManager.instance.primaryFocus?.unfocus(); + await tester.enterText(find.byKey(const Key("max-price-selector")), "1"); + FocusManager.instance.primaryFocus?.unfocus(); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key("deposit-button"))); - await tester.pumpAndSettle(); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("deposit-button"))); + await tester.pumpAndSettle(); + }); }); zGoldenTest( "When inputing the base token amount, the quote amount token should be automatically calculated", goldenFileName: "deposit_page_input_base_token_amount", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); + await tester.pumpAndSettle(); + }); }, ); @@ -824,21 +889,23 @@ void main() { "When inputing the quote token amount, the base amount token should be automatically calculated", goldenFileName: "deposit_page_input_quote_token_amount", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.pumpAndSettle(); + }); }, ); @@ -848,24 +915,26 @@ void main() { and the new base token amount should be automatically calculated""", goldenFileName: "deposit_page_input_base_token_amount_and_reverse", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); + await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); + await tester.pumpAndSettle(); + }); }, ); @@ -875,24 +944,26 @@ void main() { and the new quote token amount should be automatically calculated""", goldenFileName: "deposit_page_input_quote_token_amount_and_reverse", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); + await tester.pumpAndSettle(); + }); }, ); @@ -900,24 +971,26 @@ void main() { """When inputing the base token amount with the tokens reversed, the quote token amount should be automatically calculated""", goldenFileName: "deposit_page_input_base_token_amount_reversed", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); + await tester.pumpAndSettle(); + }); }, ); @@ -925,24 +998,26 @@ void main() { """When inputing the quote token amount with the tokens reversed, the base token amount should be automatically calculated""", goldenFileName: "deposit_page_input_quote_token_amount_reversed", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.pumpAndSettle(); + }); }, ); @@ -950,60 +1025,7 @@ void main() { then turning them normal, the quote token amount should now be the previous base token amount, and the new base token amount should be automatically calculated""", goldenFileName: "deposit_page_input_base_token_amount_and_reverse_back", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); - - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); - await tester.pumpAndSettle(); - - await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(const Key("reverse-tokens-not-reversed"))); - await tester.pumpAndSettle(); - }); - - zGoldenTest("""When inputing the quote token amount with the tokens reversed, - then turning them normal, the base token amount should now be the - previous quote token amount, and the new quote token amount should be automatically calculated""", - goldenFileName: "deposit_page_input_quote_token_amount_and_reverse_back", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); - - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); - await tester.pumpAndSettle(); - - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(const Key("reverse-tokens-not-reversed"))); - await tester.pumpAndSettle(); - }); - - zGoldenTest( - "When inputing the base token amount, then changing the range, the quote token amount should be recalculated", - goldenFileName: "deposit_page_input_base_token_amount_and_change_range", - (tester) async { + await tester.runAsync(() async { final selectedYield = YieldsDto.fixture().best24hYield; final currentPriceAsTick = BigInt.from(174072); @@ -1017,25 +1039,22 @@ void main() { await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); - await tester.pumpAndSettle(); - - await tester.enterText(find.byKey(const Key("min-price-selector")), "0.00000001"); + await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); + await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "3"); + await tester.tap(find.byKey(const Key("reverse-tokens-not-reversed"))); await tester.pumpAndSettle(); - FocusManager.instance.primaryFocus?.unfocus(); - }, - ); + }); + }); - zGoldenTest( - "When inputing the quote token amount, then changing the range, the base token amount should be recalculated", - goldenFileName: "deposit_page_input_quote_token_amount_and_change_range", - (tester) async { + zGoldenTest("""When inputing the quote token amount with the tokens reversed, + then turning them normal, the base token amount should now be the + previous quote token amount, and the new quote token amount should be automatically calculated""", + goldenFileName: "deposit_page_input_quote_token_amount_and_reverse_back", (tester) async { + await tester.runAsync(() async { final selectedYield = YieldsDto.fixture().best24hYield; final currentPriceAsTick = BigInt.from(174072); @@ -1049,94 +1068,93 @@ void main() { await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("min-price-selector")), "0.00000001"); + await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); + await tester.tap(find.byKey(const Key("reverse-tokens-not-reversed"))); await tester.pumpAndSettle(); - - await tester.enterText(find.byKey(const Key("max-price-selector")), "3"); - await tester.pumpAndSettle(); - - FocusManager.instance.primaryFocus?.unfocus(); - }, - ); + }); + }); zGoldenTest( - "When inputing the base token amount, reversing the tokens and then changing the range, the base token amount should be recalculated", - goldenFileName: "deposit_page_input_base_token_amount_reverse_tokens_and_change_range", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); - - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + "When inputing the base token amount, then changing the range, the quote token amount should be recalculated", + goldenFileName: "deposit_page_input_base_token_amount_and_change_range", + (tester) async { + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("min-price-selector")), "1200"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("min-price-selector")), "0.00000001"); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "90000"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("max-price-selector")), "3"); + await tester.pumpAndSettle(); - FocusManager.instance.primaryFocus?.unfocus(); - }); + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); + }); + }, + ); zGoldenTest( - "When inputing the quote token amount, reversing the tokens and then changing the range, the quote token amount should be recalculated", - goldenFileName: "deposit_page_input_quote_token_amount_reverse_tokens_and_change_range", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); - - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + "When inputing the quote token amount, then changing the range, the base token amount should be recalculated", + goldenFileName: "deposit_page_input_quote_token_amount_and_change_range", + (tester) async { + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("min-price-selector")), "1200"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("min-price-selector")), "0.00000001"); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "90000"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("max-price-selector")), "3"); + await tester.pumpAndSettle(); - FocusManager.instance.primaryFocus?.unfocus(); - }); + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); + }); + }, + ); zGoldenTest( - "When inputing a range, then inputing the base token amount, the quote token amount should be automatically calculated", - goldenFileName: "deposit_page_input_range_then_input_base_token_amount", - (tester) async { + "When inputing the base token amount, reversing the tokens and then changing the range, the base token amount should be recalculated", + goldenFileName: "deposit_page_input_base_token_amount_reverse_tokens_and_change_range", (tester) async { + await tester.runAsync(() async { final selectedYield = YieldsDto.fixture().best24hYield; final currentPriceAsTick = BigInt.from(174072); @@ -1150,26 +1168,30 @@ void main() { await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("min-price-selector")), "0.000001"); + await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); + await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "3"); + await tester.enterText(find.byKey(const Key("min-price-selector")), "1200"); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); + await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("max-price-selector")), "90000"); await tester.pumpAndSettle(); FocusManager.instance.primaryFocus?.unfocus(); - }, - ); + await tester.pumpAndSettle(); + }); + }); zGoldenTest( - "When inputing a range, then inputing the quote token amount, the base token amount should be automatically calculated", - goldenFileName: "deposit_page_input_range_then_input_quote_token_amount", - (tester) async { + "When inputing the quote token amount, reversing the tokens and then changing the range, the quote token amount should be recalculated", + goldenFileName: "deposit_page_input_quote_token_amount_reverse_tokens_and_change_range", (tester) async { + await tester.runAsync(() async { final selectedYield = YieldsDto.fixture().best24hYield; final currentPriceAsTick = BigInt.from(174072); @@ -1183,19 +1205,94 @@ void main() { await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("min-price-selector")), "0.000001"); + await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); + await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "3"); + await tester.enterText(find.byKey(const Key("min-price-selector")), "1200"); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("max-price-selector")), "90000"); await tester.pumpAndSettle(); FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); + }); + }); + + zGoldenTest( + "When inputing a range, then inputing the base token amount, the quote token amount should be automatically calculated", + goldenFileName: "deposit_page_input_range_then_input_base_token_amount", + (tester) async { + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); + + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("min-price-selector")), "0.000001"); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("max-price-selector")), "3"); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); + await tester.pumpAndSettle(); + + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); + }); + }, + ); + + zGoldenTest( + "When inputing a range, then inputing the quote token amount, the base token amount should be automatically calculated", + goldenFileName: "deposit_page_input_range_then_input_quote_token_amount", + (tester) async { + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); + + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("min-price-selector")), "0.000001"); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("max-price-selector")), "3"); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.pumpAndSettle(); + + FocusManager.instance.primaryFocus?.unfocus(); + }); }, ); @@ -1203,37 +1300,41 @@ void main() { "When inputing a range,reversing the tokens, then inputing the base token amount, the quote token amount should be automatically calculated", goldenFileName: "deposit_page_input_range_then_reverse_tokens_then_input_base_token_amount", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("min-price-selector")), "1200"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("min-price-selector")), "1200"); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "90000"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("max-price-selector")), "90000"); + await tester.pumpAndSettle(); - FocusManager.instance.primaryFocus?.unfocus(); // unfocus the fields to calculate the valid price + FocusManager.instance.primaryFocus?.unfocus(); // unfocus the fields to calculate the valid price + await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); + await tester.pumpAndSettle(); - FocusManager.instance.primaryFocus?.unfocus(); + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); + }); }, ); @@ -1241,6 +1342,47 @@ void main() { "When inputing a range, reversing the tokens, then inputing the quote token amount, the base token amount should be automatically calculated", goldenFileName: "deposit_page_input_range_then_reverse_tokens_then_input_quote_token_amount", (tester) async { + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); + + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("min-price-selector")), "1200"); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("max-price-selector")), "90000"); + await tester.pumpAndSettle(); + + FocusManager.instance.primaryFocus?.unfocus(); // unfocus the fields to calculate the valid price + + await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.pumpAndSettle(); + + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); + }); + }, + ); + + zGoldenTest("""When inputing base token amount, and then setting a max price out of range, + it should keep the quote token amount and disable the base token input""", + goldenFileName: "deposit_page_input_base_token_amount_then_set_max_price_out_of_range", (tester) async { + await tester.runAsync(() async { final selectedYield = YieldsDto.fixture().best24hYield; final currentPriceAsTick = BigInt.from(174072); @@ -1254,159 +1396,135 @@ void main() { await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("min-price-selector")), "1200"); + await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); await tester.pumpAndSettle(); await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "90000"); + await tester.enterText(find.byKey(const Key("max-price-selector")), "0.00000001"); await tester.pumpAndSettle(); - FocusManager.instance.primaryFocus?.unfocus(); // unfocus the fields to calculate the valid price - - await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); + FocusManager.instance.primaryFocus?.unfocus(); await tester.pumpAndSettle(); - - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); - await tester.pumpAndSettle(); - - FocusManager.instance.primaryFocus?.unfocus(); - }, - ); - - zGoldenTest("""When inputing base token amount, and then setting a max price out of range, - it should keep the quote token amount and disable the base token input""", - goldenFileName: "deposit_page_input_base_token_amount_then_set_max_price_out_of_range", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); - - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); - - await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); - await tester.pumpAndSettle(); - - await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); - await tester.pumpAndSettle(); - - await tester.enterText(find.byKey(const Key("max-price-selector")), "0.00000001"); - await tester.pumpAndSettle(); - - FocusManager.instance.primaryFocus?.unfocus(); + }); }); zGoldenTest("""When inputing quote token amount, and then setting a min price out of range, it should keep the base token amount and disable the quote token input""", goldenFileName: "deposit_page_input_quote_token_amount_then_set_min_price_out_of_range", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("min-price-selector")), "2"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("min-price-selector")), "2"); + await tester.pumpAndSettle(); - FocusManager.instance.primaryFocus?.unfocus(); + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); + }); }); zGoldenTest("""When inputing base token amount, reversing the tokens, and then setting a max price out of range, it should keep the quote token amount and disable the base token input""", goldenFileName: "deposit_page_input_base_token_amount_then_reverse_tokens_then_set_max_price_out_of_range", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); + await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "3"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("max-price-selector")), "3"); + await tester.pumpAndSettle(); - FocusManager.instance.primaryFocus?.unfocus(); + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); + }); }); zGoldenTest("""When inputing quote token amount, reversing the tokens, and then setting a min price out of range, it should keep the base token amount and disable the quote token input""", goldenFileName: "deposit_page_input_quote_token_amount_then_reverse_tokens_then_set_min_price_out_of_range", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("reverse-tokens-reversed"))); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("min-price-selector")), "70000"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("min-price-selector")), "70000"); + await tester.pumpAndSettle(); - FocusManager.instance.primaryFocus?.unfocus(); + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); + }); }); zGoldenTest( "When the user is is not connected, it should show the connect wallet button instead of the deposit button", goldenFileName: "deposit_page_not_connected", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => wallet.signer).thenReturn(null); - when(() => wallet.signerStream).thenAnswer((_) => Stream.value(null)); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => wallet.signer).thenReturn(null); + when(() => wallet.signerStream).thenAnswer((_) => Stream.value(null)); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); + }); }, ); @@ -1417,23 +1535,25 @@ void main() { """, goldenFileName: "deposit_page_not_connected_deposit_button_click", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - when(() => wallet.signer).thenReturn(null); - when(() => wallet.signerStream).thenAnswer((_) => Stream.value(null)); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => wallet.signer).thenReturn(null); + when(() => wallet.signerStream).thenAnswer((_) => Stream.value(null)); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder(), wrapper: GoldenConfig.localizationsWrapper()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder(), wrapper: GoldenConfig.localizationsWrapper()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key("deposit-button"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("deposit-button"))); + await tester.pumpAndSettle(); + }); }, ); @@ -1442,24 +1562,26 @@ void main() { the deposit button should should be disabled""", goldenFileName: "deposit_page_no_amount_deposit_button", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); - final signer = SignerMock(); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); + final signer = SignerMock(); - when(() => wallet.signer).thenReturn(signer); - when(() => wallet.signerStream).thenAnswer((_) => Stream.value(signer)); - when(() => cubit.getWalletTokenAmount(any(), network: any(named: "network"))).thenAnswer( - (_) => Future.value(0.0), - ); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => wallet.signer).thenReturn(signer); + when(() => wallet.signerStream).thenAnswer((_) => Stream.value(signer)); + when(() => cubit.getWalletTokenAmount(any(), network: any(named: "network"))).thenAnswer( + (_) => Future.value(0.0), + ); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); + }); }, ); @@ -1469,27 +1591,29 @@ void main() { the deposit button should should be disabled""", goldenFileName: "deposit_page_not_enough_base_token_balance_deposit_button", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); - final signer = SignerMock(); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); + final signer = SignerMock(); - when(() => wallet.signer).thenReturn(signer); - when(() => wallet.signerStream).thenAnswer((_) => Stream.value(signer)); - when(() => cubit.getWalletTokenAmount(any(), network: any(named: "network"))).thenAnswer( - (_) => Future.value(0.0), - ); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => wallet.signer).thenReturn(signer); + when(() => wallet.signerStream).thenAnswer((_) => Stream.value(signer)); + when(() => cubit.getWalletTokenAmount(any(), network: any(named: "network"))).thenAnswer( + (_) => Future.value(0.0), + ); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); + await tester.pumpAndSettle(); + }); }, ); @@ -1499,33 +1623,35 @@ void main() { the deposit button should should be disabled""", goldenFileName: "deposit_page_not_enough_quote_token_balance_deposit_button", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); - final signer = SignerMock(); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); + final signer = SignerMock(); - when(() => wallet.signer).thenReturn(signer); - when(() => wallet.signerStream).thenAnswer((_) => Stream.value(signer)); + when(() => wallet.signer).thenReturn(signer); + when(() => wallet.signerStream).thenAnswer((_) => Stream.value(signer)); - when(() => cubit.getWalletTokenAmount(selectedYield.token0.addresses[selectedYield.network.chainId]!, - network: any(named: "network"))).thenAnswer( - (_) => Future.value(32567352673), - ); - when(() => cubit.getWalletTokenAmount(selectedYield.token1.addresses[selectedYield.network.chainId]!, - network: any(named: "network"))).thenAnswer( - (_) => Future.value(0), - ); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => cubit.getWalletTokenAmount(selectedYield.token0.addresses[selectedYield.network.chainId]!, + network: any(named: "network"))).thenAnswer( + (_) => Future.value(32567352673), + ); + when(() => cubit.getWalletTokenAmount(selectedYield.token1.addresses[selectedYield.network.chainId]!, + network: any(named: "network"))).thenAnswer( + (_) => Future.value(0), + ); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.pumpAndSettle(); + }); }, ); @@ -1534,51 +1660,129 @@ void main() { without having enough balance of base token, the deposit button should should be disabled""", goldenFileName: "deposit_page_not_enough_base_token_balance_deposit_button_after_connecting", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); - final signerStreamController = StreamController.broadcast(); - final signer = SignerMock(); + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); + final signerStreamController = StreamController.broadcast(); + final signer = SignerMock(); - when(() => wallet.signer).thenReturn(null); - when(() => wallet.signerStream).thenAnswer((_) => signerStreamController.stream); - when(() => cubit.getWalletTokenAmount(selectedYield.token0.addresses[selectedYield.network.chainId]!, - network: any(named: "network"))).thenAnswer( - (_) => Future.value(0), - ); - when(() => cubit.getWalletTokenAmount(selectedYield.token1.addresses[selectedYield.network.chainId]!, - network: any(named: "network"))).thenAnswer( - (_) => Future.value(0), - ); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + when(() => wallet.signer).thenReturn(null); + when(() => wallet.signerStream).thenAnswer((_) => signerStreamController.stream); + when(() => cubit.getWalletTokenAmount(selectedYield.token0.addresses[selectedYield.network.chainId]!, + network: any(named: "network"))).thenAnswer( + (_) => Future.value(0), + ); + when(() => cubit.getWalletTokenAmount(selectedYield.token1.addresses[selectedYield.network.chainId]!, + network: any(named: "network"))).thenAnswer( + (_) => Future.value(0), + ); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.pumpAndSettle(); + + signerStreamController.add(signer); + await tester.pumpAndSettle(); + }); + }, + ); + + zGoldenTest( + """When the user is not connected, type an amount to deposit, and then connect + without having enough balance of quote token, the deposit button should should be disabled""", + goldenFileName: "deposit_page_not_enough_quote_token_balance_deposit_button_after_connecting", + (tester) async { + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); + final signerStreamController = StreamController.broadcast(); + final signer = SignerMock(); + + when(() => wallet.signer).thenReturn(null); + when(() => wallet.signerStream).thenAnswer((_) => signerStreamController.stream); + when(() => cubit.getWalletTokenAmount(selectedYield.token0.addresses[selectedYield.network.chainId]!, + network: any(named: "network"))).thenAnswer( + (_) => Future.value(347537253), + ); + when(() => cubit.getWalletTokenAmount(selectedYield.token1.addresses[selectedYield.network.chainId]!, + network: any(named: "network"))).thenAnswer( + (_) => Future.value(0), + ); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.pumpAndSettle(); + + signerStreamController.add(signer); + await tester.pumpAndSettle(); + }); + }, + ); + + zGoldenTest( + """When the user is connected, type an amount to deposit, and have enough balance of both tokens + the deposit button should be enabled""", + goldenFileName: "deposit_page_enough_balance_deposit_button", + (tester) async { + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; + final currentPriceAsTick = BigInt.from(174072); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + final signer = SignerMock(); - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); - await tester.pumpAndSettle(); + when(() => wallet.signer).thenReturn(signer); + when(() => wallet.signerStream).thenAnswer((_) => Stream.value(signer)); + when(() => cubit.getWalletTokenAmount(selectedYield.token0.addresses[selectedYield.network.chainId]!, + network: any(named: "network"))).thenAnswer( + (_) => Future.value(347537253), + ); + when(() => cubit.getWalletTokenAmount(selectedYield.token1.addresses[selectedYield.network.chainId]!, + network: any(named: "network"))).thenAnswer( + (_) => Future.value(32576352673), + ); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); + when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - signerStreamController.add(signer); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.pumpAndSettle(); + }); }, ); - zGoldenTest( - """When the user is not connected, type an amount to deposit, and then connect - without having enough balance of quote token, the deposit button should should be disabled""", - goldenFileName: "deposit_page_not_enough_quote_token_balance_deposit_button_after_connecting", - (tester) async { + zGoldenTest("""When the min range is out of range, and the user does not have quote token balance + but has enough balance of base token, the deposit button should be enabled""", + goldenFileName: "deposit_page_min_range_out_of_range_deposit_button", (tester) async { + await tester.runAsync(() async { final selectedYield = YieldsDto.fixture().best24hYield; final currentPriceAsTick = BigInt.from(174072); - final signerStreamController = StreamController.broadcast(); + final signer = SignerMock(); - when(() => wallet.signer).thenReturn(null); - when(() => wallet.signerStream).thenAnswer((_) => signerStreamController.stream); + when(() => wallet.signer).thenReturn(signer); + when(() => wallet.signerStream).thenAnswer((_) => Stream.value(signer)); when(() => cubit.getWalletTokenAmount(selectedYield.token0.addresses[selectedYield.network.chainId]!, network: any(named: "network"))).thenAnswer( (_) => Future.value(347537253), @@ -1597,19 +1801,18 @@ void main() { await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.enterText(find.byKey(const Key("min-price-selector")), "1"); await tester.pumpAndSettle(); - signerStreamController.add(signer); + await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); await tester.pumpAndSettle(); - }, - ); + }); + }); - zGoldenTest( - """When the user is connected, type an amount to deposit, and have enough balance of both tokens - the deposit button should be enabled""", - goldenFileName: "deposit_page_enough_balance_deposit_button", - (tester) async { + zGoldenTest("""When the max range is out of range, and the user does not have base token balance + but has enough balance of quote token, the deposit button should be enabled""", + goldenFileName: "deposit_page_max_range_out_of_range_deposit_button", (tester) async { + await tester.runAsync(() async { final selectedYield = YieldsDto.fixture().best24hYield; final currentPriceAsTick = BigInt.from(174072); @@ -1619,11 +1822,11 @@ void main() { when(() => wallet.signerStream).thenAnswer((_) => Stream.value(signer)); when(() => cubit.getWalletTokenAmount(selectedYield.token0.addresses[selectedYield.network.chainId]!, network: any(named: "network"))).thenAnswer( - (_) => Future.value(347537253), + (_) => Future.value(0), ); when(() => cubit.getWalletTokenAmount(selectedYield.token1.addresses[selectedYield.network.chainId]!, network: any(named: "network"))).thenAnswer( - (_) => Future.value(32576352673), + (_) => Future.value(3237526), ); when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); when(() => cubit.selectedYield).thenReturn(selectedYield); @@ -1635,85 +1838,18 @@ void main() { await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.enterText(find.byKey(const Key("min-price-selector")), "0.0000001"); await tester.pumpAndSettle(); - }, - ); - - zGoldenTest("""When the min range is out of range, and the user does not have quote token balance - but has enough balance of base token, the deposit button should be enabled""", - goldenFileName: "deposit_page_min_range_out_of_range_deposit_button", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); - - final signer = SignerMock(); - - when(() => wallet.signer).thenReturn(signer); - when(() => wallet.signerStream).thenAnswer((_) => Stream.value(signer)); - when(() => cubit.getWalletTokenAmount(selectedYield.token0.addresses[selectedYield.network.chainId]!, - network: any(named: "network"))).thenAnswer( - (_) => Future.value(347537253), - ); - when(() => cubit.getWalletTokenAmount(selectedYield.token1.addresses[selectedYield.network.chainId]!, - network: any(named: "network"))).thenAnswer( - (_) => Future.value(0), - ); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); - - await tester.enterText(find.byKey(const Key("min-price-selector")), "1"); - await tester.pumpAndSettle(); - - await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); - await tester.pumpAndSettle(); - }); - - zGoldenTest("""When the max range is out of range, and the user does not have base token balance - but has enough balance of quote token, the deposit button should be enabled""", - goldenFileName: "deposit_page_max_range_out_of_range_deposit_button", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; - final currentPriceAsTick = BigInt.from(174072); - - final signer = SignerMock(); - - when(() => wallet.signer).thenReturn(signer); - when(() => wallet.signerStream).thenAnswer((_) => Stream.value(signer)); - when(() => cubit.getWalletTokenAmount(selectedYield.token0.addresses[selectedYield.network.chainId]!, - network: any(named: "network"))).thenAnswer( - (_) => Future.value(0), - ); - when(() => cubit.getWalletTokenAmount(selectedYield.token1.addresses[selectedYield.network.chainId]!, - network: any(named: "network"))).thenAnswer( - (_) => Future.value(3237526), - ); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); - - await tester.enterText(find.byKey(const Key("min-price-selector")), "0.0000001"); - await tester.pumpAndSettle(); - - await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("max-price-selector")), "0"); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("max-price-selector")), "0.000001"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("max-price-selector")), "0.000001"); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.pumpAndSettle(); + }); }); zGoldenTest("When clicking the enabled deposit button, it should show the preview modal of the deposit", @@ -1760,20 +1896,22 @@ void main() { the quote token input should be loading""", goldenFileName: "deposit_page_quote_token_input_loading", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => const Stream.empty()); - when(() => cubit.latestPoolTick).thenReturn(null); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => const Stream.empty()); + when(() => cubit.latestPoolTick).thenReturn(null); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); + await tester.pumpAndSettle(); + }); }, ); @@ -1782,20 +1920,22 @@ void main() { the base token input should be loading""", goldenFileName: "deposit_page_base_token_input_loading", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => const Stream.empty()); - when(() => cubit.latestPoolTick).thenReturn(null); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => const Stream.empty()); + when(() => cubit.latestPoolTick).thenReturn(null); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.pumpAndSettle(); + }); }, ); @@ -1805,20 +1945,22 @@ void main() { """, goldenFileName: "deposit_page_quote_token_input_enabled_after_loading", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(BigInt.from(2131))); - when(() => cubit.latestPoolTick).thenReturn(null); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(BigInt.from(2131))); + when(() => cubit.latestPoolTick).thenReturn(null); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("base-token-input-card")), "1"); + await tester.pumpAndSettle(); + }); }, ); @@ -1828,20 +1970,22 @@ void main() { """, goldenFileName: "deposit_page_base_token_input_enabled_after_loading", (tester) async { - final selectedYield = YieldsDto.fixture().best24hYield; + await tester.runAsync(() async { + final selectedYield = YieldsDto.fixture().best24hYield; - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(BigInt.from(2131))); - when(() => cubit.latestPoolTick).thenReturn(null); + when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); + when(() => cubit.selectedYield).thenReturn(selectedYield); + when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(BigInt.from(2131))); + when(() => cubit.latestPoolTick).thenReturn(null); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); + await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); - await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); + await tester.pumpAndSettle(); + }); }, ); @@ -2078,113 +2222,6 @@ void main() { }, ); - zGoldenTest( - """When the user has not the wrapped native token amount but has the native token amount to deposit, - and the switch to deposit with native is enabled, it should allow the user to proceed with the deposit - (open the preview modal)""", - (tester) async { - await tester.runAsync(() async { - final selectedYield = YieldsDto.fixture().best24hYield.copyWith( - token0: TokenDto(addresses: YieldsDto.fixture().best24hYield.network.wrappedNative.addresses), - token1: TokenDto(addresses: YieldsDto.fixture().best24hYield.network.wrappedNative.addresses), - ); - - final currentPriceAsTick = BigInt.from(174072); - final signer = SignerMock(); - - when(() => wallet.signer).thenReturn(signer); - when(() => wallet.signerStream).thenAnswer((_) => Stream.value(signer)); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - when(() => cubit.selectYield(any(), any())).thenAnswer((_) async => () {}); - when(() => cubit.selectedYieldTimeframe).thenReturn(YieldTimeFrame.day); - when(() => cubit.getWalletTokenAmount(EthereumConstants.zeroAddress, network: any(named: "network"))) - .thenAnswer( - (_) => Future.value(32576352673), - ); - when(() => cubit.getWalletTokenAmount(any(that: isNot(EthereumConstants.zeroAddress)), - network: any(named: "network"))).thenAnswer((_) => Future.value(0)); - - await tester.pumpDeviceBuilder(await goldenBuilder(), wrapper: GoldenConfig.localizationsWrapper()); - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(const Key("yield-card-24h"))); - await tester.pumpAndSettle(); - - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); - - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(const Key("deposit-button"))); - await tester.pumpAndSettle(); - - expect(find.byType(PreviewDepositModal), findsOneWidget); // the next step after configuring the pool - }); - }, - ); - - zGoldenTest( - """When the user has not the native token amount but has the wrapped native token amount to deposit, - and the switch to deposit with native is false, it should allow the user to proceed with the deposit - (open the preview modal)""", - (tester) async { - await tester.runAsync(() async { - final selectedYield = YieldsDto.fixture().best24hYield.copyWith( - token0: TokenDto.fixture() - .copyWith(addresses: YieldsDto.fixture().best24hYield.network.wrappedNative.addresses), - token1: TokenDto.fixture() - .copyWith(addresses: YieldsDto.fixture().best24hYield.network.wrappedNative.addresses), - ); - - final currentPriceAsTick = BigInt.from(174072); - final signer = SignerMock(); - - when(() => wallet.signer).thenReturn(signer); - when(() => wallet.signerStream).thenAnswer((_) => Stream.value(signer)); - when(() => cubit.selectedYieldStream).thenAnswer((_) => Stream.value(selectedYield)); - when(() => cubit.selectedYield).thenReturn(selectedYield); - when(() => cubit.state).thenReturn(DepositState.success(YieldsDto.fixture())); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(currentPriceAsTick)); - when(() => cubit.latestPoolTick).thenReturn(currentPriceAsTick); - when(() => cubit.selectYield(any(), any())).thenAnswer((_) async => () {}); - when(() => cubit.selectedYieldTimeframe).thenReturn(YieldTimeFrame.day); - when(() => cubit.getWalletTokenAmount( - selectedYield.network.wrappedNative.addresses[selectedYield.network.chainId]!, - network: any(named: "network"))).thenAnswer( - (_) => Future.value(32576352673), - ); - - when(() => cubit.getWalletTokenAmount(any(that: matches(EthereumConstants.zeroAddress)), - network: any(named: "network"))).thenAnswer((_) => Future.value(0)); - - await tester.pumpDeviceBuilder(await goldenBuilder(), wrapper: GoldenConfig.localizationsWrapper()); - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(const Key("yield-card-24h"))); - await tester.pumpAndSettle(); - - await tester.drag(find.byKey(const Key("deposit-section")), const Offset(0, -500)); - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(const Key("deposit-with-native-token-switch"))); - await tester.pumpAndSettle(); - - await tester.enterText(find.byKey(const Key("quote-token-input-card")), "1"); - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(const Key("deposit-button"))); - await tester.pumpAndSettle(); - - expect(find.byType(PreviewDepositModal), findsOneWidget); // the next step after configuring the pool - }); - }, - ); - zGoldenTest( "When loading the screen, and the network in the path param is different from the selected one, it should switch the network", (tester) async { diff --git a/test/app/create/deposit/goldens/deposit_page_base_token_input_enabled_after_loading.png b/test/app/create/deposit/goldens/deposit_page_base_token_input_enabled_after_loading.png index 904de8f..4bd8863 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_base_token_input_enabled_after_loading.png and b/test/app/create/deposit/goldens/deposit_page_base_token_input_enabled_after_loading.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_base_token_input_loading.png b/test/app/create/deposit/goldens/deposit_page_base_token_input_loading.png index 6161c04..652ad91 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_base_token_input_loading.png and b/test/app/create/deposit/goldens/deposit_page_base_token_input_loading.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_calculate_price.png b/test/app/create/deposit/goldens/deposit_page_calculate_price.png index 34ce737..368ab27 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_calculate_price.png and b/test/app/create/deposit/goldens/deposit_page_calculate_price.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_calculate_price_reversed.png b/test/app/create/deposit/goldens/deposit_page_calculate_price_reversed.png index 7614039..719ee74 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_calculate_price_reversed.png and b/test/app/create/deposit/goldens/deposit_page_calculate_price_reversed.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_enough_balance_deposit_button.png b/test/app/create/deposit/goldens/deposit_page_enough_balance_deposit_button.png index 46ed296..a4db142 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_enough_balance_deposit_button.png and b/test/app/create/deposit/goldens/deposit_page_enough_balance_deposit_button.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_base_token_amount.png b/test/app/create/deposit/goldens/deposit_page_input_base_token_amount.png index a909a39..906fddd 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_base_token_amount.png and b/test/app/create/deposit/goldens/deposit_page_input_base_token_amount.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_and_change_range.png b/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_and_change_range.png index 7a4da94..f83f505 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_and_change_range.png and b/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_and_change_range.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_and_reverse.png b/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_and_reverse.png index 309119d..9913e78 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_and_reverse.png and b/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_and_reverse.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_and_reverse_back.png b/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_and_reverse_back.png index d07a0cf..5e9bf9e 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_and_reverse_back.png and b/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_and_reverse_back.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_reverse_tokens_and_change_range.png b/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_reverse_tokens_and_change_range.png index e834e52..cdc4e9f 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_reverse_tokens_and_change_range.png and b/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_reverse_tokens_and_change_range.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_reversed.png b/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_reversed.png index 5d4822b..c79530b 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_reversed.png and b/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_reversed.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_then_reverse_tokens_then_set_max_price_out_of_range.png b/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_then_reverse_tokens_then_set_max_price_out_of_range.png index 4341cb3..08ad629 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_then_reverse_tokens_then_set_max_price_out_of_range.png and b/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_then_reverse_tokens_then_set_max_price_out_of_range.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_then_set_max_price_out_of_range.png b/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_then_set_max_price_out_of_range.png index 9f994b2..13d3ef2 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_then_set_max_price_out_of_range.png and b/test/app/create/deposit/goldens/deposit_page_input_base_token_amount_then_set_max_price_out_of_range.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount.png b/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount.png index 8896a87..5c56177 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount.png and b/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_and_change_range.png b/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_and_change_range.png index 01e7fcb..10d5ae0 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_and_change_range.png and b/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_and_change_range.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_and_reverse.png b/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_and_reverse.png index 95ef2ab..f89f954 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_and_reverse.png and b/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_and_reverse.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_and_reverse_back.png b/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_and_reverse_back.png index 658deaf..e381e1c 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_and_reverse_back.png and b/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_and_reverse_back.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_reverse_tokens_and_change_range.png b/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_reverse_tokens_and_change_range.png index 782df4d..624f263 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_reverse_tokens_and_change_range.png and b/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_reverse_tokens_and_change_range.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_reversed.png b/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_reversed.png index 0986374..2ac1fac 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_reversed.png and b/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_reversed.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_then_reverse_tokens_then_set_min_price_out_of_range.png b/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_then_reverse_tokens_then_set_min_price_out_of_range.png index 6fb02e2..6aa2343 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_then_reverse_tokens_then_set_min_price_out_of_range.png and b/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_then_reverse_tokens_then_set_min_price_out_of_range.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_then_set_min_price_out_of_range.png b/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_then_set_min_price_out_of_range.png index 4683ea4..a8f94b2 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_then_set_min_price_out_of_range.png and b/test/app/create/deposit/goldens/deposit_page_input_quote_token_amount_then_set_min_price_out_of_range.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_range_then_input_base_token_amount.png b/test/app/create/deposit/goldens/deposit_page_input_range_then_input_base_token_amount.png index bf0e2c7..2cc9066 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_range_then_input_base_token_amount.png and b/test/app/create/deposit/goldens/deposit_page_input_range_then_input_base_token_amount.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_range_then_input_quote_token_amount.png b/test/app/create/deposit/goldens/deposit_page_input_range_then_input_quote_token_amount.png index 4ea6e5b..8f0d814 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_range_then_input_quote_token_amount.png and b/test/app/create/deposit/goldens/deposit_page_input_range_then_input_quote_token_amount.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_range_then_reverse_tokens_then_input_base_token_amount.png b/test/app/create/deposit/goldens/deposit_page_input_range_then_reverse_tokens_then_input_base_token_amount.png index 782df4d..624f263 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_range_then_reverse_tokens_then_input_base_token_amount.png and b/test/app/create/deposit/goldens/deposit_page_input_range_then_reverse_tokens_then_input_base_token_amount.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_input_range_then_reverse_tokens_then_input_quote_token_amount.png b/test/app/create/deposit/goldens/deposit_page_input_range_then_reverse_tokens_then_input_quote_token_amount.png index e834e52..cdc4e9f 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_input_range_then_reverse_tokens_then_input_quote_token_amount.png and b/test/app/create/deposit/goldens/deposit_page_input_range_then_reverse_tokens_then_input_quote_token_amount.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_invalid_range_deposit_section.png b/test/app/create/deposit/goldens/deposit_page_invalid_range_deposit_section.png index dcae01e..4620174 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_invalid_range_deposit_section.png and b/test/app/create/deposit/goldens/deposit_page_invalid_range_deposit_section.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_max_price_less_than_min_price.png b/test/app/create/deposit/goldens/deposit_page_max_price_less_than_min_price.png index a8c13cc..cb22fde 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_max_price_less_than_min_price.png and b/test/app/create/deposit/goldens/deposit_page_max_price_less_than_min_price.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_max_price_out_of_range.png b/test/app/create/deposit/goldens/deposit_page_max_price_out_of_range.png index 61d825e..6d48682 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_max_price_out_of_range.png and b/test/app/create/deposit/goldens/deposit_page_max_price_out_of_range.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_max_price_set_to_full_range.png b/test/app/create/deposit/goldens/deposit_page_max_price_set_to_full_range.png index 34ce737..a69ec57 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_max_price_set_to_full_range.png and b/test/app/create/deposit/goldens/deposit_page_max_price_set_to_full_range.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_max_price_set_to_infinity.png b/test/app/create/deposit/goldens/deposit_page_max_price_set_to_infinity.png index 933ae3d..5720cea 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_max_price_set_to_infinity.png and b/test/app/create/deposit/goldens/deposit_page_max_price_set_to_infinity.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_max_range_out_of_range_deposit_button.png b/test/app/create/deposit/goldens/deposit_page_max_range_out_of_range_deposit_button.png index a6b9824..34ef2b6 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_max_range_out_of_range_deposit_button.png and b/test/app/create/deposit/goldens/deposit_page_max_range_out_of_range_deposit_button.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_min_and_max_price_set_to_full_range.png b/test/app/create/deposit/goldens/deposit_page_min_and_max_price_set_to_full_range.png index 34ce737..a69ec57 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_min_and_max_price_set_to_full_range.png and b/test/app/create/deposit/goldens/deposit_page_min_and_max_price_set_to_full_range.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_min_price_out_of_range.png b/test/app/create/deposit/goldens/deposit_page_min_price_out_of_range.png index dbd9e62..1e960e1 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_min_price_out_of_range.png and b/test/app/create/deposit/goldens/deposit_page_min_price_out_of_range.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_min_price_out_of_range_reversed.png b/test/app/create/deposit/goldens/deposit_page_min_price_out_of_range_reversed.png index 5828808..dd43559 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_min_price_out_of_range_reversed.png and b/test/app/create/deposit/goldens/deposit_page_min_price_out_of_range_reversed.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_min_price_out_of_range_reversed_in_range.png b/test/app/create/deposit/goldens/deposit_page_min_price_out_of_range_reversed_in_range.png index 01b4f8e..68deb04 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_min_price_out_of_range_reversed_in_range.png and b/test/app/create/deposit/goldens/deposit_page_min_price_out_of_range_reversed_in_range.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_min_price_set_to_full_range.png b/test/app/create/deposit/goldens/deposit_page_min_price_set_to_full_range.png index 34ce737..a69ec57 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_min_price_set_to_full_range.png and b/test/app/create/deposit/goldens/deposit_page_min_price_set_to_full_range.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_min_range_out_of_range_deposit_button.png b/test/app/create/deposit/goldens/deposit_page_min_range_out_of_range_deposit_button.png index 9a27744..341a677 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_min_range_out_of_range_deposit_button.png and b/test/app/create/deposit/goldens/deposit_page_min_range_out_of_range_deposit_button.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_no_amount_deposit_button.png b/test/app/create/deposit/goldens/deposit_page_no_amount_deposit_button.png index 78ff412..f551014 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_no_amount_deposit_button.png and b/test/app/create/deposit/goldens/deposit_page_no_amount_deposit_button.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_not_connected.png b/test/app/create/deposit/goldens/deposit_page_not_connected.png index 80ca84d..eb229fb 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_not_connected.png and b/test/app/create/deposit/goldens/deposit_page_not_connected.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_not_connected_deposit_button_click.png b/test/app/create/deposit/goldens/deposit_page_not_connected_deposit_button_click.png index 3388211..abf8220 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_not_connected_deposit_button_click.png and b/test/app/create/deposit/goldens/deposit_page_not_connected_deposit_button_click.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_not_enough_base_token_balance_deposit_button.png b/test/app/create/deposit/goldens/deposit_page_not_enough_base_token_balance_deposit_button.png index c322acf..774f534 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_not_enough_base_token_balance_deposit_button.png and b/test/app/create/deposit/goldens/deposit_page_not_enough_base_token_balance_deposit_button.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_not_enough_base_token_balance_deposit_button_after_connecting.png b/test/app/create/deposit/goldens/deposit_page_not_enough_base_token_balance_deposit_button_after_connecting.png index f0bfd9a..200e1a1 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_not_enough_base_token_balance_deposit_button_after_connecting.png and b/test/app/create/deposit/goldens/deposit_page_not_enough_base_token_balance_deposit_button_after_connecting.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_not_enough_quote_token_balance_deposit_button.png b/test/app/create/deposit/goldens/deposit_page_not_enough_quote_token_balance_deposit_button.png index 4ce269e..448b36e 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_not_enough_quote_token_balance_deposit_button.png and b/test/app/create/deposit/goldens/deposit_page_not_enough_quote_token_balance_deposit_button.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_not_enough_quote_token_balance_deposit_button_after_connecting.png b/test/app/create/deposit/goldens/deposit_page_not_enough_quote_token_balance_deposit_button_after_connecting.png index 4ce269e..448b36e 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_not_enough_quote_token_balance_deposit_button_after_connecting.png and b/test/app/create/deposit/goldens/deposit_page_not_enough_quote_token_balance_deposit_button_after_connecting.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_preview_modal.png b/test/app/create/deposit/goldens/deposit_page_preview_modal.png index 21d2cec..0bb3184 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_preview_modal.png and b/test/app/create/deposit/goldens/deposit_page_preview_modal.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_quote_token_input_enabled_after_loading.png b/test/app/create/deposit/goldens/deposit_page_quote_token_input_enabled_after_loading.png index e44b71c..09a5502 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_quote_token_input_enabled_after_loading.png and b/test/app/create/deposit/goldens/deposit_page_quote_token_input_enabled_after_loading.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_quote_token_input_loading.png b/test/app/create/deposit/goldens/deposit_page_quote_token_input_loading.png index 3d0d898..197c07d 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_quote_token_input_loading.png and b/test/app/create/deposit/goldens/deposit_page_quote_token_input_loading.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_range_section_mobile.png b/test/app/create/deposit/goldens/deposit_page_range_section_mobile.png index 48308b8..40defe1 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_range_section_mobile.png and b/test/app/create/deposit/goldens/deposit_page_range_section_mobile.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_reverse_tokens.png b/test/app/create/deposit/goldens/deposit_page_reverse_tokens.png index 0743e50..7aff470 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_reverse_tokens.png and b/test/app/create/deposit/goldens/deposit_page_reverse_tokens.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_reverse_tokens_back.png b/test/app/create/deposit/goldens/deposit_page_reverse_tokens_back.png index 086db15..111650d 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_reverse_tokens_back.png and b/test/app/create/deposit/goldens/deposit_page_reverse_tokens_back.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_select_yield_scroll.png b/test/app/create/deposit/goldens/deposit_page_select_yield_scroll.png index 87d0acb..d20acbc 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_select_yield_scroll.png and b/test/app/create/deposit/goldens/deposit_page_select_yield_scroll.png differ diff --git a/test/app/create/deposit/goldens/deposit_page_selected_yield_stream.png b/test/app/create/deposit/goldens/deposit_page_selected_yield_stream.png index 0a04faf..2bd391e 100644 Binary files a/test/app/create/deposit/goldens/deposit_page_selected_yield_stream.png and b/test/app/create/deposit/goldens/deposit_page_selected_yield_stream.png differ diff --git a/test/app/create/deposit/widgets/deposit_success_modal_test.dart b/test/app/create/deposit/widgets/deposit_success_modal_test.dart index 9a8bd42..1d9ce4e 100644 --- a/test/app/create/deposit/widgets/deposit_success_modal_test.dart +++ b/test/app/create/deposit/widgets/deposit_success_modal_test.dart @@ -7,9 +7,7 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. import 'package:zup_app/app/create/deposit/widgets/deposit_success_modal.dart'; import 'package:zup_app/core/dtos/protocol_dto.dart'; import 'package:zup_app/core/dtos/yield_dto.dart'; -import 'package:zup_app/core/enums/networks.dart'; import 'package:zup_app/core/injections.dart'; -import 'package:zup_app/widgets/token_avatar.dart'; import 'package:zup_app/widgets/zup_cached_image.dart'; import '../../../../golden_config.dart'; @@ -38,7 +36,6 @@ void main() { Future goldenBuilder({ YieldDto? customYield, bool showAsBottomSheet = false, - bool depositedWithNative = false, }) async => await goldenDeviceBuilder(Builder(builder: (context) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -46,7 +43,6 @@ void main() { context, depositedYield: customYield ?? YieldDto.fixture().copyWith(), showAsBottomSheet: showAsBottomSheet, - depositedWithNative: depositedWithNative, ); }); @@ -115,57 +111,4 @@ void main() { ); await tester.pumpAndSettle(); }); - - zGoldenTest( - """When passing depositedWithNative to true, the modal should use the native token symbol in the description - if the token0 or token1 are wrapped natives""", (tester) async { - final wrappedNativeYield = YieldDto.fixture().copyWith( - chainId: AppNetworks.sepolia.chainId, - token0: AppNetworks.sepolia.wrappedNative, - token1: AppNetworks.sepolia.wrappedNative, - ); - - await tester.pumpDeviceBuilder( - await goldenBuilder( - depositedWithNative: true, - customYield: wrappedNativeYield, - ), - wrapper: GoldenConfig.localizationsWrapper(), - ); - await tester.pumpAndSettle(); - - expect( - find.text( - "You have successfully deposited into ETH/ETH Pool at ${wrappedNativeYield.protocol.name} on ${wrappedNativeYield.network.label}", - findRichText: true, - ), - findsOne, - ); - }); - - zGoldenTest( - """When passing depositedWithNative to true, the modal should use the native token images in the tokenavatars - (if the token0 or token1 are wrapped natives)""", (tester) async { - final wrappedNativeYield = YieldDto.fixture().copyWith( - chainId: AppNetworks.sepolia.chainId, - token0: AppNetworks.sepolia.wrappedNative, - token1: AppNetworks.sepolia.wrappedNative, - ); - - await tester.pumpDeviceBuilder( - await goldenBuilder( - depositedWithNative: true, - customYield: wrappedNativeYield, - ), - wrapper: GoldenConfig.localizationsWrapper(), - ); - await tester.pumpAndSettle(); - - final tokenAvatars = find.byType(TokenAvatar).evaluate(); - final token0Avatar = tokenAvatars.first.widget as TokenAvatar; - final token1Avatar = tokenAvatars.last.widget as TokenAvatar; - - expect(token0Avatar.asset, wrappedNativeYield.maybeNativeToken0(permitNative: true)); - expect(token1Avatar.asset, wrappedNativeYield.maybeNativeToken1(permitNative: true)); - }); } 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 d5191c1..464a145 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 @@ -10,20 +10,22 @@ import 'package:web3kit/core/dtos/transaction_receipt.dart'; import 'package:web3kit/core/dtos/transaction_response.dart'; import 'package:web3kit/core/exceptions/ethers_exceptions.dart'; import 'package:zup_app/abis/erc_20.abi.g.dart'; -import 'package:zup_app/abis/uniswap_position_manager.abi.g.dart'; +import 'package:zup_app/abis/uniswap_permit2.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/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal_cubit.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/injections.dart'; +import 'package:zup_app/core/pool_service.dart'; import 'package:zup_app/core/slippage.dart'; -import 'package:zup_app/core/v3_pool_constants.dart'; +import 'package:zup_app/core/v3_v4_pool_constants.dart'; import 'package:zup_app/core/zup_analytics.dart'; import 'package:zup_app/widgets/zup_cached_image.dart'; import '../../../../../golden_config.dart'; -import '../../../../../matchers.dart'; import '../../../../../mocks.dart'; import '../../../../../wrappers.dart'; @@ -40,9 +42,12 @@ void main() { late Wallet wallet; late Signer signer; late TransactionResponse transactionResponse; - late UniswapPositionManager uniswapPositionManager; - late UniswapPositionManagerImpl uniswapPositionManagerImpl; + late UniswapV3PositionManager uniswapPositionManager; + late UniswapV3PositionManagerImpl uniswapPositionManagerImpl; late ZupAnalytics zupAnalytics; + late UniswapPermit2 permit2; + late UniswapPermit2Impl permit2Impl; + late PoolService poolService; setUp(() { uniswapV3Pool = UniswapV3PoolMock(); @@ -52,20 +57,23 @@ void main() { erc20Impl = Erc20ImplMock(); signer = SignerMock(); transactionResponse = TransactionResponseMock(); - uniswapPositionManager = UniswapPositionManagerMock(); - uniswapPositionManagerImpl = UniswapPositionManagerImplMock(); + uniswapPositionManager = UniswapV3PositionManagerMock(); + uniswapPositionManagerImpl = UniswapV3PositionManagerImplMock(); zupAnalytics = ZupAnalyticsMock(); + permit2 = UniswapPermit2Mock(); + permit2Impl = UniswapPermit2ImplMock(); + poolService = PoolServiceMock(); sut = PreviewDepositModalCubit( initialPoolTick: initialPoolTick, - uniswapV3Pool: uniswapV3Pool, currentYield: currentYield, erc20: erc20, wallet: wallet, - depositWithNative: false, uniswapPositionManager: uniswapPositionManager, navigatorKey: GlobalKey(), zupAnalytics: zupAnalytics, + permit2: permit2, + poolService: poolService, ); registerFallbackValue(const ChainInfo(hexChainId: "0x1")); @@ -73,6 +81,7 @@ void main() { registerFallbackValue(BigInt.one); registerFallbackValue((amount: BigInt.from(1), token: "")); registerFallbackValue(YieldDto.fixture()); + registerFallbackValue(Duration.zero); registerFallbackValue(( amount0Desired: BigInt.zero, amount0Min: BigInt.zero, @@ -113,6 +122,11 @@ void main() { unlocked: true, )); + when(() => permit2.fromSigner(contractAddress: any(named: "contractAddress"), signer: any(named: "signer"))) + .thenReturn(permit2Impl); + when(() => permit2.fromRpcProvider(contractAddress: any(named: "contractAddress"), rpcUrl: any(named: "rpcUrl"))) + .thenReturn(permit2Impl); + when(() => erc20.fromRpcProvider(contractAddress: any(named: "contractAddress"), rpcUrl: any(named: "rpcUrl"))) .thenReturn(erc20Impl); @@ -131,6 +145,18 @@ void main() { (_) async => TransactionReceipt(hash: transactionHash), ); + when( + () => permit2Impl.approve( + token: any(named: "token"), + spender: any(named: "spender"), + amount: any(named: "amount"), + expiration: any(named: "expiration")), + ).thenAnswer((_) async => transactionResponse); + + when(() => permit2Impl.allowance(any(), any(), any())).thenAnswer( + (_) async => (amount: BigInt.zero, expiration: BigInt.zero, nonce: BigInt.zero), + ); + when(() => wallet.connectedNetwork).thenAnswer((_) async => currentYield.network.chainInfo); when(() => uniswapPositionManager.fromRpcProvider( @@ -147,6 +173,16 @@ void main() { ).thenAnswer((_) async => transactionResponse); when(() => uniswapPositionManager.getMintCalldata(params: any(named: "params"))).thenReturn("0x"); + + when(() => poolService.sendV3PoolDepositTransaction(any(), any(), + amount0Desired: any(named: "amount0Desired"), + amount1Desired: any(named: "amount1Desired"), + amount0Min: any(named: "amount0Min"), + amount1Min: any(named: "amount1Min"), + deadline: any(named: "deadline"), + recipient: any(named: "recipient"), + tickLower: any(named: "tickLower"), + tickUpper: any(named: "tickUpper"))).thenAnswer((_) async => transactionResponse); }); void sutCopyWith({ @@ -155,17 +191,19 @@ void main() { UniswapV3Pool? customUniswapV3Pool, Erc20? customErc20, Wallet? customWallet, - UniswapPositionManager? customUniswapPositionManager, + UniswapV3PositionManager? customUniswapPositionManager, YieldDto? customYield, GlobalKey? customNavigatorKey, + UniswapPermit2? customPermit2, + PoolService? customPoolService, }) { sut = PreviewDepositModalCubit( initialPoolTick: customInitialPoolTick ?? initialPoolTick, - uniswapV3Pool: customUniswapV3Pool ?? uniswapV3Pool, + permit2: customPermit2 ?? permit2, + poolService: customPoolService ?? poolService, currentYield: customYield ?? currentYield, erc20: customErc20 ?? erc20, wallet: customWallet ?? wallet, - depositWithNative: customDepositWithNative ?? false, uniswapPositionManager: customUniswapPositionManager ?? uniswapPositionManager, navigatorKey: customNavigatorKey ?? GlobalKey(), zupAnalytics: zupAnalytics, @@ -197,15 +235,7 @@ void main() { BigInt latestEmittedTick = BigInt.zero; const minutesPassed = 2; - when(() => uniswapV3PoolImpl.slot0()).thenAnswer((_) async => ( - feeProtocol: BigInt.zero, - observationCardinality: BigInt.zero, - observationCardinalityNext: BigInt.zero, - observationIndex: BigInt.zero, - sqrtPriceX96: BigInt.zero, - tick: expectedEmittedTick, - unlocked: true, - )); + when(() => poolService.getPoolTick(any())).thenAnswer((_) async => expectedEmittedTick); fakeAsync((async) { sut.setup(); @@ -233,7 +263,7 @@ void main() { expectedEmittedTick, reason: "`latestPoolTick` should be updated", ); - verify(() => uniswapV3PoolImpl.slot0()).called(2); + verify(() => poolService.getPoolTick(any())).called(2); }, ); @@ -271,11 +301,11 @@ void main() { sut = PreviewDepositModalCubit( uniswapPositionManager: uniswapPositionManager, initialPoolTick: initialPoolTick, - uniswapV3Pool: uniswapV3Pool, + permit2: permit2, + poolService: poolService, currentYield: customYield, erc20: erc20, wallet: wallet, - depositWithNative: false, navigatorKey: GlobalKey(), zupAnalytics: zupAnalytics); @@ -331,16 +361,19 @@ void main() { sut = PreviewDepositModalCubit( navigatorKey: GlobalKey(), initialPoolTick: initialPoolTick, - uniswapV3Pool: uniswapV3Pool, + permit2: permit2, + poolService: poolService, currentYield: customYield, erc20: erc20, wallet: wallet, - depositWithNative: false, uniswapPositionManager: uniswapPositionManager, zupAnalytics: zupAnalytics); when(() => wallet.switchOrAddNetwork(any())).thenAnswer((_) async {}); when(() => wallet.connectedNetwork).thenAnswer((_) async => AppNetworks.mainnet.chainInfo); + when(() => permit2Impl.allowance(any(), any(), any())).thenAnswer( + (_) async => (amount: BigInt.zero, expiration: BigInt.zero, nonce: BigInt.zero), + ); await sut.approveToken(currentYield.token0, BigInt.from(32761)); @@ -360,11 +393,11 @@ void main() { navigatorKey: GlobalKey(), uniswapPositionManager: uniswapPositionManager, initialPoolTick: initialPoolTick, - uniswapV3Pool: uniswapV3Pool, + permit2: permit2, + poolService: poolService, currentYield: customYield, erc20: erc20, wallet: wallet, - depositWithNative: false, zupAnalytics: zupAnalytics); when(() => wallet.switchOrAddNetwork(any())).thenAnswer((_) async {}); @@ -720,7 +753,7 @@ void main() { }, ); - test("When calling `deposit`, the tokens amount sent to the contract call should match the passed values", () async { + test("When calling `deposit`, the tokens amount sent to the pool service to deposit should be correct", () async { final token0Amount = BigInt.from(32421); final token1Amount = BigInt.from(8729889); @@ -737,25 +770,24 @@ void main() { ); verify( - () => uniswapPositionManagerImpl.mint( - params: any( - named: "params", - that: ExpectedMatcher( - expects: (item) { - expect(item.amount0Desired, token0Amount); - expect(item.amount1Desired, token1Amount); - expect(item.token0, currentYield.token0.addresses[currentYield.network.chainId]!); - expect(item.token1, currentYield.token1.addresses[currentYield.network.chainId]!); - }, - ), - ), + () => poolService.sendV3PoolDepositTransaction( + any(), + any(), + amount0Desired: token0Amount, + amount1Desired: token1Amount, + amount0Min: any(named: "amount0Min"), + amount1Min: any(named: "amount1Min"), + deadline: any(named: "deadline"), + recipient: any(named: "recipient"), + tickLower: any(named: "tickLower"), + tickUpper: any(named: "tickUpper"), ), ); }); test( - """When calling `deposit`, the amount1Min in the depositData should be - the passed token1 amount minus the slippage percent""", + """When calling `deposit`, the amount1Min sent to the pool service to deposit + should be the one calculated from the slippage""", () async { final token1Amount = BigInt.from(6721); const slippage = Slippage.halfPercent; @@ -777,35 +809,29 @@ void main() { ); verify( - () => uniswapPositionManager.getMintCalldata( - params: any( - named: "params", - that: ExpectedMatcher( - expects: (item) { - expect( - item.amount1Min, - slippage.calculateTokenAmountFromSlippage(token1Amount), - ); - }, - ), - ), + () => poolService.sendV3PoolDepositTransaction( + any(), + any(), + amount0Desired: any(named: "amount0Desired"), + amount1Desired: any(named: "amount1Desired"), + amount0Min: any(named: "amount0Min"), + amount1Min: slippage.calculateMinTokenAmountFromSlippage(token1Amount), + deadline: any(named: "deadline"), + recipient: any(named: "recipient"), + tickLower: any(named: "tickLower"), + tickUpper: any(named: "tickUpper"), ), ).called(1); }, ); test( - """When calling `deposit`, the amount0Min in the depositData should be - the passed token0 amount minus the slippage percent""", + """When calling `deposit`, the amount0Min sent to the pool service to deposit + should be the one calculated from the slippage""", () async { final token0Amount = BigInt.from(32421); final slippage = Slippage.fromValue(50); - // when(() => feeControllerImpl.calculateJoinPoolFee( - // token0Amount: any(named: "token0Amount"), token1Amount: any(named: "token1Amount"))).thenAnswer( - // (_) async => (feeToken0: feeAmount, feeToken1: feeAmount), - // ); - when(() => uniswapPositionManager.getMintCalldata(params: any(named: "params"))).thenReturn(""); sutCopyWith(customDepositWithNative: true); @@ -822,25 +848,24 @@ void main() { ); verify( - () => uniswapPositionManager.getMintCalldata( - params: any( - named: "params", - that: ExpectedMatcher( - expects: (item) { - expect( - item.amount0Min, - slippage.calculateTokenAmountFromSlippage(token0Amount), - ); - }, - ), - ), + () => poolService.sendV3PoolDepositTransaction( + any(), + any(), + amount0Desired: any(named: "amount0Desired"), + amount1Desired: any(named: "amount1Desired"), + amount0Min: slippage.calculateMinTokenAmountFromSlippage(token0Amount), + amount1Min: any(named: "amount1Min"), + deadline: any(named: "deadline"), + recipient: any(named: "recipient"), + tickLower: any(named: "tickLower"), + tickUpper: any(named: "tickUpper"), ), ).called(1); }, ); test( - "When calling `deposit`, the recipient in the depositData, should be the connected signer address", + "When calling `deposit`, the recipient sent to the pool service to deposit should be the signer address", () async { const signerAddress = "0x0000000000000000000000000000000000000231"; when(() => signer.address).thenAnswer((_) async => signerAddress); @@ -859,17 +884,25 @@ void main() { isReversed: false, ); - verify(() => uniswapPositionManager.getMintCalldata( - params: any( - named: "params", - that: ExpectedMatcher(expects: (item) => expect(item.recipient, signerAddress)), - ), - )).called(1); + verify( + () => poolService.sendV3PoolDepositTransaction( + any(), + any(), + amount0Desired: any(named: "amount0Desired"), + amount1Desired: any(named: "amount1Desired"), + amount0Min: any(named: "amount0Min"), + amount1Min: any(named: "amount1Min"), + deadline: any(named: "deadline"), + recipient: signerAddress, + tickLower: any(named: "tickLower"), + tickUpper: any(named: "tickUpper"), + ), + ).called(1); }, ); test("""When calling `deposit` the minPrice is infinity, and is not reversed, - the tick lower in the depositData should be the min tick (adjusted for the tick spacing)""", () async { + the tick lower send to the pool service should be the min tick (adjusted for the tick spacing)""", () async { sutCopyWith(customDepositWithNative: true); await sut.deposit( @@ -885,25 +918,27 @@ void main() { ); verify( - () => uniswapPositionManager.getMintCalldata( - params: any( - named: "params", - that: ExpectedMatcher( - expects: (item) => expect( - item.tickLower, - V3PoolConversorsMixinWrapper().tickToClosestValidTick( - tick: V3PoolConstants.minTick, - tickSpacing: currentYield.tickSpacing, - ), - ), - ), + () => poolService.sendV3PoolDepositTransaction( + any(), + any(), + amount0Desired: any(named: "amount0Desired"), + amount1Desired: any(named: "amount1Desired"), + amount0Min: any(named: "amount0Min"), + amount1Min: any(named: "amount1Min"), + deadline: any(named: "deadline"), + recipient: any(named: "recipient"), + tickLower: V3PoolConversorsMixinWrapper().tickToClosestValidTick( + tick: V3V4PoolConstants.minTick, + tickSpacing: currentYield.tickSpacing, ), + tickUpper: any(named: "tickUpper"), ), ).called(1); }); test("""When calling `deposit` with the maxPrice infinity, and reversed, - the tick lower in the depositData should be the min tick (but adjusted for the tick spacing)""", () async { + the tick lower sent to the pool service should be the min tick + (but adjusted for the tick spacing)""", () async { sutCopyWith(customDepositWithNative: true); await sut.deposit( @@ -919,25 +954,26 @@ void main() { ); verify( - () => uniswapPositionManager.getMintCalldata( - params: any( - named: "params", - that: ExpectedMatcher( - expects: (item) => expect( - item.tickLower, - V3PoolConversorsMixinWrapper().tickToClosestValidTick( - tick: V3PoolConstants.minTick, - tickSpacing: currentYield.tickSpacing, - ), - ), - ), + () => poolService.sendV3PoolDepositTransaction( + any(), + any(), + amount0Desired: any(named: "amount0Desired"), + amount1Desired: any(named: "amount1Desired"), + amount0Min: any(named: "amount0Min"), + amount1Min: any(named: "amount1Min"), + deadline: any(named: "deadline"), + recipient: any(named: "recipient"), + tickLower: V3PoolConversorsMixinWrapper().tickToClosestValidTick( + tick: V3V4PoolConstants.minTick, + tickSpacing: currentYield.tickSpacing, ), + tickUpper: any(named: "tickUpper"), ), ).called(1); }); test( - "When calling `deposit` with a min price that is not infinity, it should calculate the correct tickLower for the depositData", + "When calling `deposit` with a min price that is not infinity, it should calculate the correct tickLower and send it to the pool service", () async { sutCopyWith(customDepositWithNative: true); @@ -956,24 +992,25 @@ void main() { ); verify( - () => uniswapPositionManager.getMintCalldata( - params: any( - named: "params", - that: ExpectedMatcher( - expects: (item) => expect( - item.tickLower, - V3PoolConversorsMixinWrapper().tickToClosestValidTick( - tick: V3PoolConversorsMixinWrapper().priceToTick( - price: minPrice, - poolToken0Decimals: currentYield.token0.decimals, - poolToken1Decimals: currentYield.token1.decimals, - isReversed: false, - ), - tickSpacing: currentYield.tickSpacing, - ), - ), + () => poolService.sendV3PoolDepositTransaction( + any(), + any(), + amount0Desired: any(named: "amount0Desired"), + amount1Desired: any(named: "amount1Desired"), + amount0Min: any(named: "amount0Min"), + amount1Min: any(named: "amount1Min"), + deadline: any(named: "deadline"), + recipient: any(named: "recipient"), + tickLower: V3PoolConversorsMixinWrapper().tickToClosestValidTick( + tick: V3PoolConversorsMixinWrapper().priceToTick( + price: minPrice, + poolToken0Decimals: currentYield.token0.decimals, + poolToken1Decimals: currentYield.token1.decimals, + isReversed: false, ), + tickSpacing: currentYield.tickSpacing, ), + tickUpper: any(named: "tickUpper"), ), ).called(1); }, @@ -981,8 +1018,8 @@ void main() { test( """When calling `deposit` with a max price that is not infinity, and it's reversed, - it should calculate the correct tickLower for the depositData, using the max price - (because it's reversed) + it should calculate the correct tickLower and, using the max price + (because it's reversed), and send it to the pool service """, () async { sutCopyWith(customDepositWithNative: true); @@ -1003,24 +1040,25 @@ void main() { ); verify( - () => uniswapPositionManager.getMintCalldata( - params: any( - named: "params", - that: ExpectedMatcher( - expects: (item) => expect( - item.tickLower, - V3PoolConversorsMixinWrapper().tickToClosestValidTick( - tick: V3PoolConversorsMixinWrapper().priceToTick( - price: maxPrice, - poolToken0Decimals: currentYield.token0.decimals, - poolToken1Decimals: currentYield.token1.decimals, - isReversed: isReversed, - ), - tickSpacing: currentYield.tickSpacing, - ), - ), + () => poolService.sendV3PoolDepositTransaction( + any(), + any(), + amount0Desired: any(named: "amount0Desired"), + amount1Desired: any(named: "amount1Desired"), + amount0Min: any(named: "amount0Min"), + amount1Min: any(named: "amount1Min"), + deadline: any(named: "deadline"), + recipient: any(named: "recipient"), + tickLower: V3PoolConversorsMixinWrapper().tickToClosestValidTick( + tick: V3PoolConversorsMixinWrapper().priceToTick( + price: maxPrice, + poolToken0Decimals: currentYield.token0.decimals, + poolToken1Decimals: currentYield.token1.decimals, + isReversed: isReversed, ), + tickSpacing: currentYield.tickSpacing, ), + tickUpper: any(named: "tickUpper"), ), ).called(1); }, @@ -1028,7 +1066,7 @@ void main() { test( """When calling `deposit` with a max price that is infinity, and it's not reversed, - the tick upper in the depositData should be the max tick (but adjusted for the tick spacing)""", + the tick upper should be the max tick (but adjusted for the tick spacing)""", () async { sutCopyWith(customDepositWithNative: true); @@ -1045,18 +1083,19 @@ void main() { ); verify( - () => uniswapPositionManager.getMintCalldata( - params: any( - named: "params", - that: ExpectedMatcher( - expects: (item) => expect( - item.tickUpper, - V3PoolConversorsMixinWrapper().tickToClosestValidTick( - tick: V3PoolConstants.maxTick, - tickSpacing: currentYield.tickSpacing, - ), - ), - ), + () => poolService.sendV3PoolDepositTransaction( + any(), + any(), + amount0Desired: any(named: "amount0Desired"), + amount1Desired: any(named: "amount1Desired"), + amount0Min: any(named: "amount0Min"), + amount1Min: any(named: "amount1Min"), + deadline: any(named: "deadline"), + recipient: any(named: "recipient"), + tickLower: any(named: "tickLower"), + tickUpper: V3PoolConversorsMixinWrapper().tickToClosestValidTick( + tick: V3V4PoolConstants.maxTick, + tickSpacing: currentYield.tickSpacing, ), ), ).called(1); @@ -1065,7 +1104,7 @@ void main() { test( """When calling `deposit` with a min price that is infinity, and it's reversed, - the tick upper in the depositData should be the max tick (but adjusted for the tick spacing)""", + the tick upper should be the max tick (but adjusted for the tick spacing)""", () async { sutCopyWith(customDepositWithNative: true); @@ -1083,20 +1122,20 @@ void main() { isMaxPriceInfinity: false, isReversed: isReversed, ); - verify( - () => uniswapPositionManager.getMintCalldata( - params: any( - named: "params", - that: ExpectedMatcher( - expects: (item) => expect( - item.tickUpper, - V3PoolConversorsMixinWrapper().tickToClosestValidTick( - tick: V3PoolConstants.maxTick, - tickSpacing: currentYield.tickSpacing, - ), - ), - ), + () => poolService.sendV3PoolDepositTransaction( + any(), + any(), + amount0Desired: any(named: "amount0Desired"), + amount1Desired: any(named: "amount1Desired"), + amount0Min: any(named: "amount0Min"), + amount1Min: any(named: "amount1Min"), + deadline: any(named: "deadline"), + recipient: any(named: "recipient"), + tickLower: any(named: "tickLower"), + tickUpper: V3PoolConversorsMixinWrapper().tickToClosestValidTick( + tick: V3V4PoolConstants.maxTick, + tickSpacing: currentYield.tickSpacing, ), ), ).called(1); @@ -1105,7 +1144,7 @@ void main() { test( """When calling `deposit` with a max price that is not infinity, - and it's not reversed, the tick upper in the depositData should be + and it's not reversed, the tick upper should be calculated based on the max price""", () async { sutCopyWith(customDepositWithNative: true); @@ -1125,23 +1164,24 @@ void main() { ); verify( - () => uniswapPositionManager.getMintCalldata( - params: any( - named: "params", - that: ExpectedMatcher( - expects: (item) => expect( - item.tickUpper, - V3PoolConversorsMixinWrapper().tickToClosestValidTick( - tick: V3PoolConversorsMixinWrapper().priceToTick( - price: maxPrice, - poolToken0Decimals: currentYield.token0.decimals, - poolToken1Decimals: currentYield.token1.decimals, - isReversed: false, - ), - tickSpacing: currentYield.tickSpacing, - ), - ), + () => poolService.sendV3PoolDepositTransaction( + any(), + any(), + amount0Desired: any(named: "amount0Desired"), + amount1Desired: any(named: "amount1Desired"), + amount0Min: any(named: "amount0Min"), + amount1Min: any(named: "amount1Min"), + deadline: any(named: "deadline"), + recipient: any(named: "recipient"), + tickLower: any(named: "tickLower"), + tickUpper: V3PoolConversorsMixinWrapper().tickToClosestValidTick( + tick: V3PoolConversorsMixinWrapper().priceToTick( + price: maxPrice, + poolToken0Decimals: currentYield.token0.decimals, + poolToken1Decimals: currentYield.token1.decimals, + isReversed: false, ), + tickSpacing: currentYield.tickSpacing, ), ), ).called(1); @@ -1150,7 +1190,7 @@ void main() { test( """When calling `deposit` with a max price that is not infinity, - and it's reversed, the tick upper in the depositData should be + and it's reversed, the tick upper should be calculated based on the min price""", () async { sutCopyWith(customDepositWithNative: true); @@ -1171,110 +1211,30 @@ void main() { ); verify( - () => uniswapPositionManager.getMintCalldata( - params: any( - named: "params", - that: ExpectedMatcher( - expects: (item) => expect( - item.tickUpper, - V3PoolConversorsMixinWrapper().tickToClosestValidTick( - tick: V3PoolConversorsMixinWrapper().priceToTick( - price: minPrice, - poolToken0Decimals: currentYield.token0.decimals, - poolToken1Decimals: currentYield.token1.decimals, - isReversed: isReversed, - ), - tickSpacing: currentYield.tickSpacing, - ), - ), + () => poolService.sendV3PoolDepositTransaction( + any(), + any(), + amount0Desired: any(named: "amount0Desired"), + amount1Desired: any(named: "amount1Desired"), + amount0Min: any(named: "amount0Min"), + amount1Min: any(named: "amount1Min"), + deadline: any(named: "deadline"), + recipient: any(named: "recipient"), + tickLower: any(named: "tickLower"), + tickUpper: V3PoolConversorsMixinWrapper().tickToClosestValidTick( + tick: V3PoolConversorsMixinWrapper().priceToTick( + price: minPrice, + poolToken0Decimals: currentYield.token0.decimals, + poolToken1Decimals: currentYield.token1.decimals, + isReversed: isReversed, ), + tickSpacing: currentYield.tickSpacing, ), ), ).called(1); }, ); - test("When calling `deposit` the fee in the depositData should be the feeTier of the yield pool", () async { - sutCopyWith(customDepositWithNative: true); - - await sut.deposit( - deadline: const Duration(minutes: 30), - slippage: Slippage.halfPercent, - token0Amount: BigInt.one, - token1Amount: BigInt.one, - minPrice: 1200, - maxPrice: 3000.50, - isMinPriceInfinity: false, - isMaxPriceInfinity: false, - isReversed: false, - ); - - verify( - () => uniswapPositionManager.getMintCalldata( - params: any( - named: "params", - that: ExpectedMatcher( - expects: (item) => expect(item.fee, BigInt.from(currentYield.feeTier)), - ), - ), - ), - ).called(1); - }); - - test("When calling `deposit` the token0 in the depositData should be the token0 of the yield pool", () async { - sutCopyWith(customDepositWithNative: true); - - await sut.deposit( - deadline: const Duration(minutes: 30), - slippage: Slippage.halfPercent, - token0Amount: BigInt.one, - token1Amount: BigInt.one, - minPrice: 1200, - maxPrice: 3000.50, - isMinPriceInfinity: false, - isMaxPriceInfinity: false, - isReversed: false, - ); - - verify( - () => uniswapPositionManager.getMintCalldata( - params: any( - named: "params", - that: ExpectedMatcher( - expects: (item) => expect(item.token0, currentYield.token0.addresses[currentYield.network.chainId]!), - ), - ), - ), - ).called(1); - }); - - test("When calling `deposit` the token1 in the depositData should be the token1 of the yield pool", () async { - sutCopyWith(customDepositWithNative: true); - - await sut.deposit( - deadline: const Duration(minutes: 30), - slippage: Slippage.halfPercent, - token0Amount: BigInt.one, - token1Amount: BigInt.one, - minPrice: 1200, - maxPrice: 3000.50, - isMinPriceInfinity: false, - isMaxPriceInfinity: false, - isReversed: false, - ); - - verify( - () => uniswapPositionManager.getMintCalldata( - params: any( - named: "params", - that: ExpectedMatcher( - expects: (item) => expect(item.token1, currentYield.token1.addresses[currentYield.network.chainId]!), - ), - ), - ), - ).called(1); - }); - test( "When calling `deposit`, after the transcation to deposit is sent, it should emit the waiting transaction state", () async { @@ -1283,8 +1243,17 @@ void main() { when(() => transactionResponse.hash).thenReturn(txId); when( - () => uniswapPositionManagerImpl.mint( - params: any(named: "params"), + () => poolService.sendV3PoolDepositTransaction( + any(), + any(), + amount0Desired: any(named: "amount0Desired"), + amount1Desired: any(named: "amount1Desired"), + amount0Min: any(named: "amount0Min"), + amount1Min: any(named: "amount1Min"), + deadline: any(named: "deadline"), + recipient: any(named: "recipient"), + tickLower: any(named: "tickLower"), + tickUpper: any(named: "tickUpper"), ), ).thenAnswer((_) async { expectLater( @@ -1381,7 +1350,18 @@ void main() { and right after, it should emit the initial state""", () async { when( - () => uniswapPositionManagerImpl.mint(params: any(named: "params")), + () => poolService.sendV3PoolDepositTransaction( + any(), + any(), + amount0Desired: any(named: "amount0Desired"), + amount1Desired: any(named: "amount1Desired"), + amount0Min: any(named: "amount0Min"), + amount1Min: any(named: "amount1Min"), + deadline: any(named: "deadline"), + recipient: any(named: "recipient"), + tickLower: any(named: "tickLower"), + tickUpper: any(named: "tickUpper"), + ), ).thenThrow("dale error"); expectLater( @@ -1411,7 +1391,18 @@ void main() { "When calling `deposit` and an error of User rejected action occurs, it should emit the initial state", () async { when( - () => uniswapPositionManagerImpl.mint(params: any(named: "params")), + () => poolService.sendV3PoolDepositTransaction( + any(), + any(), + amount0Desired: any(named: "amount0Desired"), + amount1Desired: any(named: "amount1Desired"), + amount0Min: any(named: "amount0Min"), + amount1Min: any(named: "amount1Min"), + deadline: any(named: "deadline"), + recipient: any(named: "recipient"), + tickLower: any(named: "tickLower"), + tickUpper: any(named: "tickUpper"), + ), ).thenThrow(UserRejectedAction()); expectLater( @@ -1443,7 +1434,6 @@ void main() { withClock(Clock(() => date), () async { const deadline = Duration(minutes: 54); - final expectedDeadline = date.add(deadline).millisecondsSinceEpoch; when(() => uniswapPositionManagerImpl.multicall(data: any(named: "data"))) .thenAnswer((_) async => transactionResponse); @@ -1463,13 +1453,17 @@ void main() { ); verify( - () => uniswapPositionManager.getMintCalldata( - params: any( - named: "params", - that: ExpectedMatcher( - expects: (item) => expect(item.deadline, BigInt.from(expectedDeadline)), - ), - ), + () => poolService.sendV3PoolDepositTransaction( + any(), + any(), + amount0Desired: any(named: "amount0Desired"), + amount1Desired: any(named: "amount1Desired"), + amount0Min: any(named: "amount0Min"), + amount1Min: any(named: "amount1Min"), + deadline: deadline, + recipient: any(named: "recipient"), + tickLower: any(named: "tickLower"), + tickUpper: any(named: "tickUpper"), ), ).called(1); }); @@ -1650,7 +1644,18 @@ void main() { it should emit the slippage check error state and the initial state""", () async { when( - () => uniswapPositionManagerImpl.mint(params: any(named: "params")), + () => poolService.sendV3PoolDepositTransaction( + any(), + any(), + amount0Desired: any(named: "amount0Desired"), + amount1Desired: any(named: "amount1Desired"), + amount0Min: any(named: "amount0Min"), + amount1Min: any(named: "amount1Min"), + deadline: any(named: "deadline"), + recipient: any(named: "recipient"), + tickLower: any(named: "tickLower"), + tickUpper: any(named: "tickUpper"), + ), ).thenThrow("SLIPPAGE_ERROR"); expectLater( @@ -1729,4 +1734,259 @@ void main() { )); }, ); + + test( + "When calling `approveToken` and the pool type is v4, it should approve the permit2 contract as well ", + () async { + when(() => permit2Impl.allowance(any(), any(), any())) + .thenAnswer((_) async => (amount: BigInt.zero, expiration: BigInt.zero, nonce: BigInt.zero)); + + when( + () => permit2Impl.approve( + token: any(named: "token"), + spender: any(named: "spender"), + amount: any(named: "amount"), + expiration: any(named: "expiration")), + ).thenAnswer((_) async => transactionResponse); + + const permit2Address = "0x1234"; + final currentYield0 = currentYield.copyWith(poolType: PoolType.v4, permit2: permit2Address); + + sut = PreviewDepositModalCubit( + initialPoolTick: initialPoolTick, + poolService: poolService, + currentYield: currentYield0, + erc20: erc20, + wallet: wallet, + uniswapPositionManager: uniswapPositionManager, + permit2: permit2, + navigatorKey: GlobalKey(), + zupAnalytics: zupAnalytics, + ); + + final token = currentYield.token0; + final value = BigInt.one; + + await sut.approveToken(token, value); + + verify( + () => permit2Impl.approve( + token: token.addresses[currentYield.network.chainId]!, + spender: currentYield0.positionManagerAddress, + amount: EthereumConstants.uint160Max, + expiration: EthereumConstants.uint48Max, + ), + ).called(1); + }, + ); + + test( + "When calling `approveToken` with the pool type v4, the allowance is more than the needed value, and the expiration is not expired, it should not approve the permit2 contract ", + () async { + final allowedAmount = BigInt.from(1275); + + when(() => permit2Impl.allowance(any(), any(), any())).thenAnswer( + (_) async => (amount: allowedAmount, expiration: EthereumConstants.uint48Max, nonce: BigInt.zero), + ); + + when( + () => permit2Impl.approve( + token: any(named: "token"), + spender: any(named: "spender"), + amount: any(named: "amount"), + expiration: any(named: "expiration")), + ).thenAnswer((_) async => transactionResponse); + + const permit2Address = "0x1234"; + final currentYield0 = currentYield.copyWith(poolType: PoolType.v4, permit2: permit2Address); + + sut = PreviewDepositModalCubit( + initialPoolTick: initialPoolTick, + poolService: poolService, + currentYield: currentYield0, + erc20: erc20, + wallet: wallet, + uniswapPositionManager: uniswapPositionManager, + permit2: permit2, + navigatorKey: GlobalKey(), + zupAnalytics: zupAnalytics, + ); + + final token = currentYield.token0; + final value = allowedAmount - BigInt.one; + + await sut.approveToken(token, value); + + verifyNever( + () => permit2Impl.approve( + token: token.addresses[currentYield.network.chainId]!, + spender: currentYield0.positionManagerAddress, + amount: EthereumConstants.uint160Max, + expiration: EthereumConstants.uint48Max, + ), + ); + }, + ); + + test( + """When calling `approveToken` with the pool type v4, the allowance is more than the needed value, + but the expiration is already expired,it should approve the permit2 contract""", + () async { + final allowedAmount = BigInt.from(1275); + + when(() => permit2Impl.allowance(any(), any(), any())).thenAnswer( + (_) async => ( + amount: allowedAmount, + expiration: BigInt.from((DateTime.now().millisecondsSinceEpoch / 1000) - 1), + nonce: BigInt.zero + ), + ); + + when( + () => permit2Impl.approve( + token: any(named: "token"), + spender: any(named: "spender"), + amount: any(named: "amount"), + expiration: any(named: "expiration")), + ).thenAnswer((_) async => transactionResponse); + + const permit2Address = "0x1234"; + final currentYield0 = currentYield.copyWith(poolType: PoolType.v4, permit2: permit2Address); + + sut = PreviewDepositModalCubit( + initialPoolTick: initialPoolTick, + poolService: poolService, + currentYield: currentYield0, + erc20: erc20, + wallet: wallet, + uniswapPositionManager: uniswapPositionManager, + permit2: permit2, + navigatorKey: GlobalKey(), + zupAnalytics: zupAnalytics, + ); + + final token = currentYield.token0; + final value = allowedAmount - BigInt.one; + + await sut.approveToken(token, value); + + verify( + () => permit2Impl.approve( + token: token.addresses[currentYield.network.chainId]!, + spender: currentYield0.positionManagerAddress, + amount: EthereumConstants.uint160Max, + expiration: EthereumConstants.uint48Max, + ), + ).called(1); + }, + ); + + test( + "When calling `approveToken` and the pool type is v4, it should approve the token for the permit2 address", + () async { + const permit2Address = "0x1234"; + final currentYield0 = currentYield.copyWith(poolType: PoolType.v4, permit2: permit2Address); + when(() => permit2Impl.allowance(any(), any(), any())).thenAnswer( + (_) async => (amount: BigInt.zero, expiration: BigInt.zero, nonce: BigInt.zero), + ); + + sut = PreviewDepositModalCubit( + initialPoolTick: initialPoolTick, + poolService: poolService, + currentYield: currentYield0, + erc20: erc20, + wallet: wallet, + uniswapPositionManager: uniswapPositionManager, + permit2: permit2, + navigatorKey: GlobalKey(), + zupAnalytics: zupAnalytics, + ); + + final token = currentYield.token0; + final value = BigInt.one; + + await sut.approveToken(token, value); + + verify( + () => erc20Impl.approve( + spender: permit2Address, + value: value, + ), + ).called(1); + }, + ); + + test( + "when calling `deposit` and the pool type is v4, it should call the pool service to deposit on v4 with the correct parameters", + () async { + final currentYield0 = currentYield.copyWith( + poolType: PoolType.v4, + permit2: "0x1234", + ); + final token0Amount = BigInt.one; + final token1Amount = BigInt.two; + const minPrice = 1200.43; + const maxPrice = 4000.12; + const isMinPriceInfinity = false; + const isMaxPriceInfinity = false; + const isReversed = false; + final slippage = Slippage.fromValue(32); + const deadline = Duration(minutes: 30); + final recipient = await signer.address; + final tickLower = V3PoolConversorsMixinWrapper().tickToClosestValidTick( + tick: V3PoolConversorsMixinWrapper().priceToTick( + price: minPrice, + poolToken0Decimals: currentYield0.token0.decimals, + poolToken1Decimals: currentYield0.token1.decimals, + ), + tickSpacing: currentYield0.tickSpacing); + + final tickUpper = V3PoolConversorsMixinWrapper().tickToClosestValidTick( + tick: V3PoolConversorsMixinWrapper().priceToTick( + price: maxPrice, + poolToken0Decimals: currentYield0.token0.decimals, + poolToken1Decimals: currentYield0.token1.decimals, + ), + tickSpacing: currentYield0.tickSpacing); + + sut = PreviewDepositModalCubit( + initialPoolTick: initialPoolTick, + poolService: poolService, + currentYield: currentYield0, + erc20: erc20, + wallet: wallet, + uniswapPositionManager: uniswapPositionManager, + permit2: permit2, + navigatorKey: GlobalKey(), + zupAnalytics: zupAnalytics, + ); + + await sut.deposit( + token0Amount: token0Amount, + token1Amount: token1Amount, + minPrice: minPrice, + maxPrice: maxPrice, + isMinPriceInfinity: isMinPriceInfinity, + isMaxPriceInfinity: isMaxPriceInfinity, + isReversed: isReversed, + slippage: slippage, + deadline: deadline, + ); + + verify( + () => poolService.sendV4PoolDepositTransaction( + currentYield0, + signer, + amount0toDeposit: token0Amount, + amount1ToDeposit: token1Amount, + maxAmount0ToDeposit: slippage.calculateMaxTokenAmountFromSlippage(token0Amount), + maxAmount1ToDeposit: slippage.calculateMaxTokenAmountFromSlippage(token1Amount), + deadline: deadline, + tickLower: tickLower, + tickUpper: tickUpper, + recipient: recipient, + currentPoolTick: initialPoolTick, + ), + ).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 296fcd4..1581893 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 @@ -9,17 +9,20 @@ import 'package:mocktail/mocktail.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; import 'package:web3kit/web3kit.dart'; import 'package:zup_app/abis/erc_20.abi.g.dart'; -import 'package:zup_app/abis/uniswap_position_manager.abi.g.dart'; +import 'package:zup_app/abis/uniswap_permit2.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/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal.dart'; import 'package:zup_app/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal_cubit.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/injections.dart'; +import 'package:zup_app/core/pool_service.dart'; import 'package:zup_app/core/slippage.dart'; -import 'package:zup_app/core/v3_pool_constants.dart'; +import 'package:zup_app/core/v3_v4_pool_constants.dart'; import 'package:zup_app/core/zup_analytics.dart'; +import 'package:zup_app/core/zup_links.dart'; import 'package:zup_app/core/zup_navigator.dart'; import 'package:zup_app/widgets/zup_cached_image.dart'; import 'package:zup_ui_kit/zup_ui_kit.dart'; @@ -33,6 +36,8 @@ void main() { late ZupNavigator navigator; late UrlLauncherPlatform urlLauncherPlatform; late ConfettiController confettiController; + late PoolService poolService; + late UniswapPermit2 permit2; final scaffoldMessengerKey = GlobalKey(); final currentYield = YieldDto.fixture(); @@ -41,6 +46,8 @@ void main() { cubit = PreviewDepositModalCubitMock(); urlLauncherPlatform = UrlLauncherPlatformCustomMock(); confettiController = ConfettiControllerMock(); + poolService = PoolServiceMock(); + permit2 = UniswapPermit2Mock(); UrlLauncherPlatform.instance = urlLauncherPlatform; @@ -52,10 +59,12 @@ void main() { () => confettiController, instanceName: InjectInstanceNames.confettiController10s, ); + inject.registerFactory(() => ZupLinksMock()); + inject.registerFactory(() => permit2); inject.registerFactory(() => ZupAnalyticsMock()); inject.registerFactory(() => UniswapV3PoolMock()); inject.registerFactory(() => Erc20Mock()); - inject.registerFactory(() => UniswapPositionManagerMock()); + inject.registerFactory(() => UniswapV3PositionManagerMock()); inject.registerFactory(() => WalletMock()); inject.registerLazySingleton(() => cubit); inject.registerFactory(() => mockZupCachedImage()); @@ -65,10 +74,11 @@ void main() { () => GoldenConfig.scrollController, instanceName: InjectInstanceNames.appScrollController, ); + inject.registerFactory(() => poolService); inject.registerFactory>(() => GlobalKey()); when(() => cubit.setup()).thenAnswer((_) async {}); - when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(V3PoolConstants.maxTick)); + when(() => cubit.poolTickStream).thenAnswer((_) => Stream.value(V3V4PoolConstants.maxTick)); when(() => cubit.latestPoolTick).thenReturn(BigInt.from(3247)); when(() => cubit.stream).thenAnswer((_) => Stream.value( PreviewDepositModalState.initial( @@ -116,7 +126,6 @@ void main() { double token1DepositAmount = 3, Duration deadline = const Duration(minutes: 30), Slippage slippage = Slippage.halfPercent, - bool depositWithNativeToken = false, }) => goldenDeviceBuilder( Builder(builder: (context) { @@ -129,7 +138,6 @@ void main() { value: cubit, child: PreviewDepositModal( yieldTimeFrame: YieldTimeFrame.day, - depositWithNativeToken: depositWithNativeToken, deadline: deadline, maxSlippage: slippage, currentYield: customYield ?? currentYield, @@ -292,28 +300,6 @@ void main() { }, ); - zGoldenTest( - "When depositWithNativeToken is true, the wrapped native token should be the native token", - goldenFileName: "preview_deposit_modal_deposit_with_native_token", - (tester) async { - await tester.pumpDeviceBuilder( - await goldenBuilder( - depositWithNativeToken: true, - customYield: currentYield.copyWith( - token0: TokenDto.fixture().copyWith(addresses: { - currentYield.network.chainId: currentYield.network.wrappedNative.addresses[currentYield.network.chainId] - }), - token1: TokenDto.fixture().copyWith(addresses: { - currentYield.network.chainId: currentYield.network.wrappedNative.addresses[currentYield.network.chainId] - }), - ), - ), - wrapper: GoldenConfig.localizationsWrapper()); - - await tester.pumpAndSettle(); - }, - ); - zGoldenTest( "When the min price is higher than the current price, it should be in out of range state", goldenFileName: "preview_deposit_modal_out_of_range_min_price", @@ -928,7 +914,6 @@ void main() { await tester.pumpAndSettle(); }, ); - zGoldenTest( "When calling `.show` method and the device is mobile, it should should a bottom sheet instead of a dialog", goldenFileName: "preview_deposit_modal_show_mobile", @@ -940,7 +925,6 @@ void main() { WidgetsBinding.instance.addPostFrameCallback((_) async { PreviewDepositModal( yieldTimeFrame: YieldTimeFrame.day, - depositWithNativeToken: false, currentYield: currentYield, isReversed: true, minPrice: (isInfinity: true, price: 0), @@ -974,7 +958,6 @@ void main() { WidgetsBinding.instance.addPostFrameCallback((_) async { PreviewDepositModal( yieldTimeFrame: YieldTimeFrame.day, - depositWithNativeToken: false, currentYield: currentYield, isReversed: true, minPrice: (isInfinity: true, price: 0), diff --git a/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_connected.png b/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_connected.png index 81f4516..394ab0a 100644 Binary files a/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_connected.png and b/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_connected.png differ diff --git a/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_disabled.png b/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_disabled.png index 3211f70..21d16e9 100644 Binary files a/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_disabled.png and b/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_disabled.png differ diff --git a/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_large_number_left_border.png b/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_large_number_left_border.png index cd7499d..d19adf8 100644 Binary files a/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_large_number_left_border.png and b/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_large_number_left_border.png differ diff --git a/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_large_number_right_border.png b/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_large_number_right_border.png index c074267..367683f 100644 Binary files a/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_large_number_right_border.png and b/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_large_number_right_border.png differ diff --git a/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_non_number.png b/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_non_number.png index 052da22..71d3e31 100644 Binary files a/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_non_number.png and b/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_non_number.png differ diff --git a/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_signer_changes.png b/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_signer_changes.png index 81f4516..394ab0a 100644 Binary files a/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_signer_changes.png and b/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_signer_changes.png differ diff --git a/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_update_token.png b/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_update_token.png index 40ccda9..e6ac881 100644 Binary files a/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_update_token.png and b/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_update_token.png differ diff --git a/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_usd_equivalent.png b/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_usd_equivalent.png new file mode 100644 index 0000000..259cc15 Binary files /dev/null and b/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_usd_equivalent.png differ diff --git a/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_user_balance_click.png b/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_user_balance_click.png index 81f4516..4d41159 100644 Binary files a/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_user_balance_click.png and b/test/app/create/deposit/widgets/token_amount_input_card/goldens/token_amount_card_user_balance_click.png differ diff --git a/test/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_cubit_test.dart b/test/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_cubit_test.dart new file mode 100644 index 0000000..c337c76 --- /dev/null +++ b/test/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_cubit_test.dart @@ -0,0 +1,66 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:zup_app/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_cubit.dart'; +import 'package:zup_app/core/dtos/token_dto.dart'; +import 'package:zup_app/core/dtos/token_price_dto.dart'; +import 'package:zup_app/core/enums/networks.dart'; +import 'package:zup_app/core/mixins/keys_mixin.dart'; +import 'package:zup_app/core/repositories/tokens_repository.dart'; +import 'package:zup_core/zup_core.dart'; + +import '../../../../../mocks.dart'; + +class _KeysMixinWrapper with KeysMixin {} + +void main() { + late TokenAmountInputCardCubit sut; + late TokensRepository tokensRepository; + late ZupSingletonCache zupSingletonCache; + late ZupHolder zupHolder; + + setUp(() { + registerFallbackValue(AppNetworks.sepolia); + + tokensRepository = TokensRepositoryMock(); + zupSingletonCache = ZupSingletonCache.shared; + zupHolder = ZupHolder(); + + sut = TokenAmountInputCardCubit(tokensRepository, zupSingletonCache, zupHolder); + + when(() => tokensRepository.getTokenPrice(any(), any())).thenAnswer((_) async => TokenPriceDto.fixture()); + }); + + test("When calling `getTokenPrice` it should use ZupHolder to not make too many requests at once", () async { + zupHolder = ZupHolderMock(); + + when(() => zupHolder.hold(any())).thenAnswer((_) async => 31); + final sut0 = TokenAmountInputCardCubit(tokensRepository, zupSingletonCache, zupHolder); + + await sut0.getTokenPrice(token: TokenDto.fixture(), network: AppNetworks.sepolia); + verify(() => zupHolder.hold(any())).called(1); + }); + + test("When calling `getTokenPrice` it should use ZupSingletonCache with a expiration of 1 minute", () async { + zupSingletonCache = ZupSingletonCacheMock(); + final sut0 = TokenAmountInputCardCubit(tokensRepository, zupSingletonCache, zupHolder); + final token = TokenDto.fixture(); + const network = AppNetworks.sepolia; + + when(() => zupSingletonCache.run(any(), expiration: any(named: "expiration"), key: any(named: "key"))) + .thenAnswer((_) async => 31); + + await sut0.getTokenPrice(token: token, network: network); + verify(() => zupSingletonCache.run(any(), + expiration: const Duration(minutes: 1), + key: _KeysMixinWrapper() + .tokenPriceCacheKey(tokenAddress: token.addresses[network.chainId]!, network: network))).called(1); + }); + + test("When calling `getTokenPrice` it should use tokensRepository to get the token price", () async { + final token = TokenDto.fixture(); + const network = AppNetworks.sepolia; + + await sut.getTokenPrice(token: token, network: network); + verify(() => tokensRepository.getTokenPrice(token.addresses[network.chainId]!, network)).called(1); + }); +} diff --git a/test/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_test.dart b/test/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_test.dart index fff6c63..c195907 100644 --- a/test/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_test.dart +++ b/test/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_test.dart @@ -7,8 +7,10 @@ import 'package:mocktail/mocktail.dart'; import 'package:web3kit/web3kit.dart'; import 'package:zup_app/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card.dart'; import 'package:zup_app/core/dtos/token_dto.dart'; +import 'package:zup_app/core/dtos/token_price_dto.dart'; import 'package:zup_app/core/enums/networks.dart'; import 'package:zup_app/core/injections.dart'; +import 'package:zup_app/core/repositories/tokens_repository.dart'; import 'package:zup_app/widgets/zup_cached_image.dart'; import 'package:zup_core/zup_core.dart'; @@ -18,19 +20,28 @@ import '../../../../../mocks.dart'; void main() { late Wallet wallet; late Signer signer; + late TokensRepository tokensRepository; setUp(() { + registerFallbackValue(AppNetworks.sepolia); + wallet = WalletMock(); signer = SignerMock(); + tokensRepository = TokensRepositoryMock(); inject.registerFactory(() => wallet); inject.registerFactory(() => ZupSingletonCache.shared); inject.registerFactory(() => mockZupCachedImage()); + inject.registerFactory(() => tokensRepository); + inject.registerFactory(() => ZupHolder()); + when(() => tokensRepository.getTokenPrice(any(), any())).thenAnswer((_) async => TokenPriceDto.fixture()); when(() => wallet.signer).thenReturn(signer); when(() => signer.address).thenAnswer((_) => Future.value("0x99E3CfADCD8Feecb5DdF91f88998cFfB3145F78c")); when(() => wallet.tokenBalance(any(), rpcUrl: any(named: "rpcUrl"))).thenAnswer((_) => Future.value(12.1)); when(() => wallet.signerStream).thenAnswer((_) => const Stream.empty()); + when(() => wallet.nativeOrTokenBalance(any(), rpcUrl: any(named: "rpcUrl"))) + .thenAnswer((_) => Future.value(43727653762.1)); }); tearDown(() async { @@ -70,10 +81,12 @@ void main() { zGoldenTest("When there is not a connected wallet, it should not show the user balance", goldenFileName: "token_amount_card_not_connected", (tester) async { - when(() => wallet.signer).thenReturn(null); + await tester.runAsync(() async { + when(() => wallet.signer).thenReturn(null); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + }); }); zGoldenTest("""" @@ -82,19 +95,21 @@ void main() { it should get the balance of the new connected wallet and display it """, goldenFileName: "token_amount_card_signer_changes", (tester) async { - final signerStreamController = StreamController.broadcast(); + await tester.runAsync(() async { + final signerStreamController = StreamController.broadcast(); - when(() => wallet.signerStream).thenAnswer((_) => signerStreamController.stream); - when(() => wallet.signer).thenReturn(null); + when(() => wallet.signerStream).thenAnswer((_) => signerStreamController.stream); + when(() => wallet.signer).thenReturn(null); - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpDeviceBuilder(await goldenBuilder()); - signerStreamController.add(signer); - when(() => wallet.signer).thenReturn(signer); + signerStreamController.add(signer); + when(() => wallet.signer).thenReturn(signer); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); - verify(() => wallet.nativeOrTokenBalance(any(), rpcUrl: any(named: "rpcUrl"))).called(1); + verify(() => wallet.nativeOrTokenBalance(any(), rpcUrl: any(named: "rpcUrl"))).called(1); + }); }); zGoldenTest("""" @@ -103,177 +118,200 @@ void main() { it should get the balance of the new connected wallet and display it """, goldenFileName: "token_amount_card_signer_changes_not_null", (tester) async { - final signer1 = signer; - final signer2 = SignerMock(); + await tester.runAsync(() async { + final signer1 = signer; + final signer2 = SignerMock(); - final signerStreamController = StreamController.broadcast(); + final signerStreamController = StreamController.broadcast(); - when(() => wallet.signerStream).thenAnswer((_) => signerStreamController.stream); - when(() => wallet.signer).thenReturn(signer1); - when(() => signer2.address).thenAnswer((_) => Future.value("0x99E3CfADCD8Feecb5DdF91f88998cFfB3145F78c")); - when(() => wallet.nativeOrTokenBalance(any(), rpcUrl: any(named: "rpcUrl"))) - .thenAnswer((_) => Future.value(43727653762.1)); + when(() => wallet.signerStream).thenAnswer((_) => signerStreamController.stream); + when(() => wallet.signer).thenReturn(signer1); + when(() => signer2.address).thenAnswer((_) => Future.value("0x99E3CfADCD8Feecb5DdF91f88998cFfB3145F78c")); + when(() => wallet.nativeOrTokenBalance(any(), rpcUrl: any(named: "rpcUrl"))) + .thenAnswer((_) => Future.value(43727653762.1)); - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.pumpAndSettle(); + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); - signerStreamController.add(signer2); - when(() => wallet.signer).thenReturn(signer2); + signerStreamController.add(signer2); + when(() => wallet.signer).thenReturn(signer2); - await tester.pumpAndSettle(); - verify(() => wallet.nativeOrTokenBalance(any(), rpcUrl: any(named: "rpcUrl"))).called(1); + await tester.pumpAndSettle(); + verify(() => wallet.nativeOrTokenBalance(any(), rpcUrl: any(named: "rpcUrl"))).called(1); + }); }); zGoldenTest("""When there is a connected wallet, it should get the user balance and show it in a button""", goldenFileName: "token_amount_card_connected", (tester) async { - when(() => wallet.signer).thenReturn(signer); + await tester.runAsync(() async { + when(() => wallet.signer).thenReturn(signer); - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpDeviceBuilder(await goldenBuilder()); + }); }); zGoldenTest("""When the disabled text param is passed, the card should be disabled and with the text passed """, goldenFileName: "token_amount_card_disabled", (tester) async { - await tester.pumpDeviceBuilder(await goldenBuilder(disabledText: "This card is disabled")); + await tester.runAsync(() async { + await tester.pumpDeviceBuilder(await goldenBuilder(disabledText: "This card is disabled")); + }); }); zGoldenTest("When inputting something in the textfield, it should callback with the value", (tester) async { - double expectedValue = 1.2; - double? actualValue; - await tester.pumpDeviceBuilder(await goldenBuilder(onInput: (typed) => actualValue = typed)); + await tester.runAsync(() async { + double expectedValue = 1.2; + double? actualValue; + await tester.pumpDeviceBuilder(await goldenBuilder(onInput: (typed) => actualValue = typed)); - await tester.enterText(find.byType(TextField), expectedValue.toString()); - await tester.pumpAndSettle(); + await tester.enterText(find.byType(TextField), expectedValue.toString()); + await tester.pumpAndSettle(); - expect(actualValue, expectedValue); + expect(actualValue, expectedValue); + }); }); zGoldenTest("When inputting a non-number in the textfield, it should not accept", goldenFileName: "token_amount_card_non_number", (tester) async { - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.runAsync(() async { + await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.enterText(find.byType(TextField), "abcdefgj"); - await tester.pumpAndSettle(); + await tester.enterText(find.byType(TextField), "abcdefgj"); + await tester.pumpAndSettle(); + }); }); zGoldenTest("When clicking in the user balance button, it should input the balance", goldenFileName: "token_amount_card_user_balance_click", (tester) async { - when(() => wallet.tokenBalance(any(), rpcUrl: any(named: "rpcUrl"))).thenAnswer((_) => Future.value(43727653762.1)); + await tester.runAsync(() async { + when(() => wallet.tokenBalance(any(), rpcUrl: any(named: "rpcUrl"))) + .thenAnswer((_) => Future.value(43727653762.1)); - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.tap(find.byKey(const Key("user-balance-button"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("user-balance-button"))); + await tester.pumpAndSettle(); + }); }); zGoldenTest("When clicking in the user balance button, it should callback with the balance", (tester) async { - const expectedValue = 43727653762.1; - double? actualValue; + await tester.runAsync(() async { + const expectedValue = 43727653762.1; + double? actualValue; - when(() => wallet.nativeOrTokenBalance(any(), rpcUrl: any(named: "rpcUrl"))) - .thenAnswer((_) => Future.value(expectedValue)); + when(() => wallet.nativeOrTokenBalance(any(), rpcUrl: any(named: "rpcUrl"))) + .thenAnswer((_) => Future.value(expectedValue)); - await tester.pumpDeviceBuilder(await goldenBuilder(onInput: (value) => actualValue = value)); + await tester.pumpDeviceBuilder(await goldenBuilder(onInput: (value) => actualValue = value)); - await tester.tap(find.byKey(const Key("user-balance-button"))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("user-balance-button"))); + await tester.pumpAndSettle(); - expect(actualValue, expectedValue); + expect(actualValue, expectedValue); + }); }); zGoldenTest("When clicking in the refresh button, it should get the token amount again, ignoring the cache", goldenFileName: "token_amount_card_refresh", (tester) async { - when(() => wallet.nativeOrTokenBalance(any(), rpcUrl: any(named: "rpcUrl"))) - .thenAnswer((_) => Future.value(43727653762.1)); + await tester.runAsync(() async { + when(() => wallet.nativeOrTokenBalance(any(), rpcUrl: any(named: "rpcUrl"))) + .thenAnswer((_) => Future.value(43727653762.1)); - await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpDeviceBuilder(await goldenBuilder()); - when(() => wallet.nativeOrTokenBalance(any(), rpcUrl: any(named: "rpcUrl"))) - .thenAnswer((_) => Future.value(12345.43)); - await tester.tap(find.byKey(const Key("refresh-balance-button"))); + when(() => wallet.nativeOrTokenBalance(any(), rpcUrl: any(named: "rpcUrl"))) + .thenAnswer((_) => Future.value(12345.43)); + await tester.tap(find.byKey(const Key("refresh-balance-button"))); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); + }); }); zGoldenTest( "When updating the widget with a different token, it should update the token in the cubit and get the balance again", goldenFileName: "token_amount_card_update_token", (tester) async { - const key = Key("token-amount-card"); - const newTokenAddress = "0xN3W_T0K3N"; - final newToken = TokenDto.fixture().copyWith( - addresses: {AppNetworks.sepolia.chainId: newTokenAddress}, - symbol: "NEW_TOKEN", - ); - - await tester.pumpDeviceBuilder(await goldenBuilder(key: key)); - await tester.pumpDeviceBuilder(await goldenBuilder(key: key, token: newToken)); - - verify(() => wallet.nativeOrTokenBalance(newTokenAddress, rpcUrl: any(named: "rpcUrl"))).called(1); + await tester.runAsync(() async { + const key = Key("token-amount-card"); + const newTokenAddress = "0xN3W_T0K3N"; + final newToken = TokenDto.fixture().copyWith( + addresses: {AppNetworks.sepolia.chainId: newTokenAddress}, + symbol: "NEW_TOKEN", + ); + + await tester.pumpDeviceBuilder(await goldenBuilder(key: key)); + await tester.pumpDeviceBuilder(await goldenBuilder(key: key, token: newToken)); + + verify(() => wallet.nativeOrTokenBalance(newTokenAddress, rpcUrl: any(named: "rpcUrl"))).called(1); + }); }, ); zGoldenTest( "When updating the widget from a native token, for a different native token, it should update the token in the cubit and get the balance again", (tester) async { - const key = Key("token-amount-card"); - - const oldTokenNetwork = AppNetworks.scroll; - const newTokenNetwork = AppNetworks.sepolia; - - final oldTokenAddress = AppNetworks.scroll.wrappedNativeTokenAddress; - final oldToken = TokenDto.fixture().copyWith( - addresses: {AppNetworks.scroll.chainId: oldTokenAddress}, - symbol: "OLD_TOKEN", - ); - - final newTokenAddress = AppNetworks.sepolia.wrappedNativeTokenAddress; - final newToken = TokenDto.fixture().copyWith( - addresses: {AppNetworks.sepolia.chainId: newTokenAddress}, - symbol: "NEW_TOKEN", - ); - - await tester.pumpDeviceBuilder( - await goldenBuilder(key: key, token: oldToken, isNative: true, network: oldTokenNetwork), - ); - await tester.pumpDeviceBuilder( - await goldenBuilder(key: key, token: newToken, isNative: true, network: newTokenNetwork), - ); - - verify(() => wallet.nativeOrTokenBalance(EthereumConstants.zeroAddress, rpcUrl: newTokenNetwork.rpcUrl)) - .called(1); + await tester.runAsync(() async { + const key = Key("token-amount-card"); + + const oldTokenNetwork = AppNetworks.scroll; + const newTokenNetwork = AppNetworks.sepolia; + + final oldTokenAddress = AppNetworks.scroll.wrappedNativeTokenAddress; + final oldToken = TokenDto.fixture().copyWith( + addresses: {AppNetworks.scroll.chainId: oldTokenAddress}, + symbol: "OLD_TOKEN", + ); + + final newTokenAddress = AppNetworks.sepolia.wrappedNativeTokenAddress; + final newToken = TokenDto.fixture().copyWith( + addresses: {AppNetworks.sepolia.chainId: newTokenAddress}, + symbol: "NEW_TOKEN", + ); + + await tester.pumpDeviceBuilder( + await goldenBuilder(key: key, token: oldToken, isNative: true, network: oldTokenNetwork), + ); + await tester.pumpDeviceBuilder( + await goldenBuilder(key: key, token: newToken, isNative: true, network: newTokenNetwork), + ); + + verify(() => wallet.nativeOrTokenBalance(EthereumConstants.zeroAddress, rpcUrl: newTokenNetwork.rpcUrl)) + .called(1); + }); }, ); zGoldenTest( "When updating the widget from a non-native token, for a different non-native token, it should update the token in the cubit and get the balance again", (tester) async { - const key = Key("token-amount-card"); - - const oldTokenNetwork = AppNetworks.scroll; - const newTokenNetwork = AppNetworks.sepolia; - - final oldTokenAddress = AppNetworks.scroll.wrappedNativeTokenAddress; - final oldToken = TokenDto.fixture().copyWith( - addresses: {AppNetworks.scroll.chainId: oldTokenAddress}, - symbol: "OLD_TOKEN", - ); - - final newTokenAddress = AppNetworks.sepolia.wrappedNativeTokenAddress; - final newToken = TokenDto.fixture().copyWith( - addresses: {AppNetworks.sepolia.chainId: newTokenAddress}, - symbol: "NEW_TOKEN", - ); - - await tester.pumpDeviceBuilder( - await goldenBuilder(key: key, token: oldToken, isNative: false, network: oldTokenNetwork), - ); - await tester.pumpDeviceBuilder( - await goldenBuilder(key: key, token: newToken, isNative: false, network: newTokenNetwork), - ); - - verify(() => wallet.nativeOrTokenBalance(newTokenAddress, rpcUrl: newTokenNetwork.rpcUrl)).called(1); + await tester.runAsync(() async { + const key = Key("token-amount-card"); + + const oldTokenNetwork = AppNetworks.scroll; + const newTokenNetwork = AppNetworks.sepolia; + + final oldTokenAddress = AppNetworks.scroll.wrappedNativeTokenAddress; + final oldToken = TokenDto.fixture().copyWith( + addresses: {AppNetworks.scroll.chainId: oldTokenAddress}, + symbol: "OLD_TOKEN", + ); + + final newTokenAddress = AppNetworks.sepolia.wrappedNativeTokenAddress; + final newToken = TokenDto.fixture().copyWith( + addresses: {AppNetworks.sepolia.chainId: newTokenAddress}, + symbol: "NEW_TOKEN", + ); + + await tester.pumpDeviceBuilder( + await goldenBuilder(key: key, token: oldToken, isNative: false, network: oldTokenNetwork), + ); + await tester.pumpDeviceBuilder( + await goldenBuilder(key: key, token: newToken, isNative: false, network: newTokenNetwork), + ); + + verify(() => wallet.nativeOrTokenBalance(newTokenAddress, rpcUrl: newTokenNetwork.rpcUrl)).called(1); + }); }, ); @@ -282,8 +320,10 @@ void main() { but instead do a soft clip with a gradient""", goldenFileName: "token_amount_card_large_number_left_border", (tester) async { - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.enterText(find.byType(TextField), "1234567890123456789021762561752615261"); + await tester.runAsync(() async { + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.enterText(find.byType(TextField), "1234567890123456789021762561752615261"); + }); }, ); @@ -292,11 +332,92 @@ void main() { but instead do a soft clip with a gradient""", goldenFileName: "token_amount_card_large_number_right_border", (tester) async { - await tester.pumpDeviceBuilder(await goldenBuilder()); - await tester.enterText(find.byType(TextField), "1234567890123456789021762561752615261"); - await tester.drag(find.byType(TextField), const Offset(-1, 0)); + await tester.runAsync(() async { + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.enterText(find.byType(TextField), "1234567890123456789021762561752615261"); + await tester.drag(find.byType(TextField), const Offset(-1, 0)); + + FocusManager.instance.primaryFocus?.unfocus(); + }); + }, + ); + + zGoldenTest( + "When instanciating the widget, it should update the native token variable in the cubit", + (tester) async { + await tester.runAsync(() async { + await tester.pumpDeviceBuilder(await goldenBuilder(isNative: true)); + await tester.pumpAndSettle(); - FocusManager.instance.primaryFocus?.unfocus(); + verify(() => wallet.nativeOrTokenBalance(EthereumConstants.zeroAddress, rpcUrl: any(named: "rpcUrl"))) + .called(1); + }); }, ); + + zGoldenTest( + "When the wallet emits a new signer, and the current token isNative, it should fetch the native balance", + (tester) async { + await tester.runAsync(() async { + final signerStreamController = StreamController.broadcast(); + when(() => wallet.signer).thenReturn(null); + when(() => wallet.signerStream).thenAnswer((_) => signerStreamController.stream); + + await tester.pumpDeviceBuilder(await goldenBuilder(isNative: true)); + await tester.pumpAndSettle(); + + when(() => wallet.signer).thenReturn(signer); + + signerStreamController.add(SignerMock()); + await tester.pumpAndSettle(); + + verify(() => wallet.nativeOrTokenBalance(EthereumConstants.zeroAddress, rpcUrl: any(named: "rpcUrl"))) + .called(1); + verifyNever(() => + wallet.nativeOrTokenBalance(any(that: isNot(EthereumConstants.zeroAddress)), rpcUrl: any(named: "rpcUrl"))); + }); + }, + ); + + zGoldenTest( + "When the wallet emits a new signer, and the current token is not native, it should fetch the non-native balance", + (tester) async { + await tester.runAsync(() async { + final token = TokenDto.fixture(); + const network = AppNetworks.scroll; + + final signerStreamController = StreamController.broadcast(); + when(() => wallet.signer).thenReturn(null); + when(() => wallet.signerStream).thenAnswer((_) => signerStreamController.stream); + + await tester.pumpDeviceBuilder(await goldenBuilder(isNative: false)); + await tester.pumpAndSettle(); + + when(() => wallet.signer).thenReturn(signer); + + signerStreamController.add(SignerMock()); + await tester.pumpAndSettle(); + + verify(() => wallet.nativeOrTokenBalance(token.addresses[network.chainId]!, rpcUrl: any(named: "rpcUrl"))) + .called(1); + verifyNever(() => wallet.nativeOrTokenBalance(EthereumConstants.zeroAddress, rpcUrl: any(named: "rpcUrl"))); + }); + }, + ); + + zGoldenTest("When typing an amount, it should show the USD equivalent of the amount", + goldenFileName: "token_amount_card_usd_equivalent", (tester) async { + await tester.runAsync(() async { + const tokenUSDPrice = 121.85; + const tokenAmount = 1.2; + + when(() => tokensRepository.getTokenPrice(any(), any())) + .thenAnswer((_) async => TokenPriceDto(usdPrice: tokenUSDPrice, address: "")); + + await tester.pumpDeviceBuilder(await goldenBuilder(controller: TextEditingController(text: "$tokenAmount"))); + await tester.pumpAndSettle(); + + expect(find.text("\$${(tokenAmount * tokenUSDPrice).toStringAsFixed(2)}"), findsOneWidget); + }); + }); } diff --git a/test/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_user_balance_cubit_test.dart b/test/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_user_balance_cubit_test.dart index 9995855..f40a9df 100644 --- a/test/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_user_balance_cubit_test.dart +++ b/test/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_user_balance_cubit_test.dart @@ -5,6 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:web3kit/web3kit.dart'; import 'package:zup_app/app/create/deposit/widgets/token_amount_input_card/token_amount_input_card_user_balance_cubit.dart'; +import 'package:zup_app/core/dtos/token_dto.dart'; import 'package:zup_app/core/enums/networks.dart'; import 'package:zup_core/zup_core.dart'; @@ -300,6 +301,8 @@ void main() { """When calling 'updateTokenAndNetwork' with 'asNativeToken' true, it should pass the address zero to wallet to get the native balance""", () async { + when(() => wallet.signerStream).thenAnswer((_) => const Stream.empty()); + const tokenAddress = "0x99E3CfADCD8Feecb5DdF91f88998cFfB3145F78c"; final sut0 = TokenAmountCardUserBalanceCubit( @@ -315,4 +318,132 @@ void main() { verify(() => wallet.nativeOrTokenBalance(EthereumConstants.zeroAddress, rpcUrl: any(named: "rpcUrl"))).called(1); }, ); + + test( + """When calling 'updateNativeTokenAndFetch' it should update the is native variable + so when a new signer is emitted it will get the native balance based on the variable. + (isNative = true testcase)""", + () async { + final wallet0 = WalletMock(); + + final signerStreamController = StreamController.broadcast(); + final signerStream = signerStreamController.stream; + when(() => wallet0.signerStream).thenAnswer((_) => signerStream); + when(() => wallet0.signer).thenAnswer((_) => null); + + final sut0 = TokenAmountCardUserBalanceCubit( + wallet0, + "0x99E3CfADCD8Feecb5DdF91f88998cFfB3145F78c", + AppNetworks.sepolia, + ZupSingletonCache.shared, + () {}, + ); + + await sut0.updateNativeTokenAndFetch(isNative: true); + + when(() => wallet0.signer).thenAnswer((_) => signer); + + await ZupSingletonCache.shared.clear(); + signerStreamController.add(signer); + + await Future.delayed(const Duration(seconds: 0)); + + verify(() => wallet0.nativeOrTokenBalance(EthereumConstants.zeroAddress, rpcUrl: any(named: "rpcUrl"))).called(1); + }, + ); + + test( + """When calling 'updateNativeTokenAndFetch' it should update the is native variable + so when a new signer is emitted it will get the native balance based on the variable. + (isNative = false testcase)""", + () async { + final wallet0 = WalletMock(); + final signerStreamController = StreamController.broadcast(); + final signerStream = signerStreamController.stream; + const tokenAddress = "0x99E3CfADCD8Feecb5DdF91f88998cFfB3145F78c"; + + when(() => wallet0.signerStream).thenAnswer((_) => signerStream); + when(() => wallet0.signer).thenAnswer((_) => null); + + final sut0 = TokenAmountCardUserBalanceCubit( + wallet0, + tokenAddress, + AppNetworks.sepolia, + ZupSingletonCache.shared, + () {}, + ); + + await sut0.updateNativeTokenAndFetch(isNative: false); + + when(() => wallet0.signer).thenAnswer((_) => signer); + + await ZupSingletonCache.shared.clear(); + signerStreamController.add(signer); + + await Future.delayed(const Duration(seconds: 0)); + + verify(() => wallet0.nativeOrTokenBalance(tokenAddress, rpcUrl: any(named: "rpcUrl"))).called(1); + }, + ); + + test("When calling 'updateNativeTokenAndFetch' and the signer is null, it should not get the user token balance", + () async { + final sut0 = TokenAmountCardUserBalanceCubit( + wallet, + tokenAddress, + AppNetworks.sepolia, + ZupSingletonCache.shared, + () {}, + ); + + when(() => wallet.signer).thenAnswer((_) => null); + + await sut0.updateNativeTokenAndFetch(isNative: false); + + verifyNever(() => wallet.nativeOrTokenBalance(any(), rpcUrl: any(named: "rpcUrl"))); + }); + + test("When calling `updateNativeTokenAndFetch` and the signer is not null, it should get the user token balance", + () async { + final sut0 = TokenAmountCardUserBalanceCubit( + wallet, + tokenAddress, + AppNetworks.sepolia, + ZupSingletonCache.shared, + () {}, + ); + + when(() => wallet.signer).thenAnswer((_) => signer); + + await sut0.updateNativeTokenAndFetch(isNative: false); + + verify(() => wallet.nativeOrTokenBalance(any(), rpcUrl: any(named: "rpcUrl"))) + .called(2); // two times because of the initial load + }); + + test("""When calling `updateTokenAndNetwork` passsing the is native true, it should update the isNative variable, + and when a new signer is emitted it will get the native balance""", () async { + final signerStreamController = StreamController.broadcast(); + + when(() => wallet.signerStream).thenAnswer((_) => signerStreamController.stream); + + final sut0 = TokenAmountCardUserBalanceCubit( + wallet, + tokenAddress, + AppNetworks.sepolia, + ZupSingletonCache.shared, + () {}, + ); + + final token = TokenDto.fixture(); + const network = AppNetworks.sepolia; + + await sut0.updateTokenAndNetwork(token.addresses[network.chainId]!, network, asNativeToken: true); + + signerStreamController.add(signer); + await Future.delayed(const Duration(seconds: 0)); + + verify(() => wallet.nativeOrTokenBalance(EthereumConstants.zeroAddress, rpcUrl: any(named: "rpcUrl"))) + .called(2); // two because of the initial load + }); } diff --git a/test/app/create/goldens/create_page_initial_stage.png b/test/app/create/goldens/create_page_initial_stage.png index f4f50bf..2c36037 100644 Binary files a/test/app/create/goldens/create_page_initial_stage.png and b/test/app/create/goldens/create_page_initial_stage.png differ diff --git a/test/app/create/goldens/create_page_select_tokens_stage_change_a_token_to_same_token_as_b.png b/test/app/create/goldens/create_page_select_tokens_stage_change_a_token_to_same_token_as_b.png index 117a3bb..013ee4c 100644 Binary files a/test/app/create/goldens/create_page_select_tokens_stage_change_a_token_to_same_token_as_b.png and b/test/app/create/goldens/create_page_select_tokens_stage_change_a_token_to_same_token_as_b.png differ diff --git a/test/app/create/goldens/create_page_select_tokens_stage_change_b_token_to_same_token_as_a.png b/test/app/create/goldens/create_page_select_tokens_stage_change_b_token_to_same_token_as_a.png index 5da265f..42a26ae 100644 Binary files a/test/app/create/goldens/create_page_select_tokens_stage_change_b_token_to_same_token_as_a.png and b/test/app/create/goldens/create_page_select_tokens_stage_change_b_token_to_same_token_as_a.png differ diff --git a/test/app/create/goldens/create_page_select_tokens_stage_default_a_token.png b/test/app/create/goldens/create_page_select_tokens_stage_default_a_token.png index f4f50bf..2c36037 100644 Binary files a/test/app/create/goldens/create_page_select_tokens_stage_default_a_token.png and b/test/app/create/goldens/create_page_select_tokens_stage_default_a_token.png differ diff --git a/test/app/create/goldens/create_page_select_tokens_stage_mobile.png b/test/app/create/goldens/create_page_select_tokens_stage_mobile.png index 6563383..1c452b6 100644 Binary files a/test/app/create/goldens/create_page_select_tokens_stage_mobile.png and b/test/app/create/goldens/create_page_select_tokens_stage_mobile.png differ diff --git a/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_add_badge.png b/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_add_badge.png index 6a3ac07..9f74f5e 100644 Binary files a/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_add_badge.png and b/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_add_badge.png differ diff --git a/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_default.png b/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_default.png index d135433..50a4715 100644 Binary files a/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_default.png and b/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_default.png differ diff --git a/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_not_default.png b/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_not_default.png index 6a3ac07..9f74f5e 100644 Binary files a/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_not_default.png and b/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_not_default.png differ 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 09b6f2f..ebcd14b 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/goldens/create_page_select_tokens_stage_pool_search_settings_remove_badge.png b/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_remove_badge.png index d135433..50a4715 100644 Binary files a/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_remove_badge.png and b/test/app/create/goldens/create_page_select_tokens_stage_pool_search_settings_remove_badge.png differ diff --git a/test/app/create/goldens/create_page_select_tokens_stage_reset_tokens_from_network.png b/test/app/create/goldens/create_page_select_tokens_stage_reset_tokens_from_network.png index 6a3ac07..9f74f5e 100644 Binary files a/test/app/create/goldens/create_page_select_tokens_stage_reset_tokens_from_network.png and b/test/app/create/goldens/create_page_select_tokens_stage_reset_tokens_from_network.png differ diff --git a/test/app/create/goldens/create_page_select_tokens_stage_token_a_selected_disabled_button.png b/test/app/create/goldens/create_page_select_tokens_stage_token_a_selected_disabled_button.png index 117a3bb..013ee4c 100644 Binary files a/test/app/create/goldens/create_page_select_tokens_stage_token_a_selected_disabled_button.png and b/test/app/create/goldens/create_page_select_tokens_stage_token_a_selected_disabled_button.png differ diff --git a/test/app/create/goldens/create_page_select_tokens_stage_token_enabled_button.png b/test/app/create/goldens/create_page_select_tokens_stage_token_enabled_button.png index 0464053..1ca3ee3 100644 Binary files a/test/app/create/goldens/create_page_select_tokens_stage_token_enabled_button.png and b/test/app/create/goldens/create_page_select_tokens_stage_token_enabled_button.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 97f58cf..ebfe786 100644 --- a/test/app/create/widgets/create_page_settings_dropdown_test.dart +++ b/test/app/create/widgets/create_page_settings_dropdown_test.dart @@ -127,4 +127,157 @@ void main() { ).called(1); }, ); + + zGoldenTest( + "When hovering over the info icon in the allowed pool types section, it should show a tooltip explaining the field", + goldenFileName: "create_page_settings_dropdown_pool_types_tooltip_hover", + (tester) async { + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + await tester.hover(find.byKey(const Key("pool-types-allowed-tooltip"))); + await tester.pumpAndSettle(); + }, + ); + + zGoldenTest("When clicking to disable the v4 switch, it should update the UI", + goldenFileName: "create_page_settings_dropdown_v4_pool_type_disabled", (tester) async { + when(() => cache.getPoolSearchSettings()).thenReturn( + PoolSearchSettingsDto().copyWith(allowV4Search: true), + ); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + when(() => cache.getPoolSearchSettings()).thenReturn( + PoolSearchSettingsDto().copyWith(allowV4Search: false), + ); // using this when because it fetches again after click + + await tester.tap(find.byKey(const Key("pool-types-allowed-v4-switch"))); + await tester.pumpAndSettle(); + }); + + zGoldenTest("When clicking to disable the v3 switch, it should update the UI", + goldenFileName: "create_page_settings_dropdown_v3_pool_type_disabled", (tester) async { + when(() => cache.getPoolSearchSettings()).thenReturn( + PoolSearchSettingsDto().copyWith(allowV3Search: true), + ); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + when(() => cache.getPoolSearchSettings()).thenReturn( + PoolSearchSettingsDto().copyWith(allowV3Search: false), + ); // using this when because it fetches again after click + + await tester.tap(find.byKey(const Key("pool-types-allowed-v3-switch"))); + await tester.pumpAndSettle(); + }); + + zGoldenTest( + "When clicking to disable the v4 switch, it should call the cache to update the settings only for the v4 switch", + (tester) async { + final initialSettings = PoolSearchSettingsDto.fixture().copyWith(allowV3Search: true, allowV4Search: true); + when(() => cache.getPoolSearchSettings()).thenReturn(initialSettings); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key("pool-types-allowed-v4-switch"))); + await tester.pumpAndSettle(); + + verify( + () => cache.savePoolSearchSettings(settings: initialSettings.copyWith(allowV4Search: false)), + ).called(1); + }, + ); + + zGoldenTest( + "When clicking to disable the v3 switch, it should call the cache to update the settings only for the v3 switch", + (tester) async { + final initialSettings = PoolSearchSettingsDto.fixture().copyWith(allowV3Search: true, allowV4Search: true); + when(() => cache.getPoolSearchSettings()).thenReturn(initialSettings); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key("pool-types-allowed-v3-switch"))); + await tester.pumpAndSettle(); + + verify( + () => cache.savePoolSearchSettings(settings: initialSettings.copyWith(allowV3Search: false)), + ).called(1); + }, + ); + + zGoldenTest("When clicking to enable the v4 switch, it should update the UI", + goldenFileName: "create_page_settings_dropdown_v4_pool_type_enable", (tester) async { + final initialSettings = PoolSearchSettingsDto.fixture().copyWith(allowV3Search: false, allowV4Search: false); + when(() => cache.getPoolSearchSettings()).thenReturn( + initialSettings, + ); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + when(() => cache.getPoolSearchSettings()).thenReturn( + initialSettings.copyWith(allowV4Search: true), + ); // using this when because it fetches again after click + + await tester.tap(find.byKey(const Key("pool-types-allowed-v4-switch"))); + await tester.pumpAndSettle(); + }); + + zGoldenTest("When clicking to disable the v3 switch, it should update the UI", + goldenFileName: "create_page_settings_dropdown_v3_pool_type_enable", (tester) async { + final initialSettings = PoolSearchSettingsDto.fixture().copyWith(allowV3Search: false, allowV4Search: false); + + when(() => cache.getPoolSearchSettings()).thenReturn(initialSettings); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + when(() => cache.getPoolSearchSettings()).thenReturn( + initialSettings.copyWith(allowV3Search: true), + ); // using this when because it fetches again after click + + await tester.tap(find.byKey(const Key("pool-types-allowed-v3-switch"))); + await tester.pumpAndSettle(); + }); + + zGoldenTest( + "When clicking to enable the v4 switch, it should call the cache to update the settings only for the v4 switch", + (tester) async { + final initialSettings = PoolSearchSettingsDto.fixture().copyWith(allowV3Search: false, allowV4Search: false); + when(() => cache.getPoolSearchSettings()).thenReturn(initialSettings); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key("pool-types-allowed-v4-switch"))); + await tester.pumpAndSettle(); + + verify( + () => cache.savePoolSearchSettings(settings: initialSettings.copyWith(allowV4Search: true)), + ).called(1); + }, + ); + + zGoldenTest( + "When clicking to disable the v3 switch, it should call the cache to update the settings only for the v3 switch", + (tester) async { + final initialSettings = PoolSearchSettingsDto.fixture().copyWith(allowV3Search: false, allowV4Search: false); + when(() => cache.getPoolSearchSettings()).thenReturn(initialSettings); + + await tester.pumpDeviceBuilder(await goldenBuilder()); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key("pool-types-allowed-v3-switch"))); + await tester.pumpAndSettle(); + + verify( + () => cache.savePoolSearchSettings(settings: initialSettings.copyWith(allowV3Search: 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 a9aed59..f399b79 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 dbeddd0..8d2ed69 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 98cccc3..88cf67b 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 0f0da21..63d3ca9 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 6a51c36..fcc6d03 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 new file mode 100644 index 0000000..501f6fe Binary files /dev/null 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_v3_pool_type_disabled.png b/test/app/create/widgets/goldens/create_page_settings_dropdown_v3_pool_type_disabled.png new file mode 100644 index 0000000..268ed13 Binary files /dev/null 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 new file mode 100644 index 0000000..c619409 Binary files /dev/null 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 new file mode 100644 index 0000000..303eebf Binary files /dev/null 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 new file mode 100644 index 0000000..650fd94 Binary files /dev/null and b/test/app/create/widgets/goldens/create_page_settings_dropdown_v4_pool_type_enable.png differ diff --git a/test/core/bigint_extension_test.dart b/test/core/bigint_extension_test.dart index 100a460..8997dfb 100644 --- a/test/core/bigint_extension_test.dart +++ b/test/core/bigint_extension_test.dart @@ -1,16 +1,16 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:zup_app/core/extensions/bigint_extension.dart'; -import 'package:zup_app/core/v3_pool_constants.dart'; +import 'package:zup_app/core/v3_v4_pool_constants.dart'; void main() { test("`isMaxTick` extension should return if the bigInt passed is the max tick based on the V3 pool", () { - BigInt maxTick = V3PoolConstants.maxTick; + BigInt maxTick = V3V4PoolConstants.maxTick; expect(maxTick.isMaxTick, true); }); test("`isMinTick` extension should return if the bigInt passed is the min tick based on the V3 pool", () { - BigInt minTick = V3PoolConstants.minTick; + BigInt minTick = V3V4PoolConstants.minTick; expect(minTick.isMinTick, true); }); diff --git a/test/core/dto/yield_dto_test.dart b/test/core/dto/yield_dto_test.dart deleted file mode 100644 index be677c9..0000000 --- a/test/core/dto/yield_dto_test.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:flutter_test/flutter_test.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'; - -void main() { - test("When calling `maybeNativeToken1` with `permitNative` false, it should return the yield token1", () { - final sut = YieldDto.fixture(); - final token = sut.maybeNativeToken1(permitNative: false); - - expect(token, sut.token1); - }); - - test("When calling `maybeNativeToken0` with `permitNative` false, it should return the yield token0", () { - final sut = YieldDto.fixture(); - final token = sut.maybeNativeToken0(permitNative: false); - - expect(token, sut.token0); - }); - - test("""When calling `maybeNativeToken1` with `permitNative` true, - and a token 1 that is not the wrapped native address, - it should return the yield token1""", () { - final chainId = AppNetworks.mainnet.chainId; - - final sut = YieldDto.fixture().copyWith( - chainId: chainId, - token1: TokenDto( - addresses: {chainId: "0x123"}, - ), - ); - final token = sut.maybeNativeToken1(permitNative: true); - - expect(token, sut.token1); - }); - - test("""When calling `maybeNativeToken0` with `permitNative` true, - and a token 0 that is not the wrapped native address, - it should return the yield token0""", () { - final chainId = AppNetworks.mainnet.chainId; - - final sut = YieldDto.fixture().copyWith( - chainId: chainId, - token0: TokenDto( - addresses: {chainId: "0x123"}, - ), - ); - final token = sut.maybeNativeToken0(permitNative: true); - - expect(token, sut.token0); - }); - - test("""When calling `maybeNativeToken1` with `permitNative` true, - and a token 1 that is the wrapped native address, - it should return the native token for the yield network""", () { - const network = AppNetworks.sepolia; - final sut = YieldDto.fixture().copyWith( - token1: TokenDto(addresses: {network.chainId: network.wrappedNativeTokenAddress}), - chainId: network.chainId, - ); - - final token = sut.maybeNativeToken1(permitNative: true); - - expect( - token, - TokenDto( - addresses: {network.chainId: network.wrappedNativeTokenAddress}, - decimals: network.chainInfo.nativeCurrency!.decimals, - logoUrl: network.chainInfo.nativeCurrency!.logoUrl, - symbol: network.chainInfo.nativeCurrency!.symbol, - name: network.chainInfo.nativeCurrency!.name, - )); - }); - - test("""When calling `maybeNativeToken0` with `permitNative` true, - and a token 0 that is the wrapped native address, - it should return the native token for the yield network""", () { - const network = AppNetworks.sepolia; - - final sut = YieldDto.fixture().copyWith( - token0: TokenDto(addresses: {network.chainId: network.wrappedNativeTokenAddress}), - chainId: network.chainId, - ); - - final token = sut.maybeNativeToken0(permitNative: true); - - expect( - token, - TokenDto( - addresses: {network.chainId: network.wrappedNativeTokenAddress}, - decimals: network.chainInfo.nativeCurrency!.decimals, - logoUrl: network.chainInfo.nativeCurrency!.logoUrl, - symbol: network.chainInfo.nativeCurrency!.symbol, - name: network.chainInfo.nativeCurrency!.name, - )); - }); -} diff --git a/test/core/dtos/yield_dto_test.dart b/test/core/dtos/yield_dto_test.dart new file mode 100644 index 0000000..b33cfc1 --- /dev/null +++ b/test/core/dtos/yield_dto_test.dart @@ -0,0 +1,51 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:web3kit/core/ethereum_constants.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'; + +void main() { + test("When calling `isToken0Native` and the token0 address in the yield network is zero, it should return true", () { + const network = AppNetworks.sepolia; + expect( + YieldDto.fixture() + .copyWith( + chainId: network.chainId, + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: EthereumConstants.zeroAddress})) + .isToken0Native, + true, + ); + }); + + test("When calling `isToken1Native` and the token1 address in the yield network is zero, it should return true", () { + const network = AppNetworks.sepolia; + expect( + YieldDto.fixture() + .copyWith( + chainId: network.chainId, + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: EthereumConstants.zeroAddress})) + .isToken1Native, + true, + ); + }); + + test("When calling `isToken0Native` and the token0 address in the yield network is not, it should return false", () { + const network = AppNetworks.sepolia; + expect( + YieldDto.fixture() + .copyWith(chainId: network.chainId, token0: TokenDto.fixture().copyWith(addresses: {network.chainId: "0x1"})) + .isToken0Native, + false, + ); + }); + + test("When calling `isToken1Native` and the token1 address in the yield network is not, it should return false", () { + const network = AppNetworks.sepolia; + expect( + YieldDto.fixture() + .copyWith(chainId: network.chainId, token1: TokenDto.fixture().copyWith(addresses: {network.chainId: "0x1"})) + .isToken1Native, + false, + ); + }); +} diff --git a/test/core/enums/goldens/scroll_network_icon.png b/test/core/enums/goldens/scroll_network_icon.png new file mode 100644 index 0000000..a72a778 Binary files /dev/null and b/test/core/enums/goldens/scroll_network_icon.png differ diff --git a/test/core/enums/goldens/unichain_network_icon.png b/test/core/enums/goldens/unichain_network_icon.png new file mode 100644 index 0000000..33e339e Binary files /dev/null and b/test/core/enums/goldens/unichain_network_icon.png differ diff --git a/test/core/enums/networks_test.dart b/test/core/enums/networks_test.dart index 2d7b151..41fa609 100644 --- a/test/core/enums/networks_test.dart +++ b/test/core/enums/networks_test.dart @@ -18,15 +18,21 @@ void main() { }); test("When calling 'fromValue' it should get a network from a string value", () { - expect(AppNetworks.fromValue("sepolia"), AppNetworks.sepolia); - expect(AppNetworks.fromValue("mainnet"), AppNetworks.mainnet); - expect(AppNetworks.fromValue("scroll"), AppNetworks.scroll); + expect(AppNetworks.fromValue("sepolia"), AppNetworks.sepolia, reason: "Sepolia should match"); + expect(AppNetworks.fromValue("mainnet"), AppNetworks.mainnet, reason: "Mainnet should match"); + expect(AppNetworks.fromValue("scroll"), AppNetworks.scroll, reason: "Scroll should match"); + 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"); }); test("Label extension should match for all networks", () { expect(AppNetworks.sepolia.label, "Sepolia", reason: "Sepolia Label should match"); expect(AppNetworks.mainnet.label, "Ethereum", reason: "Ethereum Label should match"); expect(AppNetworks.scroll.label, "Scroll", reason: "Scroll Label should match"); + 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"); }); test("`testnets` method should return all testnets in the enum, excluding the 'all networks'", () { @@ -34,7 +40,16 @@ void main() { }); test("`mainnets` method should return all mainnets in the enum, including the 'all networks'", () { - expect(AppNetworks.mainnets, [AppNetworks.allNetworks, AppNetworks.mainnet, AppNetworks.scroll]); + expect( + AppNetworks.mainnets, + containsAll([ + AppNetworks.allNetworks, + AppNetworks.mainnet, + AppNetworks.scroll, + // AppNetworks.base, + AppNetworks.unichain, + ]), + ); }); test("`isTestnet` method should return true for sepolia", () { @@ -49,6 +64,14 @@ void main() { expect(AppNetworks.scroll.isTestnet, false); }); + test("`isTestnet` method should return false for base", () { + // expect(AppNetworks.base.isTestnet, false); + }); + + test("`isTestnet` method should return false for unichain", () { + expect(AppNetworks.unichain.isTestnet, false); + }); + test("Chain info extension should match for all networks", () { expect( AppNetworks.sepolia.chainInfo, @@ -84,6 +107,30 @@ void main() { ), reason: "Scroll ChainInfo should match", ); + + // expect( + // AppNetworks.base.chainInfo, + // ChainInfo( + // hexChainId: "0x2105", + // chainName: "Base", + // blockExplorerUrls: const ["https://basescan.org"], + // nativeCurrency: NativeCurrencies.eth.currencyInfo, + // rpcUrls: const ["https://base-rpc.publicnode.com"], + // ), + // reason: "Base ChainInfo should match", + // ); + + expect( + AppNetworks.unichain.chainInfo, + ChainInfo( + hexChainId: "0x82", + chainName: "Unichain", + blockExplorerUrls: const ["https://uniscan.xyz/"], + nativeCurrency: NativeCurrencies.eth.currencyInfo, + rpcUrls: const ["https://unichain-rpc.publicnode.com"], + ), + reason: "Unichain ChainInfo should match", + ); }); test("wrapped native token address should match for all networks", () { @@ -104,6 +151,18 @@ void main() { "0x5300000000000000000000000000000000000004", reason: "Scroll wrapped native token address should match", ); + + // expect( + // AppNetworks.base.wrappedNativeTokenAddress, + // "0x4200000000000000000000000000000000000006", + // reason: "Base wrapped native token address should match", + // ); + + expect( + AppNetworks.unichain.wrappedNativeTokenAddress, + "0x4200000000000000000000000000000000000006", + reason: "Unichain wrapped native token address should match", + ); }); test("wrapped native token should match for all networks", () { @@ -149,6 +208,35 @@ void main() { ), 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", + ); }); test("RpcUrl extension should return the correct rpc url", () { @@ -169,6 +257,18 @@ void main() { "https://scroll-rpc.publicnode.com", reason: "Scroll rpc url should match", ); + + // expect( + // AppNetworks.base.rpcUrl, + // "https://base-rpc.publicnode.com", + // reason: "Base rpc url should match", + // ); + + expect( + AppNetworks.unichain.rpcUrl, + "https://unichain-rpc.publicnode.com", + reason: "Unichain rpc url should match", + ); }); test("openTx should open the correct url for each network", () async { @@ -187,48 +287,6 @@ void main() { } }); - test("'nativeCurrency' should return the correct currency for sepolia network", () { - expect( - AppNetworks.sepolia.nativeCurrencyTokenDto, - TokenDto( - addresses: {}, - name: NativeCurrencies.eth.currencyInfo.name, - decimals: NativeCurrencies.eth.currencyInfo.decimals, - symbol: NativeCurrencies.eth.currencyInfo.symbol, - logoUrl: NativeCurrencies.eth.currencyInfo.logoUrl, - ), - reason: "Sepolia native currency should match", - ); - }); - - test("'nativeCurrency' should return the correct currency for ethereum network", () { - expect( - AppNetworks.mainnet.nativeCurrencyTokenDto, - TokenDto( - addresses: {}, - name: NativeCurrencies.eth.currencyInfo.name, - decimals: NativeCurrencies.eth.currencyInfo.decimals, - symbol: NativeCurrencies.eth.currencyInfo.symbol, - logoUrl: NativeCurrencies.eth.currencyInfo.logoUrl, - ), - reason: "Ethereum native currency should match", - ); - }); - - test("'nativeCurrency' should return the correct currency for scroll network", () { - expect( - AppNetworks.scroll.nativeCurrencyTokenDto, - TokenDto( - addresses: {}, - name: NativeCurrencies.eth.currencyInfo.name, - decimals: NativeCurrencies.eth.currencyInfo.decimals, - symbol: NativeCurrencies.eth.currencyInfo.symbol, - logoUrl: NativeCurrencies.eth.currencyInfo.logoUrl, - ), - reason: "Scroll native currency should match", - ); - }); - test("'fromChainId' should return the correct network from the chain id", () { for (final network in AppNetworks.values) { if (network.isAllNetworks) continue; @@ -247,6 +305,7 @@ void main() { test("'isAllNetworks' should return false if the network is not all networks", () { expect(AppNetworks.scroll.isAllNetworks, false); + expect(AppNetworks.unichain.isAllNetworks, false); }); test("'chainId' should return the correct chain id for each network", () { @@ -270,4 +329,25 @@ void main() { device: GoldenDevice.square, )); }); + + // zGoldenTest("Base network icon should match", goldenFileName: "base_network_icon", (tester) async { + // await tester.pumpDeviceBuilder(await goldenDeviceBuilder( + // AppNetworks.base.icon, + // device: GoldenDevice.square, + // )); + // }); + + zGoldenTest("Scroll network icon should match", goldenFileName: "scroll_network_icon", (tester) async { + await tester.pumpDeviceBuilder(await goldenDeviceBuilder( + AppNetworks.scroll.icon, + device: GoldenDevice.square, + )); + }); + + zGoldenTest("Unichain network icon should match", goldenFileName: "unichain_network_icon", (tester) async { + await tester.pumpDeviceBuilder(await goldenDeviceBuilder( + AppNetworks.unichain.icon, + device: GoldenDevice.square, + )); + }); } diff --git a/test/core/enums/pool_type_test.dart b/test/core/enums/pool_type_test.dart new file mode 100644 index 0000000..ddcfa80 --- /dev/null +++ b/test/core/enums/pool_type_test.dart @@ -0,0 +1,25 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:zup_app/core/enums/pool_type.dart'; + +void main() { + test('When calling `isV3` and the pool is indeed v3, it should return true', () { + expect(PoolType.v3.isV3, true); + }); + + test('When calling `isV3` and the pool is not v3, it should return false', () { + expect(PoolType.v4.isV3, false); + }); + + test('When calling `isV4` and the pool is indeed v4, it should return true', () { + expect(PoolType.v4.isV4, true); + }); + + test('When calling `isV4` and the pool is not v4, it should return false', () { + expect(PoolType.v3.isV4, false); + }); + + test('label should return correct string', () { + expect(PoolType.v3.label, "V3"); + expect(PoolType.v4.label, "V4"); + }); +} diff --git a/test/core/mixins/keys_mixin_test.dart b/test/core/mixins/keys_mixin_test.dart index 7378699..c71dfd7 100644 --- a/test/core/mixins/keys_mixin_test.dart +++ b/test/core/mixins/keys_mixin_test.dart @@ -30,4 +30,16 @@ void main() { expect(key, 'poolTick-$poolAddress-${network.name}'); }); + + test("`tokenPriceCacheKey` should return correct key", () { + const tokenAddress = '0xTokenAddress'; + const network = AppNetworks.sepolia; + + final key = _KeysMixinWrapper().tokenPriceCacheKey( + tokenAddress: tokenAddress, + network: network, + ); + + expect(key, 'tokenPrice-$tokenAddress-${network.name}'); + }); } diff --git a/test/core/mixins/v3_pool_conversors_mixin_test.dart b/test/core/mixins/v3_pool_conversors_mixin_test.dart index 46417ea..b8d7aef 100644 --- a/test/core/mixins/v3_pool_conversors_mixin_test.dart +++ b/test/core/mixins/v3_pool_conversors_mixin_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:zup_app/core/mixins/v3_pool_conversors_mixin.dart'; -import 'package:zup_app/core/v3_pool_constants.dart'; +import 'package:zup_app/core/v3_v4_pool_constants.dart'; class _V3PoolConversorsMixinTest with V3PoolConversorsMixin {} @@ -29,21 +29,21 @@ void main() { test("""When calling `tickToClosestValidTick` and the closest valid tick is lower than the minimum tick, it should return the higher valid tick""", () { final closestValidTick = _V3PoolConversorsMixinTest().tickToClosestValidTick( - tick: V3PoolConstants.minTick - BigInt.from(1), + tick: V3V4PoolConstants.minTick - BigInt.from(1), tickSpacing: 1, ); - expect(closestValidTick, V3PoolConstants.minTick); + expect(closestValidTick, V3V4PoolConstants.minTick); }); test("""When calling `tickToClosestValidTick` and the closest valid tick is higher than the maximum tick, it should return the lower valid tick""", () { final closestValidTick = _V3PoolConversorsMixinTest().tickToClosestValidTick( - tick: V3PoolConstants.maxTick + BigInt.from(2), + tick: V3V4PoolConstants.maxTick + BigInt.from(2), tickSpacing: 4, ); - expect(closestValidTick, V3PoolConstants.maxTick); + expect(closestValidTick, V3V4PoolConstants.maxTick); }); test("""`tickToClosestValidTick` should correctly convert a diff --git a/test/core/mixins/v4_pool_liquidity_calculations_mixin_test.dart b/test/core/mixins/v4_pool_liquidity_calculations_mixin_test.dart new file mode 100644 index 0000000..e71d746 --- /dev/null +++ b/test/core/mixins/v4_pool_liquidity_calculations_mixin_test.dart @@ -0,0 +1,95 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:zup_app/core/mixins/v4_pool_liquidity_calculations_mixin.dart'; + +class _V4PoolLiquidityCalculationsMixinTest with V4PoolLiquidityCalculationsMixin {} + +void main() { + test( + "When calling `getLiquidityForAmount0` it should return the correct value based on the v4 pool math", + () { + BigInt sqrtPriceAX96 = BigInt.parse("4242269098745952767280720"); + BigInt sqrtPriceBX96 = BigInt.parse("4242269098745952767280721"); + BigInt amount0 = BigInt.from(1241555); + + expect( + _V4PoolLiquidityCalculationsMixinTest().getLiquidityForAmount0(sqrtPriceAX96, sqrtPriceBX96, amount0), + BigInt.parse("282021882116526385819866125"), + ); + }, + ); + + test( + "When calling `getLiquidityForAmount1` it should return the correct value based on the v4 pool math", + () { + BigInt sqrtPriceAX96 = BigInt.parse("4242269098745952767280720"); + BigInt sqrtPriceBX96 = BigInt.parse("4242269098745952767280721"); + BigInt amount0 = BigInt.from(1241555); + + expect( + _V4PoolLiquidityCalculationsMixinTest().getLiquidityForAmount1(sqrtPriceAX96, sqrtPriceBX96, amount0), + BigInt.parse("98366121310397459660952459259412480"), + ); + }, + ); + + test( + "When calling `getLiquidityForAmounts` and the sqrtpricea is bigger, it should return the token0 liquidity calculated", + () { + BigInt sqrtPriceX96 = BigInt.parse("4242269098745952767280720"); + BigInt sqrtPriceAX96 = BigInt.parse("4242269098745952767280720"); + BigInt sqrtPriceBX96 = BigInt.parse("4242269098745952767280721"); + BigInt amount0 = BigInt.from(1241555); + BigInt amount1 = BigInt.from(1241555); + + expect( + _V4PoolLiquidityCalculationsMixinTest() + .getLiquidityForAmounts(sqrtPriceX96, sqrtPriceAX96, sqrtPriceBX96, amount0, amount1), + BigInt.parse("282021882116526385819866125"), + ); + }, + ); + + test( + "When calling `getLiquidityForAmounts` and the sqrtpriceA is lower, it should return the token1 liquidity calculated", + () { + BigInt sqrtPriceX96 = BigInt.parse("4242269098745952767280720"); + BigInt sqrtPriceAX96 = BigInt.parse("4242269098745952767280724"); + BigInt sqrtPriceBX96 = BigInt.parse("4242269098745952767280721"); + BigInt amount0 = BigInt.from(1241555); + BigInt amount1 = BigInt.from(1241555); + + expect( + _V4PoolLiquidityCalculationsMixinTest() + .getLiquidityForAmounts(sqrtPriceX96, sqrtPriceAX96, sqrtPriceBX96, amount0, amount1), + BigInt.parse("94007294038842128606622041"), + ); + }, + ); + + test('getSqrtPriceAtTick should return correct value for tick 0', () { + final result = _V4PoolLiquidityCalculationsMixinTest().getSqrtPriceAtTick(BigInt.zero); + expect(result, BigInt.parse('79228162514264337593543950336')); + }); + + test('getSqrtPriceAtTick should return correct value for positive tick', () { + final result = _V4PoolLiquidityCalculationsMixinTest().getSqrtPriceAtTick(BigInt.from(60)); + expect(result, BigInt.parse('79466191966197645195421774833')); + }); + + test('getSqrtPriceAtTick should return correct value for negative tick', () { + final result = _V4PoolLiquidityCalculationsMixinTest().getSqrtPriceAtTick(BigInt.from(-60)); + expect(result, BigInt.parse('78990846045029531151608375686')); + }); + + test('getSqrtPriceAtTick should throw for tick out of range', () { + expect( + () => _V4PoolLiquidityCalculationsMixinTest().getSqrtPriceAtTick(BigInt.from(887273)), + throwsException, + ); + + expect( + () => _V4PoolLiquidityCalculationsMixinTest().getSqrtPriceAtTick(BigInt.from(-887273)), + throwsException, + ); + }); +} diff --git a/test/core/pool_service_test.dart b/test/core/pool_service_test.dart new file mode 100644 index 0000000..c19d037 --- /dev/null +++ b/test/core/pool_service_test.dart @@ -0,0 +1,1334 @@ +import 'package:clock/clock.dart'; +import 'package:flutter_test/flutter_test.dart'; +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/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/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/mixins/v4_pool_liquidity_calculations_mixin.dart'; +import 'package:zup_app/core/pool_service.dart'; +import 'package:zup_app/core/v4_pool_constants.dart'; + +import '../mocks.dart'; + +class _V4PoolLiquidityCalculationsMixinWrapper with V4PoolLiquidityCalculationsMixin {} + +void main() { + late PoolService sut; + late UniswapV4StateView stateView; + late UniswapV3Pool uniswapV3Pool; + late UniswapV3PositionManager positionManagerV3; + late UniswapV4PositionManager positionManagerV4; + late Signer signer; + late YieldDto currentYield; + late TransactionResponse transactionResponse; + + late UniswapV4StateViewImpl stateViewImpl; + late UniswapV3PoolImpl uniswapV3PoolImpl; + late UniswapV3PositionManagerImpl positionManagerV3Impl; + late UniswapV4PositionManagerImpl positionManagerV4Impl; + late EthereumAbiCoder ethereumAbiCoder; + + setUp(() { + registerFallbackValue(( + amount0Desired: BigInt.zero, + amount0Min: BigInt.zero, + amount1Desired: BigInt.zero, + amount1Min: BigInt.zero, + deadline: BigInt.zero, + fee: BigInt.zero, + recipient: "", + tickLower: BigInt.zero, + tickUpper: BigInt.zero, + token0: "", + token1: "", + )); + registerFallbackValue(SignerMock()); + registerFallbackValue(BigInt.zero); + + transactionResponse = TransactionResponseMock(); + stateView = UniswapV4StateViewMock(); + uniswapV3Pool = UniswapV3PoolMock(); + positionManagerV3 = UniswapV3PositionManagerMock(); + positionManagerV4 = UniswapV4PositionManagerMock(); + ethereumAbiCoder = EthereumAbiCoderMock(); + signer = SignerMock(); + + stateViewImpl = UniswapV4StateViewImplMock(); + uniswapV3PoolImpl = UniswapV3PoolImplMock(); + positionManagerV3Impl = UniswapV3PositionManagerImplMock(); + positionManagerV4Impl = UniswapV4PositionManagerImplMock(); + + currentYield = YieldDto.fixture(); + + sut = PoolService(stateView, uniswapV3Pool, positionManagerV3, positionManagerV4, ethereumAbiCoder); + + when(() => stateView.fromRpcProvider(contractAddress: any(named: "contractAddress"), rpcUrl: any(named: "rpcUrl"))) + .thenReturn(stateViewImpl); + + when(() => uniswapV3Pool.fromRpcProvider( + contractAddress: any(named: "contractAddress"), + rpcUrl: any(named: "rpcUrl"), + )).thenReturn(uniswapV3PoolImpl); + + when(() => positionManagerV3.fromRpcProvider( + contractAddress: any(named: "contractAddress"), + rpcUrl: any(named: "rpcUrl"))).thenReturn(positionManagerV3Impl); + + when(() => + positionManagerV3.fromSigner(contractAddress: any(named: "contractAddress"), signer: any(named: "signer"))) + .thenReturn(positionManagerV3Impl); + + when(() => + positionManagerV4.fromSigner(contractAddress: any(named: "contractAddress"), signer: any(named: "signer"))) + .thenReturn(positionManagerV4Impl); + + when(() => positionManagerV4.fromRpcProvider( + contractAddress: any(named: "contractAddress"), + rpcUrl: any(named: "rpcUrl"))).thenReturn(positionManagerV4Impl); + + when(() => signer.address).thenAnswer((_) async => "0xS0M3_4ddr355"); + + when(() => transactionResponse.waitConfirmation()).thenAnswer((_) async => TransactionReceipt(hash: "0x123")); + when(() => transactionResponse.hash).thenReturn("0x123"); + }); + + test( + "When calling `getPoolTick` and the pool is v4, it should use the state view contract to get it", + () async { + final expectedTick = BigInt.from(87654); + when(() => stateViewImpl.getSlot0(poolId: any(named: "poolId"))).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); + + expect(result, expectedTick); + verify(() => stateViewImpl.getSlot0(poolId: currentYield0.poolAddress)).called(1); + }, + ); + + test( + "When calling `getPoolTick` and the pool is v3, it should use the v3 pool contract to get it", + () async { + final expectedTick = BigInt.from(2127); + when(() => uniswapV3PoolImpl.slot0()).thenAnswer((_) async => ( + feeProtocol: BigInt.from(0), + observationCardinality: BigInt.from(0), + observationCardinalityNext: BigInt.from(0), + observationIndex: BigInt.from(0), + sqrtPriceX96: BigInt.from(0), + tick: expectedTick, + unlocked: true + )); + + final currentYield0 = currentYield.copyWith(poolType: PoolType.v3); + final result = await sut.getPoolTick(currentYield0); + + expect(result, expectedTick); + verify(() => uniswapV3PoolImpl.slot0()).called(1); + }, + ); + + test( + """when calling `sendV3PoolDepositTransaction` with token0 native, + it should send a multicall transaction with the mint calldata and a native + refund calldata""", + () async { + const mintCalldata = "0x25"; + const refundCalldata = "0x26"; + + const network = AppNetworks.mainnet; + final currentYield0 = currentYield.copyWith( + poolType: PoolType.v3, + chainId: network.chainId, + token0: TokenDto.fixture().copyWith(addresses: { + network.chainId: EthereumConstants.zeroAddress, + }), + token1: TokenDto.fixture().copyWith(addresses: { + network.chainId: "0x123", + })); + + when(() => positionManagerV3.getMintCalldata(params: any(named: "params"))).thenReturn(mintCalldata); + when(() => positionManagerV3.getRefundETHCalldata()).thenReturn(refundCalldata); + when(() => positionManagerV3Impl.multicall(data: any(named: "data"), ethValue: any(named: "ethValue"))) + .thenAnswer( + (_) async => transactionResponse, + ); + + final amount0Desired = BigInt.from(100); + final amount1Desired = BigInt.from(100); + const deadline = Duration.zero; + final amount0Min = BigInt.from(12); + final amount1Min = BigInt.from(12); + final recipient = await signer.address; + final tickLower = BigInt.from(0); + final tickUpper = BigInt.from(0); + + await sut.sendV3PoolDepositTransaction( + currentYield0, + signer, + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + deadline: deadline, + amount0Min: amount0Min, + amount1Min: amount1Min, + recipient: recipient, + tickLower: tickLower, + tickUpper: tickUpper, + ); + + verify( + () => positionManagerV3Impl.multicall( + data: [mintCalldata, refundCalldata], + ethValue: any(named: "ethValue"), + ), + ).called(1); + }, + ); + + test( + """when calling `sendV3PoolDepositTransaction` with token0 native, it should correctly + pass the params to get the mint calldata, with the token0 being the wrapped native address""", + () async { + withClock(Clock.fixed(DateTime(2028)), () async { + const mintCalldata = "0x25"; + const refundCalldata = "0x26"; + const token0Address = EthereumConstants.zeroAddress; + const token1Address = "0x20172891"; + + const network = AppNetworks.mainnet; + final currentYield0 = currentYield.copyWith( + poolType: PoolType.v3, + chainId: network.chainId, + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: token0Address}), + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: token1Address})); + + when(() => positionManagerV3.getMintCalldata(params: any(named: "params"))).thenReturn(mintCalldata); + when(() => positionManagerV3.getRefundETHCalldata()).thenReturn(refundCalldata); + when(() => positionManagerV3Impl.multicall(data: any(named: "data"), ethValue: any(named: "ethValue"))) + .thenAnswer( + (_) async => transactionResponse, + ); + + final amount0Desired = BigInt.from(4311); + final amount1Desired = BigInt.from(1031900); + const deadline = Duration(days: 1); + final amount0Min = BigInt.from(1390); + final amount1Min = BigInt.from(432); + final recipient = await signer.address; + final tickLower = BigInt.from(321); + final tickUpper = BigInt.from(1222); + + await sut.sendV3PoolDepositTransaction( + currentYield0, + signer, + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + deadline: deadline, + amount0Min: amount0Min, + amount1Min: amount1Min, + recipient: recipient, + tickLower: tickLower, + tickUpper: tickUpper, + ); + + verify( + () => positionManagerV3.getMintCalldata(params: ( + amount0Desired: amount0Desired, + amount0Min: amount0Min, + amount1Desired: amount1Desired, + amount1Min: amount1Min, + deadline: BigInt.from(clock.now().add(deadline).millisecondsSinceEpoch), + fee: BigInt.from(currentYield0.feeTier), + recipient: recipient, + tickLower: tickLower, + tickUpper: tickUpper, + token0: network.wrappedNative.addresses[network.chainId]!, + token1: token1Address, + )), + ).called(1); + }); + }, + ); + + test( + """when calling `sendV3PoolDepositTransaction` with token1 native, it should correctly + pass the params to get the mint calldata, with the token1 being the wrapped native address""", + () async { + withClock(Clock.fixed(DateTime(2028)), () async { + const mintCalldata = "0x25"; + const refundCalldata = "0x26"; + const token0Address = "0x20172891"; + const token1Address = EthereumConstants.zeroAddress; + + const network = AppNetworks.mainnet; + final currentYield0 = currentYield.copyWith( + poolType: PoolType.v3, + chainId: network.chainId, + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: token0Address}), + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: token1Address})); + + when(() => positionManagerV3.getMintCalldata(params: any(named: "params"))).thenReturn(mintCalldata); + when(() => positionManagerV3.getRefundETHCalldata()).thenReturn(refundCalldata); + when(() => positionManagerV3Impl.multicall(data: any(named: "data"), ethValue: any(named: "ethValue"))) + .thenAnswer( + (_) async => transactionResponse, + ); + + final amount0Desired = BigInt.from(100); + final amount1Desired = BigInt.from(31); + const deadline = Duration.zero; + final amount0Min = BigInt.from(320); + final amount1Min = BigInt.from(12); + final recipient = await signer.address; + final tickLower = BigInt.from(32); + final tickUpper = BigInt.from(14489); + + await sut.sendV3PoolDepositTransaction( + currentYield0, + signer, + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + deadline: deadline, + amount0Min: amount0Min, + amount1Min: amount1Min, + recipient: recipient, + tickLower: tickLower, + tickUpper: tickUpper, + ); + + verify( + () => positionManagerV3.getMintCalldata(params: ( + amount0Desired: amount0Desired, + amount0Min: amount0Min, + amount1Desired: amount1Desired, + amount1Min: amount1Min, + deadline: BigInt.from(clock.now().add(deadline).millisecondsSinceEpoch), + fee: BigInt.from(currentYield0.feeTier), + recipient: recipient, + tickLower: tickLower, + tickUpper: tickUpper, + token0: token0Address, + token1: network.wrappedNativeTokenAddress, + )), + ).called(1); + }); + }, + ); + + test( + """when calling `sendV3PoolDepositTransaction` with token0 native, it should correctly + send the token0amount as ethValue""", + () async { + withClock(Clock.fixed(DateTime(2028)), () async { + const mintCalldata = "0x25"; + const refundCalldata = "0x26"; + const token0Address = EthereumConstants.zeroAddress; + const token1Address = "0x20172891"; + + const network = AppNetworks.mainnet; + final currentYield0 = currentYield.copyWith( + poolType: PoolType.v3, + chainId: network.chainId, + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: token0Address}), + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: token1Address})); + + when(() => positionManagerV3.getMintCalldata(params: any(named: "params"))).thenReturn(mintCalldata); + when(() => positionManagerV3.getRefundETHCalldata()).thenReturn(refundCalldata); + when(() => positionManagerV3Impl.multicall(data: any(named: "data"), ethValue: any(named: "ethValue"))) + .thenAnswer( + (_) async => transactionResponse, + ); + + final amount0Desired = BigInt.from(4311); + final amount1Desired = BigInt.from(1031900); + const deadline = Duration(days: 1); + final amount0Min = BigInt.from(1390); + final amount1Min = BigInt.from(432); + final recipient = await signer.address; + final tickLower = BigInt.from(321); + final tickUpper = BigInt.from(1222); + + await sut.sendV3PoolDepositTransaction( + currentYield0, + signer, + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + deadline: deadline, + amount0Min: amount0Min, + amount1Min: amount1Min, + recipient: recipient, + tickLower: tickLower, + tickUpper: tickUpper, + ); + + verify( + () => positionManagerV3Impl.multicall(ethValue: amount0Desired, data: any(named: "data")), + ).called(1); + }); + }, + ); + + test( + """when calling `sendV3PoolDepositTransaction` with token1 native, it should correctly + send the token1amount as ethValue""", + () async { + withClock(Clock.fixed(DateTime(2028)), () async { + const mintCalldata = "0x25"; + const refundCalldata = "0x26"; + const token1Address = EthereumConstants.zeroAddress; + const token0Address = "0x20172891"; + + const network = AppNetworks.mainnet; + final currentYield0 = currentYield.copyWith( + poolType: PoolType.v3, + chainId: network.chainId, + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: token0Address}), + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: token1Address})); + + when(() => positionManagerV3.getMintCalldata(params: any(named: "params"))).thenReturn(mintCalldata); + when(() => positionManagerV3.getRefundETHCalldata()).thenReturn(refundCalldata); + when(() => positionManagerV3Impl.multicall(data: any(named: "data"), ethValue: any(named: "ethValue"))) + .thenAnswer( + (_) async => transactionResponse, + ); + + final amount0Desired = BigInt.from(4311); + final amount1Desired = BigInt.from(1031900); + const deadline = Duration(days: 1); + final amount0Min = BigInt.from(1390); + final amount1Min = BigInt.from(432); + final recipient = await signer.address; + final tickLower = BigInt.from(321); + final tickUpper = BigInt.from(1222); + + await sut.sendV3PoolDepositTransaction( + currentYield0, + signer, + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + deadline: deadline, + amount0Min: amount0Min, + amount1Min: amount1Min, + recipient: recipient, + tickLower: tickLower, + tickUpper: tickUpper, + ); + + verify( + () => positionManagerV3Impl.multicall(ethValue: amount1Desired, data: any(named: "data")), + ).called(1); + }); + }, + ); + + test( + """When calling `sendV3PoolDepositTransaction` and there is no native token, + it should call `mint` in the v3 position manager passing the correct params""", + () { + withClock(Clock.fixed(DateTime(2028)), () async { + const token1Address = "0x315768"; + const token0Address = "0x20172891"; + + when(() => positionManagerV3Impl.mint(params: any(named: "params"), ethValue: any(named: "ethValue"))) + .thenAnswer( + (_) async => transactionResponse, + ); + + const network = AppNetworks.mainnet; + final currentYield0 = currentYield.copyWith( + feeTier: 3982, + poolType: PoolType.v3, + chainId: network.chainId, + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: token0Address}), + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: token1Address})); + + final amount0Desired = BigInt.from(4311); + final amount1Desired = BigInt.from(1031900); + const deadline = Duration(days: 1); + final amount0Min = BigInt.from(1390); + final amount1Min = BigInt.from(432); + final recipient = await signer.address; + final tickLower = BigInt.from(321); + final tickUpper = BigInt.from(1222); + + await sut.sendV3PoolDepositTransaction( + currentYield0, + signer, + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + deadline: deadline, + amount0Min: amount0Min, + amount1Min: amount1Min, + recipient: recipient, + tickLower: tickLower, + tickUpper: tickUpper, + ); + + verify( + () => positionManagerV3Impl.mint( + params: ( + token0: token0Address, + token1: token1Address, + fee: BigInt.from(currentYield0.feeTier), + tickLower: tickLower, + tickUpper: tickUpper, + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + amount0Min: amount0Min, + amount1Min: amount1Min, + recipient: recipient, + deadline: BigInt.from(clock.now().add(deadline).millisecondsSinceEpoch), + ), + ethValue: null, + ), + ).called(1); + }); + }, + ); + + test( + "When calling `sendV4PoolDepositTransaction` and the token0 is native, it should encode packed the correct actions including the sweep", + () async { + when(() => ethereumAbiCoder.encodePacked(any(), any())).thenReturn("0x"); + when(() => ethereumAbiCoder.encode(any(), any())).thenReturn("0x"); + when( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: any(named: "unlockData"), deadline: any(named: "deadline"), ethValue: any(named: "ethValue")), + ).thenAnswer((_) async => transactionResponse); + + const network = AppNetworks.mainnet; + final amount0Desired = BigInt.from(4311); + final amount1Desired = BigInt.from(1031900); + const deadline = Duration(days: 1); + final amount0Max = BigInt.from(4312); + final amount1Max = BigInt.from(1031901); + final recipient = await signer.address; + final tickLower = BigInt.from(321); + final tickUpper = BigInt.from(1222); + final currentPoolTick = BigInt.from(123); + final currentYield0 = currentYield.copyWith( + chainId: network.chainId, + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: EthereumConstants.zeroAddress}), + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: "0x1"}), + ); + + await sut.sendV4PoolDepositTransaction( + currentYield0, + signer, + deadline: deadline, + tickLower: tickLower, + tickUpper: tickUpper, + amount0toDeposit: amount0Desired, + amount1ToDeposit: amount1Desired, + maxAmount0ToDeposit: amount0Max, + maxAmount1ToDeposit: amount1Max, + recipient: recipient, + currentPoolTick: currentPoolTick, + ); + + verify(() => ethereumAbiCoder.encodePacked([ + "uint8", + "uint8", + "uint8" + ], [ + V4PoolConstants.mintPositionActionValue, + V4PoolConstants.settlePairActionValue, + V4PoolConstants.sweepActionValue + ])).called(1); + }); + + test( + "When calling `sendV4PoolDepositTransaction` and the token1 is native, it should encode packed the correct actions including the sweep", + () async { + when(() => ethereumAbiCoder.encodePacked(any(), any())).thenReturn("0x"); + when(() => ethereumAbiCoder.encode(any(), any())).thenReturn("0x"); + when( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: any(named: "unlockData"), deadline: any(named: "deadline"), ethValue: any(named: "ethValue")), + ).thenAnswer((_) async => transactionResponse); + + const network = AppNetworks.mainnet; + final amount0Desired = BigInt.from(4311); + final amount1Desired = BigInt.from(1031900); + const deadline = Duration(days: 1); + final amount0Max = BigInt.from(4312); + final amount1Max = BigInt.from(1031901); + final recipient = await signer.address; + final tickLower = BigInt.from(321); + final tickUpper = BigInt.from(1222); + final currentPoolTick = BigInt.from(123); + final currentYield0 = currentYield.copyWith( + chainId: network.chainId, + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: EthereumConstants.zeroAddress}), + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: "0x1"}), + ); + + await sut.sendV4PoolDepositTransaction( + currentYield0, + signer, + deadline: deadline, + tickLower: tickLower, + tickUpper: tickUpper, + amount0toDeposit: amount0Desired, + amount1ToDeposit: amount1Desired, + maxAmount0ToDeposit: amount0Max, + maxAmount1ToDeposit: amount1Max, + recipient: recipient, + currentPoolTick: currentPoolTick, + ); + + verify(() => ethereumAbiCoder.encodePacked([ + "uint8", + "uint8", + "uint8" + ], [ + V4PoolConstants.mintPositionActionValue, + V4PoolConstants.settlePairActionValue, + V4PoolConstants.sweepActionValue + ])).called(1); + }); + + test( + "When calling `sendV4PoolDepositTransaction` and none of the tokens are native, it should not include the sweep action", + () async { + when(() => ethereumAbiCoder.encodePacked(any(), any())).thenReturn("0x"); + when(() => ethereumAbiCoder.encode(any(), any())).thenReturn("0x"); + when( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: any(named: "unlockData"), deadline: any(named: "deadline"), ethValue: any(named: "ethValue")), + ).thenAnswer((_) async => transactionResponse); + + const network = AppNetworks.mainnet; + final amount0Desired = BigInt.from(4311); + final amount1Desired = BigInt.from(1031900); + const deadline = Duration(days: 1); + final amount0Max = BigInt.from(4312); + final amount1Max = BigInt.from(1031901); + final recipient = await signer.address; + final tickLower = BigInt.from(321); + final tickUpper = BigInt.from(1222); + final currentPoolTick = BigInt.from(123); + final currentYield0 = currentYield.copyWith( + chainId: network.chainId, + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: "0x2"}), + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: "0x1"}), + ); + + await sut.sendV4PoolDepositTransaction( + currentYield0, + signer, + deadline: deadline, + tickLower: tickLower, + tickUpper: tickUpper, + amount0toDeposit: amount0Desired, + amount1ToDeposit: amount1Desired, + maxAmount0ToDeposit: amount0Max, + maxAmount1ToDeposit: amount1Max, + recipient: recipient, + currentPoolTick: currentPoolTick, + ); + + verify(() => ethereumAbiCoder.encodePacked([ + "uint8", + "uint8", + ], [ + V4PoolConstants.mintPositionActionValue, + V4PoolConstants.settlePairActionValue, + ])).called(1); + }); + + test( + "When calling `sendV4PoolDepositTransaction` the mint action params should be correctly encoded", + () async { + when(() => ethereumAbiCoder.encodePacked(any(), any())).thenReturn("0x"); + when(() => ethereumAbiCoder.encode(any(), any())).thenReturn("0x"); + when( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: any(named: "unlockData"), deadline: any(named: "deadline"), ethValue: any(named: "ethValue")), + ).thenAnswer((_) async => transactionResponse); + + const token0Address = "0x1"; + const token1Address = "0x2"; + const network = AppNetworks.mainnet; + final amount0Desired = BigInt.from(4311); + final amount1Desired = BigInt.from(1031900); + const deadline = Duration(days: 1); + final amount0Max = BigInt.from(4312); + final amount1Max = BigInt.from(1031901); + final recipient = await signer.address; + final tickLower = BigInt.from(321); + final tickUpper = BigInt.from(1222); + final currentPoolTick = BigInt.from(123); + final currentYield0 = currentYield.copyWith( + chainId: network.chainId, + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: token1Address}), + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: token0Address}), + ); + + await sut.sendV4PoolDepositTransaction( + currentYield0, + signer, + deadline: deadline, + tickLower: tickLower, + tickUpper: tickUpper, + amount0toDeposit: amount0Desired, + amount1ToDeposit: amount1Desired, + maxAmount0ToDeposit: amount0Max, + maxAmount1ToDeposit: amount1Max, + recipient: recipient, + currentPoolTick: currentPoolTick, + ); + + verify(() => ethereumAbiCoder.encode([ + "tuple(address,address,int32,int24,address)", + "int24", + "int24", + "uint256", + "uint128", + "uint128", + "address", + "bytes" + ], [ + [ + token0Address, + token1Address, + BigInt.from(currentYield0.feeTier), + BigInt.from(currentYield0.tickSpacing), + currentYield0.v4Hooks, + ], + tickLower, + tickUpper, + _V4PoolLiquidityCalculationsMixinWrapper().getLiquidityForAmounts( + _V4PoolLiquidityCalculationsMixinWrapper().getSqrtPriceAtTick(currentPoolTick), + _V4PoolLiquidityCalculationsMixinWrapper().getSqrtPriceAtTick(tickLower), + _V4PoolLiquidityCalculationsMixinWrapper().getSqrtPriceAtTick(tickUpper), + amount0Desired, + amount1Desired, + ), + amount0Max, + amount1Max, + recipient, + EthereumConstants.emptyBytes, + ])).called(1); + }, + ); + + test( + "When calling `sendV4PoolDepositTransaction` the settle pair action params should be correctly encoded", + () async { + when(() => ethereumAbiCoder.encodePacked(any(), any())).thenReturn("0x"); + when(() => ethereumAbiCoder.encode(any(), any())).thenReturn("0x"); + when( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: any(named: "unlockData"), deadline: any(named: "deadline"), ethValue: any(named: "ethValue")), + ).thenAnswer((_) async => transactionResponse); + + const token0Address = "0x1"; + const token1Address = "0x2"; + const network = AppNetworks.mainnet; + final amount0Desired = BigInt.from(4311); + final amount1Desired = BigInt.from(1031900); + const deadline = Duration(days: 1); + final amount0Max = BigInt.from(4312); + final amount1Max = BigInt.from(1031901); + final recipient = await signer.address; + final tickLower = BigInt.from(321); + final tickUpper = BigInt.from(1222); + final currentPoolTick = BigInt.from(123); + final currentYield0 = currentYield.copyWith( + chainId: network.chainId, + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: token1Address}), + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: token0Address}), + ); + + await sut.sendV4PoolDepositTransaction( + currentYield0, + signer, + deadline: deadline, + tickLower: tickLower, + tickUpper: tickUpper, + amount0toDeposit: amount0Desired, + amount1ToDeposit: amount1Desired, + maxAmount0ToDeposit: amount0Max, + maxAmount1ToDeposit: amount1Max, + recipient: recipient, + currentPoolTick: currentPoolTick, + ); + + verify(() => ethereumAbiCoder.encode(["address", "address"], [token0Address, token1Address])).called(1); + }, + ); + + test( + "When calling `sendV4PoolDepositTransaction` and the token0 is native the sweep action params should be correctly encoded", + () async { + when(() => ethereumAbiCoder.encodePacked(any(), any())).thenReturn("0x"); + when(() => ethereumAbiCoder.encode(any(), any())).thenReturn("0x"); + when( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: any(named: "unlockData"), deadline: any(named: "deadline"), ethValue: any(named: "ethValue")), + ).thenAnswer((_) async => transactionResponse); + + const token0Address = EthereumConstants.zeroAddress; + const token1Address = "0x2"; + const network = AppNetworks.mainnet; + final amount0Desired = BigInt.from(4311); + final amount1Desired = BigInt.from(1031900); + const deadline = Duration(days: 1); + final amount0Max = BigInt.from(4312); + final amount1Max = BigInt.from(1031901); + final recipient = await signer.address; + final tickLower = BigInt.from(321); + final tickUpper = BigInt.from(1222); + final currentPoolTick = BigInt.from(123); + final currentYield0 = currentYield.copyWith( + chainId: network.chainId, + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: token1Address}), + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: token0Address}), + ); + + await sut.sendV4PoolDepositTransaction( + currentYield0, + signer, + deadline: deadline, + tickLower: tickLower, + tickUpper: tickUpper, + amount0toDeposit: amount0Desired, + amount1ToDeposit: amount1Desired, + maxAmount0ToDeposit: amount0Max, + maxAmount1ToDeposit: amount1Max, + recipient: recipient, + currentPoolTick: currentPoolTick, + ); + + verify( + () => ethereumAbiCoder.encode( + ["address", "address"], + [EthereumConstants.zeroAddress, recipient], + ), + ).called(1); + }, + ); + + test( + "When calling `sendV4PoolDepositTransaction` and the token1 is native the sweep action params should be correctly encoded", + () async { + when(() => ethereumAbiCoder.encodePacked(any(), any())).thenReturn("0x"); + when(() => ethereumAbiCoder.encode(any(), any())).thenReturn("0x"); + when( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: any(named: "unlockData"), deadline: any(named: "deadline"), ethValue: any(named: "ethValue")), + ).thenAnswer((_) async => transactionResponse); + + const token0Address = "0x2"; + const token1Address = EthereumConstants.zeroAddress; + const network = AppNetworks.mainnet; + final amount0Desired = BigInt.from(4311); + final amount1Desired = BigInt.from(1031900); + const deadline = Duration(days: 1); + final amount0Max = BigInt.from(4312); + final amount1Max = BigInt.from(1031901); + final recipient = await signer.address; + final tickLower = BigInt.from(321); + final tickUpper = BigInt.from(1222); + final currentPoolTick = BigInt.from(123); + final currentYield0 = currentYield.copyWith( + chainId: network.chainId, + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: token0Address}), + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: token1Address}), + ); + + await sut.sendV4PoolDepositTransaction( + currentYield0, + signer, + deadline: deadline, + tickLower: tickLower, + tickUpper: tickUpper, + amount0toDeposit: amount0Desired, + amount1ToDeposit: amount1Desired, + maxAmount0ToDeposit: amount0Max, + maxAmount1ToDeposit: amount1Max, + recipient: recipient, + currentPoolTick: currentPoolTick, + ); + + verify( + () => ethereumAbiCoder.encode( + ["address", "address"], + [EthereumConstants.zeroAddress, recipient], + ), + ).called(1); + }, + ); + + test( + """When calling `sendV4PoolDepositTransaction` and the token0 is native, + it should send the correct unlock data to the contract to add liquidity""", + () async { + const actionsEncoded = "0xhvaaa"; + const mintPositionActionParamsEncoded = "0xaaaa"; + const settlePairActionParamsEncoded = "0xbbbb"; + const sweepActionParamsEncoded = "0xcccc"; + const unlockData = "0xaaaaa77777AAA"; + + const token0Address = EthereumConstants.zeroAddress; + const token1Address = "0x2"; + const network = AppNetworks.mainnet; + final amount0Desired = BigInt.from(4311); + final amount1Desired = BigInt.from(1031900); + const deadline = Duration(days: 1); + final amount0Max = BigInt.from(4312); + final amount1Max = BigInt.from(1031901); + final recipient = await signer.address; + final tickLower = BigInt.from(321); + final tickUpper = BigInt.from(1222); + final currentPoolTick = BigInt.from(123); + final currentYield0 = currentYield.copyWith( + chainId: network.chainId, + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: token0Address}), + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: token1Address}), + ); + + when(() => ethereumAbiCoder.encodePacked(["uint8", "uint8", "uint8"], any())).thenReturn(actionsEncoded); + when(() => ethereumAbiCoder.encode([ + "tuple(address,address,int32,int24,address)", + "int24", + "int24", + "uint256", + "uint128", + "uint128", + "address", + "bytes" + ], any())).thenReturn(mintPositionActionParamsEncoded); + when(() => ethereumAbiCoder.encode(["address", "address"], [token0Address, token1Address])) + .thenReturn(settlePairActionParamsEncoded); + + when(() => ethereumAbiCoder.encode(["address", "address"], [EthereumConstants.zeroAddress, recipient])) + .thenReturn(sweepActionParamsEncoded); + + when(() => ethereumAbiCoder.encode([ + "bytes", + "bytes[]" + ], [ + actionsEncoded, + [mintPositionActionParamsEncoded, settlePairActionParamsEncoded, sweepActionParamsEncoded] + ])).thenReturn(unlockData); + + when( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: any(named: "unlockData"), deadline: any(named: "deadline"), ethValue: any(named: "ethValue")), + ).thenAnswer((_) async => transactionResponse); + + await sut.sendV4PoolDepositTransaction( + currentYield0, + signer, + deadline: deadline, + tickLower: tickLower, + tickUpper: tickUpper, + amount0toDeposit: amount0Desired, + amount1ToDeposit: amount1Desired, + maxAmount0ToDeposit: amount0Max, + maxAmount1ToDeposit: amount1Max, + recipient: recipient, + currentPoolTick: currentPoolTick, + ); + + verify( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: unlockData, + deadline: any(named: "deadline"), + ethValue: any(named: "ethValue"), + ), + ).called(1); + }, + ); + + test( + """When calling `sendV4PoolDepositTransaction` and the token1 is native, + it should send the correct unlock data to the contract to add liquidity""", + () async { + const actionsEncoded = "0xhvaaa"; + const mintPositionActionParamsEncoded = "0xaaaa"; + const settlePairActionParamsEncoded = "0xbbbb"; + const sweepActionParamsEncoded = "0xcccc"; + const unlockData = "0xaaaaa77777AAA"; + + const token1Address = EthereumConstants.zeroAddress; + const token0Address = "0x2"; + const network = AppNetworks.mainnet; + final amount0Desired = BigInt.from(4311); + final amount1Desired = BigInt.from(1031900); + const deadline = Duration(days: 1); + final amount0Max = BigInt.from(4312); + final amount1Max = BigInt.from(1031901); + final recipient = await signer.address; + final tickLower = BigInt.from(321); + final tickUpper = BigInt.from(1222); + final currentPoolTick = BigInt.from(123); + final currentYield0 = currentYield.copyWith( + chainId: network.chainId, + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: token0Address}), + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: token1Address}), + ); + + when(() => ethereumAbiCoder.encodePacked(["uint8", "uint8", "uint8"], any())).thenReturn(actionsEncoded); + when(() => ethereumAbiCoder.encode([ + "tuple(address,address,int32,int24,address)", + "int24", + "int24", + "uint256", + "uint128", + "uint128", + "address", + "bytes" + ], any())).thenReturn(mintPositionActionParamsEncoded); + when(() => ethereumAbiCoder.encode(["address", "address"], [token0Address, token1Address])) + .thenReturn(settlePairActionParamsEncoded); + + when(() => ethereumAbiCoder.encode(["address", "address"], [EthereumConstants.zeroAddress, recipient])) + .thenReturn(sweepActionParamsEncoded); + + when(() => ethereumAbiCoder.encode([ + "bytes", + "bytes[]" + ], [ + actionsEncoded, + [mintPositionActionParamsEncoded, settlePairActionParamsEncoded, sweepActionParamsEncoded] + ])).thenReturn(unlockData); + + when( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: any(named: "unlockData"), deadline: any(named: "deadline"), ethValue: any(named: "ethValue")), + ).thenAnswer((_) async => transactionResponse); + + await sut.sendV4PoolDepositTransaction( + currentYield0, + signer, + deadline: deadline, + tickLower: tickLower, + tickUpper: tickUpper, + amount0toDeposit: amount0Desired, + amount1ToDeposit: amount1Desired, + maxAmount0ToDeposit: amount0Max, + maxAmount1ToDeposit: amount1Max, + recipient: recipient, + currentPoolTick: currentPoolTick, + ); + + verify( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: unlockData, + deadline: any(named: "deadline"), + ethValue: any(named: "ethValue"), + ), + ).called(1); + }, + ); + + test( + """When calling `sendV4PoolDepositTransaction` and none of the tokens are native, + it should send the correct unlock data to the contract to add liquidity (without sweep)""", + () async { + const actionsEncoded = "0xhvaaa"; + const mintPositionActionParamsEncoded = "0xaaaa"; + const settlePairActionParamsEncoded = "0xbbbb"; + const unlockData = "0xaaaaa77777AAA"; + + const token0Address = "0x1"; + const token1Address = "0x2"; + const network = AppNetworks.mainnet; + final amount0Desired = BigInt.from(4311); + final amount1Desired = BigInt.from(1031900); + const deadline = Duration(days: 1); + final amount0Max = BigInt.from(4312); + final amount1Max = BigInt.from(1031901); + final recipient = await signer.address; + final tickLower = BigInt.from(321); + final tickUpper = BigInt.from(1222); + final currentPoolTick = BigInt.from(123); + final currentYield0 = currentYield.copyWith( + chainId: network.chainId, + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: token0Address}), + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: token1Address}), + ); + + when(() => ethereumAbiCoder.encodePacked(["uint8", "uint8"], any())).thenReturn(actionsEncoded); + when(() => ethereumAbiCoder.encode([ + "tuple(address,address,int32,int24,address)", + "int24", + "int24", + "uint256", + "uint128", + "uint128", + "address", + "bytes" + ], any())).thenReturn(mintPositionActionParamsEncoded); + when(() => ethereumAbiCoder.encode(["address", "address"], [token0Address, token1Address])) + .thenReturn(settlePairActionParamsEncoded); + + when(() => ethereumAbiCoder.encode([ + "bytes", + "bytes[]" + ], [ + actionsEncoded, + [mintPositionActionParamsEncoded, settlePairActionParamsEncoded] + ])).thenReturn(unlockData); + + when( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: any(named: "unlockData"), deadline: any(named: "deadline"), ethValue: any(named: "ethValue")), + ).thenAnswer((_) async => transactionResponse); + + await sut.sendV4PoolDepositTransaction( + currentYield0, + signer, + deadline: deadline, + tickLower: tickLower, + tickUpper: tickUpper, + amount0toDeposit: amount0Desired, + amount1ToDeposit: amount1Desired, + maxAmount0ToDeposit: amount0Max, + maxAmount1ToDeposit: amount1Max, + recipient: recipient, + currentPoolTick: currentPoolTick, + ); + + verify( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: unlockData, + deadline: any(named: "deadline"), + ethValue: any(named: "ethValue"), + ), + ).called(1); + }, + ); + + test( + """When calling `sendV4PoolDepositTransaction` it should send the correct deadline to the contract to add liquidity + (now + deadline)""", + () async { + withClock(Clock(() => DateTime(2022, 1, 1)), () async { + const token0Address = "0x1"; + const token1Address = "0x2"; + const network = AppNetworks.mainnet; + final amount0Desired = BigInt.from(4311); + final amount1Desired = BigInt.from(1031900); + const deadline = Duration(days: 1); + final amount0Max = BigInt.from(4312); + final amount1Max = BigInt.from(1031901); + final recipient = await signer.address; + final tickLower = BigInt.from(321); + final tickUpper = BigInt.from(1222); + final currentPoolTick = BigInt.from(123); + final currentYield0 = currentYield.copyWith( + chainId: network.chainId, + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: token0Address}), + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: token1Address}), + ); + + when(() => ethereumAbiCoder.encodePacked(any(), any())).thenReturn("0x"); + when(() => ethereumAbiCoder.encode(any(), any())).thenReturn("0x"); + + when( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: any(named: "unlockData"), deadline: any(named: "deadline"), ethValue: any(named: "ethValue")), + ).thenAnswer((_) async => transactionResponse); + + await sut.sendV4PoolDepositTransaction( + currentYield0, + signer, + deadline: deadline, + tickLower: tickLower, + tickUpper: tickUpper, + amount0toDeposit: amount0Desired, + amount1ToDeposit: amount1Desired, + maxAmount0ToDeposit: amount0Max, + maxAmount1ToDeposit: amount1Max, + recipient: recipient, + currentPoolTick: currentPoolTick, + ); + + verify( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: any(named: "unlockData"), + deadline: BigInt.from(clock.now().add(deadline).millisecondsSinceEpoch), + ethValue: any(named: "ethValue"), + ), + ).called(1); + }); + }, + ); + + test( + """When calling `sendV4PoolDepositTransaction` and none of the tokens are native, it should not send any eth value""", + () async { + const token0Address = "0x1"; + const token1Address = "0x2"; + const network = AppNetworks.mainnet; + final amount0Desired = BigInt.from(4311); + final amount1Desired = BigInt.from(1031900); + const deadline = Duration(days: 1); + final amount0Max = BigInt.from(4312); + final amount1Max = BigInt.from(1031901); + final recipient = await signer.address; + final tickLower = BigInt.from(321); + final tickUpper = BigInt.from(1222); + final currentPoolTick = BigInt.from(123); + final currentYield0 = currentYield.copyWith( + chainId: network.chainId, + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: token0Address}), + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: token1Address}), + ); + + when(() => ethereumAbiCoder.encodePacked(any(), any())).thenReturn("0x"); + when(() => ethereumAbiCoder.encode(any(), any())).thenReturn("0x"); + + when( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: any(named: "unlockData"), deadline: any(named: "deadline"), ethValue: any(named: "ethValue")), + ).thenAnswer((_) async => transactionResponse); + + await sut.sendV4PoolDepositTransaction( + currentYield0, + signer, + deadline: deadline, + tickLower: tickLower, + tickUpper: tickUpper, + amount0toDeposit: amount0Desired, + amount1ToDeposit: amount1Desired, + maxAmount0ToDeposit: amount0Max, + maxAmount1ToDeposit: amount1Max, + recipient: recipient, + currentPoolTick: currentPoolTick, + ); + + verify( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: any(named: "unlockData"), + deadline: any(named: "deadline"), + ethValue: null, + ), + ).called(1); + }, + ); + + test( + """When calling `sendV4PoolDepositTransaction` and the token0 is native, it should send the eth value from the + token0amount""", + () async { + const token0Address = EthereumConstants.zeroAddress; + const token1Address = "0x2"; + const network = AppNetworks.mainnet; + final amount0Desired = BigInt.from(4311); + final amount1Desired = BigInt.from(1031900); + const deadline = Duration(days: 1); + final amount0Max = BigInt.from(4312); + final amount1Max = BigInt.from(1031901); + final recipient = await signer.address; + final tickLower = BigInt.from(321); + final tickUpper = BigInt.from(1222); + final currentPoolTick = BigInt.from(123); + final currentYield0 = currentYield.copyWith( + chainId: network.chainId, + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: token0Address}), + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: token1Address}), + ); + + when(() => ethereumAbiCoder.encodePacked(any(), any())).thenReturn("0x"); + when(() => ethereumAbiCoder.encode(any(), any())).thenReturn("0x"); + + when( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: any(named: "unlockData"), deadline: any(named: "deadline"), ethValue: any(named: "ethValue")), + ).thenAnswer((_) async => transactionResponse); + + await sut.sendV4PoolDepositTransaction( + currentYield0, + signer, + deadline: deadline, + tickLower: tickLower, + tickUpper: tickUpper, + amount0toDeposit: amount0Desired, + amount1ToDeposit: amount1Desired, + maxAmount0ToDeposit: amount0Max, + maxAmount1ToDeposit: amount1Max, + recipient: recipient, + currentPoolTick: currentPoolTick, + ); + + verify( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: any(named: "unlockData"), + deadline: any(named: "deadline"), + ethValue: amount0Desired, + ), + ).called(1); + }, + ); + + test( + """When calling `sendV4PoolDepositTransaction` and the token1 is native, it should send the eth value from the + token1amount""", + () async { + const token0Address = "0x1"; + const token1Address = EthereumConstants.zeroAddress; + const network = AppNetworks.mainnet; + final amount0Desired = BigInt.from(4311); + final amount1Desired = BigInt.from(1031900); + const deadline = Duration(days: 1); + final amount0Max = BigInt.from(4312); + final amount1Max = BigInt.from(1031901); + final recipient = await signer.address; + final tickLower = BigInt.from(321); + final tickUpper = BigInt.from(1222); + final currentPoolTick = BigInt.from(123); + final currentYield0 = currentYield.copyWith( + chainId: network.chainId, + token0: TokenDto.fixture().copyWith(addresses: {network.chainId: token0Address}), + token1: TokenDto.fixture().copyWith(addresses: {network.chainId: token1Address}), + ); + + when(() => ethereumAbiCoder.encodePacked(any(), any())).thenReturn("0x"); + when(() => ethereumAbiCoder.encode(any(), any())).thenReturn("0x"); + + when( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: any(named: "unlockData"), deadline: any(named: "deadline"), ethValue: any(named: "ethValue")), + ).thenAnswer((_) async => transactionResponse); + + await sut.sendV4PoolDepositTransaction( + currentYield0, + signer, + deadline: deadline, + tickLower: tickLower, + tickUpper: tickUpper, + amount0toDeposit: amount0Desired, + amount1ToDeposit: amount1Desired, + maxAmount0ToDeposit: amount0Max, + maxAmount1ToDeposit: amount1Max, + recipient: recipient, + currentPoolTick: currentPoolTick, + ); + + verify( + () => positionManagerV4Impl.modifyLiquidities( + unlockData: any(named: "unlockData"), + deadline: any(named: "deadline"), + ethValue: amount1Desired, + ), + ).called(1); + }, + ); +} diff --git a/test/core/repositories/tokens_repository_test.dart b/test/core/repositories/tokens_repository_test.dart index beb937c..a6c8070 100644 --- a/test/core/repositories/tokens_repository_test.dart +++ b/test/core/repositories/tokens_repository_test.dart @@ -2,6 +2,7 @@ import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:zup_app/core/dtos/token_dto.dart'; +import 'package:zup_app/core/dtos/token_price_dto.dart'; import 'package:zup_app/core/enums/networks.dart'; import 'package:zup_app/core/repositories/tokens_repository.dart'; @@ -123,4 +124,45 @@ void main() { "chainId": network.chainId, })).called(1); }); + + test("When calling `getTokenPrice` it should call the correct endpoint with correct params", () async { + const address = "0x123"; + const network = AppNetworks.sepolia; + + when(() => dio.get(any(), queryParameters: any(named: "queryParameters"))).thenAnswer( + (_) async => Response( + data: TokenPriceDto.fixture().toJson(), + statusCode: 200, + requestOptions: RequestOptions(), + ), + ); + + await sut.getTokenPrice(address, network); + + verify(() => dio.get( + "/tokens/price", + queryParameters: { + "address": address, + "chainId": network.chainId, + }, + )).called(1); + }); + + test("When calling `getTokenPrice` it should correctly parse the response", () async { + const address = "0x123"; + const network = AppNetworks.sepolia; + final tokenPriceDto = TokenPriceDto.fixture(); + + when(() => dio.get(any(), queryParameters: any(named: "queryParameters"))).thenAnswer( + (_) async => Response( + data: tokenPriceDto.toJson(), + statusCode: 200, + requestOptions: RequestOptions(), + ), + ); + + final response = await sut.getTokenPrice(address, network); + + expect(response, tokenPriceDto); + }); } diff --git a/test/core/repositories/yield_repository_test.dart b/test/core/repositories/yield_repository_test.dart index ec26e06..2962c19 100644 --- a/test/core/repositories/yield_repository_test.dart +++ b/test/core/repositories/yield_repository_test.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:zup_app/core/dtos/pool_search_settings_dto.dart'; import 'package:zup_app/core/dtos/yields_dto.dart'; import 'package:zup_app/core/enums/networks.dart'; import 'package:zup_app/core/repositories/yield_repository.dart'; @@ -16,10 +17,10 @@ void main() { sut = YieldRepository(dio); }); - test("When calling `getYields` it should call the correct endpoint with the correct params", () async { + test("When calling `getSingleNetworkYield` it should call the correct endpoint with the correct params", () async { final yields = YieldsDto.fixture(); - when(() => dio.get(any(), queryParameters: any(named: "queryParameters"))).thenAnswer( + when(() => dio.post(any(), queryParameters: any(named: "queryParameters"), data: any(named: "data"))).thenAnswer( (_) async => Response( data: {"bestYields": yields.toJson()}, statusCode: 200, @@ -31,23 +32,109 @@ void main() { const token1Address = "0x456"; const network = AppNetworks.sepolia; const minTvlUsd = 1213; + final searchSettings = PoolSearchSettingsDto.fixture().copyWith(minLiquidityUSD: minTvlUsd); await sut.getSingleNetworkYield( token0Address: token0Address, token1Address: token1Address, network: network, - minTvlUsd: minTvlUsd, + searchSettings: searchSettings, ); - verify(() => dio.get("/pools/search/${network.chainId}", - queryParameters: {"token0Address": token0Address, "token1Address": token1Address, "minTvlUsd": minTvlUsd})) - .called(1); + verify( + () => dio.post("/pools/search/${network.chainId}", queryParameters: { + "token0Address": token0Address, + "token1Address": token1Address + }, data: { + "filters": { + "minTvlUsd": searchSettings.minLiquidityUSD, + "allowedPoolTypes": [ + "V3", + "V4", + ], + } + }), + ).called(1); + }); + + test("When the V3 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(allowV3Search: 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": [ + "V4", + ], + } + }), + ).called(1); + }); + + test("When the V4 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(allowV4Search: 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"], + } + }), + ).called(1); }); test("When calling `getYields` it should correctly parse the response", () async { final yields = YieldsDto.fixture(); - when(() => dio.get(any(), queryParameters: any(named: "queryParameters"))).thenAnswer( + when(() => dio.post(any(), queryParameters: any(named: "queryParameters"), data: any(named: "data"))).thenAnswer( (_) async => Response( data: yields.toJson(), statusCode: 200, @@ -63,7 +150,7 @@ void main() { token0Address: token0Address, token1Address: token1Address, network: network, - minTvlUsd: 0, + searchSettings: PoolSearchSettingsDto.fixture().copyWith(minLiquidityUSD: 0), ); expect(response, yields); @@ -72,7 +159,90 @@ void main() { test("when calling 'getAllNetworksYield' it should call the correct endpoint with the correct params", () async { final yields = YieldsDto.fixture(); - when(() => dio.get(any(), queryParameters: any(named: "queryParameters"))).thenAnswer( + when(() => dio.post(any(), queryParameters: any(named: "queryParameters"), data: any(named: "data"))).thenAnswer( + (_) async => Response( + data: {"bestYields": yields.toJson()}, + statusCode: 200, + requestOptions: RequestOptions(), + ), + ); + + const token0Id = "0x123"; + const token1Id = "0x456"; + const minTvlUsd = 1213; + final searchSettings = PoolSearchSettingsDto.fixture().copyWith(minLiquidityUSD: minTvlUsd); + + await sut.getAllNetworksYield( + token0InternalId: token0Id, + token1InternalId: token1Id, + searchSettings: searchSettings, + testnetMode: true, + ); + + verify( + () => dio.post("/pools/search/all", queryParameters: { + "token0Id": token0Id, + "token1Id": token1Id, + }, data: { + "filters": { + "testnetMode": true, + "minTvlUsd": searchSettings.minLiquidityUSD, + "allowedPoolTypes": [ + "V3", + "V4", + ], + } + }), + ).called(1); + }); + + test( + "when calling 'getAllNetworksYield' and the search settings has the v4 pool disallowed, it should not be included in the params", + () 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 token0Id = "0x123"; + const token1Id = "0x456"; + const minTvlUsd = 1213; + final searchSettings = PoolSearchSettingsDto.fixture().copyWith(minLiquidityUSD: minTvlUsd, allowV4Search: false); + + await sut.getAllNetworksYield( + token0InternalId: token0Id, + token1InternalId: token1Id, + searchSettings: searchSettings, + testnetMode: true, + ); + + verify( + () => dio.post("/pools/search/all", queryParameters: { + "token0Id": token0Id, + "token1Id": token1Id, + }, data: { + "filters": { + "testnetMode": true, + "minTvlUsd": searchSettings.minLiquidityUSD, + "allowedPoolTypes": [ + "V3", + ], + } + }), + ).called(1); + }); + + test( + "when calling 'getAllNetworksYield' and the search settings has the v3 pool disallowed, it should not be included in the params", + () 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, @@ -83,23 +253,35 @@ void main() { const token0Id = "0x123"; const token1Id = "0x456"; const minTvlUsd = 1213; + final searchSettings = PoolSearchSettingsDto.fixture().copyWith(minLiquidityUSD: minTvlUsd, allowV3Search: false); await sut.getAllNetworksYield( token0InternalId: token0Id, token1InternalId: token1Id, - minTvlUsd: minTvlUsd, + searchSettings: searchSettings, testnetMode: true, ); - verify(() => dio.get("/pools/search/all", - queryParameters: {"token0Id": token0Id, "token1Id": token1Id, "testnetMode": true, "minTvlUsd": minTvlUsd})) - .called(1); + verify( + () => dio.post("/pools/search/all", queryParameters: { + "token0Id": token0Id, + "token1Id": token1Id, + }, data: { + "filters": { + "testnetMode": true, + "minTvlUsd": searchSettings.minLiquidityUSD, + "allowedPoolTypes": [ + "V4", + ], + } + }), + ).called(1); }); test("when calling 'getAllNetworksYield' it should correctly parse the response", () async { final yields = YieldsDto.fixture(); - when(() => dio.get(any(), queryParameters: any(named: "queryParameters"))).thenAnswer( + when(() => dio.post(any(), queryParameters: any(named: "queryParameters"), data: any(named: "data"))).thenAnswer( (_) async => Response( data: yields.toJson(), statusCode: 200, @@ -113,7 +295,7 @@ void main() { final response = await sut.getAllNetworksYield( token0InternalId: token0Id, token1InternalId: token1Id, - minTvlUsd: 0, + searchSettings: PoolSearchSettingsDto.fixture().copyWith(minLiquidityUSD: 0), testnetMode: true, ); diff --git a/test/core/slippage_test.dart b/test/core/slippage_test.dart index e436db8..b373578 100644 --- a/test/core/slippage_test.dart +++ b/test/core/slippage_test.dart @@ -89,37 +89,68 @@ void main() { expect(Slippage.zeroPointOnePercent.props, [Slippage.zeroPointOnePercent.value]); }); - group("""`calculateTokenAmountFromSlippage` should calculate the token amount with the slippage applied. + group("""`calculateMinTokenAmountFromSlippage` should calculate the token amount with the slippage applied. basicaly it's the amount - (x)%""", () { test("(50% test case)", () { expect( - Slippage.fromValue(50).calculateTokenAmountFromSlippage(BigInt.from(1000000)), + Slippage.fromValue(50).calculateMinTokenAmountFromSlippage(BigInt.from(1000000)), BigInt.from(500000), ); }); test("(10% test case)", () { expect( - Slippage.fromValue(10).calculateTokenAmountFromSlippage(BigInt.from(1000000)), + Slippage.fromValue(10).calculateMinTokenAmountFromSlippage(BigInt.from(1000000)), BigInt.from(900000), ); }); test("(0% test case)", () { expect( - Slippage.fromValue(0).calculateTokenAmountFromSlippage(BigInt.from(1000000)), + Slippage.fromValue(0).calculateMinTokenAmountFromSlippage(BigInt.from(1000000)), BigInt.from(1000000), ); }); test("(100% test case)", () { expect( - Slippage.fromValue(100).calculateTokenAmountFromSlippage(BigInt.from(1000000)), + Slippage.fromValue(100).calculateMinTokenAmountFromSlippage(BigInt.from(1000000)), BigInt.from(0), ); }); }); + group("""`calculateMaxTokenAmountFromSlippage` should calculate the token amount with the slippage applied. + basicaly it's the amount + (x)%""", () { + test("(50% test case)", () { + expect( + Slippage.fromValue(50).calculateMaxTokenAmountFromSlippage(BigInt.from(1000000)), + BigInt.from(1500000), + ); + }); + + test("(10% test case)", () { + expect( + Slippage.fromValue(10).calculateMaxTokenAmountFromSlippage(BigInt.from(1000000)), + BigInt.from(1100000), + ); + }); + + test("(0% test case)", () { + expect( + Slippage.fromValue(0).calculateMaxTokenAmountFromSlippage(BigInt.from(1000000)), + BigInt.from(1000000), + ); + }); + + test("(100% test case)", () { + expect( + Slippage.fromValue(100).calculateMaxTokenAmountFromSlippage(BigInt.from(1000000)), + BigInt.from(2000000), + ); + }); + }); + test("`valueBasisPoints` should return the slippage value in basis points", () { expect(Slippage.zeroPointOnePercent.valueBasisPoints, 10, reason: "zeroPointOnePercent is 10 basis points"); expect(Slippage.halfPercent.valueBasisPoints, 50, reason: "halfPercent is 50 basis points"); diff --git a/test/core/v4_pool_constants_test.dart b/test/core/v4_pool_constants_test.dart new file mode 100644 index 0000000..09e994b --- /dev/null +++ b/test/core/v4_pool_constants_test.dart @@ -0,0 +1,16 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:zup_app/core/v4_pool_constants.dart'; + +void main() { + test('mintPositionActionValue should return the correct value', () { + expect(V4PoolConstants.mintPositionActionValue, 0x02); + }); + + test('settlePairActionValue should return the correct value', () { + expect(V4PoolConstants.settlePairActionValue, 0x0d); + }); + + test('sweepActionValue should return the correct value', () { + expect(V4PoolConstants.sweepActionValue, 0x14); + }); +} diff --git a/test/mocks.dart b/test/mocks.dart index 24163c9..f167e89 100644 --- a/test/mocks.dart +++ b/test/mocks.dart @@ -12,13 +12,17 @@ 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/uniswap_position_manager.abi.g.dart'; +import 'package:zup_app/abis/uniswap_permit2.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/app/app_cubit/app_cubit.dart'; import 'package:zup_app/app/create/deposit/deposit_cubit.dart'; import 'package:zup_app/app/create/deposit/widgets/preview_deposit_modal/preview_deposit_modal_cubit.dart'; import 'package:zup_app/core/cache.dart'; import 'package:zup_app/core/debouncer.dart'; +import 'package:zup_app/core/pool_service.dart'; import 'package:zup_app/core/repositories/positions_repository.dart'; import 'package:zup_app/core/repositories/tokens_repository.dart'; import 'package:zup_app/core/repositories/yield_repository.dart'; @@ -28,6 +32,7 @@ import 'package:zup_app/core/zup_navigator.dart'; import 'package:zup_app/gen/assets.gen.dart'; import 'package:zup_app/widgets/token_selector_modal/token_selector_modal_cubit.dart'; import 'package:zup_app/widgets/zup_cached_image.dart'; +import 'package:zup_core/zup_holder.dart'; import 'package:zup_core/zup_singleton_cache.dart'; class $AssetsLottiesGenMock extends Mock implements $AssetsLottiesGen {} @@ -62,9 +67,25 @@ class TokensRepositoryMock extends Mock implements TokensRepository {} class TransactionResponseMock extends Mock implements TransactionResponse {} -class UniswapPositionManagerImplMock extends Mock implements UniswapPositionManagerImpl {} +class UniswapV3PositionManagerImplMock extends Mock implements UniswapV3PositionManagerImpl {} -class UniswapPositionManagerMock extends Mock implements UniswapPositionManager {} +class EthereumAbiCoderMock extends Mock implements EthereumAbiCoder {} + +class UniswapV3PositionManagerMock extends Mock implements UniswapV3PositionManager {} + +class PoolServiceMock extends Mock implements PoolService {} + +class UniswapPermit2Mock extends Mock implements UniswapPermit2 {} + +class UniswapPermit2ImplMock extends Mock implements UniswapPermit2Impl {} + +class UniswapV4StateViewMock extends Mock implements UniswapV4StateView {} + +class UniswapV4StateViewImplMock extends Mock implements UniswapV4StateViewImpl {} + +class UniswapV4PositionManagerMock extends Mock implements UniswapV4PositionManager {} + +class UniswapV4PositionManagerImplMock extends Mock implements UniswapV4PositionManagerImpl {} class UniswapV3PoolImplMock extends Mock implements UniswapV3PoolImpl {} @@ -90,6 +111,8 @@ class ConfettiControllerMock extends Mock implements ConfettiController {} class FirebaseAnalyticsMock extends Mock implements FirebaseAnalytics {} +class ZupHolderMock extends Mock implements ZupHolder {} + class ChangeNotifierMock extends Mock with ChangeNotifier { void notify() => notifyListeners(); } diff --git a/web/index.html b/web/index.html index e49f19e..a6bc3ec 100644 --- a/web/index.html +++ b/web/index.html @@ -61,6 +61,6 @@ inject(); - +