-
Notifications
You must be signed in to change notification settings - Fork 54
fix(sdk): wallet-flow network fixes for SwiftExampleApp #3772
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v3.1-dev
Are you sure you want to change the base?
Changes from 2 commits
cea465e
ee59225
207b4f8
65807e7
076c0a0
cee4b2c
815751a
14d353e
5e76921
7f14210
92b6b28
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ struct CreateWalletView: View { | |
| @Environment(\.dismiss) var dismiss | ||
| @Environment(\.modelContext) private var modelContext | ||
| @EnvironmentObject var walletManager: PlatformWalletManager | ||
| @EnvironmentObject var walletManagerStore: WalletManagerStore | ||
| @EnvironmentObject var platformState: AppState | ||
|
|
||
| @State private var walletLabel: String = "" | ||
|
|
@@ -313,94 +314,102 @@ struct CreateWalletView: View { | |
| print("PIN length: \(walletPin.count)") | ||
| print("Import option enabled: \(showImportOption)") | ||
|
|
||
| // Determine primary network to create the wallet in (SDK enforces unique wallet per mnemonic) | ||
| let selectedNetworks: [Network] = [ | ||
| createForMainnet ? Network.mainnet : nil, | ||
| createForTestnet ? Network.testnet : nil, | ||
| (createForDevnet && shouldShowDevnet) ? Network.devnet : nil, | ||
| (createForRegtest && shouldShowRegtest) ? Network.regtest : nil, | ||
| ].compactMap { $0 } | ||
|
|
||
| guard let platformNetwork = selectedNetworks.first else { | ||
| guard !selectedNetworks.isEmpty else { | ||
| struct MissingNetwork: LocalizedError { | ||
| var errorDescription: String? { "No network selected" } | ||
| } | ||
| throw MissingNetwork() | ||
| } | ||
|
|
||
| // Create exactly one wallet via PlatformWalletManager. | ||
| // The Rust-side wallet creation emits | ||
| // `persistWalletMetadata` + `setWalletName`, which | ||
| // the persister callback translates into a | ||
| // `PersistentWallet` SwiftData row — no separate | ||
| // HDWallet mirror to maintain. We only have to | ||
| // patch `isImported` after-the-fact because that | ||
| // flag is UI-cosmetic and the persister doesn't | ||
| // know about it. | ||
| // Create the wallet in EVERY ticked network. Each | ||
| // network has its own `PlatformWalletManager` (the | ||
| // Rust manager is network-locked at construction and | ||
| // stamps its own network onto the wallet), so routing | ||
| // through the active manager alone would ignore the | ||
| // passed `network`. `backgroundManager(for:)` returns | ||
| // the warm cached manager for the active network and | ||
| // builds one on demand for the others. `walletId` is | ||
| // network-independent, so the Keychain mnemonic + | ||
| // metadata are written once and the `isImported` flag | ||
| // is stamped on every per-network row. | ||
| try await MainActor.run { | ||
| let managed = try walletManager.createWallet( | ||
| mnemonic: mnemonicPhrase, | ||
| network: platformNetwork, | ||
| name: walletLabel | ||
| ) | ||
| var createdWalletId: Data? | ||
| for net in selectedNetworks { | ||
| do { | ||
| let mgr = try walletManagerStore.backgroundManager(for: net) | ||
| let managed = try mgr.createWallet( | ||
| mnemonic: mnemonicPhrase, | ||
| network: net, | ||
| name: walletLabel | ||
| ) | ||
| createdWalletId = managed.walletId | ||
| } catch { | ||
| // An "already exists" throw for one network | ||
| // (wallet was previously created there) is | ||
| // non-fatal — keep creating the others. | ||
| SDKLogger.error( | ||
| "Wallet creation skipped for \(net.displayName): \(error.localizedDescription)" | ||
| ) | ||
| } | ||
|
Comment on lines
+365
to
+386
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: Brittle substring match on "already exists" to classify a benign error The branch classifying the per-network failure as benign relies on a case-insensitive substring search for source: ['claude']
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed it's brittle. Acknowledged but not changing in this PR — the proper fix is a typed FFI error code / dedicated
Comment on lines
+364
to
+386
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: Benign 'wallet already exists' classification still relies on a substring match against a Rust display string, now duplicated into WalletDetailView Carried-forward from cee4b2c — re-validated and STILL VALID. The per-network create loop classifies a benign duplicate by This PR also propagates the same heuristic to source: ['claude', 'codex']
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same finding as the existing thread on this file (#discussion_r3331111688) — acknowledged and deferred. The robust fix is a typed
Comment on lines
+365
to
+386
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: Duplicate-wallet classification still relies on substring-matching a Rust display string Carried forward from prior reviews (cee4b2c, 815751a) and still valid at 14d353e. The per-network create loop classifies a benign duplicate by source: ['claude', 'codex']
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Acknowledged, deferring (same as the existing thread on this file). The robust fix is a typed |
||
| } | ||
|
|
||
| guard let walletId = createdWalletId else { | ||
| struct AllNetworksFailed: LocalizedError { | ||
| var errorDescription: String? { | ||
| "Wallet could not be created on any selected network." | ||
| } | ||
| } | ||
| throw AllNetworksFailed() | ||
| } | ||
|
|
||
| // Persist the mnemonic in the iOS Keychain keyed | ||
| // by walletId so multiple wallets coexist and the | ||
| // recovery flow can enumerate all of them on | ||
| // launch. Best-effort — failure here doesn't | ||
| // block wallet creation. | ||
| // by walletId (network-independent) so the | ||
| // recovery flow can enumerate wallets on launch. | ||
| // Best-effort — failure here doesn't block. | ||
| let storage = WalletStorage() | ||
| do { | ||
| try storage.storeMnemonic( | ||
| mnemonicPhrase, | ||
| for: managed.walletId | ||
| ) | ||
| try storage.storeMnemonic(mnemonicPhrase, for: walletId) | ||
| } catch { | ||
| SDKLogger.error( | ||
| "Failed to persist mnemonic to keychain: \(error.localizedDescription)" | ||
| ) | ||
| } | ||
| // Stamp the `isImported` flag on the | ||
| // just-created PersistentWallet row. The | ||
| // persister callback runs synchronously from | ||
| // `walletManager.createWallet` via the | ||
| // background context; SwiftData's | ||
| // `autosaveEnabled = true` on that context | ||
| // propagates the row into the main context | ||
| // before this fetch runs. If the row somehow | ||
| // isn't there yet, the flag stays `false` | ||
| // (the default on `PersistentWallet`) — a | ||
| // cosmetic miss, not a correctness issue. | ||
| let walletIdMatch = managed.walletId | ||
|
|
||
| // Stamp `isImported` on every per-network row for | ||
| // this walletId. The persister callbacks run | ||
| // synchronously from `createWallet` via the | ||
| // background contexts; autosave propagates the | ||
| // rows into the main context before this fetch. | ||
| let descriptor = FetchDescriptor<PersistentWallet>( | ||
| predicate: #Predicate { $0.walletId == walletIdMatch } | ||
| predicate: PersistentWallet.predicate(walletId: walletId) | ||
| ) | ||
| let row = try? modelContext.fetch(descriptor).first | ||
| if let row = row { | ||
| let rows = (try? modelContext.fetch(descriptor)) ?? [] | ||
| for row in rows { | ||
| row.isImported = showImportOption | ||
| } | ||
| if !rows.isEmpty { | ||
| try? modelContext.save() | ||
| } | ||
| // Mirror the user-typed name + the networks the | ||
| // user explicitly ticked + the SPV-tip-derived | ||
| // birth height into the keychain alongside the | ||
| // mnemonic. Read back by the orphan-mnemonic | ||
| // recovery flow so a wipe + reinstall restores | ||
| // the original label / networks / birth height | ||
| // instead of resurrecting the wallet on testnet | ||
| // with a synthetic genesis. | ||
| // | ||
| // `selectedNetworks` carries every network the | ||
| // user ticked even though `walletManager` only | ||
| // currently consumes the first; persisting the | ||
| // full list now means the multi-network TODO on | ||
| // the Rust side won't need a metadata migration. | ||
|
|
||
| // Mirror the name + ticked networks + birth height | ||
| // into the keychain alongside the mnemonic so an | ||
| // orphan-recovery after a wipe restores the | ||
| // original label / networks / birth height. | ||
| do { | ||
| let metadata = WalletKeychainMetadata( | ||
| name: walletLabel, | ||
| walletDescription: nil, | ||
| networks: selectedNetworks.map { $0.networkName }, | ||
| birthHeight: row?.birthHeight | ||
| birthHeight: rows.first?.birthHeight | ||
| ) | ||
| try storage.setMetadata(metadata, for: managed.walletId) | ||
| try storage.setMetadata(metadata, for: walletId) | ||
| } catch { | ||
| SDKLogger.error( | ||
| "Failed to persist wallet metadata to keychain: \(error.localizedDescription)" | ||
|
|
@@ -409,14 +418,19 @@ struct CreateWalletView: View { | |
| dismiss() | ||
| } | ||
|
|
||
| print("=== WALLET CREATION SUCCESS - Created 1 wallet for \(platformNetwork.displayName) ===") | ||
| print("=== WALLET CREATION SUCCESS - networks: \(selectedNetworks.map { $0.displayName }) ===") | ||
| } catch { | ||
| print("=== WALLET CREATION ERROR ===") | ||
| print("Error: \(error)") | ||
|
|
||
| await MainActor.run { | ||
| self.error = error | ||
| isCreating = false | ||
| // Pop the pushed `SeedBackupView` so the error alert | ||
| // (bound to this view) is actually visible — otherwise | ||
| // the backup screen sits on top with its submit button | ||
| // stuck disabled and no feedback. | ||
| showBackupScreen = false | ||
| } | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💬 Nitpick:
predicate(walletId:network:)is over-specified now that walletId is globally uniqueWith the latest delta's invariant that
walletIdis network-scoped and globally unique on its own (the network byte is folded into the digest, and the#Uniqueconstraint on the model is now[\.walletId]), the composite predicate$0.walletId == walletId && $0.networkRaw == networkRawcan never narrow the result set further than the walletId clause alone, but it CAN spuriously return empty for rows whosenetworkRawisnil— the property's own doc-comment (lines 47–49) explicitly permits pre-persistWalletMetadatarows to havenetworkRaw == nil. This is a low-impact mismatch with the model rather than a correctness break, and is worth flagging only because the just-rewritten walletId doc-comment advertises that the network is already baked intowalletId— keeping a sibling lookup helper that still requires anetworkRawmatch invites confusion at future call sites. Consider either dropping the network clause from this predicate or removing the helper in favor ofpredicate(walletId:).source: ['claude']