| 1 | import 'package:cake_wallet/core/execution_state.dart'; |
| 2 | import 'package:cake_wallet/core/utilities.dart'; |
| 3 | import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; |
| 4 | import 'package:cake_wallet/entities/fiat_currency.dart'; |
| 5 | import 'package:cake_wallet/evm/evm.dart'; |
| 6 | import 'package:cake_wallet/store/app_store.dart'; |
| 7 | import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; |
| 8 | import 'package:cake_wallet/store/settings_store.dart'; |
| 9 | import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; |
| 10 | import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; |
| 11 | import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; |
| 12 | import 'package:cw_core/amount/money.dart'; |
| 13 | import 'package:cw_core/crypto_currency.dart'; |
| 14 | import 'package:cw_core/parse_fixed.dart'; |
| 15 | import 'package:cw_core/pending_transaction.dart'; |
| 16 | import 'package:cw_core/wallet_base.dart'; |
| 17 | import 'package:mobx/mobx.dart'; |
| 18 | |
| 19 | part 'deuro_view_model.g.dart'; |
| 20 | |
| 21 | class DEuroViewModel = DEuroViewModelBase with _$DEuroViewModel; |
| 22 | |
| 23 | abstract class DEuroViewModelBase with Store { |
| 24 | final AppStore _appStore; |
| 25 | |
| 26 | static Money get MIN_ACCRUED_INTEREST => |
| 27 | Money(BigInt.parse("1000000000000"), CryptoCurrency.deuro); |
| 28 | |
| 29 | static Money get ZERO => Money.zero(CryptoCurrency.deuro); |
| 30 | |
| 31 | DEuroViewModelBase( |
| 32 | this._appStore, this.balanceViewModel, this._settingsStore, this._fiatConversationStore, |
| 33 | [this.hardwareWalletViewModel]) { |
| 34 | reloadInterestRate(); |
| 35 | reloadSavingsUserData(); |
| 36 | } |
| 37 | |
| 38 | final BalanceViewModel balanceViewModel; |
| 39 | final HardwareWalletViewModel? hardwareWalletViewModel; |
| 40 | final SettingsStore _settingsStore; |
| 41 | final FiatConversionStore _fiatConversationStore; |
| 42 | |
| 43 | WalletBase get wallet => this._appStore.wallet!; |
| 44 | |
| 45 | @computed |
| 46 | bool get isFiatDisabled => balanceViewModel.isFiatDisabled; |
| 47 | |
| 48 | @computed |
| 49 | bool get isFistTime => _settingsStore.shouldShowDEuroDisclaimer; |
| 50 | |
| 51 | @action |
| 52 | void acceptDisclaimer() => _settingsStore.shouldShowDEuroDisclaimer = false; |
| 53 | |
| 54 | FiatCurrency get fiat => _settingsStore.fiatCurrency; |
| 55 | |
| 56 | @computed |
| 57 | String get pendingTransactionFiatAmountFormatted { |
| 58 | final amount = transaction == null ? "0.00" : _getDEuroFiatAmount(transaction!.amount); |
| 59 | return isFiatDisabled ? "" : "$amount ${fiat.symbol}"; |
| 60 | } |
| 61 | |
| 62 | @computed |
| 63 | String get pendingTransactionFeeFiatAmountFormatted { |
| 64 | var amount = "0.00"; |
| 65 | try { |
| 66 | if (transaction != null) { |
| 67 | final feeCurrency = CryptoCurrency.eth; |
| 68 | amount = calculateFiatAmount( |
| 69 | price: _fiatConversationStore.prices[feeCurrency]!, |
| 70 | cryptoAmount: transaction!.feeFormattedValue, |
| 71 | ); |
| 72 | } |
| 73 | } catch (_) {} |
| 74 | return isFiatDisabled ? "" : "$amount ${fiat.symbol}"; |
| 75 | } |
| 76 | |
| 77 | @computed |
| 78 | Money get accountBalance { |
| 79 | final dEuroKey = balanceViewModel.balances.keys |
| 80 | .firstWhereOrNull((e) => e.title == CryptoCurrency.deuro.title); |
| 81 | if (dEuroKey == null) return ZERO; |
| 82 | return wallet.balance[dEuroKey]?.available ?? ZERO; |
| 83 | } |
| 84 | |
| 85 | @observable |
| 86 | Money savingsBalance = ZERO; |
| 87 | |
| 88 | @computed |
| 89 | String get fiatSavingsBalanceFormated => _getDEuroFiatAmount(savingsBalance); |
| 90 | |
| 91 | @observable |
| 92 | Money? savingsBalanceV1 = null; |
| 93 | |
| 94 | @computed |
| 95 | String get fiatSavingsBalanceV1Formated => _getDEuroFiatAmount(savingsBalanceV1); |
| 96 | |
| 97 | @observable |
| 98 | ExecutionState state = InitialExecutionState(); |
| 99 | |
| 100 | @observable |
| 101 | String interestRateFormated = "0"; |
| 102 | |
| 103 | @observable |
| 104 | Money accruedInterest = ZERO; |
| 105 | |
| 106 | @computed |
| 107 | String get accruedInterestFormated => accruedInterest.toStringWithPrecision(fractionalDigits: 6); |
| 108 | |
| 109 | @computed |
| 110 | String get fiatAccruedInterestFormated => _getDEuroFiatAmount(accruedInterest); |
| 111 | |
| 112 | @observable |
| 113 | BigInt approvedTokens = BigInt.zero; |
| 114 | |
| 115 | @computed |
| 116 | bool get isEnabled => approvedTokens > BigInt.zero; |
| 117 | |
| 118 | @computed |
| 119 | bool get isSavingsActionsEnabled => isEnabled && accruedInterest >= MIN_ACCRUED_INTEREST; |
| 120 | |
| 121 | @observable |
| 122 | bool isLoading = true; |
| 123 | |
| 124 | @observable |
| 125 | DEuroActionType actionType = DEuroActionType.none; |
| 126 | |
| 127 | @observable |
| 128 | PendingTransaction? transaction = null; |
| 129 | |
| 130 | @observable |
| 131 | PendingTransaction? approvalTransaction = null; |
| 132 | |
| 133 | @action |
| 134 | Future<void> reloadSavingsUserData() async { |
| 135 | approvedTokens = await evm!.getDEuroSavingsApproved(_appStore.wallet!) ?? BigInt.zero; |
| 136 | savingsBalance = await evm!.getDEuroSavingsBalance(_appStore.wallet!) ?? ZERO; |
| 137 | accruedInterest = await evm!.getDEuroAccruedInterest(_appStore.wallet!) ?? ZERO; |
| 138 | |
| 139 | final v1Balance = await evm!.getDEuroSavingsV1Balance(_appStore.wallet!) ?? ZERO; |
| 140 | savingsBalanceV1 = v1Balance.isZero ? null : v1Balance; |
| 141 | |
| 142 | isLoading = false; |
| 143 | } |
| 144 | |
| 145 | @action |
| 146 | Future<void> reloadInterestRate() async { |
| 147 | final interestRateRaw = await evm!.getDEuroInterestRate(_appStore.wallet!) ?? BigInt.zero; |
| 148 | |
| 149 | interestRateFormated = (interestRateRaw / BigInt.from(10000)).toString(); |
| 150 | } |
| 151 | |
| 152 | @action |
| 153 | Future<void> prepareApproval() async { |
| 154 | final ethBalance = balanceViewModel.balances[CryptoCurrency.eth]?.availableBalance ?? "0"; |
| 155 | if ((double.tryParse(ethBalance) ?? 0) == 0) { |
| 156 | state = NoEtherState(); |
| 157 | return; |
| 158 | } |
| 159 | try { |
| 160 | state = TransactionCommitting(); |
| 161 | final priority = _appStore.settingsStore.getPriority(wallet.type, chainId: wallet.chainId)!; |
| 162 | final approval = await evm!.enableDEuroSaving(_appStore.wallet!, priority); |
| 163 | if (approval == null) throw Exception("DEuro saving not available"); |
| 164 | |
| 165 | approvalTransaction = approval; |
| 166 | state = InitialExecutionState(); |
| 167 | } catch (e) { |
| 168 | state = FailureState(e.toString()); |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | @action |
| 173 | Future<void> prepareSavingsEdit(String amountRaw, bool isAdding) async { |
| 174 | try { |
| 175 | state = TransactionCommitting(); |
| 176 | |
| 177 | if (amountRaw.isEmpty || amountRaw.trim().isEmpty) { |
| 178 | throw Exception("Invalid amount: amount cannot be empty"); |
| 179 | } |
| 180 | |
| 181 | final amount = tryParseFixed(amountRaw, 18); |
| 182 | |
| 183 | if (amount == BigInt.zero || amount == null) { |
| 184 | throw Exception("Invalid amount: amount cannot be zero"); |
| 185 | } |
| 186 | |
| 187 | final priority = _appStore.settingsStore.getPriority(wallet.type, chainId: wallet.chainId)!; |
| 188 | actionType = isAdding ? DEuroActionType.deposit : DEuroActionType.withdraw; |
| 189 | final tx = await (isAdding |
| 190 | ? evm!.addDEuroSaving(_appStore.wallet!, amount, priority) |
| 191 | : evm!.removeDEuroSaving(_appStore.wallet!, amount, priority)); |
| 192 | if (tx == null) throw Exception("DEuro saving not available"); |
| 193 | |
| 194 | transaction = tx; |
| 195 | state = InitialExecutionState(); |
| 196 | } catch (e) { |
| 197 | state = FailureState(e.toString()); |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | @action |
| 202 | Future<void> prepareSavingsV1Withdraw() async { |
| 203 | try { |
| 204 | state = TransactionCommitting(); |
| 205 | actionType = DEuroActionType.withdraw; |
| 206 | final priority = _appStore.settingsStore.getPriority(wallet.type, chainId: wallet.chainId)!; |
| 207 | final tx = await evm!.withdrawDEuroSavingV1(_appStore.wallet!, priority); |
| 208 | if (tx == null) throw Exception("DEuro saving not available"); |
| 209 | |
| 210 | transaction = tx; |
| 211 | state = InitialExecutionState(); |
| 212 | } catch (e) { |
| 213 | state = FailureState(e.toString()); |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | Future<void> prepareCollectInterest() async { |
| 218 | if (accruedInterest < MIN_ACCRUED_INTEREST) { |
| 219 | state = FailureState("Accrued interest is below minimum threshold"); |
| 220 | return; |
| 221 | } |
| 222 | |
| 223 | final formatted = accruedInterestFormated; |
| 224 | if (formatted.isEmpty || formatted == "0.000000") { |
| 225 | state = FailureState("Invalid accrued interest amount"); |
| 226 | return; |
| 227 | } |
| 228 | |
| 229 | await prepareSavingsEdit(formatted, false); |
| 230 | } |
| 231 | |
| 232 | Future<void> prepareReinvestInterest() async { |
| 233 | try { |
| 234 | state = TransactionCommitting(); |
| 235 | actionType = DEuroActionType.reinvest; |
| 236 | final priority = _appStore.settingsStore.getPriority(wallet.type, chainId: wallet.chainId)!; |
| 237 | final tx = await evm!.reinvestDEuroInterest(_appStore.wallet!, priority); |
| 238 | if (tx == null) throw Exception("DEuro saving not available"); |
| 239 | |
| 240 | transaction = tx; |
| 241 | state = InitialExecutionState(); |
| 242 | } catch (e) { |
| 243 | state = FailureState(e.toString()); |
| 244 | } |
| 245 | } |
| 246 | |
| 247 | @action |
| 248 | Future<void> commitTransaction() async { |
| 249 | if (transaction != null) { |
| 250 | try { |
| 251 | state = TransactionCommitting(); |
| 252 | await transaction!.commit(); |
| 253 | transaction = null; |
| 254 | actionType = DEuroActionType.none; |
| 255 | reloadSavingsUserData(); |
| 256 | state = TransactionCommitted(); |
| 257 | } catch (e) { |
| 258 | state = FailureState(e.toString()); |
| 259 | } |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | @action |
| 264 | Future<void> commitApprovalTransaction() async { |
| 265 | if (approvalTransaction != null) { |
| 266 | try { |
| 267 | state = TransactionCommitting(); |
| 268 | await approvalTransaction!.commit(); |
| 269 | approvalTransaction = null; |
| 270 | reloadSavingsUserData(); |
| 271 | state = TransactionCommitted(); |
| 272 | } catch (e) { |
| 273 | state = FailureState(e.toString()); |
| 274 | } |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | @action |
| 279 | void dismissTransaction() { |
| 280 | transaction = null; |
| 281 | approvalTransaction = null; |
| 282 | actionType = DEuroActionType.none; |
| 283 | state = InitialExecutionState(); |
| 284 | } |
| 285 | |
| 286 | String _getDEuroFiatAmount(Money? amount) { |
| 287 | if (amount == null) return "0.00"; |
| 288 | |
| 289 | try { |
| 290 | var dEuro = CryptoCurrency.deuro; |
| 291 | final keys = _fiatConversationStore.prices.keys.toList(); |
| 292 | for (final key in keys) { |
| 293 | if (key.title == "DEURO") dEuro = key; |
| 294 | } |
| 295 | return calculateFiatAmount( |
| 296 | price: _fiatConversationStore.prices[dEuro]!, |
| 297 | cryptoAmount: amount.toStringWithPrecision(fractionalDigits: 6), |
| 298 | ); |
| 299 | } catch (_) { |
| 300 | return "0.00"; |
| 301 | } |
| 302 | } |
| 303 | } |
| 304 | |
| 305 | class NoEtherState extends ExecutionState {} |
| 306 | |
| 307 | enum DEuroActionType { deposit, withdraw, reinvest, none } |