Conversation
- UI mostly the same - move SBBS to main receive screen - requested amount now allows any asset - update locales
the "light" theme is dark enough
- chat bubbles - better msg length handling - only time in msg, date like other messaging services
… ctor `Rules::s_pInstance` is `thread_local`. `OpenWalletProgressViewController` dispatches `restore:` onto a background queue, which doesn't go through `init`/`start`/`chooseRandomNode` where Rules is currently installed, so the first BEAM call inside `restore:` (`make_shared<WalletModel>` / `wallet->start`) threw "no rules" and bounced the user back to Welcome at the end of snapshot download. Call `[self loadRules]` upfront in `-restore:`. `loadRules` is idempotent — the underlying singleton is `std::call_once`-guarded — so this just assigns `s_pInstance` on the current thread, mirroring the pattern already used in `+chooseRandomNode`.
…t ticks BEAM core fires OnProgress thousands of times per second on sub-1KB granularity. Each callback dispatches onto the main queue to update the restore-progress UI, and the blocks accumulate faster than UIKit can drain them, blowing past the highwater limit and stalling the screen. Drop everything unless integer percent advanced or done >= total. Tracks the last reported percent on the RecoveryProgress instance and updates the NSLog line to include the percent for easier observability.
Order expiration was offered up to 7 days, but DEX orders ride on SBBS which expires bulletins after ~12 hours — anything beyond that is unreachable on the bulletin board even though the order still claims to be active. Cap the publish path defensively in AppModel and drop the 24h / 7d options from the create-order picker. Also swaps the action-sheet picker for a new BMOptionPickerViewController and inlines the placeholder assignments into configureFields setup so they're not overridden by the per-field initialization that follows.
…he data section Clear local data was sitting in section_0 next to lock screen / currency / min confirmations, which is preferences territory. It belongs with the other destructive data actions (export, import, clear wallet) in section_3. Also tidies a few closure formatting nits in the same file.
Stale on-disk copy of an older ReceiveViewController. Verified absent from BeamWallet.xcodeproj/project.pbxproj (no build-phase or file-ref entry), so it has not been compiled — the active ReceiveViewController.swift declares the same class without a duplicate-symbol error precisely because b.swift was an orphan.
Pure formatting pass — closure args inlined onto the same line as the
opening brace, `}) { … }` rewritten as `}, completion: { … }`, trailing
whitespace removed, empty `viewDidAppear` / `awakeFromNib` overrides
deleted, `// MARK:` spacing normalized.
No functional changes. Includes the trailing-whitespace cleanup inside
the four AppModel.mm hunks within `-restore:` and `-prepareSend:`.
Sweep across the first-party source files (`BeamSDK/`, `Controls/Cell/`,
and `ViewControllers/`) that carried "Copyright © {2019|2020|2021} Denis."
headers and update them to 2026.
ThirdParty/ is skipped — those carry their own upstream attribution
(LGSideMenuController, MASegmentedControl) and should not be rewritten.
Rewrite the UTXO screen so coins are grouped by asset, each section topped with an asset header card (icon, unit name, Split pill button). Replaces the prior available/in-progress/spent/unavailable paging tabs. Tapping Split presents a bottom sheet that splits the largest UTXO of the asset into N equal coins (2/3/5/8/10) — the common workaround for the failure mode where the only large coin is locked in an in-flight tx and the wallet has nothing to fund the next send. Splits go through a new AppModel splitCoins:outputGroths:fee: bridge that constructs CreateSplitTransactionParameters and submits via wallet->getAsync()-> startTransaction. AssetIconView reworked to scale its glyph proportionally so the same view renders correctly at the larger header size as well as the existing 28pt cell size.
The main settings page left a large dead gap between the last row and the version/blockchain-height footer (especially on X devices, where the footer was hardcoded to 280pt with labels pinned to its bottom), forcing users to scroll to see content that should fit on screen. Compute the footer height dynamically from the remaining visible area instead, with a 70pt minimum, and anchor the labels to the bottom of the footer via Auto Layout so they always sit just above the safe-area inset. Also drop the stray space in "v 7.4" → "v7.4". Disable alwaysBounceVertical on BaseTableViewController so non- scrollable pages (settings, node selection, notifications, ...) no longer bob when content fits the screen.
The custom swipe-back recognizer in BaseNavigationController denied the gesture for any top VC matching SettingsViewController / AddressesViewController / DAOAppsViewController / NotificationsViewController. Those classes are reused for pushed sub-pages too (Settings → General/Privacy/Utilities/Node, address detail), so the gesture was silently denied on every settings sub-page, the address detail page, and a handful of others. Replace the class allowlist with the actual intent: deny only when the top VC is the root of its nav stack (side-menu landing pages, which have nothing to pop to). Keep the BEAMX DAO carve-out — its embedded WebView horizontal pan still conflicts with the swipe. Net: 25 lines down to 7, no more "remember to add new root VCs to the allowlist" maintenance tax.
The v7.4 wallet API accepts never/24h/auto/expired for an address's
expiration value, and AppModel.editAddress: already mapped them
correctly. The iOS edit screen had no UI affordance to pick "Never",
though — its section-2 actions ("Extend" / "Active address") always
re-armed to 24h unless the address was already on duration=0. Net
effect: a never-expires address could only stay never; a 24h address
could only stay 24h. No path between the two states.
Make the section-0 expiry detail row tappable: it now pushes the
already-scaffolded .address_expire data picker (Extend / Never), and
the completion writes duration / isNowActiveDuration / isNowActive /
isNowExpired / createTime so the existing Save path persists the new
expiration. Switches the expiry row from the read-only BMMultiLinesCell
to AddressExpiresCell, which already renders the right state-matrix
label and shows a chevron advertising tappability.
The existing section-2 shortcuts are kept — they still drive the same
isNowActive* fields and remain useful.
Settings, Select Node, Payment Proof and Asset Swaps each stuffed their fixed bottom UI (version label, action buttons, segmented header) into tableView.tableFooterView / tableHeaderView at hard-coded heights. When content + the fixed slot exceeded the screen, the table scrolled even though the visible content fit; on Face-ID devices the bottom row could land behind the home indicator. A previous commit (64456e3) papered over the symptom on Settings only with a measure-then-resize loop on every layout pass. Lift the bottom-anchored content out of the table entirely with a new shared BaseTableViewController.bottomAccessoryView hook. The accessory is pinned above the bottom safe area and the table is sized to the area above it — no measurement, no resize loop, no behind-the-home-indicator overlap. Adjacent fixes the new layout enables: - alwaysBounceVertical is toggled on while the keyboard is up so keyboardDismissMode = .interactive has something to grab on screens with no row overflow (Payment Proof input case). - viewDidLayoutSubviews reserves safeAreaInsets.bottom even without an accessory, so Utilities/General/Privacy stop overlapping the home indicator. - SettingsViewController hoists sectionHeaderHeight = 15 / sectionFooterHeight = 15 out of the .main branch so non-.main pages stop inheriting the ~36pt grouped-style defaults that wasted ~120pt of space on Utilities. - AssetSwapsViewController pins its segmented header outside the table view (was a tableHeaderView) so the tabs stay visually fixed instead of scrolling with the order rows. Net: ~50 lines of measure/resize/footer-rebuild machinery removed, ~12 lines of shared hook + frame math added. Pattern is now consistent and discoverable through the base class.
A fresh launch fired ~1,755 getAssetInfo calls in the first 40 seconds (195 unique asset IDs requested 7-9 times each), saturating the main thread enough to make Send/Receive feel unresponsive. Two interacting bugs in WalletModel.mm: 1. onAssetInfo extracted the asset's name into a local NSString but never wrote it onto bmAsset.name. The "needs metadata" check in onStatus then re-detected the asset as missing on every wallet-status tick and re-fired getAssetInfo for it. Wallet-status updates fire continuously during sync, hence the storm. 2. Even with bmAsset.name fixed, several onStatus callbacks can land before onAssetInfo responses come back, so each one queued a fresh round of N async requests for the same IDs. Fix: - onAssetInfo now writes bmAsset.name = name alongside the other bmAsset.X = X assignments, plus erases the ID from the pending set. - onStatus wraps the getAssetInfo call in an m_pendingAssetInfo.insert(...).second guard (new std::set member on WalletModel) so each ID can have at most one in-flight request. - onStatus also assigns asset.name = @"BEAM" in the assetId == 0 fast path so the BEAM row participates in the same invariant. After the fix a fresh launch produces ≤ 195 GET ASSET log lines total.
generateNewWalletAddressWithBlockAndAmount: passed false for the C++ core's newAddress flag when calling generateToken(TokenType::RegularNewStyle, ...). With newAddress = false the core falls back to walletDb->getDefaultAddressAlways(), which returns a stable default rather than creating a new address. As a result, opening Receive always re-tokenised the user's existing default address from the address book, never a new one. Flip the 5th argument of generateToken from false to true so the bridge method actually creates and persists a fresh wallet address per call. The companion bridge generateNewWalletAddressWithBlock: stays at false (correct for Send and Onboarding, which want the default address). The view-model side that re-tokenises (rather than re-creates) on amount keystrokes lands in the address-type-picker change.
ReceiveViewController.viewDidLoad called AppModel.loadFullAssetsList unconditionally on every Receive open. The bridge forwarded directly to wallet->getAsync()->loadFullAssetsList() with no idempotency, so the underlying RequestAssetsListAt(MaxHeight, …) round-trip + 194-asset ProcessAssetInfo walk + 194-callback storm fired every time. Same unguarded call lived in AssetSearchViewController and AssetSwapCreateViewModel. Add a didLoadFullAssetsList flag on AppModel, prefetch once at onWalledOpened, gate loadFullAssetsList on the flag, and reset it in all three teardown paths (resetOnlyWallet / restartWallet / resetWallet) so a fresh session re-fetches. The flag is set pre-dispatch (rather than in the callback) on purpose: that prevents in-flight duplication if call sites fire twice in the same runloop turn before the async response lands. Existing call sites in ReceiveViewController / AssetSearchViewController / AssetSwapCreateViewModel stay as no-op safety nets in case the user races the prefetch (e.g. via a deep-link). After the fix: one loadFullAssetsList round-trip and at most one onAssetInfo per asset per session.
Replaces the binary "Maximum Anonymity Set" toggle in Receive → Advanced with a 5-entry picker (SBBS / Regular / Max Privacy / Offline / Public Offline) that maps to the BEAM core TokenType enum and the v7.4 create_address API. The default-receive labels match the desktop wallet — Regular and Offline both produce TokenType::Offline tokens with different voucher counts. When the picker is on a voucher-capable type (Regular or Offline) a "Vouchers count" numeric input appears below it, validated to 1...30 and re-tokenising the address on every valid change. Defaults are per-type (Regular = 1, Offline = 10), matching desktop and BEAM core's GenerateOfflineToken default. The 30-cap is project policy. Bridge changes (AppModel): - generateSBBSAddress: / generatePublicOfflineAddress: mirror the existing generateMaxPrivacyAddress: pattern. - generateOfflineAddress: gains an offlineCount: parameter and switches from the async generateToken (which hardcodes 1 voucher in WalletClient::generateToken) to a synchronous GenerateOfflineToken call so the count is honored. - BMAddress gains nullable sbbsToken / publicOfflineToken properties. Voucher-count cache: AppModel.offlinePaymentsByWalletId is populated from WalletModel::onGetAddress (already invoked for Send's onMaxPrivacyTokensLeft). Per-walletId cache + idempotent requestOfflinePaymentsCountForWalletId: prevents fan-out during table scroll. New optional WalletModelDelegate method is broadcast only when a value changes — no reload thrash. UI: - BMDataPickerViewController: new .address_type case wired through the existing picker scaffolding. - ReceiveAddressViewModel.ReceiveTokenType drives generateTokens() dispatch (calls only the relevant bridge method) and exposes supportsVouchers / supportsOnlineReceive / defaultVouchers per type. - ReceiveViewController: picker tap on (3,1), voucher-count cell on (3,2) gated by supportsVouchers, per-type footer info text gating (sbbs → online-only copy; regular/offline → choice copy on own node; publicOffline → identifiability warning), section reload instead of ad-hoc insert/delete, currentToken used uniformly for copy/share/QR. - QRCodeSmallViewController: parallel isSbbsOnly / isPublicOffline flags so the modal caption matches the address card. Address-book + details: - BMAddressCell reuses the previously-hidden expiredLabel to show "Offline transactions left: N" for non-contact rows. - DetailAddressViewModel appends the count to the details list and re-renders only when its own walletId changes. - AddressViewModel re-fires onDataChanged on any cached count change. BMFieldCell: keyboardType getter/setter + setText(_:) helper, and textFieldShouldBeginEditing no longer nukes input accessory views on numeric/decimal-pad keyboards. Localization: 5 new Swift accessors (max_privacy_address, public_offline_address already-existed-as-keys; vouchers_count, vouchers_count_hint, vouchers_count_error are new) and the new voucher-count strings added to all 14 locales (English text used as placeholder per the existing convention).
Three issues on Asset Swap Details: 1. Tapping "Accept Offer" without enough of the asset being sold silently popped back to the orders list. AppModel.acceptDexOrder returned YES synchronously after queueing the C++ tx, with no pre-flight balance check anywhere on the accept path. Users thought the swap had been submitted; it failed asynchronously in the background with no surfacing. 2. Cancel and Accept buttons were inside the scroll view's contentView with bottomAnchor pinned to contentView (not the safe area), so on smaller devices they sat below the home indicator and required a scroll to reach. 3. The page scrolled even when content fit because of #2. Fixes: AssetSwapDetailsViewModel gains validationError, mirroring the create flow's existing check — guards canAccept, returns asset_swap_insufficient_funds when the local user can't fund the order's send side. Cancellation (mine + active) doesn't require funds, so the guard returns nil for that case. Also adds sendAsset / receiveAsset / statusColor / isExpiringSoon helpers used by the redesigned VC. AssetSwapDetailsViewController is restructured: footer is pinned to view.safeAreaLayoutGuide.bottomAnchor (not inside the scroll view), the scroll view's bottomAnchor anchors to the footer's topAnchor (so content that fits never scrolls), and the body is laid out into a header card (icons + amounts), rate strip, and details card with an expandable technical-details section (created date, peer SBBS, order ID). onAccept now calls alert(message:) on a non-nil validationError and returns without popping; the user sees the error and the page stays on screen. Localization: 6 new keys (asset_swap_created / _expires_in / _peer_id / _order_id / _show_details / _hide_details) added to all 14 locales (English text used as placeholder per the existing convention). Deliberately not in scope: wiring the VC into WalletModelDelegate to wait for an async tx-status callback after Accept. The dominant silent-close case is "not enough funds" and the pre-flight covers it; network-level failures still surface through the regular transactions list.
Settings → Node now opens a grouped list (Node type / Node peers / Owner key); Owner Key relocated out of Privacy. New NodePeersViewController shows active-node status + last-seen and the random-pool peers in random mode. Adds +[AppModel defaultPeerAddresses] and lastConnectionChangedAt on the bridge.
Adds a 500 ms quiet window at the start of the onlyConnect path so near-synced wallets skip the 0% → 100% flash and slide straight into the main page; otherwise commits to rendering and replays the last snapshot. New SyncPhase resolver swaps the percent line for descriptive copy at the boundaries (connecting / reconnecting / almost done / finalizing) and wires the previously-unused onNetwotkStartConnecting / onNetwotkStartReconnecting delegate methods.
…dom-node create AppDelegate routed users to the "Reset and retry" alert on first launch of v7.4 because `needsRecovery` keyed off `isWalletInitializedFlag`, which is only written inside `onWalledOpened` — existing v7.3 installs upgrading to v7.4 had never written it, so the prompt fired and `resetWallet(true)` wiped the DB + seed-keychain. Backfill the flag in AppDelegate before the recovery check when `isWalletAlreadyAdded()` and the flag is unset. Separately, the random-node create branch in SelectNodeViewController called createWalletForCurrentNode() and then pushed OpenWalletProgressViewController, whose viewDidLoad reruns createWallet. The second call short-circuits today on `walletDb != nil` but onWalledOpened still runs twice and any future failure path lands in abortCreateAndReset(), deleting the DB just made. Drop the redundant call; let OpenWalletProgressViewController own creation.
…, watchdog gate loadFullAssetsList was called from onWalledOpened before `start` runs in the manual-restore branch, so `wallet` was null and the prefetch silently no-op'd while didLoadFullAssetsList stayed false in spirit. Move the call into -[AppModel start] after wallet->start, where wallet is guaranteed non-null. sqlite3_temp_directory was set inside createWallet via sqlite3_mprintf with no sqlite3_free on the prior value (leak on retry), and not set at all in openWallet / canOpenWallet — existing-install opens could still SQLITE_MISUSE on temp spill. Centralize in ensureSqliteTempDir() guarded by dispatch_once; call it from +load and at the top of the three DB entry points. OpenWalletProgressViewController.onTimeOut unconditionally advanced to the main page; on flaky networks during a fresh-install random-node create the user got dumped into a zero-balance wallet, indistinguishable from a wipe. Gate the advance on isPresented || isSynced() || isChangedNode(); on bare timeout surface the existing wallet_not_opened alert that pops to root.
…ll reuse Publish and Accept on Asset Swaps both called the C++ tx and then animated popViewController; a fast double-tap re-entered before the pop completed and fired a duplicate publishDexOrder / acceptDexOrder. Add an isSubmitting flag per VC, disable the trigger button on entry, reset on the error branch. AssetSwapCreateViewModel.grothFrom did `UInt64(beam * 1e8)` with no range check; values beyond ~1.85e11 BEAM crashed the app on every keystroke. Guard with isFinite / non-negative / < UInt64.max before the cast and return 0 on overflow (validationError treats 0 as amount-invalid). BMFieldCell never reset copyText or keyboardType in prepareForReuse, so a cell that previously had a copy bar could install BMInputCopyBar over the numeric voucher pad after recycling. Override prepareForReuse to clear both.
… bounce restore Messenger and offline-payment-count callbacks (onInstantMessage, onGetChatList, onGetChatMessages, onChatRemoved, onGetAddress) mutated NSMutableArray/NSMutableDictionary on the reactor thread while UI read them on main — would eventually crash with "collection mutated while being enumerated". Wrap each mutate-and-broadcast body in a main-queue dispatch, mirroring the existing addressbook pattern. SplitCoinsViewModel's output slices summed to exactly the largest UTXO; for BEAM the wallet had to pay fee from other UTXOs, but if the largest was the dominant BEAM coin the C++ tx failed silently in coin-selection. Subtract feeGroth from the last slice when assetId == 0; surface validationError if no slice has headroom. Settings.setDefaultDarkMode short-circuited on isSetDarkModeKey presence, so users from before that marker existed (with darkModeKey already set) got retroactively flipped to light. Pre-empt: if darkModeKey is present but the marker isn't, write the marker now and honour the persisted value. BaseTableViewController.keyboardWillHide always set alwaysBounceVertical = false, stomping any subclass that opted in. Save the pre-keyboard value on show, restore on hide. AssetSwapCreateViewModel.submit() clamps expirationMinutes to min(value, 720) before the bridge call — SBBS bulletins TTL out at ~12h so anything larger is unreachable.
…her clamp - ReceiveAddressViewModel.generateTokens: five token-generation completion handlers now capture self weakly. - BMField: lineColor / lineHeight are KVC-compatible stubs only; added a TODO so the next reader doesn't expect them to render. - AssetSearchCell: remove manual iconView.awakeFromNib() call — AssetIconView's init(frame:) already runs commonInit(). - BaseNavigationController.gestureRecognizerShouldBegin: split the combined guard so a missing navigationController returns false rather than falling through to true. - ReceiveAddressViewModel.vouchersCount setter clamps to 1...maxVouchersCount; matches the VC-side clamp. - RecoveryProgress.OnProgress early-returns when total == 0 so the initial 0/0 callback no longer fires the delegate with a fake 0%.
…ths; thread DEX callbacks on main The mobile-node (items[1]) and own-node create branches in SelectNodeViewController still called createWalletForCurrentNode() before pushing OpenWalletProgressViewController, whose viewDidLoad reruns createWallet. The second call short-circuits on `walletDb != nil` today but onWalledOpened still runs twice and any future failure path would land in abortCreateAndReset(), wiping the DB just made. Drop the redundant call from both branches; since the random-node branch already migrated and no callers remain, delete the createWalletForCurrentNode() method entirely. WalletModel::onDexOrdersChanged and onFindDexOrder mutated [AppModel sharedManager].dexOrders on the reactor thread while UI read them from main — same race class as the messenger callbacks. Snapshot the C++ vector off-main, then move the mutate-and-broadcast inside dispatch_async(dispatch_get_main_queue()).
…wide amounts clipping the row caption Append `(assetId)` to the asset symbol on the swap create fields, orders-list amounts, and the details header rows so two assets sharing a short name are distinguishable end-to-end (matches the existing picker format). In the details asset row, lock caption/symbol horizontal compression resistance to required and drop the amount label's to defaultLow so Auto Layout squeezes the amount frame on long values. adjustsFontSizeToFitWidth then scales the amount text instead of overrunning the "YOU SEND" / "YOU RECEIVE" captions.
- UTXO settings page now lists one row per asset and pushes a per-asset UTXO list on tap, so wallets with many assets scroll cleanly. - Row reuses AssetAvailableCell with an opt-in "N coins ›" accessory (lazy, hidden by default — wallet/assets pages unchanged). - AssetUTXOListViewController shows the UTXOs of a single asset; the section header gains a Consolidate pill next to Split. - Consolidate reuses the Split sheet via SplitCoinsViewModel.Mode (.split / .consolidate). Outputs collapse to [total - fee] (BEAM) or [total]; SplitCoinsViewController branches title / CTA / preview and hides the split-into selector in consolidate mode. - 4 new locale keys (consolidate, consolidate_coins_subtitle, consolidate_coins_cta, consolidate_started_message) copied across all 13 locales.
Adds an opt-in switch on the Split / Consolidate sheet that routes the operation through a Lelantus push tx to a self-generated offline address instead of producing public UTXOs. Increases the shielded anonymity set and breaks input/output linkability. - SplitCoinsViewModel: sendOffline flag with mode-agnostic isShielded; outputAmounts / validationError collapse to a single shielded output (sourceGroth - fee for BEAM, sourceGroth otherwise); recalculateFee switches to getMinMaxPrivacyFeeInGroth + isShielded:true. - submitShielded() resolves the wallet's default own address via generateNewWalletAddress (uses _id, the underlying hex WalletID — the walletId getter falls back to the token string, which FromHex rejects), generates a self offline-token, and sends with isOffline:true. - SplitCoinsViewController: toggle row always visible; split-into selector + concentration warning collapse when shielded; preview leading label clears so only the resulting amount shows. - 2 new locale keys (consolidate_offline_toggle, consolidate_offline_hint) across all 13 locales.
Adds the install/uninstall pipeline (WalletAPIClient over the embedded IPFS
node, DAppManager, DAppStore), the My DApps and Publishers screens, the
dapp:// WKURLSchemeHandler that gives WKWebView proper HTTP status codes,
MIME types, and CORS for installed apps (native file:// returns status 0
to XHR and rejects fetch(), which breaks the Utils.download("./*.wasm")
path Beam DApps use to ship shader bytes into invoke_contract), and
sideloaded .dapp open-URL handling from Files / iCloud Drive. BeamX DAO
and BeamX DAO Voting ship as bundled .dapp packages. Embedded go-ipfs
node is brought up at wallet-open time; swarm key is derived from
beam::Rules::Network in BEAM core so dappnet / testnet / mainnet /
masternet each join the right private swarm.
A wallet-owned AppsApiUI client (distinct from the per-DApp one used by
DAOViewController) handles store / IPFS / oracle calls with call IDs >=
1_000_000; AppModel.sendDAOApiResult: routes by call ID. The Oracle
start/stop hooks ride along here because they're intermixed with the
WalletAPIClient setup in AppModel.mm restore: / start: / resetWallet:.
Bundles localization for the v7.4 string sweep (oracle, public-offline
retirement, dapps_* additions across 14 locales) so the index stays clean
behind this commit.
Settings → "Use on-chain price oracle" switches BEAM/USD from the centralised feed to the Nephrite stablecoin's Oracle2 contract. OraclePriceManager polls action=view_median through the wallet-owned WalletAPIClient every 60s, parses the val field (raw / 1e9), and writes BMCurrencyUSD/BEAM in ExchangeManager using the same shape WalletModel::onExchangeRates would have produced. WalletModel.onExchangeRates continues to receive the centralised BEAM/USD ticks; the loop now `continue`s past that pair when the toggle is on so the oracle value isn't clobbered between refreshes. BMDataPickerViewController hides BTC/ETH currency choices when the oracle is on (it only emits BEAM/USD). Foreground notification triggers an immediate refetch; initial fetch retries on the "api not ready" error AppsApiUI returns while the wallet thread is still bringing up the IPFS / API plumbing. SettingsViewModel here also drops the standalone "Show public offline address" entry — its enum renumbering is intermixed with the new price_oracle case. The replacement (pinned row in Active addresses) follows in a separate commit.
Replaces the standalone Settings → "Show public offline address" row with a pinned, non-deletable entry at the top of the Active addresses list. Same destination (OfflineAddressViewController); the share affordance now lives where users already look for their addresses. AddressViewModel fetches the token via AppModel.getPublicAddress(...) once per process and caches it across instances (static cachedPublicOfflineAddress). filterAddresses() removes any existing row with the same walletId and inserts a synthetic BMAddress at index 0 — label "Public offline", duration 0 (never expires). Swipe-actions for this row only expose Copy (no Delete / Edit); tap routes straight to OfflineAddressViewController.
New themed alert presenter (custom card on a tinted backdrop) replaces direct UIAlertController construction across send, messenger, and asset-swap flows. Dark-mode styling lives in one place; destructive- button detection runs off a localised title allowlist (delete, remove_wallet, dapps_uninstall, asset_swap_cancel_order). The UIViewController extension helpers (alert, confirmAlert, confirmAndSkipAlert) now build BMAlertViewController.Action arrays and delegate to present(from:title:message:actions:). The three remaining direct UIAlertController call sites (MessengerChatViewController.onDeleteChat, AssetSwapDetailsViewController.onCancel, LegacyWalletSendViewController "Activate security mode") are migrated to the extension helpers.
getWalletStatus now triggers OraclePriceManager.fetchPriceNow when the on-chain oracle is enabled, so unlocking the wallet (or any other sync path) immediately requests a fresh BEAM/USD price instead of waiting on the 60s refresh timer.
Repoint the header/library/framework search paths and the lib group from beam-ios-7.5.14464 to beam-ios-7.5.14510 (BeamMW/beam CI build 7.5.14510). Libraries themselves are fetched out-of-band (gitignored), same as before.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
BEAM 7.5.14510 upgrade — wallet create/restore, Receive redesign, faucet & sync fixes
Summary
Bundles the bug fixes and UX work needed to ship the BEAM 7.5.14510 upgrade.
Restore / create flow. Force-load the bundled sqlcipher
libsqlite.aover the iOS systemlibsqlite3.dylib(without itWalletDB::initalways failedSQLITE_MISUSE); plus stale-DB cleanup, the iOS sandbox temp-dir restriction, reactor scope aroundWalletDB::init, the random-node create flow skipping the sync UI, the create-flow watchdog, the password screen swallowing taps, the "no rules" alert (thread_localRules::s_pInstancenot installed on the dispatch thread that runs-[AppModel restore:]), an iOS jetsam kill on lower-memory devices (RecoveryProgress::OnProgressre-dispatching ~8000 events/sec to main), and a stuck "Synchronizing with node: 0%" caused byopenMainPage()removing its own delegate before itsisWaitingRestoreearly-return.Receive screen.
walletId/labelnil; fixed in the bridge and inBMAddress.fromAddress.8WW…jwmfhgtruncation), inline SBBS row with "use for CEX & mining" hint, prominent "copy address & close" CTA, asset picker that works with 0 non-BEAM assets.receive_description{,_2}reworded to "online vs offline" consistently, 12-hour window now means "open and connect once," and the per-token-type routing on the Receive screen and QR popup is fixed (SBBS-only no longer claims a "choice"; Public Offline shows the existing identifiability warning).newAddress: falseand re-tokenised the same default).Receive — address-type picker (replaces Max Anonymity toggle). 5-entry picker (SBBS / Regular / Max Privacy / Offline / Public Offline) with user-tunable voucher count (1–30) for Regular / Offline. Two new SDK bridge methods (
generateSBBSAddress:,generatePublicOfflineAddress:) and a rework ofgenerateOfflineAddress:to takeofflineCount:and call the synchronousGenerateOfflineTokendirectly (the async path hardcodes 1 voucher). Address details + address-book cell now show "Offline transactions left: N" for every own SBBS address.Beam Messenger. New sidebar entry + chat list / conversation / new chat screens wiring the iOS bridge to the SBBS-based messenger that already ships in the desktop wallet (E2E via SBBS, no extra protocol or server).
Asset Swaps (DEX). Intra-Beam asset swaps — confidential atomic exchange between two assets on the same SBBS bulletin-board pattern. New sidebar entry, three screens (orders / create / details),
BMDexOrderbridge wired toWalletClient::publishDexOrder/getDexOrders/cancelDexOrderandDexBoard::IObserver. Sidebar's BeamX DAO + DAO Voting shortcuts removed to make room (still launchable from dAppStore).UTXO management. Per-asset drill-down (asset list → per-asset UTXO page) with Split and Consolidate actions on each asset, plus an opt-in offline toggle that routes either operation through a Lelantus push tx to a self-generated offline address. Split breaks the largest UTXO into N equal coins (2/3/5/8/10); Consolidate folds the available pool into one UTXO. Both prevent the failure mode where the only large coin is locked in an in-flight tx and the wallet can't fund the next send; the offline toggle additionally pushes the resulting output into the Lelantus shielded pool. New
AppModel.splitCoins:outputGroths:fee:bridge.Edit Address — set expiry to Never. The expiry detail row is now tappable and pushes the
.address_expiredata picker so a 24h address can be promoted to never-expires. Bridge was already correct; gap was UI-only.Sync screen spacing (#589). Normalised
syncing_with_blockchainacross all 13 locales — fixes the double space inSyncing with the blockchain: 42%(en) and the embedded double space in the other 12 locales.Asset-info storm at startup. Every wallet-status update fired
getAssetInfofor all 195 assets — ~1755 calls in the first 40s.onAssetInfowasn't writingname, soonStatusperpetually re-detected the asset as needing metadata. One-line fix + an in-flight dedup guard collapses the storm to ≤195 calls total.First-install theme default. Always light, regardless of system appearance. Existing installs unaffected (saved preference still wins).
Runtime log cleanup.
NSKeyedUnarchiver"this will be disallowed in the future" warnings (BMWalletStatus/BMAssetswitched to typeddecodeObjectOfClass:);BMFieldlineColor/lineHeightKVC-undefined warnings.Settings / nav misc.
tableView.tableFooterViewinto a sharedbottomAccessoryViewonBaseTableViewController(Settings, Select Node, Payment Proof) — fixes pages scrolling past their content.alwaysBounceVerticalon while keyboard is up).BaseNavigationControllerwith "is this VC the root of its stack?" — every pushed sub-page now gets the gesture.DApp store and sideloaded DApps. New surface that talks to the on-chain DApp Store contract over IPFS through a wallet-owned
WalletAPIClient(invoke_contractRPC), with separate My DApps and Publishers screens..dappopen-URL handling from Files / iCloud Drive routes throughDAppManager.installFromZip(staged extraction, manifest GUID validation, path-traversal sanitisation). Each installed DApp loads under a privatedapp://app/...WKURLSchemeHandlerso XHR / fetch get real HTTP status codes, MIME types, and CORS — nativefile://returns status 0 to XHR and rejectsfetch()outright, which breaksUtils.download("./*.wasm")(the path Beam DApps use to ship shader bytes intoinvoke_contract). BeamX DAO and BeamX DAO Voting ship as bundled.dapppackages. Embedded go-ipfs node is brought up at wallet-open time (asio_ipfs::configrepo underDocuments/ipfs-repo; private swarm key derived frombeam::Rules::Networkso dappnet / testnet / mainnet / masternet each join the right swarm).On-chain BEAM/USD price oracle (Nephrite Oracle2). New Settings toggle that switches BEAM/USD from the centralised feed to the Nephrite stablecoin oracle.
OraclePriceManagerpollsaction=view_medianthrough the wallet-owned API client every 60s, parses thevalfield (raw / 1e9 → BEAM/USD), and writes intoExchangeManager's USD/BEAM slot using the same shapeWalletModel::onExchangeRateswould have produced.WalletModel.onExchangeRatesignores remote BEAM/USD ticks while the toggle is on so the oracle value isn't clobbered.BMDataPickerViewControllerhides the BTC / ETH choices.Public Offline pinned at the top of the Active addresses list. Replaces the old Settings → "Show public offline address" row with a pinned, non-deletable entry at the top of the active addresses list. Same destination (
OfflineAddressViewController); the share affordance now lives where users already look for their addresses.Themed in-app alert presenter. New
BMAlertViewController(custom card on a tinted backdrop) replaces directUIAlertControllerconstruction across send, messenger, and asset-swap flows. Dark-mode styling lives in one place; destructive-button detection runs off a localised title allowlist.New features
1. Beam Messenger
Ports the upstream
beam-uiMessenger surface to iOS — SBBS-backed instant messaging using the wallet's existing addresses, no additional crypto layer or server. Lives under a new "Messenger" entry in the top sidebar section.Bridge. New
BMInstantMessage/BMChatmodel objects mirrorwallet::InstantMessage/MessengerChatList::ChatItem.WalletModeloverridesonInstantMessage/onGetChatList/onGetChatMessages/onChatRemovedand dispatches to four new optionalWalletModelDelegatemethods.AppModelexposessendInstantMessage:/requestChats/requestMessagesForPeer:/markChatAsRead:/removeChat:/addChatStub:/resolvedPeerWalletId:. Send/receive goes straight towallet->getAsync()->sendInstantMessage(...)etc. Address parsing handles both raw 68-char SBBS hex IDs and Beam tokens (WalletID::FromHexfirst, thenParseParameters → TxParameterID::PeerAddr). Defensive UTF-8 decoding (with Latin-1 fallback) keeps non-strict-UTF-8 payloads from rendering blank.Persistence. No new schema — the prebuilt
libwallet.aalready creates and migrates theIMSQLite table. Live updates flow through the existingIWalletDbObserver::onIMSavedpath.View models / VCs.
MessengerChatListViewModel/MessengerConversationViewModelcache the data and reload on the new delegate methods. Four VCs underViewControllers/Main/Messenger/: chat list (with+/ pull-to-refresh / swipe-to-delete), conversation (bubble list withbrightSkyBlue/heliotropebubbles, marks as read on appear), new-chat (my-address picker + peer + optional contact name, validates viaAppModel.isAddress:), bubble cell, chat cell. Sidebar wiring +IconMessenger()icon helper (PNG rendered frombeam-ui/ui/view/assets/icon-beam_messenger.svg). 9 new localization keys, all 14.stringsfiles updated. 12 new files registered into all three targets via thexcodeprojgem.2. Asset Swaps (intra-Beam DEX)
Wires the iOS bridge to the BEAM 7.4 asset-swap surface — same DEX as the desktop wallet, atomic confidential swaps between two confidential assets. New sidebar entry after dAppStore (replacing the BeamX DAO + DAO Voting shortcut rows, both still reachable from dAppStore).
Lib bundle.
libwallet_client.arebuilt withBEAM_ASSET_SWAP_SUPPORT=ON(without it theDexBoard*/publishDexOrder/getDexOrderssymbols are guarded out). Same macro added toGCC_PREPROCESSOR_DEFINITIONSfor all six target × config combos.Bridge.
BMDexOrdermirrorswallet::DexOrder, with directional accessors (sendAmount/receiveAmount/ etc.) populated viaDexOrder::getSendAmount/getReceiveAmount/...— those already flip first/second based on_isMine, so the iOS layer doesn't have to.WalletModeloverridesonDexOrdersChanged/onFindDexOrder.AppModelexposesrequestDexOrders/publishDexOrderWithSendAsset:.../cancelDexOrderWithID:/acceptDexOrder:. Publish generates a fresh SBBS-only address viawalletDb->createAddress(...)so each order has its own listener key. Accept builds params viaCreateDexTransactionParams(...)flipping send/receive per the viewer's perspective. Default fee fromgetDefaultFeeInGroth.Tx-status surfacing.
WalletModel::onTxStatusgains awallet::TxType::DexSimpleSwap(= 13) branch — settlement transactions show up in the existing tx list with the localized "Asset Swaps" prefix on the status string. No separate transactions list needed.View models / VCs.
AssetSwapsViewModelbuckets orders by tab (Open / My / History).AssetSwapCreateViewModelowns form state with a singlevalidationErrordriving both the inline label and the publish button. Three VCs underViewControllers/Main/AssetSwaps/: list (segmented Open/My/History), create form (asset chips reusingAssetSearchViewController, expiration capped at 12h to match SBBS TTL), details (Cancel/Accept/hidden). 26 new localization keys; 9 new files registered into all three targets.3. UTXO management — per-asset drill-down, Split + Consolidate, Lelantus toggle
Reworks the UTXO screen into a flat list of one row per asset that drills down to a per-asset UTXO list, plus Split and Consolidate actions on each asset and an opt-in offline toggle that pushes the result into the Lelantus shielded pool. Why: Mimblewimble selects unspent UTXOs whose sum ≥ amount + fee, and any UTXO involved in an in-flight tx is locked until it confirms — so a wallet holding a single fat coin per asset can't fund a second send while the first is pending. The offline toggle additionally breaks input/output linkability and grows the shielded anonymity set.
Bridge. New
AppModel.splitCoins:outputGroths:fee:builds anAmountListfrom the requested per-output groths, constructsCreateSplitTransactionParameters, setsFee/AssetID, and submits viawallet->getAsync()->startTransaction. The split looks like a regularSimpleTransactionto the rest of the wallet (multiple outputs, no peer) so it shows up in the existing tx list with no extra surfacing. Consolidate reuses the same bridge with a single output groth covering the whole asset pool. The offline path doesn't go throughsplitCoins: it callsgenerateNewWalletAddress(resolves the default own address viagetDefaultAddressAlways), usesaddress._id— the underlying hexWalletIDrather than the token-stylewalletIdgetter whichFromHexwould reject — to generate a self offline-token viagenerateOfflineAddress(...offlineCount: 1), then callssend(...isOffline: true)to that token. The vouchers +TxType::PushTransactionroute the output to the shielded pool.View models.
AssetUTXOGroup(value type, sorts UTXOs descending, exposeslargestUtxo/concentrationRatio/isConcentrated/canSplit).UTXOViewModel.groupedByAsset()buckets available regular + shielded UTXOs byassetId(BEAM first).SplitCoinsViewModelowns both flows via aModeenum (.split/.consolidate) and asendOfflineflag (derived:isShielded).sourceGrothis the largest UTXO in split mode and the total available pool in consolidate mode.outputAmountscollapses to a singlesourceGroth − fee(BEAM) /sourceGroth(other assets) shielded output whenisShielded; otherwise N equal slices for split (remainder absorbed into the last slice, fee subtracted for BEAM) or a single public output for consolidate.recalculateFeeswitches togetMinMaxPrivacyFeeInGroth+isShielded: truewhen the toggle is on. Defaults tosuggestedSplitCount(for:allowed:)whenisConcentrated, else 3; consolidate fixes the count at 1.View controllers.
UTXOViewControllernow renders one row perAssetUTXOGroupusing the wallet-pageAssetAvailableCellwith a new opt-insetUTXOAccessory(count:)that lazily appends "N coins ›" at the trailing edge — hidden by default so the Wallet and Assets pages are unchanged. Tapping a row pushesAssetUTXOListViewController, the per-asset UTXO list. Its section header (AssetUTXOSectionHeaderView, layout updated to a two-row card) carries two pills: Split and Consolidate. Either pill opensSplitCoinsViewController— the same modal bottom sheet — in the appropriate mode; the sheet branches title / CTA / subtitle / summary / preview labels onviewModel.mode, hides the "Split into" pill row (2/3/5/8/10) in consolidate mode, and gains a labelledUISwitchrow ("Send to shielded pool / Increases Lelantus pool size and breaks linkability"). When the toggle is on the split-into row, concentration warning, and preview leading label all collapse, so only the resulting shielded amount and fee show. Consolidate requires ≥2 UTXOs; the shielded paths requiresourceGroth > feefor BEAM. HonoursSettings.isNeedaskPasswordForSend.AssetIconViewprogrammatic-init fix. Setup moved into a sharedcommonInit()so programmatic instances (the new section header) render correctly; the XIB path is unchanged. 19 new localization keys (13 Split + 4 Consolidate + 2 offline toggle); 7 new files registered into all three targets.4. Address-type picker on Receive + per-address offline-tx-token count
Replaces the binary "Maximum Anonymity Set" toggle in Receive → Advanced with a 5-entry picker, and surfaces the offline-payment voucher count on every own SBBS address. Picker labels map to the
TokenTypeenum and the v7.4create_addressAPI; defaults matchbeam-ui'sreceive_view.cppand BEAM core'sGenerateOfflineToken:TokenTypeRegularOldStyleOfflineMaxPrivacyOfflineGenerateOfflineTokenand CLI--offlinePublicBridge. Two new
AppModelmethods (generateSBBSAddress:/generatePublicOfflineAddress:) callinggenerateToken(TokenType::RegularOldStyle/Public, ...).generateOfflineAddress:reworked to takeofflineCount:and call the synchronousGenerateOfflineTokendirectly — the asyncgenerateToken(Offline, ...)path hardcodes 1 voucher.BMAddressgains nullablesbbsToken/publicOfflineTokenproperties.Voucher-count cache.
AppModel.offlinePaymentsByWalletId(NSDict keyed by SBBS WalletID hex), populated fromWalletModel::onGetAddress(WalletID, address, offlinePayments).requestOfflinePaymentsCountForWalletId:is idempotent. New optionalWalletModelDelegate.onOfflinePaymentsCountForWalletId:count:broadcast only when the value changes. Cache cleared inresetWallet:.Picker.
BMDataPickerViewControllergains anaddress_typeDataTypereusing existing scaffolding. Receive section 3 row 1 (formerly the max-privacy switch) becomes aBMPickerCellthat pushes the picker.ReceiveAddressViewModel.generateTokens()dispatches onselectedTokenTypeand only generates the relevant token — no longer producing both max-privacy and offline tokens on every call.Voucher-count input. When the picker is on a voucher-capable type (Regular / Offline), Receive → Advanced grows a numeric
BMFieldCelltitled "Vouchers count" with hint "Up to 30 vouchers per address". Gating lives in one place —var supportsVouchers: BoolonReceiveTokenType— read by both the VM (regenerate on change) andnumberOfRowsInSection. Defaults viavar defaultVouchers: Int(Offline = 10, else 1). Input validation:1...30writes the VM; out-of-range or non-numeric flips the cell toBMFieldCell.errorand does not write the VM, so token generation never runs against a bad count. Not persisted.Address-details + cell row.
DetailAddressViewModelappends "Offline transactions left: N" via the existingoffline_left_addresskey whencount >= 0.BMAddressCellreuses the previously-hiddenexpiredLabeloutlet to show the count (avoids editing the xib).Footer info text gating.
var supportsOnlineReceive: BoolonReceiveTokenType— true for.sbbs/.regular/.offline. Per-type caption:.maxPrivacykeeps its existing fee/lockup copy;.sbbsshowsreceive_description_2only (no offline path);.regularand.offlineshowreceive_descriptionon an own node, fall back toreceive_description_2otherwise;.publicOfflineshows the existingpublic_offline_address_info("Publishing this address will allow you to be identified") so the privacy trade-off is visible at the point of sharing. Same gating applied toQRCodeSmallViewController(it had the same broken own-node-only branch). No new localized strings — all already in 14 locales.5. Edit Address — allow setting expiry to Never
The v7.4
create_address/edit_addressacceptsnever/24h/auto/expiredforexpiration, but the iOS edit screen had no UI to select Never — the section-2 actions ("Extend" / "Active address") always re-armed to 24h unlessduration == 0was already true. So a 24h address could never be promoted.Fix.
EditAddressViewControllersection 0 row 2 (the expiry detail row) switched from a read-onlyBMMultiLinesCelltoAddressExpiresCell(already designed for this matrix, advertises the row as tappable). NewpresentExpiryPicker()pushes the previously-unused.address_expireBMDataPickerViewController(Extend = 24, Never = 0). Picker completion writesviewModel.newAddressdirectly:durationandisNowActiveDurationgo to 0 orSettings.maxAddressDurationSeconds;isNowActive = true,isNowExpired = false,createTime = now. Bridge inAppModel.editAddress:was already correct — no changes there. No new files, no new strings.6. Node Settings — restructured + new Peers screen, Owner Key relocated
Settings → Node was a bare
SelectNodeViewController(3 type cards) with no visibility into which nodes the wallet talks to and no home for Owner Key, which sat awkwardly under Privacy.Restructure. Settings → Node now opens a standard grouped list (
SettingsViewController(type: .node), same look as General / Privacy) with three rows: Node type (detail shows Random / Mobile / Own, pushes the existing card picker), Node peers (new screen), Owner key (existingBMDoubleAuthViewController(event: .owner)flow). Owner Key removed from Privacy. NewSettingsItemType.node_type = 41/.node_peers = 40;currentNodeTypeLabel()derives the label fromSettings.isNodeProtocolEnabled/connectToRandomNode.Node Peers screen.
NodePeersViewController(BaseTableViewController, grouped, dequeued.subtitle-styledNodePeerCell): an Active-node row with colored Connected / Connecting / Disconnected status + "Last seen ", and a Random-pool section listed only in random mode. Live updates via threeWalletModelDelegateconnection callbacks funneled through onerefreshFromNetworkChange()helper. Read-only.Bridge.
+[AppModel defaultPeerAddresses]returns the filtered random-peer pool fromgetDefaultPeers();+chooseRandomNoderewritten to pick from it (single source of truth, no duplicate filter). NewlastConnectionChangedAtstamped insetIsConnected:when the flag flips. iOS doesn't run an embeddednode::Node(Mobile Node isenableBodyRequests, not a full node), so no PeerManager rating / inbound peers — the screen stays within what the wallet client knows.8 new localization keys × 14
.stringsfiles. 1 new file registered into all three targets.7. Sync screen — phase-aware messages and stabilization window on resume
The wallet's resume path always pushes
OpenWalletProgressViewController(onlyConnect: true)fromEnterWalletPasswordViewController, but on a near-synced wallet the BEAM core reports(done=0, total=1)followed almost immediately by(done=1, total=1)— the screen flashed "0%" and then "100%" before opening the main wallet. The singleSyncing with the blockchain X%line also said nothing about what the wallet was actually doing during a real sync.Stabilization window. New 500 ms quiet window at the start of the
onlyConnectpath: incomingonSyncProgressUpdated(done, total)events are stashed intopendingSnapshotinstead of redrawing the percent label. When the timer fires (onStabilizationFinished), the VC decides once: open the main page ifAppModel.isSynced()is already true or fewer thankTrivialSyncThreshold(= 2) requests remain; otherwise commit to rendering and replay the last snapshot through the regular renderer. The 0%→100% flash is gone — small catch-ups slide straight into the wallet, real sync work renders normally. Recovery (isWaitingRestore) and the seed-restore flows are explicitly excluded from the gate; they have real work to display from frame one.Phase resolver. New
SyncPhaseenum (connecting/reconnecting/almostDone/finalizing) withresolveBoundaryPhase(done:total:)returningnilfor steady-state — caller then leaves the existing percent text alone. Resolution rules: not yet connected →.connecting; reconnect signalled and not yet back →.reconnecting;done == total && !isSynced()→.finalizing; remaining ≤kTrivialSyncThreshold→.almostDone. The bulk of the sync screen still reads"Syncing with the blockchain X%"; only the boundaries get descriptive copy.Connection callbacks wired.
OpenWalletProgressViewControllernow implements two previously-unusedWalletModelDelegatemethods that the bridge has been firing for a while:onNetwotkStartConnecting:sets the "Connecting…" copy while the stabilization timer is still running;onNetwotkStartReconnectingflips to "Reconnecting…" on a mid-sync drop.onNetwotkStatusChange:now also stampshasConnected = trueand clearsisReconnectingon the first successful connect so the phase resolver can tell "never connected" apart from "transient drop". The existingphrase != nil && !isNodeProtocolEnabledconnect-then-open shortcut is unchanged.Localization. 4 new keys:
sync_phase_connecting("Connecting to node…"),sync_phase_reconnecting("Reconnecting to node…"),sync_phase_almost_done("Almost done…"),sync_phase_finalizing("Finalizing…"). Added to all 14.lproj/Localizable.stringsfiles (English placeholder where translation is pending) plus theLocalizable.shared.stringsaccessor.Deferred — per-phase data from the C++ core.
WalletClient::onSyncProgressUpdated(int done, int total)collapses six distinct request types (StateSummary/BodyPack/Body/Utxo/Kernel/Eventsfromwallet/core/wallet.h:324) into one aggregate counter, so iOS still can't show real per-phase copy like "Downloading 47/100 blocks" or "Scanning your coins". The change is additive (Wallet::SyncDetailstruct + newonSyncProgressDetailUpdatedvirtual + iOS bridge wiring) but requires rebuilding the prebuiltFrameworks/beam-ios-*perBUILD.md, so it's deliberately out of scope for this PR.8. DApp store, sideloaded install, and My DApps
A standalone DApp surface alongside the legacy dAppStore. Three pieces ship together: the on-chain store browser (Publishers → DApps), the My DApps screen for installed apps, and a sideload path that accepts
.dappfiles from Files / iCloud Drive.Store query (
DAppStore). Loads the bundleddapps_store_app.wasmshader, callsinvoke_contractwithaction=view_publishers,cid=<storeCID>thenaction=view_dapps,...through the wallet-ownedWalletAPIClient. Publisher list cached for the session; unwanted publishers filtered out via a UserDefaults toggle managed byPublishersViewController. BeamX DAO and BeamX DAO Voting always appear as "bundled" entries even when the on-chain store is empty.Install pipeline (
DAppManager).installFromZipstages the SSZipArchive extraction intotmp/dapp-stage-<UUID>first, parsesmanifest.json, validates the manifestguidagainst[a-fA-F0-9]{32,64}, then moves the directory intoApplication Support/dapps/<guid>. Manifesturlis normalised throughvalidateManifestURL(rejects.., leading/, anything outsidelocalapp//app/). After extraction,sanitizeExtractedTreewalks the directory and deletes any file whose canonical path escaped the root — SSZipArchive accepts traversal entries verbatim on older builds, so the cleanup is a defence-in-depth pass. Bundled.dapppackages (dao-core-app.dapp,dao-voting-app.dapp) auto-install on first launch viainstallFromBundle.Per-DApp
WKURLSchemeHandler.DAppURLSchemeHandler(dapp://app/<relative>) wraps each installed DApp's filesystem load behind a synthetic HTTP-style response: real status codes, MIME types frompathExtension(application/wasmfor.wasm,image/svg+xmlfor.svg, …), and CORS-permissive headers. Nativefile://returns status 0 to XHR and rejectsfetch()outright; Beam DApps callUtils.download("./*.wasm")to ship shader bytes intoinvoke_contract, which can't work otherwise. The handler also injects a tiny<script>shim into the entry HTML that forwardsconsole.log/info/warn/errorto adappLogWebKit message handler so DApp output ends up in the iOS device log. Path-traversal guard refuses anything that resolves outside the DApp root afterstandardizedFileURLnormalisation. The handler is per-WKWebViewConfigurationbecause WebKit rejects scheme registration on a live configuration;DAOViewControllerretains it for the lifetime of the load.Sideloaded
.dappfrom Files / iCloud Drive.AppDelegate.application(_:open:options:)matchesurl.pathExtension == "dapp", opens the security-scoped resource, reads the bytes, and runs them throughDAppManager.installFromZip(fallbackName: "Sideloaded DApp"). On success the top-most VC pushesMyDAppsViewController.Wallet-owned
WalletAPIClient. A secondAppsApiUIinstance distinct from the per-DApp one used byDAOViewController. Created once at wallet open (afterIPFSconfig soClientThread_Create(ipfsnode=true)can resolve the IPFS handle), with call IDs starting at1_000_000so result routing inAppModel.sendDAOApiResult:can disambiguate. Consumed byDAppStore(view_publishers/view_dapps), the IPFS download path (ipfs_get), andOraclePriceManager(view_median).Embedded IPFS config.
AppModel.configureEmbeddedIPFS(guarded onBEAM_IPFS_SUPPORT) writesasio_ipfs::configwithrepo_root = Documents/ipfs-repobefore theAppsApiis created. Swarm key is auto-derived inipfs_imp.cppfrombeam::Rules::Network— no explicit configuration needed.WebAPICreator.createApiis flipped toipfsnode=trueso the resultingApiInitDatacarries the IPFS service handle (without it,ipfs_getreturnsApiError::NotSupportedeven when the feature is compiled in).Result routing. Three concurrent API consumers now share
AppsApiUIcallbacks: the DApp page running inDAOViewController(per-DApp), the wallet-owned client (store / IPFS / oracle), and the legacy DAO path.AppModel.sendDAOApiResult:first asksWalletAPIClientwhether the call ID (≥1_000_000) is its own; if so it routes there, otherwise to the DApp page.29 new localization keys (
myDApps,dapps_*) across 14.stringsfiles. 12 new Swift / Obj-C++ files registered in all three targets. 3 binary assets (Resources/dao-core-app.dapp,dao-voting-app.dapp,dapps_store_app.wasm).9. On-chain BEAM/USD price oracle toggle
Optional Settings toggle that switches the BEAM/USD secondary price from the centralised feed to the Nephrite stablecoin's Oracle2 contract.
Bridge / shader.
OraclePriceManagerpollsinvoke_contractwithrole=manager,action=view_median,cid=<oracleCID>every 60s through the wallet-ownedWalletAPIClient. The response carries a Beam-manager-formatted string underresult["output"]({"res": ["val": <u64>,"hEnd": <h>}]}— not valid JSON because of the square brackets withkey:valuepairs); the manager also returns a directvalfield on the top level which the parser picks up first, falling back to a regex overoutput. Oracle returns price × 1e9; divides into BEAM/USD before normalising to BEAM core'sprice × Rules::Coin(1e8) writeback.Writeback.
ExchangeManager.shared().currenciesis mutated in place — the existing USD/BEAM row'svalue/realValue/codeis updated; if the row doesn't exist yet (cold launch, oracle answers before the centralised feed) a newBMCurrencyis appended. BroadcastsonExchangeRatesChange.BMNetworkStatusViewonly re-renders its "rate not received" suffix on network-status callbacks — the first successful oracle write also nudgesonNetwotkStatusChange:once to drop the suffix; subsequent 60s ticks don't re-fire that callback to avoid the status bar re-debounce flicker.Conflict with centralised feed.
WalletModel::onExchangeRatescontinues to receive remoteCurrency::USD↔Currency::BEAMticks; the loop nowcontinues past that pair whenSettings.isOracleEnabledso the oracle's value isn't clobbered between 60s refreshes.Resilience. Foreground notification triggers an immediate refetch (timer-based polling pauses when the app suspends). Initial fetch retries on
"api not ready"errors (returned byAppsApiUIwhile the wallet thread is still bringing up the IPFS / API plumbing) — up to 6 retries with a 2s delay. Cold-launch races between API creation and the first poll are absorbed by the same retry loop.UI knock-on.
BMDataPickerViewControllerhides the BTC and ETH currency choices when the toggle is on (the oracle only emits BEAM/USD). Settings → "Show amounts in" is forced to USD on toggle-on.AppModel.refreshAddresses()runs on toggle-off so the next address-list render pulls from the centralised cache. 1 new localization key (use_on_chain_price_oracle) in 14 locales. 1 new binary asset (Resources/oracle2-app.wasm).10. Pin Public Offline at the top of the Active addresses list
Replaces the standalone Settings → "Show public offline address" row with a pinned entry at the top of the Active addresses list. Same destination (
OfflineAddressViewController); the share affordance now lives where users already look for their addresses.Implementation.
AddressViewModelfetches the public offline token viaAppModel.getPublicAddress(...)once per process and caches it across instances (static var cachedPublicOfflineAddress).filterAddresses()removes any existing entry with the samewalletIdand inserts a syntheticBMAddressat index 0 — label "Public offline",duration = 0(never expires),isContact = true.AddressTableViewswipe-actions detect the row viaisPublicOfflineEntryand only expose Copy (no Delete / Edit). Tap routes straight toOfflineAddressViewControllerwithout going throughAddressViewController.Settings cleanup.
SettingsViewModel.SettingsItemType.offline_addressandonOfflineAddress()deleted. Two locale keys retired (show_public_offline,connect_node_offline_public) across 14 locales — no entry point left to gate with the "enable own node" alert.11. Themed in-app alert presenter
BMAlertViewController— custom card on a tinted backdrop — replaces directUIAlertControllerconstruction across send, messenger, and asset-swap flows. Dark-mode styling lives in one place; destructive-button detection runs off a localised title allowlist (delete,remove_wallet,dapps_uninstall,asset_swap_cancel_order).Migration.
UIViewControllerextension helpers (alert(...),confirmAlert(...),confirmAndSkipAlert(...)) now buildBMAlertViewController.Actionarrays and delegate toBMAlertViewController.present(from:title:message:actions:). Three call sites that previously builtUIAlertControllerdirectly (MessengerChatViewController.onDeleteChat,AssetSwapDetailsViewController.onCancel,LegacyWalletSendViewController"Activate security mode") are migrated to the extension helpers.Bugs and fixes
1.
WalletDB::initalways fails withSQLITE_MISUSEon a clean installSymptom. Every fresh "Create wallet → Random node → Start" produced "Error: wallet not created" with
WalletDB::init failed: sqlite error code=21andBUG IN CLIENT OF libsqlite3.dylib: vnode unlinked while in use.Root cause. Symbol conflict: BEAM's bundled
libsqlite.ahas sqlcipher (SQLITE_HAS_CODEC,sqlite3_key); the iOS systemlibsqlite3.dylib(pulled in transitively by Pods) does not. The linker was resolving mostsqlite3_*symbols against the system lib, so BEAM opened the DB on a connection that doesn't speak sqlcipher andsqlite3_key()then returnedSQLITE_MISUSE. BEAM's error path unlinks the half-written file while sqlite still holds the fd → iOS panic.Fix. Add
-Wl,-force_load,$(SRCROOT)/Frameworks/beam-ios-7.5.14510/lib/libsqlite.atoOTHER_LDFLAGSfor all six target × config combos. Preflight log now printssqliteVersion=3.28.0 sqliteHasCodec=1.2. Stale
wallet.dbstrands the create flow foreverRoot cause.
createWalletearly-returned whenWalletDB::isInitialized(justfs::exists) was true.resetWallethad ahadWalletDbgate skipping the unlink whenwalletDb == nil— but the "Restore or create new wallet" flow callsresetWallet:YESfrom the password screen before the wallet has been opened, so the file was never deleted.Fix. Drop the
hadWalletDbgate (always unlinkwallet.db+-wal/-shm/-journal). Drop theWalletDB::isInitializedearly-return increateWalletand defensively unlinkwallet.db,wallet.db.private, and sidecars right beforeWalletDB::init.3. iOS sandbox blocks SQLite's default temp directory
SQLite falls back to
/tmpfor journals, which the iOS sandbox forbids.createWalletnow setssqlite3_temp_directory = sqlite3_mprintf("%s", NSTemporaryDirectory())beforeWalletDB::init. Logged via the new preflighttmpdir=line.4. Reactor scope around
WalletDB::initBEAM 7.5.14510's
WalletDB::initregisters I/O callbacks against the current reactor; reusing the reactor scoped in/out duringAppModel.initproducedSQLITE_MISUSEon real devices.createWalletnow resets and recreateswalletReactor, opens anio::Reactor::ScopearoundWalletDB::init, and only emitsonWalledOpenedwhile the scope is active.5. Restore screen hangs forever when the connected node is already synced
Symptom. Recovery hit 100% and the screen sat on "Sync with node: 0%" forever.
Root cause.
OpenWalletProgressViewController.onSyncProgressUpdatedadvanced only whentotal == done && total > 0 && isWaitingRestore. An already-synced node legitimately reportstotal == done == 0, so the guard was permanently false.Fix. New condition:
isWaitingRestore && (total == done || AppModel.sharedManager().isSynced()). Also delete an unrelated emptyifblock at the same site.6. Random-node create flow skipped the sync UI entirely
SelectNodeViewController.onNextfor random-node create calledopenMain()immediately aftercreateWallet()returned, landing the user on a 0-BEAM main view while sync ran invisibly. Now also pushesOpenWalletProgressViewController(password:phrase:)like the mobile-node branch.7. Create-flow watchdog timer was a no-op for fresh installs
OpenWalletProgressViewController.onTimeOutadvanced only whenSettings.isChangedNode()was true. On a fresh install the 10s watchdog fired and did nothing, hanging the screen indefinitely if no sync progress arrived.onTimeOutnow always callsopenMainPage()(already idempotent viaisPresented).8. Password field's keyboard never opened
Root cause. The create-password screen had no auto-focus call (only the change-password flow did), and
hideKeyboardWhenTappedAroundsettapGesture.cancelsTouchesInView = true, which swallows taps beforeUITextFieldcan begin editing.Fix. Flip
cancelsTouchesInViewtofalse; add aviewDidAppearoverride onCreateWalletPasswordViewControllerthat focusesoldPassField/passFieldinsideDispatchQueue.main.async.9. Faucet tap crashes the app (#587)
Symptom. Tapping "Receive faucet" on a fresh wallet crashed with
+[NSString stringWithString:nil].Root cause. Token-style address callbacks in
AppModel.mmset onlyaddress.address;walletIdandlabelwere nil unlessParseParametershappened to populatewalletId.OnboardManager.receiveFaucetthen calledBMAddress.fromAddress(result)whose copy-ctor did[NSString stringWithString:address.walletId].Fix. Both token-generation callbacks initialize
walletId = @""andlabel = @""before the optional parse.BMAddress.fromAddress:guards everystringWithString:against nil and now also copies theaddressfield (which the previous impl silently dropped).10. Receive screen — SBBS hidden, address truncated, primary actions buried (#588)
Symptom. Address middle-truncated to
8WW…jwmfhg. SBBS field reachable only through "address details".copy_address_closeCTA also buried there. Asset chip unresponsive when wallet held 0 non-BEAM assets. Send screen auto-pasted clipboard and forced the keyboard on entry.Fixes.
ReceiveTokenCellrewritten programmatically (XIB deleted): title row +address detailslink; full-width middle-truncated address; copy/QR/share button row withfillEqually; conditional SBBS section (separator + value + own copy + italic gray hint); optional bottom hint. Hidden whensbbsAddressis nil/empty or equal to the primary token.ReceiveViewController— registers the programmatic cell, derives the embedded SBBS viagetTransactionParameters(token).addressfor own-node regular/maxprivacy tokens, callsloadFullAssetsList()inviewDidLoadto pre-warm the picker, and now opens the asset picker as a full-screenAssetSearchViewController(new) wrapped in aBaseNavigationControllerinstead ofBMPopoverMenu. Cell configured withallowAllAssets = trueso the chip stays tappable at 0 non-BEAM assets.AppModel.loadFullAssetsListbridge →wallet->getAsync()->loadFullAssetsList(). NewWalletModel::onFullAssetsListLoadedoverride firesonAssetInfoChangeonce the full list is in.ReceiveAddressButtonsCell— share button removed from the XIB (now lives in the address card); a 240×44copy_address_closeCTA is added programmatically and drives the cell's auto-sized height.BMAmountCellgainsallowAllAssets: Boolopt-in (currency chip stays enabled regardless of held balance).BMCellProtocolgains@objc optional func onClickCopyAndClose().SendViewController.viewDidAppearno longer triggersviewModel.isNeedFocus.sbbs_address_hint= "use for CEX withdrawals and mining"; existingsbbs_address_newupdated for consistency.11. Receive screen — confusing payment-mode wording (#593)
Old copy:
Mixed "regular" vs "online" terminology, and "get online during 12 hours" reads like the wallet must stay connected for 12 hours.
Fix. Rewritten in
en.lproj/Localizable.strings:receive_description= "Sender will be given a choice between online and offline payment.\n\nFor online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."receive_description_2= same second sentence on its own.Both keys previously existed only in
en.lproj; added explicitly to all 13 other locales (English placeholder) so future translators have a canonical key set. Removed the unusedsender_choicekey from the Swift accessor and all 14 locale files.12. Sync screen — double space between label and progress percentage (#589)
en.lprojhad"Syncing with the blockchain: "(trailing colon + space); callsites interpolate"\(syncing_with_blockchain) \(progress)%"so the output collapsed to: 42%. The 12 non-English locales had a separate double-space typo ("Syncing with blockchain"). All 13 normalised to"Syncing with the blockchain"(the two callsites that need a colon already append": "themselves).13. NSKeyedUnarchiver secure-coding leniency floods the log on every launch
Symptom. Each launch wrote a deprecation warning per decoded property across
BMWalletStatus(15 keys) andBMAsset(19 keys): "allowed unarchiving safe plist type … even though it was not explicitly included … This will be disallowed in the future."Root cause. Both classes declared
+supportsSecureCoding == YESand were unarchived through the secure top-level API, but every property inside-initWithCoder:used the non-securedecodeObjectForKey:, falling back to NSKeyedUnarchiver's lenient allow-list.Fix.
-initWithCoder:in both classes switched todecodeObjectOfClass:[NSString class] forKey:for string fields and[NSNumber class]for numeric fields.BMTransaction.m/BMNotification.m/BMCurrency.muse the same pattern but didn't appear in this run's log — flagged as a follow-up audit.14.
BMFieldKVC-undefined warnings logged on every screen with a text inputTen xibs set User Defined Runtime Attributes
lineColor(UIColor) andlineHeight(NSNumber) onBMField, leftover from an older underline design.BMFieldnever exposed either property.Fix. Add
@IBInspectable var lineColor: UIColor?and@IBInspectable var lineHeight: CGFloat = 0(stored only, no rendering). Editing each xib to drop the dead attributes was rejected — those values still represent intent (Beam-brand teal, 2pt height) we may want to wire up later.15. Fresh install adopted the device's dark mode
AppDelegate.didFinishLaunchingWithOptionsreadtraitCollection.userInterfaceStyle == .darkand passed it toSettings.setDefaultDarkMode(_:), which writes only on first install — so the device's appearance got latched in. Replaced with an unconditionalsetDefaultDarkMode(false). Existing installs unaffected (setDefaultDarkModeshort-circuits onceisSetDarkModeKeyis present).16. "no rules" alert at the end of snapshot download during wallet restore
Symptom. After the snapshot finished downloading during a wallet restore, "no rules" alert popped and the user was bounced to Welcome.
Root cause.
beam::Rules::s_pInstanceisthread_local. The app installs it on three threads — main, the reactor's worker, and any thread that calls+chooseRandomNode— butOpenWalletProgressViewController.restoreCompleteddispatchesAppModel.restore(...)onto a freshDispatchQueue.global(qos: .background). The first BEAM call inrestore:to touchRules::get()(make_shared<WalletModel>orwallet->start, both synchronous on the calling thread) throws. The existingmakeIWTCallworkaround only installs Rules on the reactor worker thread, so it can't help.Fix.
-(void)restore:calls[self loadRules]immediately after the file-existence check, before thetryblock. Idempotent (Rulessingleton itself isstd::call_once-guarded). Mirrors the same pattern in+chooseRandomNode.17. Restore screen stuck at "Restored 100%" after recovery completes
Root cause. When recovery hits 100%,
onRecoveryProgressUpdatedtransitions to "Loading wallet" / "Synchronizing with node: 0%" and resetsoldProgress = 0. BEAM'sWalletDB::ImportRecoverytypically fires more than one progress event at completion (99.9% → finaldone == totalflush, sometimes a redundant 100%). Each subsequent callback setsprogressView.progress = 1.0and writes "Restored 100%" again — overwriting the sync label. Theif !stopRestoreshort-circuit then prevents a second chance to re-write.Fix.
onRecoveryProgressUpdatedearly-returns ifstopRestoreis already true. Once the transition has happened, no further recovery events touch the labels or the bar.18. iOS jetsam kill (highwater) mid-recovery on lower-memory devices
Symptom. On iPhone X (3 GB / iOS 16.7.10) the app was killed roughly halfway through recovery — JetsamEvent showed
"reason": "highwater",rpages: 25760× 16 KB = ~422 MB resident, snapshot file 452 MB. Instrumentation log showedRecoveryProgressevents firing every ~100 µs (~8000/sec) for the entire ~3 min recovery.Root cause.
IWalletDB::IRecoveryProgress::OnProgress(done, total)is called by BEAM'sWalletDB::ImportRecoveryat sub-1 KB granularity (BEAM's design, not iOS). The bridge inRecoveryProgress.mmdid three allocator-heavy things per call:NSLog, a copy of the weak-pointer delegates array, and[delegate onRecoveryProgressUpdated:total:time:]— which immediately didDispatchQueue.main.async { ... }. The main thread can't drain 8000 UI-update blocks per second; dirty memory crosses the per-app highwater threshold and the OS sendsSIGKILL.Fix. Throttle in
OnProgressitself, before any allocation. Newint m_lastReportedPercent = -1;. Compute integer percent first; early-return when it hasn't advanced (unlessdone >= total, so the final completion call always crosses the bridge). Collapses ~8000 callbacks/sec into ~100 deliveries total. TheNSLogis moved below the throttle. The duplicate-100%-callback handling from fix 17 still applies.19. Restore stuck on "Synchronizing with node: 0%" — node-connect handler unhooks the delegate prematurely
Root cause. Race between three callbacks:
onRecoveryProgressUpdatedat 100% setsstopRestore = true,isRestoreFlow = false,isWaitingRestore = true, label = "Synchronizing with node: 0%".onNodeConnectionChanged 1fires next.onNetwotkStatusChangechecks!onlyConnect && connected && !isRestoreFlow && phrase != nil && !isNodeProtocolEnabled— step 1 just flippedisRestoreFlowto false, so the gate opens. SchedulesopenMainPage()after 0.5s.When the delayed
openMainPage()fires, the original ordering inside it ranremoveDelegate(self)before theif isWaitingRestore { return }early-return — so the VC was unhooked from delegates while still on screen. SubsequentonSyncProgressUpdatedevents skipped this VC, and the terminal1232/1232event (which would have advanced the screen via fix 5's branch) never reached the Swift handler.Fix. Reorder
openMainPage()so the early-returns run beforeremoveDelegate(self)— the VC only unhooks itself when it's actually leaving the screen. The redundant secondremoveDelegatefurther down is idempotent.20. Asset-info storm at startup —
onStatusre-firesgetAssetInfofor all 195 assetsSymptom. ~1755
GET ASSET <id>lines in the first ~40s (195 unique IDs × 7–9 bursts). Each request also broadcastonAssetInfoChangeto every delegate, saturating the main thread enough that Send/Receive felt unresponsive.Root cause. Two interacting bugs in
WalletModel.mm:onAssetInfoextractednameinto a local but never wrotebmAsset.name = name. The property declared onBMAsset.hstayed empty.onStatusgatedgetAssetInfoonif (asset.name == nil || asset.name.isEmpty). Because step 1 never populatedname, the gate was always true, so eachonStatusre-fired requests for all 195 assets.Fix. Add
bmAsset.name = name;next to the other assignments inonAssetInfo. Wrap theonStatusgetAssetInfocall in anm_pendingAssetInfo.insert(asset.assetId).secondguard (newstd::set<beam::Asset::ID>member; erased on entry toonAssetInfo). Alsoasset.name = @"BEAM"in theassetId == 0fast path so BEAM participates in the same invariant. Re-captured log: ≤195GET ASSETlines total.21. Receive screen always shows the user's existing default address instead of generating a new one
Root cause.
generateNewWalletAddressWithBlockAndAmount:assetId:amount:result:ended withwallet->getAsync()->generateToken(TokenType::RegularNewStyle, bAmount, bAsset, version, false, func)— the 5th param isnewAddress. Withfalse, the core callsgetDefaultAddressAlways(wa)instead ofcreateAddress. The bridge name was misleading: it was just re-tokenising the same default. The companiongenerateNewWalletAddressWithBlock:sharesfalsebut that's correct for its callers (Send "from" field; faucet binding).Compounding issue.
ReceiveAddressViewModel.generateTokens()is called both fromcreateAddress()completion (once on Receive open) and fromamount.didSet(every keystroke). Flipping the flag naïvely would persist a brand-new wallet entry per keystroke — clear regression.Fix. Flip the 5th argument in
generateNewWalletAddressWithBlockAndAmount:totrue.ReceiveAddressViewModel.generateTokens().regularand!isOwncases switch to the synchronousgenerateRegularAddress:assetId:amount:isPermanentAddress:helper, which fetches the persistedWalletAddressviawalletDb->getAddress(m_walletID)and runsGenerateRegularNewToken(...)— re-tokenises the existing address with the new amount instead of creating a new one.currentTokenupdated to readaddress?.address ?? address?.walletId. Net: Receive opens once → one address persisted; subsequent keystrokes re-tokenise. Send/Onboarding unchanged.22. Settings / Select Node / Payment Proof — pages scroll past their content because the bottom action lives inside the table footer
Root cause. Three VCs stuffed bottom-anchored content (version label, action button) into
tableView.tableFooterViewat hard-coded heights. Even when the visible content was small, the fixed footer pushed total content past the screen and the table scrolled. A prior commit had papered over this inSettingsViewControlleronly viaadjustVersionFooterIfNeeded()measuringcontentSizeand mutating footer height — runtime resize loop on every layout pass.Fix. Lift bottom-anchored content out of the table entirely. New
bottomAccessoryView: UIView?onBaseTableViewController:didSetadds/removes fromview;viewDidLayoutSubviewslays it out atview.bounds.height - safeAreaInsets.bottom - hand subtractsh + safeAreaInsets.bottomfrom the table height.keyboardWillShowtranslates the accessory above the keyboard;keyboardWillHideresets. HonoursisHidden.SettingsViewController— deletesversionFooterMinHeight,adjustVersionFooterIfNeeded(), theviewDidLayoutSubviewsoverride, and 7tableFooterViewassignments.versionView()→makeVersionAccessory().SelectNodeViewController— deletesfooterView()(33 lines) and 6 assignments; a singleactionButton: BMButtonlives in a 70pt accessory; newrefreshActionButton()toggles visibility based onisCreateWallet || items[2].selected.PaymentProofDetailViewController—lazy var footerViewbecomes a 70ptbottomAccessory.BMDataPickerViewControlleraudited, unchanged. ~50 lines removed, ~12 added.23. Payment Proof — swipe-down doesn't dismiss the keyboard
Root cause.
PaymentProofDetailViewControllersetstableView.keyboardDismissMode = .interactive, which requires a drag against scrollable content. The input field lives intableView.tableHeaderViewand there are no rows yet, so contentSize equals bounds. The recenttableView.alwaysBounceVertical = falsebaseline closes the last loophole — nothing to drag.Fix. Toggle bounce on whenever the keyboard is visible, in
BaseTableViewController.keyboardWillShow/keyboardWillHide. Preserves the no-bounce baseline elsewhere; gives.interactivesomething to grab while typing. Every other VC inheritingBaseTableViewControllerwith a text input picks up the same fix automatically.24. Edge-swipe back gesture broken on every settings sub-page
Root cause. The wallet ships its own swipe-back via
BaseNavigationController(systeminteractivePopGestureRecognizeris unused).gestureRecognizerShouldBeginwas a class-name allowlist:SettingsViewControlleris reused for every settings sub-page (init(type: .general/.privacy/.utilites/.node)), so the gesture was silently denied for every one. Same forAddressesViewController(address detail is also an instance).Fix. Replace the allowlist with the actual intent — "is this VC the root of its navigation stack?":
~25 lines down to ~7. Every pushed sub-page gets the gesture; every side-menu root continues to block.
25. Settings → Utilities scrolls even though the rows fit
Root cause. Two compounding sources of dead vertical space:
SettingsViewControlleronly sets the tightsectionHeaderHeight = 15 / sectionFooterHeight = 15insideif type == .main. Every other type inherits the system default (~36pt × 2 per section in.groupedstyle); 3 sections silently add ~120pt.BaseTableViewController.viewDidLayoutSubviewsonly subtractedsafeAreaInsets.bottomfrom the table when abottomAccessoryViewwas set — the last row rendered behind the home indicator on Face-ID devices.Fix.
BaseTableViewController.viewDidLayoutSubviewsinitialisesbottomReservedwithsafeAreaInsets.bottom(single-line diff).SettingsViewControllerlifts the 15pt section-header/footer heights out of the.mainbranch. Utilities content drops from ~841pt → ~695pt; the page now fits cleanly above the home indicator on iPhone 14, 15, 15 Pro Max.