Skip to content

feat: let users view and export federation invite codes - #570

Open
FrankChinedu wants to merge 5 commits into
fedimint:masterfrom
FrankChinedu:feat/export-invite-codes
Open

feat: let users view and export federation invite codes#570
FrankChinedu wants to merge 5 commits into
fedimint:masterfrom
FrankChinedu:feat/export-invite-codes

Conversation

@FrankChinedu

Copy link
Copy Markdown
Contributor

The seed phrase restores funds but records nothing about which federations hold them, and the Nostr backup of the invite codes is best effort by design (relays may prune). Until now the app gave the user no way to keep their own copy, so if that backup missed there was no fallback (fixes #501).

Adds a Settings screen listing each joined federation with its invite code, individually copyable plus a "copy all" that labels each code with its federation so the export is meaningful pasted beside a written seed phrase. Also nudges toward it from the seed backup screen, which is where the user is actually writing a backup down.

Backed by a new get_federation_invite_codes rather than the existing get_all_invite_codes: the latter returns a flat list with one code per guardian peer and no federation attached, which is right for the Nostr backup (redundancy raises the chance of a successful rejoin) but wrong for a list a human reads. The new one returns one code per federation, taking the first peer whose code resolves rather than assuming peer 0 exists.

Invite codes carry no spending authority, so unlike the seed phrase the screen is deliberately not behind the PIN gate.

The seed phrase restores funds but records nothing about which
federations hold them, and the Nostr backup of the invite codes is
best effort by design (relays may prune). Until now the app gave the
user no way to keep their own copy, so if that backup missed there
was no fallback (fixes fedimint#501).

Adds a Settings screen listing each joined federation with its invite
code, individually copyable plus a "copy all" that labels each code
with its federation so the export is meaningful pasted beside a
written seed phrase. Also nudges toward it from the seed backup
screen, which is where the user is actually writing a backup down.

Backed by a new `get_federation_invite_codes` rather than the
existing `get_all_invite_codes`: the latter returns a flat list with
one code per guardian peer and no federation attached, which is right
for the Nostr backup (redundancy raises the chance of a successful
rejoin) but wrong for a list a human reads. The new one returns one
code per federation, taking the first peer whose code resolves rather
than assuming peer 0 exists.

Invite codes carry no spending authority, so unlike the seed phrase
the screen is deliberately not behind the PIN gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cae16cb2d2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread lib/screens/invite_codes_screen.dart Outdated
child: CopyableDetailRow(
label: entry.$1.federationName,
value: entry.$2,
abbreviate: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Show the full invite code for paper backups

When a user follows the screen's guidance to save these codes alongside a written seed phrase, abbreviate: true reduces every invite code to its first and last seven characters, with no control to reveal the omitted text. Copying supports a digital export, but the advertised view cannot be transcribed or otherwise inspected; render the complete wrapping value or provide an explicit full-code/QR view.

Useful? React with 👍 / 👎.

The screen tells the user to record these codes alongside a written
seed phrase, but rendered each one abbreviated: getAbbreviatedText
reduces anything over 14 characters to first7...last7, so a ~300
character invite code displayed as about 17, with no way to reveal
the rest. Copying still worked, but the view the screen advertises
could not be written down or checked against an earlier copy.

Drops `abbreviate` so the value renders in full and wraps. The row
already uses a monospace face, which is what makes reading a long
code back character by character workable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 75c0f57f2f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread lib/screens/invite_codes_screen.dart Outdated
void _copyAll(List<(FederationSelector, String)> codes) {
Clipboard.setData(ClipboardData(text: _asExportText(codes)));
ToastService().show(
message: context.l10n.allInviteCodesCopied,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Await the clipboard write before reporting success

If the platform clipboard call rejects—for example because the channel is unavailable or browser clipboard access is denied—this ignored future still allows the success toast to appear, leaving the user with no exported backup despite being told all codes were copied. Make _copyAll async, await Clipboard.setData, and show an error toast when it fails.

AGENTS.md reference: AGENTS.md:L146-L148

Useful? React with 👍 / 👎.

Comment thread rust/ecashapp/src/multimint.rs Outdated
Comment on lines +6318 to +6320
if let Some(invite_code) = client.invite_code(*peer).await {
invite_codes.push((selector, invite_code.to_string()));
break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve guardian fallback in exported backups

When the single guardian represented by this chosen code is unreachable at restore time, the saved backup cannot bootstrap the federation even if its other guardians and quorum are healthy. The existing Nostr backup deliberately retains every peer code for exactly this fallback, so the user-facing export should likewise retain all peer codes per federation (while grouping them under one label) rather than stopping after the first.

Useful? React with 👍 / 👎.

Two review findings on the invite code export.

The exported code was built from a single guardian, so a backup made
from it could not bootstrap the federation if that one guardian was
unreachable at restore time, even with the rest of the federation
healthy. Build the code with InviteCode::new_with_essential_num_guardians
instead, which packs max_evil() + 1 guardians into one code — enough to
always reach a working federation.

That keeps one code per federation rather than listing every peer's, as
the Nostr backup does. The redundancy lives inside the single code, so
the list stays something a person can read and transcribe, which is the
point of the screen.

The "all invite codes copied" toast also fired without awaiting the
clipboard write, so a rejected write — an unavailable channel, or a
browser denying access — still told the user they had a backup. Await
it and toast the failure instead, per the error handling convention in
AGENTS.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5347feab9d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread rust/ecashapp/src/multimint.rs Outdated
Comment on lines +6310 to +6312
if !joined.contains(&key.id) {
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include persisted federations when clients are absent

When recovery finishes but reopening the replacement client fails, retire_recovery_client leaves the federation's config persisted while removing its client until the next launch; this filter therefore silently omits that federation from the exported backup even though the stored config below contains everything needed to construct its invite code. Since leaving deletes the config before removing the client, iterating all remaining configs is sufficient and preserves the user's backup in this recovery-failure state.

Useful? React with 👍 / 👎.

Comment thread lib/screens/invite_codes_screen.dart Outdated
Comment on lines +159 to +162
child: CopyableDetailRow(
label: entry.$1.federationName,
value: entry.$2,
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle failures when copying an individual code

Although _copyAll now awaits its clipboard write, each per-card copy action still delegates to CopyableDetailRow._copyToClipboard, which ignores the Clipboard.setData future and immediately displays its check icon. If clipboard access is rejected or the platform channel is unavailable, the user is therefore told this backup succeeded when nothing was copied; make this path await the write and surface a failure toast as well.

AGENTS.md reference: AGENTS.md:L146-L148

Useful? React with 👍 / 👎.

Comment thread rust/ecashapp/src/multimint.rs Outdated
Comment on lines +6342 to +6343
let invite_code =
InviteCode::new_with_essential_num_guardians(&peer_urls, key.id).to_string();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve API secrets in exported invite codes

For a private federation whose original invite carries an API secret, rebuilding the export from only peer_urls and the federation ID drops that credential. The client-generated invite path retains and later passes invite.api_secret() when constructing the guardian API, so the displayed backup produced here cannot authenticate to such a federation during restore; construct the multi-guardian code while preserving the client's invite secret, or export the original authenticated peer codes.

Useful? React with 👍 / 👎.

Three more review findings on the invite code export.

Rebuilding the code from peer URLs and the federation id alone dropped
the api secret, so a private federation's exported backup could not
authenticate on restore. Build with InviteCode::from_map, which takes
the same multi-guardian set plus the secret. The secret is not in the
persisted config, only on the client's own invite code, so it is read
from there when a client is open; when none is, the federation is
still exported without it rather than omitted.

Listing was also filtered by the client map, which drops a federation
whose config outlives its client — retire_recovery_client leaves
exactly that gap when reopening the replacement client fails. Since
leave_federation deletes the config before removing the client, every
remaining config is a federation the user is still in, so iterate
those instead.

CopyableDetailRow ignored its Clipboard.setData future and showed the
copied tick regardless, so a rejected write told the user their value
was copied when nothing was. Await it and toast the failure instead.
That tick is the only feedback the row gives, and it is used for
seed-adjacent values across the app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b469fffd45

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}
}

let invite_code = InviteCode::from_map(&peer_urls, key.id, api_secret).to_string();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not export unauthenticated private-federation codes

When a private federation's client is absent—such as after retire_recovery_client removes it and reopening the replacement fails—api_secret remains None, yet this line still exports the code and the UI presents it as a usable backup; restoration will fail because guardians cannot authenticate it. Fresh evidence in the current patch is the explicit client-less fallback that knowingly constructs the code without the required secret. Persist the authenticated invite data before retiring the client, or return/surface an error instead of claiming that this federation was backed up.

AGENTS.md reference: AGENTS.md:L146-L148

Useful? React with 👍 / 👎.

Comment thread lib/screens/invite_codes_screen.dart Outdated
Widget build(BuildContext context) {
final theme = Theme.of(context);

return Scaffold(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Protect authenticated invite codes as bearer secrets

For private federations, the Rust path deliberately embeds an API secret that authenticates whoever holds the resulting invite code, so the assertion that every displayed code is non-secret is incorrect. Returning a bare Scaffold also bypasses both the sensitive-action PIN check and SecureScreen, whose repository documentation says bearer secrets must be protected from Android screenshots, screen recordings, and app-switcher thumbnails; gate this screen and wrap the rendered codes accordingly, or distinguish public codes from secret-bearing ones.

Useful? React with 👍 / 👎.

let mut api_secret = None;
if let Some(client) = clients.get(&key.id) {
for peer in peer_urls.keys() {
if let Some(invite) = client.invite_code(*peer).await {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound guardian lookups while loading the backup

When the first queried guardian is unreachable or black-holed, this per-peer network request has no timeout, and the sequential loop cannot try another peer or return codes for any other federation. InviteCodesScreen._load awaits the entire function while showing only a progress indicator, so opening the backup screen can remain stuck indefinitely; apply a per-peer timeout and continue to the next endpoint, or bound the complete operation and surface the failure.

Useful? React with 👍 / 👎.

Exported codes now embed a private federation's api secret, which
authenticates whoever holds it, so this screen can render a bearer
credential. It was returning a bare Scaffold, leaving the codes free
to reach a screenshot, a screen recording or the app-switcher
thumbnail — and a backup screenshotted into a cloud-synced camera
roll is exactly the leak worth closing. Wrap it in SecureScreen, as
the seed phrase and ecash token screens already are.

Left off the PIN gate deliberately. None of these codes can spend
funds — that needs the seed phrase, which is gated separately — and
this screen exists to encourage a backup the user does not currently
make. Friction works against that, and the gate is a no-op anyway for
anyone who has not set a PIN. The doc comment claiming every code is
non-secret is corrected: that is true of public federations only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Let users view and export their federation invite codes

1 participant