diff --git a/gui-orchid/lib/util/listenable_builder.dart b/gui-orchid/lib/util/listenable_builder.dart index f8db7fdd1..a23b90645 100644 --- a/gui-orchid/lib/util/listenable_builder.dart +++ b/gui-orchid/lib/util/listenable_builder.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; // TODO: This is no longer needed in later versions of Flutter. Remove when redundant. +/// Buildier that can listen to any Listenable including ChangeNotifier, ValueNotifier, etc. /// This is a trivial subclass of AnimatedBuilder that serves to rename /// it more appropriately for use as a plain listenable builder. /// (This really should be the name of the base class in Flutter core.) diff --git a/gui-orchid/lib/util/poller.dart b/gui-orchid/lib/util/poller.dart index eb444c0a8..0ad2c1dce 100644 --- a/gui-orchid/lib/util/poller.dart +++ b/gui-orchid/lib/util/poller.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import '../api/orchid_log.dart'; /* Poller.call(foo).every(seconds: 5).dispose(disposal); @@ -21,10 +22,12 @@ class Poller { Poller every({int? seconds, int? minutes, int? hours}) { assert(seconds != null || minutes != null || hours != null); _timer?.cancel(); - _timer = Timer.periodic( - Duration( - seconds: seconds ?? 0, minutes: minutes ?? 0, hours: hours ?? 0), - _poll); + var duration = Duration( + seconds: seconds ?? 0, minutes: minutes ?? 0, hours: hours ?? 0); + if (duration.inMilliseconds <= 0) { + throw Exception("invalid duration: $duration"); + } + _timer = Timer.periodic(duration, _poll); return this; } @@ -35,7 +38,11 @@ class Poller { } void _poll(_) { - func(); + try { + func(); + } catch (e) { + log("Poller error: $e"); + } } Poller dispose(List disposal) { diff --git a/web-ethereum/account_dapp/lib/pages/dapp_home.dart b/web-ethereum/account_dapp/lib/pages/dapp_home.dart index 61c4dcbd9..c351e226c 100644 --- a/web-ethereum/account_dapp/lib/pages/dapp_home.dart +++ b/web-ethereum/account_dapp/lib/pages/dapp_home.dart @@ -117,12 +117,12 @@ class DappHomeState extends DappHomeStateBase { children: [ DappHomeHeader( web3Context: web3Context, - setNewContext: setNewContext, contractVersionsAvailable: contractVersionsAvailable, contractVersionSelected: contractVersionSelected, selectContractVersion: selectContractVersion, deployContract: deployContract, connectEthereum: connectEthereum, + connectWalletConnect: connectWalletConnect, disconnect: disconnect, ).padx(24).top(30).bottom(24), _buildMainColumn(), diff --git a/web-ethereum/stake_dapp/lib/pages/stake_dapp_home.dart b/web-ethereum/stake_dapp/lib/pages/stake_dapp_home.dart index c03067d0f..68e53b2f5 100644 --- a/web-ethereum/stake_dapp/lib/pages/stake_dapp_home.dart +++ b/web-ethereum/stake_dapp/lib/pages/stake_dapp_home.dart @@ -16,8 +16,9 @@ import 'package:orchid/common/app_dialogs.dart'; import 'package:orchid/orchid/field/orchid_labeled_address_field.dart'; import 'package:orchid/pages/tabs/location_panel.dart'; import 'package:orchid/pages/tabs/stake_tabs.dart'; -import 'package:orchid/stake_dapp/orchid_web3_stake_v0.dart'; import 'package:orchid/stake_dapp/orchid_web3_location_v0.dart'; +import 'package:orchid/stake_dapp/orchid_web3_stake_v0.dart'; +import 'package:orchid/stake_dapp/stake_detail.dart'; import 'dapp_home_base.dart'; import 'dapp_home_header.dart'; @@ -37,16 +38,20 @@ class _StakeDappHomeState extends DappHomeStateBase { final _stakeeField = AddressValueFieldController(); final _scrollController = ScrollController(); + Location? _currentLocation; + + // The current stake details for the staker/stakee pair. + StakeDetailPoller? _stakeDetail; + // The total stake staked for the stakee by all stakers. - Token? _currentStakeTotal; + Token? get _currentStakeTotal => _stakeDetail?.currentStakeTotal; // The amount and delay staked for the stakee by the current staker (wallet). - StakeResult? _currentStakeStaker; + StakeResult? get _currentStakeStaker => _stakeDetail?.currentStakeStaker; // The amount and expiration of the pulled stake pending withdrawal for the first n indexes. - List? _currentStakePendingStaker; - - Location? _currentLocation; + List? get _currentStakePendingStaker => + _stakeDetail?.currentStakePendingStaker; @override void initState() { @@ -176,8 +181,8 @@ class _StakeDappHomeState extends DappHomeStateBase { StakeTabs( web3Context: web3Context, stakee: _stakee, - currentStake: _currentStakeStaker?.amount, price: price, + currentStake: _currentStakeStaker?.amount, currentStakeDelay: _currentStakeStaker?.delay, ).top(40), ], @@ -339,55 +344,42 @@ class _StakeDappHomeState extends DappHomeStateBase { } // Start polling the correct account - // TODO: Poll this void _selectedStakeeChanged() async { if (isStakerView) { - _updateStake(); + _updateStakePoller(); } else { _updateLocation(); } } - void _updateStake() async { - if (_stakee != null && web3Context != null) { - // Get the total stake for all stakers (heft) - final orchidWeb3 = OrchidWeb3StakeV0(web3Context!); - _currentStakeTotal = await orchidWeb3.orchidGetTotalStake(_stakee!); - log("XXX: heft = $_currentStakeTotal"); - - // Get the stake for this staker (wallet) - try { - _currentStakeStaker = await orchidWeb3.orchidGetStakeForStaker( - staker: web3Context!.walletAddress!, - stakee: _stakee!, - ); - log("XXX: staker stake = $_currentStakeStaker"); - } catch (err, stack) { - log("Error getting stake for staker: $err"); - log(stack.toString()); - } + // Create a new poller for the current staker/stakee pair + void _updateStakePoller() { + // Cancel any existing poller + _clearStakePoller(); - // Get the pending stake withdrawals for this staker (wallet) - try { - List pendingList = []; - for (var i = 0; i < 3; i++) { - final pending = await orchidWeb3.orchidGetPendingWithdrawal( - staker: web3Context!.walletAddress!, - index: i, - ); - pendingList.add(pending); - } - _currentStakePendingStaker = pendingList; - log("XXX: pending = $_currentStakePendingStaker"); - } catch (err, stack) { - log("Error getting stake for staker: $err"); - log(stack.toString()); - } - } else { - _currentStakeTotal = null; - _currentStakeStaker = null; - _currentStakePendingStaker = null; + // Start a new poller if we have context, staker, and stakee + final staker = web3Context?.walletAddress; + final stakee = _stakee; + if (stakee != null && staker != null) { + _stakeDetail = StakeDetailPoller( + pollingPeriod: const Duration(seconds: 10), + web3Context: web3Context!, + staker: staker, + stakee: stakee, + ); + _stakeDetail?.addListener(_stakeUpdated); + _stakeDetail?.startPolling(); } + } + + void _clearStakePoller() { + _stakeDetail?.cancel(); + _stakeDetail?.removeListener(_stakeUpdated); + _stakeDetail = null; + } + + // Called when the stake poller has an update + void _stakeUpdated() { setState(() {}); } @@ -437,12 +429,14 @@ class _StakeDappHomeState extends DappHomeStateBase { // setState(() { // _clearAccountDetail(); // }); - super.disconnect(); + await super.disconnect(); + _updateStakePoller(); // allow the poller to cancel itself } @override void dispose() { _stakeeField.removeListener(_stakeeFieldChanged); + _clearStakePoller(); super.dispose(); } diff --git a/web-ethereum/stake_dapp/lib/pages/tabs/add_stake_panel.dart b/web-ethereum/stake_dapp/lib/pages/tabs/add_stake_panel.dart index fc5933953..7200ca056 100644 --- a/web-ethereum/stake_dapp/lib/pages/tabs/add_stake_panel.dart +++ b/web-ethereum/stake_dapp/lib/pages/tabs/add_stake_panel.dart @@ -78,7 +78,7 @@ class _AddStakePanelState extends State // Delay label (if non-null) // if (_currentStakeDelayIsZero) // Text("Added funds will be staked with no withdrawal delay.").white.caption.top(16), - // if (_currentStakeDelayIsNonZero) + if (_currentStakeDelayIsNonZero) Text("This UI does not support adding funds to an existing stake with a non-zero delay.") .caption .error diff --git a/web-ethereum/stake_dapp/lib/pages/tabs/withdraw_stake_panel.dart b/web-ethereum/stake_dapp/lib/pages/tabs/withdraw_stake_panel.dart index 905597d03..010a8db18 100644 --- a/web-ethereum/stake_dapp/lib/pages/tabs/withdraw_stake_panel.dart +++ b/web-ethereum/stake_dapp/lib/pages/tabs/withdraw_stake_panel.dart @@ -144,6 +144,8 @@ class _WithdrawStakePanelState extends State ))); _withdrawStakeAmountController.clear(); + _indexController.clear(); + _targetController.clear(); setState(() {}); } catch (err) { log('Error on withdraw funds: $err'); diff --git a/web-ethereum/stake_dapp/lib/stake_dapp/stake_detail.dart b/web-ethereum/stake_dapp/lib/stake_dapp/stake_detail.dart new file mode 100644 index 000000000..9202783ce --- /dev/null +++ b/web-ethereum/stake_dapp/lib/stake_dapp/stake_detail.dart @@ -0,0 +1,139 @@ +import 'dart:async'; +import 'package:flutter/widgets.dart'; +import 'package:orchid/api/orchid_crypto.dart'; +import 'package:orchid/api/orchid_eth/token_type.dart'; +import 'package:orchid/api/orchid_log.dart'; +import 'package:orchid/dapp/orchid_web3/orchid_web3_context.dart'; +import 'orchid_web3_stake_v0.dart'; + +class StakeDetailPoller extends ChangeNotifier { + final OrchidWeb3Context web3Context; + final EthereumAddress stakee; + final EthereumAddress staker; // wallet + + // The total stake staked for the stakee by all stakers. + Token? currentStakeTotal; + + // The amount and delay staked for the stakee by the current staker (wallet). + StakeResult? currentStakeStaker; + + // The amount and expiration of the pulled stake pending withdrawal for the first n indexes. + List? currentStakePendingStaker; + + // Manage polling state + static int nextId = 0; + final int id; + final Duration pollingPeriod; + Timer? _timer; + bool _pollInProgress = false; + bool _isCancelled = false; + DateTime? lastUpdate; + + StakeDetailPoller({ + required this.web3Context, + required this.staker, + required this.stakee, + this.pollingPeriod = const Duration(seconds: 30), + }) : this.id = nextId++ { + log("XXX: StakeDetailPoller $id created."); + } + + /// Start periodic polling + Future startPolling() async { + _timer = Timer.periodic(pollingPeriod, (_) { + _pollStake(); + }); + return _pollStake(); // kick one off immediately + } + + /// Load data once + Future pollOnce() async { + return _pollStake(); + } + + /// Load data updating caches + Future refresh() async { + return _pollStake(refresh: true); + } + + Future _pollStake({bool refresh = false}) async { + if (_isCancelled || _pollInProgress) { + log("XXX: call to _pollStake with cancelled timer or poll in progress, pollInProgress=$_pollInProgress"); + return; + } + _pollInProgress = true; + try { + await _pollStakeImpl(refresh); + } catch (err) { + log("Error polling stake details: $err"); + } finally { + _pollInProgress = false; + lastUpdate = DateTime.now(); + } + } + + // 'refresh' can be used to defeat caching, if any. + Future _pollStakeImpl(bool refresh) async { + final orchidWeb3 = OrchidWeb3StakeV0(web3Context); + + // Get the total stake for all stakers (heft) + try { + currentStakeTotal = await orchidWeb3.orchidGetTotalStake(stakee); + log("XXX: heft = $currentStakeTotal"); + } catch (err) { + log("Error getting heft for stakee: $err"); + currentStakeTotal = null; + } + this.notifyListeners(); + + // Get the stake for this staker (wallet) + try { + currentStakeStaker = await orchidWeb3.orchidGetStakeForStaker( + staker: staker, + stakee: stakee, + ); + log("XXX: staker stake = $currentStakeStaker"); + } catch (err, stack) { + log("Error getting stake for staker: $err"); + log(stack.toString()); + currentStakeStaker = null; + } + this.notifyListeners(); + + // Get the pending stake withdrawals for this staker (wallet) + try { + List pendingList = []; + for (var i = 0; i < 3; i++) { + final pending = await orchidWeb3.orchidGetPendingWithdrawal( + staker: staker, + index: i, + ); + pendingList.add(pending); + } + currentStakePendingStaker = pendingList; + log("XXX: pending = $currentStakePendingStaker"); + } catch (err, stack) { + log("Error getting stake for staker: $err"); + log(stack.toString()); + currentStakePendingStaker = null; + } + this.notifyListeners(); + } + + void cancel() { + _isCancelled = true; + _timer?.cancel(); + log("XXX: stake detail $id poller cancelled"); + } + + void dispose() { + cancel(); + super.dispose(); + } + + @override + String toString() { + return 'StakeDetailPoller{id: $id, stakee: $stakee, staker: $staker, currentStakeTotal: $currentStakeTotal, currentStakeStaker: $currentStakeStaker, currentStakePendingStaker: $currentStakePendingStaker}'; + } +} +