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
16 changes: 16 additions & 0 deletions mobile-app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,18 @@
}
}
},
"multisigCreateKeystoneAction": "Create {count} of {total} multisig",
"@multisigCreateKeystoneAction": {
"description": "What a Keystone creator is signing, shown on the Keystone sign and verify screens",
"placeholders": {
"count": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"multisigCreatePredictedAddressLabel": "MULTISIG ADDRESS",
"@multisigCreatePredictedAddressLabel": {
"description": "Label for predicted multisig address preview"
Expand Down Expand Up @@ -1470,6 +1482,10 @@
"@keystoneSignInstruction": {
"description": "Instruction on the Keystone sign screen"
},
"keystoneSignActionLabel": "ACTION",
"@keystoneSignActionLabel": {
"description": "Label for a non-transfer action on the Keystone sign and verify screens"
},
"keystoneSignYouAreSigning": "YOU ARE SIGNING",
"@keystoneSignYouAreSigning": {
"description": "Section label above the transaction details on the Keystone sign screen"
Expand Down
1 change: 1 addition & 0 deletions mobile-app/lib/l10n/app_id.arb
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"multisigCreateInvalidSigner": "Masukkan alamat penandatangan yang valid.",
"multisigCreateThresholdLabel": "AMBANG BATAS",
"multisigCreateThresholdValue": "{count} dari {total}",
"multisigCreateKeystoneAction": "Buat multisig {count} dari {total}",
"multisigCreatePredictedAddressLabel": "ALAMAT MULTISIG",
"multisigCreatePredictedAddressPlaceholder": "Tambahkan penandatangan untuk melihat alamat",
"multisigDone": "Selesai",
Expand Down
12 changes: 12 additions & 0 deletions mobile-app/lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,12 @@ abstract class AppLocalizations {
/// **'{count} of {total}'**
String multisigCreateThresholdValue(int count, int total);

/// What a Keystone creator is signing, shown on the Keystone sign and verify screens
///
/// In en, this message translates to:
/// **'Create {count} of {total} multisig'**
String multisigCreateKeystoneAction(int count, int total);

/// Label for predicted multisig address preview
///
/// In en, this message translates to:
Expand Down Expand Up @@ -1964,6 +1970,12 @@ abstract class AppLocalizations {
/// **'Open your Keystone and scan this QR code to load the transaction.'**
String get keystoneSignInstruction;

/// Label for a non-transfer action on the Keystone sign and verify screens
///
/// In en, this message translates to:
/// **'ACTION'**
String get keystoneSignActionLabel;

/// Section label above the transaction details on the Keystone sign screen
///
/// In en, this message translates to:
Expand Down
8 changes: 8 additions & 0 deletions mobile-app/lib/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,11 @@ class AppLocalizationsEn extends AppLocalizations {
return '$count of $total';
}

@override
String multisigCreateKeystoneAction(int count, int total) {
return 'Create $count of $total multisig';
}

@override
String get multisigCreatePredictedAddressLabel => 'MULTISIG ADDRESS';

Expand Down Expand Up @@ -1031,6 +1036,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get keystoneSignInstruction => 'Open your Keystone and scan this QR code to load the transaction.';

@override
String get keystoneSignActionLabel => 'ACTION';

@override
String get keystoneSignYouAreSigning => 'YOU ARE SIGNING';

Expand Down
8 changes: 8 additions & 0 deletions mobile-app/lib/l10n/app_localizations_id.dart
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,11 @@ class AppLocalizationsId extends AppLocalizations {
return '$count dari $total';
}

@override
String multisigCreateKeystoneAction(int count, int total) {
return 'Buat multisig $count dari $total';
}

@override
String get multisigCreatePredictedAddressLabel => 'ALAMAT MULTISIG';

Expand Down Expand Up @@ -1034,6 +1039,9 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get keystoneSignInstruction => 'Open your Keystone and scan this QR code to load the transaction.';

@override
String get keystoneSignActionLabel => 'ACTION';

@override
String get keystoneSignYouAreSigning => 'YOU ARE SIGNING';

Expand Down
168 changes: 82 additions & 86 deletions mobile-app/lib/services/multisig_submission_service.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:typed_data';

import 'package:convert/convert.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Expand All @@ -10,6 +11,10 @@ import 'package:resonance_network_wallet/services/multisig_creation_polling_serv
import 'package:resonance_network_wallet/services/telemetry_service.dart';
import 'package:resonance_network_wallet/shared/utils/print.dart';

/// Everything a creation needs once on-chain checks have passed: the draft
/// account (with the resolved nonce) and the network fee it was checked with.
typedef MultisigCreationPreflight = ({MultisigAccount draft, BigInt networkFee});

class MultisigSubmissionService {
MultisigSubmissionService(this._ref);

Expand All @@ -20,88 +25,7 @@ class MultisigSubmissionService {
/// Throws [MultisigAlreadyExistsException] if the predicted address already
/// exists, or [MultisigInsufficientBalanceException] if the creator cannot
/// afford pallet fee + network fee.
Future<void> preflightMultisigCreation({
required List<String> signers,
required int threshold,
required Account creator,
BigInt? nonce,
}) async {
await _runCreationPreflight(name: '', signers: signers, threshold: threshold, creator: creator, nonce: nonce);
}

/// Preflight on-chain state, then submit and track creation.
///
/// 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. 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,
required int threshold,
required Account creator,
BigInt? nonce,
}) async {
final preflight = await _runCreationPreflight(
name: name,
signers: signers,
threshold: threshold,
creator: creator,
nonce: nonce,
);

final draft = preflight.draft;
final networkFee = preflight.networkFee;

TelemetryService().sendEvent('multisig_create_started');
await _ref
.read(pendingMultisigCreationsProvider.notifier)
.add(PendingMultisigCreationEvent.fromDraft(draft, networkFee: networkFee), draft);

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

Future<void> _submitAndTrack({
required Account creator,
required List<String> signers,
required int threshold,
required BigInt nonce,
required MultisigAccount draft,
}) async {
final service = _ref.read(multisigServiceProvider);
try {
quantusPrint('[MultisigSubmission] submitting creation for ${draft.accountId}');

final hashBytes = await service.submitCreateMultisigExtrinsic(
creator: creator,
signers: signers,
threshold: threshold,
nonce: nonce,
);
final extrinsicHash = '0x${hex.encode(hashBytes)}';
quantusPrint('[MultisigSubmission] submitted $extrinsicHash');

unawaited(
_ref.read(pendingMultisigCreationsProvider.notifier).updateExtrinsicHash(draft.accountId, extrinsicHash),
);

final submittedAt = _ref.read(pendingMultisigCreationsProvider.notifier).recordFor(draft.accountId)?.submittedAt;
_ref.read(multisigCreationPollingServiceProvider).startPolling(draft, submittedAt: submittedAt);
} catch (e, stackTrace) {
// Retries live in SubstrateService.submitExtrinsic (same signed bytes);
// avoid outer retries here because each attempt re-signs with a fresh
// nonce and can double-submit if a prior submit already landed.
quantusPrint('[MultisigSubmission] submit failed: $e');
quantusPrint('Stack trace: $stackTrace');
TelemetryService().sendError('multisig_create_submit_failed', error: e);
removePendingMultisigCreation(_ref, draft.accountId);
rethrow;
}
}

Future<({MultisigAccount draft, BigInt networkFee})> _runCreationPreflight({
Future<MultisigCreationPreflight> preflightMultisigCreation({
required String name,
required List<String> signers,
required int threshold,
Expand Down Expand Up @@ -133,10 +57,7 @@ class MultisigSubmissionService {

final networkFee = await _ref
.read(substrateServiceProvider)
.getFeeForCall(
creator,
service.buildCreateMultisigCall(signers: signers, threshold: threshold, nonce: effectiveNonce),
)
.getFeeForCall(creator, buildCreateCall(draft))
.then((data) => data.fee);

final totalCost = MultisigCreationDraftFields.fromDraft(draft, networkFee: networkFee).totalCost;
Expand All @@ -147,6 +68,81 @@ class MultisigSubmissionService {

return (draft: draft, networkFee: networkFee);
}

/// The `create_multisig` call for [draft]; also the Keystone unsigned payload.
RuntimeCall buildCreateCall(MultisigAccount draft) => _ref
.read(multisigServiceProvider)
.buildCreateMultisigCall(signers: draft.signers, threshold: draft.threshold, nonce: draft.nonce);

/// Signs the creation with [creator]'s local key, submits it and tracks it.
///
/// 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<String> startMultisigCreation({required MultisigCreationPreflight preflight, required Account creator}) {
final draft = preflight.draft;
return _submitAndTrack(
preflight,
telemetryEvent: 'multisig_create_started',
submit: () => _ref
.read(multisigServiceProvider)
.submitCreateMultisigExtrinsic(
creator: creator,
signers: draft.signers,
threshold: draft.threshold,
nonce: draft.nonce,
),
);
}

/// Submits a creation signed off-device (Keystone) and tracks it.
Future<String> submitExternallySignedMultisigCreation({
required MultisigCreationPreflight preflight,
required UnsignedTransactionData unsignedData,
required Uint8List signatureWithPublicKey,
}) {
return _submitAndTrack(
preflight,
telemetryEvent: 'multisig_create_hardware',
submit: () => _ref
.read(substrateServiceProvider)
.submitExtrinsicWithExternalSignature(unsignedData, signatureWithPublicKey),
);
}

Future<String> _submitAndTrack(
MultisigCreationPreflight preflight, {
required String telemetryEvent,
required Future<Uint8List> Function() submit,
}) async {
final draft = preflight.draft;
final pending = _ref.read(pendingMultisigCreationsProvider.notifier);

TelemetryService().sendEvent(telemetryEvent);
await pending.add(PendingMultisigCreationEvent.fromDraft(draft, networkFee: preflight.networkFee), draft);

try {
quantusPrint('[MultisigSubmission] submitting creation for ${draft.accountId}');

final extrinsicHash = '0x${hex.encode(await submit())}';
quantusPrint('[MultisigSubmission] submitted $extrinsicHash');

unawaited(pending.updateExtrinsicHash(draft.accountId, extrinsicHash));

final submittedAt = pending.recordFor(draft.accountId)?.submittedAt;
_ref.read(multisigCreationPollingServiceProvider).startPolling(draft, submittedAt: submittedAt);
return extrinsicHash;
} catch (e, stackTrace) {
// Retries live in SubstrateService.submitExtrinsic (same signed bytes);
// avoid outer retries here because each attempt re-signs with a fresh
// nonce and can double-submit if a prior submit already landed.
quantusPrint('[MultisigSubmission] submit failed: $e');
quantusPrint('Stack trace: $stackTrace');
TelemetryService().sendError('multisig_create_submit_failed', error: e);
removePendingMultisigCreation(_ref, draft.accountId);
rethrow;
}
}
}

final multisigSubmissionServiceProvider = Provider<MultisigSubmissionService>((ref) {
Expand Down
Loading