diff --git a/app/lib/common/components/submit_button.dart b/app/lib/common/components/submit_button.dart index dd3bc6d9d..e7516a808 100644 --- a/app/lib/common/components/submit_button.dart +++ b/app/lib/common/components/submit_button.dart @@ -74,7 +74,7 @@ class _SubmitButtonStateSmall extends State { return ElevatedButton( onPressed: (!_submitting && widget.onPressed != null) ? _onPressed : null, style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 14), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4), backgroundColor: context.colorScheme.primary, foregroundColor: Colors.white, textStyle: context.titleSmall, diff --git a/app/lib/l10n/arb/app_de.arb b/app/lib/l10n/arb/app_de.arb index 337ba604c..304119b02 100644 --- a/app/lib/l10n/arb/app_de.arb +++ b/app/lib/l10n/arb/app_de.arb @@ -93,6 +93,8 @@ "democracyVotedNotificationTitle": "Abgestimmt", "democracySubmitProposalNotificationBody": "Du hast einen Vorschlag eingereicht, über den nun abgestimmt werden kann.", "democracySubmitProposalNotificationTitle": "Vorschlag eingereicht", + "democracyUpdatedProposalStateNotificationBody": "Du hast diesen Vorschlag aktualisiert", + "democracyUpdatedProposalStateNotificationTitle": "Vorschlag aktualisiert", "detail": "Detail", "detailsEnter": "Gib deine Details ein.", "developer": "Entwickler-Modus", @@ -240,6 +242,9 @@ "proposalFieldErrorPositiveNumberRange": "Muss eine positive Zahl sein", "proposalFieldErrorEnterInactivityTimeout": "Inaktivitätszeitlimit eingeben", "proposalFieldErrorPositiveIntegerRange": "Muss eine positive ganze Zahl sein", + "proposalClose": "Schliessen", + "proposalUpdateState": "Aktualisieren", + "proposalUpdateExplanation": "Dies wird den Status des Vorschlags aktualisieren. Wenn er zu alt ist und nicht genügend Aye-Stimmen hat, wird er abgelehnt. Wenn er lange genug bestätigt wurde, wird er angenommen.", "proposalOnlyBootstrappersOrReputablesCanSubmit": "Nur Bootstrappers oder Reputables können einen Vorschlag einreichen.", "proposalSubmit": "Vorschlag einreichen", "proposalSuperseded": "Verdrängt", diff --git a/app/lib/l10n/arb/app_en.arb b/app/lib/l10n/arb/app_en.arb index 714bb0134..a47697df2 100644 --- a/app/lib/l10n/arb/app_en.arb +++ b/app/lib/l10n/arb/app_en.arb @@ -91,6 +91,8 @@ "democracyDiscussion": "Discuss proposals in the Forum!", "democracyVotedNotificationBody": "You have voted for this proposal.", "democracyVotedNotificationTitle": "Voted", + "democracyUpdatedProposalStateNotificationBody": "You have updated this proposal", + "democracyUpdatedProposalStateNotificationTitle": "Proposal Updated", "democracySubmitProposalNotificationBody": "You made a proposal, which people can vote on now.", "democracySubmitProposalNotificationTitle": "Proposal submitted", "detail": "Detail", @@ -241,6 +243,9 @@ "proposalFieldErrorEnterInactivityTimeout": "Enter inactivity timeout", "proposalFieldErrorPositiveIntegerRange": "Must be a positive integer", "proposalOnlyBootstrappersOrReputablesCanSubmit": "Only bootstrappers or reputables can submit a proposal.", + "proposalClose": "Close", + "proposalUpdateState": "Update", + "proposalUpdateExplanation": "This will update the proposal state. If it is too old and does not have enough Aye votes, it will be rejected. If it has been confirming long enough, it will pass.", "proposalSubmit": "Submit Proposal", "proposalSuperseded": "Superseded", "proposalRejected": "Rejected", diff --git a/app/lib/l10n/arb/app_fr.arb b/app/lib/l10n/arb/app_fr.arb index b45949d39..26277c3a0 100644 --- a/app/lib/l10n/arb/app_fr.arb +++ b/app/lib/l10n/arb/app_fr.arb @@ -91,6 +91,8 @@ "democracyDiscussion": "Discute de suggestions dans le forum!", "democracyVotedNotificationBody": "Tu as voté pour cette proposition.", "democracyVotedNotificationTitle": "Voté", + "democracyUpdatedProposalStateNotificationBody": "Vous avez mis à jour cette proposition", + "democracyUpdatedProposalStateNotificationTitle": "Proposition mise à jour", "democracySubmitProposalNotificationBody": "Vous avez fait une proposition, sur laquelle les gens peuvent voter maintenant.", "democracySubmitProposalNotificationTitle": "Proposition soumise", "detail": "Détail", @@ -241,6 +243,9 @@ "proposalFieldErrorEnterInactivityTimeout": "Entrez le délai d'inactivité", "proposalFieldErrorPositiveIntegerRange": "Doit être un entier positif", "proposalOnlyBootstrappersOrReputablesCanSubmit": "Seuls les bootstrappers ou les Reputables peuvent soumettre une proposition.", + "proposalClose": "Fermer", + "proposalUpdateState": "Mettre à jour", + "proposalUpdateExplanation": "Cela mettra à jour l'état de la proposition. Si elle est trop ancienne et n'a pas suffisamment de votes Aye, elle sera rejetée. Si elle a été confirmée assez longtemps, elle sera acceptée.", "proposalSubmit": "Soumettre une proposition", "proposalSuperseded": "Epargné", "proposalRejected": "Refusé", diff --git a/app/lib/l10n/arb/app_ru.arb b/app/lib/l10n/arb/app_ru.arb index 9136d7153..d1621ff75 100644 --- a/app/lib/l10n/arb/app_ru.arb +++ b/app/lib/l10n/arb/app_ru.arb @@ -93,6 +93,8 @@ "democracyVotedNotificationTitle": "проголосовали", "democracySubmitProposalNotificationBody": "Вы внесли предложение, за которое люди могут проголосовать прямо сейчас.", "democracySubmitProposalNotificationTitle": "Предложение отправлено", + "democracyUpdatedProposalStateNotificationBody": "Вы обновили это предложение", + "democracyUpdatedProposalStateNotificationTitle": "Предложение обновлено", "detail": "Детали", "detailsEnter": "Введите свои данные", "developer": "Режим разработчика", @@ -241,6 +243,9 @@ "proposalFieldErrorEnterInactivityTimeout": "Введите тайм-аут неактивности", "proposalFieldErrorPositiveIntegerRange": "Должно быть положительное целое число", "proposalOnlyBootstrappersOrReputablesCanSubmit": "Только бутстраперы или уважаемые участники могут подать предложение.", + "proposalClose": "Закрыть", + "proposalUpdateState": "Обновить", + "proposalUpdateExplanation": "Это обновит статус предложения. Если оно слишком старое и не имеет достаточного количества голосов 'За', оно будет отклонено. Если оно подтверждается достаточно долго, оно будет принято.", "proposalSubmit": "Подать предложение", "proposalRejected": "Отменено", "proposalSuperseded": "заменен", diff --git a/app/lib/l10n/arb/app_sw.arb b/app/lib/l10n/arb/app_sw.arb index 90e55475d..808336d25 100644 --- a/app/lib/l10n/arb/app_sw.arb +++ b/app/lib/l10n/arb/app_sw.arb @@ -93,6 +93,8 @@ "democracyVotedNotificationTitle": "Umepiga kura", "democracySubmitProposalNotificationBody": "Ulitoa pendekezo, ambalo watu wanaweza kulipigia kura sasa.", "democracySubmitProposalNotificationTitle": "Pendekezo limewasilishwa", + "democracyUpdatedProposalStateNotificationBody": "Umesasisha pendekezo hili", + "democracyUpdatedProposalStateNotificationTitle": "Pendekezo limesasishwa", "detail": "Taarifa", "detailsEnter": "ingiza taarifa zako.", "developer": "Developer mode", @@ -241,6 +243,9 @@ "proposalFieldErrorEnterInactivityTimeout": "Weka muda wa kutokufanya kazi", "proposalFieldErrorPositiveIntegerRange": "Lazima iwe namba kamili chanya", "proposalOnlyBootstrappersOrReputablesCanSubmit": "Ni bootstrappers au waheshimika pekee wanaoweza kuwasilisha pendekezo.", + "proposalClose": "Funga", + "proposalUpdateState": "Sasisha", + "proposalUpdateExplanation": "Hii itasasisha hali ya pendekezo. Ikiwa ni la zamani sana na halina kura za kutosha za 'Ndio', litakataliwa. Ikiwa limekuwa likithibitishwa kwa muda wa kutosha, litakubaliwa.", "proposalSubmit": "Wasilisha Pendekezo", "proposalSuperseded": "Iliyopita", "proposalRejected": "Imekaataliwa", diff --git a/app/lib/page-encointer/democracy/helpers.dart b/app/lib/page-encointer/democracy/helpers.dart index 3bcd9f725..77f165761 100644 --- a/app/lib/page-encointer/democracy/helpers.dart +++ b/app/lib/page-encointer/democracy/helpers.dart @@ -234,7 +234,23 @@ extension ProposalExt on Proposal { return state.runtimeType == SupersededBy || state.runtimeType == Rejected; } + /// Returns true if the proposal started after now - `duration`. bool isMoreRecentThan(Duration duration) { return DateTime.now().subtract(duration).isBefore(DateTime.fromMillisecondsSinceEpoch(start.toInt())); } + + /// Returns true if the proposal started before now - `duration`. + bool isOlderThan(Duration duration) { + return !isMoreRecentThan(duration); + } + + /// Returns true if the proposal has been in `Confirming` for longer than `duration`. + /// + /// Returns null if the proposal is not in confirming state at all. + bool? isConfirmingLongerThan(Duration duration) { + if (state.runtimeType != Confirming) return null; + + final confirmingSince = (state as Confirming).since; + return DateTime.now().subtract(duration).isAfter(DateTime.fromMillisecondsSinceEpoch(confirmingSince.toInt())); + } } diff --git a/app/lib/page-encointer/democracy/widgets/proposal_tile.dart b/app/lib/page-encointer/democracy/widgets/proposal_tile.dart index 2d96c9a6f..c4be4c663 100644 --- a/app/lib/page-encointer/democracy/widgets/proposal_tile.dart +++ b/app/lib/page-encointer/democracy/widgets/proposal_tile.dart @@ -1,5 +1,6 @@ import 'package:encointer_wallet/l10n/l10.dart'; import 'package:encointer_wallet/modules/modules.dart'; +import 'package:encointer_wallet/page-encointer/democracy/widgets/update_proposal_button.dart'; import 'package:encointer_wallet/service/service.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -80,17 +81,13 @@ class _ProposalTileState extends State { ListTile( contentPadding: const EdgeInsets.symmetric(), leading: Text(widget.proposalId.toString(), style: titleSmall), - subtitle: SizedBox( - // ensure constant height even for missing texts without turnout. - height: 60, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('${l10n.proposalTurnout}: $turnout / $electorateSize'), - if (turnout != 0) Text(l10n.proposalApprovalThreshold((threshold * 100).toStringAsFixed(2))), - if (turnout != 0) passingOrFailingText(context, proposal, tally, widget.params), - ], - ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${l10n.proposalTurnout}: $turnout / $electorateSize'), + if (turnout != 0) Text(l10n.proposalApprovalThreshold((threshold * 100).toStringAsFixed(2))), + if (turnout != 0) passingOrFailingText(context, proposal, tally, widget.params), + ], ), trailing: voteButtonOrProposalStatus(context), ), @@ -175,18 +172,55 @@ class _ProposalTileState extends State { case Approved: return Text(l10n.proposalApproved, style: const TextStyle(color: Colors.green)); case Ongoing: + final proposalLifetime = Duration(milliseconds: widget.params.proposalLifetime.toInt()); + if (proposal.isOlderThan(proposalLifetime)) { + // Proposal lifetime has passed; proposal has expired. + return SizedBox( + height: 50, + width: 60, + child: UpdateProposalButton( + proposalId: widget.proposalId, + onPressed: _updateState, + ), + ); + } else { + return SizedBox( + height: 50, + width: 60, + child: VoteButton( + proposal: proposal, + proposalId: widget.proposalId, + purposeId: widget.purposeId, + democracyParams: widget.params, + onPressed: _updateState, + ), + ); + } case Confirming: - return SizedBox( - height: 50, - width: 60, - child: VoteButton( - proposal: proposal, - proposalId: widget.proposalId, - purposeId: widget.purposeId, - democracyParams: widget.params, - onPressed: _updateState, - ), - ); + final confirmDuration = Duration(milliseconds: widget.params.confirmationPeriod.toInt()); + if (proposal.isConfirmingLongerThan(confirmDuration)!) { + // confirmation time has passed + return SizedBox( + height: 50, + width: 60, + child: UpdateProposalButton( + proposalId: widget.proposalId, + onPressed: _updateState, + ), + ); + } else { + return SizedBox( + height: 50, + width: 60, + child: VoteButton( + proposal: proposal, + proposalId: widget.proposalId, + purposeId: widget.purposeId, + democracyParams: widget.params, + onPressed: _updateState, + ), + ); + } default: // should never happen. return const Text('Unknown Proposal State'); diff --git a/app/lib/page-encointer/democracy/widgets/update_proposal_button.dart b/app/lib/page-encointer/democracy/widgets/update_proposal_button.dart new file mode 100644 index 000000000..2442b4e31 --- /dev/null +++ b/app/lib/page-encointer/democracy/widgets/update_proposal_button.dart @@ -0,0 +1,94 @@ +import 'package:encointer_wallet/common/components/submit_button_cupertino.dart'; +import 'package:encointer_wallet/service/service.dart'; +import 'package:encointer_wallet/utils/alerts/app_alert.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:encointer_wallet/common/components/submit_button.dart'; +import 'package:encointer_wallet/l10n/l10.dart'; +import 'package:encointer_wallet/service/tx/lib/tx.dart'; +import 'package:encointer_wallet/store/app.dart'; + +class UpdateProposalButton extends StatefulWidget { + const UpdateProposalButton({ + super.key, + required this.proposalId, + required this.onPressed, + }); + + final BigInt proposalId; + final void Function() onPressed; + + @override + State createState() => _UpdateProposalButtonState(); +} + +class _UpdateProposalButtonState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final store = context.read(); + + return SubmitButtonSmall( + onPressed: (context) async { + await _showSubmitUpdateProposalStateDialog(store, widget.proposalId); + widget.onPressed(); + }, + child: Text(l10n.proposalClose), + ); + } + + Future _showSubmitUpdateProposalStateDialog(AppStore store, BigInt proposalId) { + final l10n = context.l10n; + + return AppAlert.showDialog( + context, + title: Text('${l10n.proposal} $proposalId'), + content: Padding( + padding: const EdgeInsets.only(top: 10), + child: Text(l10n.proposalUpdateExplanation), + ), + actions: [ + Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SubmitButtonCupertino( + onPressed: (BuildContext context) async { + await _submitUpdateProposalState(store); + Navigator.of(context).pop(); + }, + child: Text(l10n.proposalUpdateState, style: const TextStyle(color: Colors.green)), + ), + CupertinoButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.cancel), + ), + ], + ), + ], + ), + ], + ); + } + + Future _submitUpdateProposalState(AppStore store) async { + await submitDemocracyUpdateProposalState( + context, + store, + webApi, + store.account.getKeyringAccount(store.account.currentAccountPubKey!), + widget.proposalId, + txPaymentAsset: store.encointer.getTxPaymentAsset(store.encointer.chosenCid), + ); + + setState(() {}); + } +} diff --git a/app/lib/service/substrate_api/encointer/encointer_api.dart b/app/lib/service/substrate_api/encointer/encointer_api.dart index 590ca0219..70c116d1d 100644 --- a/app/lib/service/substrate_api/encointer/encointer_api.dart +++ b/app/lib/service/substrate_api/encointer/encointer_api.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:convert/convert.dart' show hex; import 'package:encointer_wallet/config/consts.dart'; +import 'package:encointer_wallet/config/networks/networks.dart'; import 'package:encointer_wallet/mocks/mock_bazaar_data.dart'; import 'package:encointer_wallet/models/bazaar/account_business_tuple.dart'; import 'package:encointer_wallet/models/bazaar/business_identifier.dart'; @@ -807,6 +808,27 @@ class EncointerApi { } DemocracyParams democracyParams() { + return switch (store.settings.currentNetwork) { + Network.encointerKusama => encointerKusamaParams(), + Network.encointerRococo => encointerKusamaParams(), + Network.gesell => encointerSoloParams(), + Network.gesellDev => encointerSoloParams(), + }; + } + + DemocracyParams encointerSoloParams() { + final minTurnout = BigInt.one; + final confirmationPeriod = BigInt.from(300000); + final proposalLifetime = BigInt.from(1200000); + + return DemocracyParams( + minTurnout: minTurnout, + confirmationPeriod: confirmationPeriod, + proposalLifetime: proposalLifetime, + ); + } + + DemocracyParams encointerKusamaParams() { final minTurnout = encointerKusama.constant.encointerDemocracy.minTurnout; final confirmationPeriod = encointerKusama.constant.encointerDemocracy.confirmationPeriod; final proposalLifetime = encointerKusama.constant.encointerDemocracy.proposalLifetime; diff --git a/app/lib/service/tx/lib/src/submit_to_inner.dart b/app/lib/service/tx/lib/src/submit_to_inner.dart index 6cc149c1f..a961fff6b 100644 --- a/app/lib/service/tx/lib/src/submit_to_inner.dart +++ b/app/lib/service/tx/lib/src/submit_to_inner.dart @@ -48,6 +48,7 @@ Future submitTxInner( ); if (report.isExtrinsicFailed) { + Log.e('[TX] Extrinsic Failed: ${report.dispatchError!.toJson()}'); _onTxError(store); onError?.call(report.dispatchError!); final message = getLocalizedTxErrorMessage(l10n, report.dispatchError!); @@ -86,6 +87,8 @@ void _showErrorDialog(BuildContext context, ErrorNotificationMsg message) { final l10n = context.l10n; final languageCode = Localizations.localeOf(context).languageCode; + print('Should show error dialog'); + AppAlert.showDialog( context, title: Text(message.title), diff --git a/app/lib/service/tx/lib/src/submit_tx_wrappers.dart b/app/lib/service/tx/lib/src/submit_tx_wrappers.dart index 3e15de786..31b27868c 100644 --- a/app/lib/service/tx/lib/src/submit_tx_wrappers.dart +++ b/app/lib/service/tx/lib/src/submit_tx_wrappers.dart @@ -456,6 +456,33 @@ Future submitDemocracyVote( ); } +Future submitDemocracyUpdateProposalState( + BuildContext context, + AppStore store, + Api api, + KeyringAccount signer, + BigInt proposalId, { + required CommunityIdentifier? txPaymentAsset, +}) async { + final call = api.encointer.encointerKusama.tx.encointerDemocracy.updateProposalState( + proposalId: proposalId, + ); + + final xt = await TxBuilder(api.provider).createSignedExtrinsic( + signer.pair, + call, + paymentAsset: txPaymentAsset?.toPolkadart(), + ); + + return submitTx( + context, + store, + api, + OpaqueExtrinsic(xt), + TxNotification.democracyUpdateProposalState(context.l10n), + ); +} + Future submitDemocracyProposal( BuildContext context, AppStore store, diff --git a/app/lib/service/tx/lib/src/tx_notification.dart b/app/lib/service/tx/lib/src/tx_notification.dart index 3512a6366..5fb6a7e27 100644 --- a/app/lib/service/tx/lib/src/tx_notification.dart +++ b/app/lib/service/tx/lib/src/tx_notification.dart @@ -27,6 +27,11 @@ class TxNotification { body: l10n.democracyVotedNotificationBody, ); + factory TxNotification.democracyUpdateProposalState(AppLocalizations l10n) => TxNotification( + title: l10n.democracyUpdatedProposalStateNotificationTitle, + body: l10n.democracyUpdatedProposalStateNotificationBody, + ); + factory TxNotification.democracySubmitProposal(AppLocalizations l10n) => TxNotification( title: l10n.democracySubmitProposalNotificationTitle, body: l10n.democracySubmitProposalNotificationBody,