Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';

enum MultisigApprovalToastKind { timeout, submitFailed }
enum MultisigApprovalToastKind { timeout }

class MultisigApprovalToastEvent {
const MultisigApprovalToastEvent(this.kind);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';

enum MultisigCancellationToastKind { timeout, submitFailed }
enum MultisigCancellationToastKind { timeout }

class MultisigCancellationToastEvent {
const MultisigCancellationToastEvent(this.kind);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:flutter_riverpod/legacy.dart';

enum MultisigCreationToastKind { ready, timeout, submitFailed }
enum MultisigCreationToastKind { ready, timeout }

class MultisigCreationToastEvent {
const MultisigCreationToastEvent(this.kind);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';

enum MultisigExecutionToastKind { timeout, submitFailed, executedByOther }
enum MultisigExecutionToastKind { timeout, executedByOther }

class MultisigExecutionToastEvent {
const MultisigExecutionToastEvent(this.kind);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';

enum MultisigProposalToastKind { timeout, submitFailed }
enum MultisigProposalToastKind { timeout }

class MultisigProposalToastEvent {
const MultisigProposalToastEvent(this.kind);
Expand Down
25 changes: 8 additions & 17 deletions mobile-app/lib/services/multisig_submission_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'dart:async';
import 'package:convert/convert.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:quantus_sdk/quantus_sdk.dart';
import 'package:resonance_network_wallet/providers/multisig_creation_toast_provider.dart';
import 'package:resonance_network_wallet/providers/multisig_providers.dart';
import 'package:resonance_network_wallet/providers/pending_multisig_creations_provider.dart';
import 'package:resonance_network_wallet/providers/wallet_providers.dart';
Expand All @@ -30,12 +29,14 @@ class MultisigSubmissionService {
await _runCreationPreflight(name: '', signers: signers, threshold: threshold, creator: creator, nonce: nonce);
}

/// Preflight on-chain state, then submit and track creation in the background.
/// Preflight on-chain state, then submit and track creation.
///
/// Returns when preflight passes and background work is scheduled. Throws
/// Awaits acceptance of the creation extrinsic by the chain before
/// completing; indexer polling then continues in the background. Throws
/// [MultisigAlreadyExistsException] if the predicted address already exists,
/// or [MultisigInsufficientBalanceException] if the creator cannot afford
/// the total creation cost.
/// the total creation cost. Rethrows on submission failure so callers can
/// surface the error instead of optimistically navigating away.
Future<void> startMultisigCreation({
required String name,
required List<String> signers,
Expand All @@ -59,18 +60,10 @@ class MultisigSubmissionService {
.read(pendingMultisigCreationsProvider.notifier)
.add(PendingMultisigCreationEvent.fromDraft(draft, networkFee: networkFee), draft);

unawaited(
_submitAndTrackBackground(
creator: creator,
signers: signers,
threshold: threshold,
nonce: draft.nonce,
draft: draft,
),
);
await _submitAndTrack(creator: creator, signers: signers, threshold: threshold, nonce: draft.nonce, draft: draft);
}

Future<void> _submitAndTrackBackground({
Future<void> _submitAndTrack({
required Account creator,
required List<String> signers,
required int threshold,
Expand Down Expand Up @@ -104,9 +97,7 @@ class MultisigSubmissionService {
quantusDebugPrint('Stack trace: $stackTrace');
TelemetryService().sendError('multisig_create_submit_failed', error: e, stackTrace: stackTrace);
removePendingMultisigCreation(_ref, draft.accountId);
_ref.read(multisigCreationToastProvider.notifier).state = const MultisigCreationToastEvent(
MultisigCreationToastKind.submitFailed,
);
rethrow;
}
}

Expand Down
72 changes: 39 additions & 33 deletions mobile-app/lib/services/transaction_submission_service.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
// mobile-app/lib/services/transaction_submission_service.dart

import 'dart:async';
import 'dart:typed_data';

import 'package:convert/convert.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:quantus_sdk/quantus_sdk.dart';
import 'package:resonance_network_wallet/providers/account_providers.dart';
import 'package:resonance_network_wallet/providers/notification_provider.dart';
import 'package:resonance_network_wallet/providers/multisig_approval_toast_provider.dart';
import 'package:resonance_network_wallet/providers/multisig_cancellation_toast_provider.dart';
import 'package:resonance_network_wallet/providers/multisig_execution_toast_provider.dart';
import 'package:resonance_network_wallet/providers/multisig_proposal_toast_provider.dart';
import 'package:resonance_network_wallet/providers/multisig_providers.dart';
import 'package:resonance_network_wallet/providers/pending_multisig_approvals_provider.dart';
import 'package:resonance_network_wallet/providers/pending_multisig_cancellations_provider.dart';
Expand Down Expand Up @@ -129,8 +124,12 @@ class TransactionSubmissionService {
);
}

/// Submits a multisig transfer proposal, tracks it optimistically, and polls
/// the indexer until the proposal is visible.
/// Submits a multisig transfer proposal and tracks it optimistically.
///
/// Awaits acceptance of the extrinsic by the chain before completing;
/// indexer polling then continues in the background. Rethrows on submission
/// failure so callers can surface the error instead of optimistically
/// navigating away.
Future<void> proposeTransfer({
required MultisigAccount msig,
required Account signer,
Expand All @@ -154,20 +153,21 @@ class TransactionSubmissionService {

TelemetryService().sendEvent('multisig_propose');

unawaited(
_submitProposalBackground(
msig: msig,
signer: signer,
recipient: recipient,
amount: amount,
expiryBlock: expiryBlock,
pending: pending,
),
await _submitProposal(
msig: msig,
signer: signer,
recipient: recipient,
amount: amount,
expiryBlock: expiryBlock,
pending: pending,
);
}

/// Submits a multisig proposal approval, tracks it optimistically, and polls
/// the indexer until the approval appears on the proposal.
/// Submits a multisig proposal approval and tracks it optimistically.
///
/// Awaits acceptance of the extrinsic by the chain before completing;
/// indexer polling then continues in the background. Rethrows on submission
/// failure so callers can surface the error.
Future<void> approveProposal({
required MultisigAccount msig,
required Account signer,
Expand All @@ -183,10 +183,10 @@ class TransactionSubmissionService {

TelemetryService().sendEvent('multisig_approve');

unawaited(_submitApproveBackground(msig: msig, signer: signer, proposalId: proposal.id, pending: pending));
await _submitApprove(msig: msig, signer: signer, proposalId: proposal.id, pending: pending);
}

Future<void> _submitApproveBackground({
Future<void> _submitApprove({
required MultisigAccount msig,
required Account signer,
required int proposalId,
Expand All @@ -204,12 +204,15 @@ class TransactionSubmissionService {
} catch (e, stackTrace) {
quantusDebugPrint('[Approve] submit failed: $e\n$stackTrace');
removePendingMultisigApproval(_ref, pending.id);
_ref.read(multisigApprovalToastProvider.notifier).show(MultisigApprovalToastKind.submitFailed);
rethrow;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate failure toasts and inline

Low Severity

After submission helpers began rethrowing on failure, approve/execute/cancel confirm sheets and the propose review screen still set inline error text when submit throws, while the submission service also fires the matching submitFailed toast. Users see the same failure message twice on the same screen.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e428348. Configure here.

}
}

/// Submits a multisig proposal execution, tracks it optimistically, and polls
/// the indexer until the proposal status becomes executed.
/// Submits a multisig proposal execution and tracks it optimistically.
///
/// Awaits acceptance of the extrinsic by the chain before completing;
/// indexer polling then continues in the background. Rethrows on submission
/// failure so callers can surface the error.
Future<void> executeProposal({
required MultisigAccount msig,
required Account signer,
Expand All @@ -227,10 +230,10 @@ class TransactionSubmissionService {

TelemetryService().sendEvent('multisig_execute');

unawaited(_submitExecuteBackground(msig: msig, signer: signer, proposalId: proposal.id, pending: pending));
await _submitExecute(msig: msig, signer: signer, proposalId: proposal.id, pending: pending);
}

Future<void> _submitExecuteBackground({
Future<void> _submitExecute({
required MultisigAccount msig,
required Account signer,
required int proposalId,
Expand All @@ -248,12 +251,15 @@ class TransactionSubmissionService {
} catch (e, stackTrace) {
quantusDebugPrint('[Execute] submit failed: $e\n$stackTrace');
removePendingMultisigExecution(_ref, pending.id);
_ref.read(multisigExecutionToastProvider.notifier).show(MultisigExecutionToastKind.submitFailed);
rethrow;
}
}

/// Submits a multisig proposal cancellation, tracks it optimistically, and polls
/// the indexer until the proposal status becomes cancelled.
/// Submits a multisig proposal cancellation and tracks it optimistically.
///
/// Awaits acceptance of the extrinsic by the chain before completing;
/// indexer polling then continues in the background. Rethrows on submission
/// failure so callers can surface the error.
Future<void> cancelProposal({
required MultisigAccount msig,
required Account proposer,
Expand All @@ -271,10 +277,10 @@ class TransactionSubmissionService {

TelemetryService().sendEvent('multisig_cancel');

unawaited(_submitCancelBackground(msig: msig, proposer: proposer, proposalId: proposal.id, pending: pending));
await _submitCancel(msig: msig, proposer: proposer, proposalId: proposal.id, pending: pending);
}

Future<void> _submitCancelBackground({
Future<void> _submitCancel({
required MultisigAccount msig,
required Account proposer,
required int proposalId,
Expand All @@ -293,11 +299,11 @@ class TransactionSubmissionService {
} catch (e, stackTrace) {
quantusDebugPrint('[Cancel] submit failed: $e\n$stackTrace');
removePendingMultisigCancellation(_ref, pending.id);
_ref.read(multisigCancellationToastProvider.notifier).show(MultisigCancellationToastKind.submitFailed);
rethrow;
}
}

Future<void> _submitProposalBackground({
Future<void> _submitProposal({
required MultisigAccount msig,
required Account signer,
required String recipient,
Expand Down Expand Up @@ -326,7 +332,7 @@ class TransactionSubmissionService {
// deposit-reserving proposals if a prior submit already landed.
quantusDebugPrint('[Propose] submit failed: $e\n$stackTrace');
removePendingMultisigProposal(_ref, pending.id);
_ref.read(multisigProposalToastProvider.notifier).show(MultisigProposalToastKind.submitFailed);
rethrow;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ class MultisigApprovalToastListener extends ConsumerWidget {
final l10n = ref.read(l10nProvider);
final message = switch (next.kind) {
MultisigApprovalToastKind.timeout => l10n.multisigApprovalTimeoutToast,
MultisigApprovalToastKind.submitFailed => l10n.multisigApproveFailed,
};
context.showErrorToaster(message: message);
ref.read(multisigApprovalToastProvider.notifier).clear();
Expand All @@ -31,8 +30,6 @@ class MultisigApprovalToastListener extends ConsumerWidget {
switch (next.kind) {
case MultisigExecutionToastKind.timeout:
context.showErrorToaster(message: l10n.multisigExecutionTimeoutToast);
case MultisigExecutionToastKind.submitFailed:
context.showErrorToaster(message: l10n.multisigExecuteFailed);
case MultisigExecutionToastKind.executedByOther:
context.showInfoToaster(message: l10n.multisigExecutedByOtherToast);
}
Expand All @@ -44,7 +41,6 @@ class MultisigApprovalToastListener extends ConsumerWidget {
final l10n = ref.read(l10nProvider);
final message = switch (next.kind) {
MultisigCancellationToastKind.timeout => l10n.multisigCancelTimeoutToast,
MultisigCancellationToastKind.submitFailed => l10n.multisigCancelFailed,
};
context.showErrorToaster(message: message);
ref.read(multisigCancellationToastProvider.notifier).clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ class MultisigCreationToastListener extends ConsumerWidget {
final message = switch (next.kind) {
MultisigCreationToastKind.ready => l10n.multisigCreateReadyToast,
MultisigCreationToastKind.timeout => l10n.multisigCreateTimeoutToast,
MultisigCreationToastKind.submitFailed => l10n.multisigCreateErrorCouldNotCreate,
};
if (next.kind == MultisigCreationToastKind.ready) {
context.showSuccessToaster(message: message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ class MultisigProposalToastListener extends ConsumerWidget {
final l10n = ref.read(l10nProvider);
final message = switch (next.kind) {
MultisigProposalToastKind.timeout => l10n.multisigProposeTimeoutToast,
MultisigProposalToastKind.submitFailed => l10n.multisigProposeSubmitFailed,
};
context.showErrorToaster(message: message);
ref.read(multisigProposalToastProvider.notifier).clear();
Expand Down
4 changes: 3 additions & 1 deletion mobile-app/lib/v2/screens/multisig/add_multisig_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,10 @@ class _AddMultisigScreenState extends ConsumerState<AddMultisigScreen> {
context.showErrorToaster(message: l10n.multisigCreateInsufficientBalance);
}
} catch (e) {
// The submission service rethrows without surfacing UI; the caller owns
// user feedback. Show a single error toast for any failure (submission or
// otherwise) and skip navigation.
quantusDebugPrint('[AddMultisigScreen] createMultisig error: $e');

if (mounted) {
context.showErrorToaster(message: l10n.multisigCreateErrorCouldNotCreate);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creation errors lose user toast

Medium Severity

The generic catch after startMultisigCreation no longer shows any error toaster. Only submission failures get feedback via the creation toast listener; other failures (e.g. persisting pending creation) only hit quantusDebugPrint, so the user sees loading stop with no explanation.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e428348. Configure here.

}
Expand Down
Loading