Discover multisig flow#506
Conversation
|
Looks good I would suggest to show the human checkphrase on all multisigs, so we can easily distinguish them, talk about them etc |
…us-Network/quantus-apps into beast/discover-multisig-flow
|
I've reviewed the full diff plus the surrounding code on the PR branch. Here's my assessment. OverallSolid PR that cleanly swaps the multisig discovery stub for a real GraphQL-backed flow, removes the now-dead "add from address" path, wires logout cleanup, and adds good unit-test coverage for the mapping/query-building helpers. The localization, menu entry, and provider changes all hang together and there are no leftover references to the removed symbols ( A few things worth addressing before merge: High priority — checksum can render against the wrong rowIn itemBuilder: (context, index) {
final account = sorted[index];
return _DiscoverMultisigRow(account: account, ...); // no key
}
Fix is a one-liner — give the row a stable identity: return _DiscoverMultisigRow(
key: ValueKey(account.accountId),
account: account,
...
);(Alternatively handle Medium — DRY: duplicated GraphQL request/error plumbing
Future<Map<String, dynamic>?> _postGraphQl({required String query, Map<String, dynamic>? variables}) async {
final body = {'query': query, if (variables != null) 'variables': variables};
final response = await _graphQlEndpointService.post(body: jsonEncode(body));
if (response.statusCode != 200) {
throw Exception('GraphQL request failed with status: ${response.statusCode}. Body: ${response.body}');
}
final responseBody = jsonDecode(response.body) as Map<String, dynamic>;
if (responseBody['errors'] != null) {
throw Exception('GraphQL errors: ${responseBody['errors']}');
}
return responseBody['data'] as Map<String, dynamic>?;
}Heads up: this exact pattern is also duplicated in Medium — silent fallbacks vs. "fail early"In final signers = signersRaw is List ? signersRaw.map((e) => e.toString()).toList() : <String>[];
final rawThreshold = record['threshold'] as int?;
final threshold = rawThreshold != null && rawThreshold >= 1 ? rawThreshold : 1;Per the fail-early rule, malformed indexer records (missing/empty Relatedly, Low — GraphQL value interpolation vs. variables
Minor / nits
I have not posted anything to GitHub. Want me to drop this in as a PR review comment, or apply the high-priority |
…lso have intFromJson for supporting hasura stringify number.
…us-Network/quantus-apps into beast/discover-multisig-flow
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 4 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 6ffead9. Configure here.
| }); | ||
|
|
||
| final current = state.value ?? []; | ||
| state = AsyncValue.data([...current, account]); |
There was a problem hiding this comment.
Multisig state clobbered on add
High Severity
add, updateName, and remove now rebuild in-memory state from state.value ?? [] after persisting. While the notifier is still loading (or in error), value is null, so updates replace the list with a single change or an empty list. A finishing _load can also overwrite newer saves with a stale snapshot, hiding multisigs in the UI until restart.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 6ffead9. Configure here.
| final colors = context.colors; | ||
| final text = context.themeText; | ||
| final discoveredAsync = ref.watch(discoveredMultisigsProvider); | ||
| final savedIds = (ref.watch(multisigAccountsProvider).value ?? []).map((a) => a.accountId).toSet(); |
There was a problem hiding this comment.
Discover ignores saved multisig load
Medium Severity
The screen treats multisigAccountsProvider as empty while it is still loading (value ?? []), so already-saved multisigs show an active Add button and default naming uses a zero count. Users can trigger duplicate add attempts and misleading labels until the provider finishes loading.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 6ffead9. Configure here.
| me, | ||
| _dummySigner('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'), | ||
| _dummySigner('5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty'), | ||
| ], |
There was a problem hiding this comment.
One bad row fails discovery
Medium Severity
discoverForUser maps each indexer record without isolating parse failures. A single malformed signers, threshold, or id field throws from multisigAccountFromIndexerRecord and fails the entire discover query instead of skipping that row.
Reviewed by Cursor Bugbot for commit 6ffead9. Configure here.
| final rawThreshold = multisig['threshold'] as int?; | ||
| final threshold = rawThreshold != null && rawThreshold >= 1 ? rawThreshold : 1; | ||
| final signers = nonEmptyStringListFromJson(multisig['signers'], 'signers'); | ||
| final threshold = multisigThresholdFromJson(multisig['threshold'], signerCount: signers.length); |
There was a problem hiding this comment.
Strict multisig parse breaks history
Medium Severity
MultisigCreatedEvent.fromMultisigGraphql now throws on missing or invalid signers/threshold, but ChainHistoryService.tryParseOtherTransferEvent still calls it without catching. One bad multisig account event can abort the whole other-transfers fetch instead of being skipped.
Reviewed by Cursor Bugbot for commit 6ffead9. Configure here.
n13
left a comment
There was a problem hiding this comment.
Review: PR #506 — Discover multisig flow
Review method: Static read-only review in an isolated throwaway git worktree, diffed against the PR's actual base branch beast/create-multisig-flow (not main), since this is a stacked PR.
git diff origin/beast/create-multisig-flow...HEAD --stat
16 files changed, +666 / -158
The worktree lacks .dart_tool / generated FRB bindings, so this is a static review only — no build, analyzer, or test execution was performed.
Summary
This PR replaces the stubbed multisig discovery with a real Hasura GraphQL discover query (filtered by the user as a signer), tightens indexer parsing to fail loudly, drops lookupByAddress / multisigLookupProvider and the paste-SS58 add flow, and wires multisig state into logout invalidation. The change is well-scoped, well-tested, and aligns strongly with the repo's "fail early" philosophy. I found no security issues, no dangling references, and no confirmed correctness bug in the normal operating path. There are two non-blocking medium design/DRY considerations and a few nits worth addressing before merge.
What's great
- Real discover query with good unit coverage.
MultisigGraphql.discoverQuery/buildDiscoverVariables(quantus_sdk/lib/src/services/multisig_graphql.dart:60-95) are covered by tests for the query shape, single vs multi-account_orvariables, and empty-inputArgumentError(quantus_sdk/test/multisig_service_test.dart:164-1197). Mapping helpers (parseMultisigDiscoverData,multisigAccountFromIndexerRecord,resolveMyMemberAccountId) are also unit-tested, including the throw-on-malformed paths. - Stricter parsing genuinely fails loudly.
intFromJson,nonEmptyStringListFromJson, andmultisigThresholdFromJson(quantus_sdk/lib/src/models/json_dynamic_parse.dart:35-59) throwFormatExceptionon missing/invalid/empty/out-of-range values rather than silently defaulting.MultisigCreatedEvent.fromMultisigGraphql(quantus_sdk/lib/src/models/multisig_created_event.dart:80-106) now uses them, replacing the oldthreshold ?? 1/signers ?? []silent defaults — and the tests were updated to assert the throws (quantus_sdk/test/chain_history_service_test.dart:49-92). This is exactly the "fail early, no silent defaults" behavior the repo rules call for. - Intra-service DRY improvement.
_postGraphQl(quantus_sdk/lib/src/services/multisig_service.dart:188-201) consolidates the POST + status-code +errors+datahandling, removing the inline duplicate previously infetchMultisigFromIndexerand sharing it with the new discover path. - Logout now clears multisig state.
logout_service.dart:34-35addsmultisigAccountsProvider.notifier).reset()andinvalidate(discoveredMultisigsProvider). This fixes a real cross-session leak (previously, saved multisigs from a prior wallet were never cleared on logout). - Mutations no longer silently dropped.
add/updateName/removenow operate onstate.value ?? []instead ofstate.whenData(...)(mobile-app/lib/providers/multisig_providers.dart:27-50). Previously, mutating while the notifier was inloading/errorwas a no-op; now it always applies. - Clean removal. No dangling references remain to
lookupByAddress,multisigLookupProvider,_dummyMultisigs, or the removed l10n keys (multisigAddPasteAddressSection,multisigAddAddressHint,multisigAddFromAddressButton) — verified by repo-wide search. - Discover provider edge handling.
discoveredMultisigsProvider(mobile-app/lib/providers/multisig_providers.dart:71-87) rethrows onAsyncError(fail-early) and, while accounts are still loading, fetches them directly instead of returning a misleading empty result.ListViewrows useValueKey(account.accountId)so reordering is correct.
Medium (non-blocking — design / DRY)
M1. Stricter parsing widens the failure blast radius in activity history
MultisigCreatedEvent.fromAccountEvent is invoked by ChainHistoryService.tryParseOtherTransferEvent (quantus_sdk/lib/src/services/chain_history_service.dart:431-433), which runs inside _pageFromEvents without a per-event try/catch (chain_history_service.dart:385-398), and fetchOtherTransfers rethrows on any exception (chain_history_service.dart:571-577).
Consequence: now that parsing throws on an invalid threshold/empty signers, a single malformed multisig row will abort the entire "other transfers" history page, rather than skipping that one row. Pre-PR it would have rendered the row with threshold=1/empty signers (wrong, but non-fatal).
In practice this only triggers on genuine indexer corruption / schema regression (on-chain creation guarantees valid threshold/signers), and failing loudly on that is consistent with the repo's philosophy — so this is a deliberate tradeoff, not a bug. But please make it a conscious one: consider isolating per-event parsing in _pageFromEvents (skip + log the bad row loudly) so one corrupt record can't take down the whole feed, while still surfacing the error.
M2. _postGraphQl duplicates a GraphQL-POST pattern that exists across many services (DRY)
_postGraphQl is a good consolidation within MultisigService, but the identical "POST → check statusCode != 200 → check responseBody['errors'] → return data" boilerplate is repeated across the SDK: chain_history_service.dart (×4, e.g. :559-566), account_discovery_service.dart:59-65, taskmaster_service.dart:558-564, wormhole_utxo_service.dart. The natural shared home already exists: GraphQlEndpointService (quantus_sdk/lib/src/services/network/redundant_endpoint.dart:18, post() at :108).
This duplication is mostly pre-existing and not introduced by this PR (the PR is net-positive on DRY locally), so I'm not blocking on it. But per the repo's strict DRY rule, the cleanest move is to lift _postGraphQl into GraphQlEndpointService (e.g. a query(...) helper) and have all callers reuse it — ideally as a small follow-up rather than re-adding yet another private copy.
Nits / low severity
reset()diverges from the established convention.MultisigAccountsNotifier.reset()setsAsyncValue.data([])(mobile-app/lib/providers/multisig_providers.dart:59-61), whereas every sibling (AccountsNotifier,ActiveAccountNotifier,AccountAssociationsNotifier) resets toAsyncValue.loading()(account_providers.dart:51-53,88-90). Since these providers are re-created on the next login (theywatchsettingsServiceProvider/accountsServiceProvider), this is effectively cosmetic — but matching theloading()convention avoids a subtle stale-empty window and keeps the codebase consistent.buildDiscoverVariablessingle-vs-multiple branch is unnecessary (multisig_graphql.dart:79-92). The_orform handles a single account id correctly, so the special case adds branching for no functional gain. Harmless and tested, but could be one code path.- Discover-time name is dead.
discoverForUserpassesname: 'Multisig $index'(multisig_service.dart:56), but the name is always overwritten on add viamultisigCreateDefaultName(savedCount + 1)(discover_multisig_screen.dart:30-32), and the row UI shows the checksum, notaccount.name. Theindexcounter / name argument are effectively unused output. - Hardcoded English fallback string.
_DiscoverMultisigRowsets_checksum = 'Error getting checksum'on checksum-lookup failure (discover_multisig_screen.dart), which is not localized like the rest of the screen. It does log viaquantusDebugPrintfirst (so fail-early is satisfied), but the user-visible string should come froml10n. - Effectively-unreachable defensive code. In
discoverForUser, theseendedup and theif (myMember == null) continueare unreachable in practice (Hasura returns distinct rows, and thewhereclause already guarantees one ofmyAccountIdsis a signer, soresolveMyMemberAccountIdwill always match). Harmless, but the silentcontinuemildly conflicts with the "no silent drops" rule if it ever did fire — a debug log there would be more in keeping with the repo style.
Things to verify outside static review
- SS58 format parity. The discover
_containsfilter andresolveMyMemberAccountIdrely on the localaccount.accountIdstrings being byte-identical to the indexer'ssignersentries (same SS58 prefix/encoding). If the indexer stores a different format, discovery silently returns empty. Worth confirming against the indexer schema. Also confirmsignersis a Postgres array column so_containsmaps to@>membership as intended. - Tests/analyzer.
multisig_service_test.dartandchain_history_service_test.dartadditions look correct by inspection; please confirmflutter test+dart analyzeare green in CI (not runnable in this worktree).
Verdict
Comment (approve with suggestions). No blocking correctness or security issues and no dangling references; the discover query, strict parsing, and logout wiring are solid and well-tested. The two medium items (activity-history failure blast radius from the stricter parse, and lifting the duplicated GraphQL-POST helper into GraphQlEndpointService) are design/DRY improvements rather than bugs and can be addressed here or as a fast follow-up; the reset() convention mismatch and other nits are minor.
…us-Network/quantus-apps into beast/discover-multisig-flow
* wip: create proposal - finish create flow - create history of proposal * feat: improve UX of propose amount and review screen * feat: finish improving the lifecycle of proposal creation and event listing presentation * feat: address code review issues * feat: another code reviews fixes * chore: formatting * fix: dart rule violation * fix: redudancy * fix: graphql query for multisig * fix: circle dependency * feat: explicitly show failed fee fetch - add retry button - show message - silently refetch network fee unless failed * fix: reduce risk of double proposal creation * fix: handle last effort before timeout * chore: formatting
* feat: localize copy writing * Format and regrenate runtime apis * feat: finish create multisig flow * fix: code reviews * fix: code reviews * feat: finish adding in-flight tracking for multisig creation - reconcilate pending multisig creation - track multisig creation * feat: improve multisig creation - make nonce calculated on the fly - only show history for creator * feat: add checksum and not truncate checksum * fix: create multisig screen * fix: add logging on error * feat: improve UX multisig creation pending and details sheet * fix: preflight balance check * fix: DRY violation * fix: silent or masking bad data * fix: persisting pending multisig creation * chore: formatting * Discover multisig flow (#506) * feat: finish adding discover multisig * fix: code review issues * fix: another code review issues * feat: add checksum on discover list item * fix: listview item non properly ordered without key * feat: refactor repeating multisig graphql handling * feat: make json reading throw early instead of silently droping it. Also have intFromJson for supporting hasura stringify number. * fix: use variable instead of string interpolation * feat: propagate error as to not confuse user * Create proposal flow (#507) * wip: create proposal - finish create flow - create history of proposal * feat: improve UX of propose amount and review screen * feat: finish improving the lifecycle of proposal creation and event listing presentation * feat: address code review issues * feat: another code reviews fixes * chore: formatting * fix: dart rule violation * fix: redudancy * fix: graphql query for multisig * fix: circle dependency * feat: explicitly show failed fee fetch - add retry button - show message - silently refetch network fee unless failed * fix: reduce risk of double proposal creation * fix: handle last effort before timeout * chore: formatting
* msig implementation 1 * linter fixes and package updates * Create multisig flow (#505) * feat: localize copy writing * Format and regrenate runtime apis * feat: finish create multisig flow * fix: code reviews * fix: code reviews * feat: finish adding in-flight tracking for multisig creation - reconcilate pending multisig creation - track multisig creation * feat: improve multisig creation - make nonce calculated on the fly - only show history for creator * feat: add checksum and not truncate checksum * fix: create multisig screen * fix: add logging on error * feat: improve UX multisig creation pending and details sheet * fix: preflight balance check * fix: DRY violation * fix: silent or masking bad data * fix: persisting pending multisig creation * chore: formatting * Discover multisig flow (#506) * feat: finish adding discover multisig * fix: code review issues * fix: another code review issues * feat: add checksum on discover list item * fix: listview item non properly ordered without key * feat: refactor repeating multisig graphql handling * feat: make json reading throw early instead of silently droping it. Also have intFromJson for supporting hasura stringify number. * fix: use variable instead of string interpolation * feat: propagate error as to not confuse user * Create proposal flow (#507) * wip: create proposal - finish create flow - create history of proposal * feat: improve UX of propose amount and review screen * feat: finish improving the lifecycle of proposal creation and event listing presentation * feat: address code review issues * feat: another code reviews fixes * chore: formatting * fix: dart rule violation * fix: redudancy * fix: graphql query for multisig * fix: circle dependency * feat: explicitly show failed fee fetch - add retry button - show message - silently refetch network fee unless failed * fix: reduce risk of double proposal creation * fix: handle last effort before timeout * chore: formatting * Sign proposal flow (#510) * feat: finish approve proposal flow from multisig account * feat: show approve event in signer account * feat: resolve DRY and simplicity issues * feat: best effort timout and balance refresh * chore: formatting * chore: revert constants * chore: remove dead code * Execute proposal flow (#511) * feat: finish approve proposal flow from multisig account * feat: show approve event in signer account * feat: resolve DRY and simplicity issues * feat: best effort timout and balance refresh * chore: formatting * wip: add execute flow * feat: regenerate polkadart metadata, clean up deposit field and rename interceptor to guardian * fix: live state resolution and update executed proposal UI * feat: improve UX executed proposals * chore: formatting * chore: revert constants * feat: show activity in executor * chore: formatting * chore: revert back chain url * fix: code reviews * fix: execute event query and data model * chore: revert constants * chore: formatting * Cancel proposal flow (#512) * feat: finish approve proposal flow from multisig account * feat: show approve event in signer account * feat: resolve DRY and simplicity issues * feat: best effort timout and balance refresh * chore: formatting * wip: add execute flow * feat: regenerate polkadart metadata, clean up deposit field and rename interceptor to guardian * fix: live state resolution and update executed proposal UI * feat: improve UX executed proposals * chore: formatting * chore: revert constants * feat: show activity in executor * chore: formatting * feat: add cancel proposal flow * feat: display cancel proposal event in proposer activity * chore: formatting * chore: revert constants * chore: revert back chain url * fix: code reviews * fix: execute event query and data model * chore: revert constants * chore: formatting * fix: code review issues * Multisig proposal polling (#513) * feat: finish approve proposal flow from multisig account * feat: show approve event in signer account * feat: resolve DRY and simplicity issues * feat: best effort timout and balance refresh * chore: formatting * wip: add execute flow * feat: regenerate polkadart metadata, clean up deposit field and rename interceptor to guardian * fix: live state resolution and update executed proposal UI * feat: improve UX executed proposals * chore: formatting * chore: revert constants * feat: show activity in executor * chore: formatting * feat: add cancel proposal flow * feat: display cancel proposal event in proposer activity * chore: formatting * chore: revert constants * feat: finish implementing polling and refresh proposals * chore: formatting * chore: revert back chain url * fix: code reviews * fix: execute event query and data model * chore: revert constants * chore: formatting * fix: code review issues * fix: code review issues * fix: broken initialization of timezone * chore: add debug on error * fix: code review issues --------- Co-authored-by: Nikolaus Heger <nheger@gmail.com>



Summary
Screenshots
Note
Medium Risk
Replaces stub discovery with live GraphQL and stricter indexer parsing, which can surface new errors or empty results; multisig account state changes on logout affect wallet session boundaries.
Overview
Adds a dedicated Discover Multisig path from Add Account (new menu row →
DiscoverMultisigScreen) so users can find on-chain multisigs where their wallet accounts are signers and add them to the app. Create Multisig stays on its own screen; the paste SS58 address flow and related copy are removed in favor of discovery-only add.SDK:
MultisigService.discoverForUseris wired to a real Hasura GraphQL discover query (replacing stub/dummy data);lookupByAddressandmultisigLookupProviderare dropped. Indexer parsing is stricter—signers/threshold must be valid (no silent defaults)—with shared helpers injson_dynamic_parse.dartand a shared_postGraphQlpath.State / lifecycle: Multisig list mutations always update provider state (not only when already loaded);
multisigAccountsProvidergetsreset()and logout invalidates discovery. Wallet import is documented/clarified as HD-only; multisigs are added via Discover Multisig.Reviewed by Cursor Bugbot for commit 6ffead9. Configure here.