Skip to content

v7.4 - #594

Draft
Maxnflaxl wants to merge 48 commits into
masterfrom
v7.4
Draft

v7.4#594
Maxnflaxl wants to merge 48 commits into
masterfrom
v7.4

Conversation

@Maxnflaxl

@Maxnflaxl Maxnflaxl commented May 8, 2026

Copy link
Copy Markdown
Member

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.a over the iOS system libsqlite3.dylib (without it WalletDB::init always failed SQLITE_MISUSE); plus stale-DB cleanup, the iOS sandbox temp-dir restriction, reactor scope around WalletDB::init, the random-node create flow skipping the sync UI, the create-flow watchdog, the password screen swallowing taps, the "no rules" alert (thread_local Rules::s_pInstance not installed on the dispatch thread that runs -[AppModel restore:]), an iOS jetsam kill on lower-memory devices (RecoveryProgress::OnProgress re-dispatching ~8000 events/sec to main), and a stuck "Synchronizing with node: 0%" caused by openMainPage() removing its own delegate before its isWaitingRestore early-return.

Receive screen.

  • Faucet crash (wallet crashes on 'get coins' faucet button press #587) — token-address callbacks left walletId / label nil; fixed in the bridge and in BMAddress.fromAddress.
  • Redesign (SBBS address on main receive screen #588) — full-width address (no more 8WW…jwmfhg truncation), inline SBBS row with "use for CEX & mining" hint, prominent "copy address & close" CTA, asset picker that works with 0 non-BEAM assets.
  • Copy clarification (Better explanations on the receive screen #593) — 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).
  • Always generates a fresh address per Receive open (the old bridge call passed newAddress: false and 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 of generateOfflineAddress: to take offlineCount: and call the synchronous GenerateOfflineToken directly (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), BMDexOrder bridge wired to WalletClient::publishDexOrder / getDexOrders / cancelDexOrder and DexBoard::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_expire data 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_blockchain across all 13 locales — fixes the double space in Syncing 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 getAssetInfo for all 195 assets — ~1755 calls in the first 40s. onAssetInfo wasn't writing name, so onStatus perpetually 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 / BMAsset switched to typed decodeObjectOfClass:); BMField lineColor / lineHeight KVC-undefined warnings.

Settings / nav misc.

  • Bottom-anchored content lifted out of tableView.tableFooterView into a shared bottomAccessoryView on BaseTableViewController (Settings, Select Node, Payment Proof) — fixes pages scrolling past their content.
  • Payment Proof: swipe-down dismiss now works (toggle alwaysBounceVertical on while keyboard is up).
  • Edge-swipe back: replaced the class-name allowlist in BaseNavigationController with "is this VC the root of its stack?" — every pushed sub-page now gets the gesture.
  • Settings → Utilities: unified tight section spacing across all settings sub-pages + safe-area reservation for the home indicator.

DApp store and sideloaded DApps. New surface that talks to the on-chain DApp Store contract over IPFS through a wallet-owned WalletAPIClient (invoke_contract RPC), with separate My DApps and Publishers screens. .dapp open-URL handling from Files / iCloud Drive routes through DAppManager.installFromZip (staged extraction, manifest GUID validation, path-traversal sanitisation). Each installed DApp loads under a private dapp://app/... WKURLSchemeHandler so XHR / fetch get real HTTP status codes, MIME types, and CORS — native file:// returns status 0 to XHR and rejects fetch() outright, which breaks Utils.download("./*.wasm") (the path Beam DApps use to ship shader bytes into invoke_contract). BeamX DAO and BeamX DAO Voting ship as bundled .dapp packages. Embedded go-ipfs node is brought up at wallet-open time (asio_ipfs::config repo under Documents/ipfs-repo; private swarm key derived from beam::Rules::Network so 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. OraclePriceManager polls action=view_median through the wallet-owned API client every 60s, parses the val field (raw / 1e9 → BEAM/USD), and writes into ExchangeManager's USD/BEAM slot using the same shape WalletModel::onExchangeRates would have produced. WalletModel.onExchangeRates ignores remote BEAM/USD ticks while the toggle is on so the oracle value isn't clobbered. BMDataPickerViewController hides 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 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.

New features

1. Beam Messenger

Ports the upstream beam-ui Messenger 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 / BMChat model objects mirror wallet::InstantMessage / MessengerChatList::ChatItem. WalletModel overrides onInstantMessage / onGetChatList / onGetChatMessages / onChatRemoved and dispatches to four new optional WalletModelDelegate methods. AppModel exposes sendInstantMessage: / requestChats / requestMessagesForPeer: / markChatAsRead: / removeChat: / addChatStub: / resolvedPeerWalletId:. Send/receive goes straight to wallet->getAsync()->sendInstantMessage(...) etc. Address parsing handles both raw 68-char SBBS hex IDs and Beam tokens (WalletID::FromHex first, then ParseParameters → 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.a already creates and migrates the IM SQLite table. Live updates flow through the existing IWalletDbObserver::onIMSaved path.

View models / VCs. MessengerChatListViewModel / MessengerConversationViewModel cache the data and reload on the new delegate methods. Four VCs under ViewControllers/Main/Messenger/: chat list (with + / pull-to-refresh / swipe-to-delete), conversation (bubble list with brightSkyBlue/heliotrope bubbles, marks as read on appear), new-chat (my-address picker + peer + optional contact name, validates via AppModel.isAddress:), bubble cell, chat cell. Sidebar wiring + IconMessenger() icon helper (PNG rendered from beam-ui/ui/view/assets/icon-beam_messenger.svg). 9 new localization keys, all 14 .strings files updated. 12 new files registered into all three targets via the xcodeproj gem.

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.a rebuilt with BEAM_ASSET_SWAP_SUPPORT=ON (without it the DexBoard* / publishDexOrder / getDexOrders symbols are guarded out). Same macro added to GCC_PREPROCESSOR_DEFINITIONS for all six target × config combos.

Bridge. BMDexOrder mirrors wallet::DexOrder, with directional accessors (sendAmount / receiveAmount / etc.) populated via DexOrder::getSendAmount/getReceiveAmount/... — those already flip first/second based on _isMine, so the iOS layer doesn't have to. WalletModel overrides onDexOrdersChanged / onFindDexOrder. AppModel exposes requestDexOrders / publishDexOrderWithSendAsset:... / cancelDexOrderWithID: / acceptDexOrder:. Publish generates a fresh SBBS-only address via walletDb->createAddress(...) so each order has its own listener key. Accept builds params via CreateDexTransactionParams(...) flipping send/receive per the viewer's perspective. Default fee from getDefaultFeeInGroth.

Tx-status surfacing. WalletModel::onTxStatus gains a wallet::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. AssetSwapsViewModel buckets orders by tab (Open / My / History). AssetSwapCreateViewModel owns form state with a single validationError driving both the inline label and the publish button. Three VCs under ViewControllers/Main/AssetSwaps/: list (segmented Open/My/History), create form (asset chips reusing AssetSearchViewController, 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 an AmountList from the requested per-output groths, constructs CreateSplitTransactionParameters, sets Fee / AssetID, and submits via wallet->getAsync()->startTransaction. The split looks like a regular SimpleTransaction to 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 through splitCoins: it calls generateNewWalletAddress (resolves the default own address via getDefaultAddressAlways), uses address._id — the underlying hex WalletID rather than the token-style walletId getter which FromHex would reject — to generate a self offline-token via generateOfflineAddress(...offlineCount: 1), then calls send(...isOffline: true) to that token. The vouchers + TxType::PushTransaction route the output to the shielded pool.

View models. AssetUTXOGroup (value type, sorts UTXOs descending, exposes largestUtxo / concentrationRatio / isConcentrated / canSplit). UTXOViewModel.groupedByAsset() buckets available regular + shielded UTXOs by assetId (BEAM first). SplitCoinsViewModel owns both flows via a Mode enum (.split / .consolidate) and a sendOffline flag (derived: isShielded). sourceGroth is the largest UTXO in split mode and the total available pool in consolidate mode. outputAmounts collapses to a single sourceGroth − fee (BEAM) / sourceGroth (other assets) shielded output when isShielded; otherwise N equal slices for split (remainder absorbed into the last slice, fee subtracted for BEAM) or a single public output for consolidate. recalculateFee switches to getMinMaxPrivacyFeeInGroth + isShielded: true when the toggle is on. Defaults to suggestedSplitCount(for:allowed:) when isConcentrated, else 3; consolidate fixes the count at 1.

View controllers. UTXOViewController now renders one row per AssetUTXOGroup using the wallet-page AssetAvailableCell with a new opt-in setUTXOAccessory(count:) that lazily appends "N coins ›" at the trailing edge — hidden by default so the Wallet and Assets pages are unchanged. Tapping a row pushes AssetUTXOListViewController, the per-asset UTXO list. Its section header (AssetUTXOSectionHeaderView, layout updated to a two-row card) carries two pills: Split and Consolidate. Either pill opens SplitCoinsViewController — the same modal bottom sheet — in the appropriate mode; the sheet branches title / CTA / subtitle / summary / preview labels on viewModel.mode, hides the "Split into" pill row (2/3/5/8/10) in consolidate mode, and gains a labelled UISwitch row ("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 require sourceGroth > fee for BEAM. Honours Settings.isNeedaskPasswordForSend.

AssetIconView programmatic-init fix. Setup moved into a shared commonInit() 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 TokenType enum and the v7.4 create_address API; defaults match beam-ui's receive_view.cpp and BEAM core's GenerateOfflineToken:

Picker label C++ TokenType Vouchers (default) Notes
SBBS RegularOldStyle 0 legacy hex format, kept for CEX/mining
Regular Offline 1, tunable 1–30 matches desktop's default Receive — sender chooses online/offline
Max Privacy MaxPrivacy shielded, single-use
Offline Offline 10, tunable 1–30 explicit offline; matches GenerateOfflineToken and CLI --offline
Public Offline Public non-expiring offline donation address

Bridge. Two new AppModel methods (generateSBBSAddress: / generatePublicOfflineAddress:) calling generateToken(TokenType::RegularOldStyle/Public, ...). generateOfflineAddress: reworked to take offlineCount: and call the synchronous GenerateOfflineToken directly — the async generateToken(Offline, ...) path hardcodes 1 voucher. BMAddress gains nullable sbbsToken / publicOfflineToken properties.

Voucher-count cache. AppModel.offlinePaymentsByWalletId (NSDict keyed by SBBS WalletID hex), populated from WalletModel::onGetAddress(WalletID, address, offlinePayments). requestOfflinePaymentsCountForWalletId: is idempotent. New optional WalletModelDelegate.onOfflinePaymentsCountForWalletId:count: broadcast only when the value changes. Cache cleared in resetWallet:.

Picker. BMDataPickerViewController gains an address_type DataType reusing existing scaffolding. Receive section 3 row 1 (formerly the max-privacy switch) becomes a BMPickerCell that pushes the picker. ReceiveAddressViewModel.generateTokens() dispatches on selectedTokenType and 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 BMFieldCell titled "Vouchers count" with hint "Up to 30 vouchers per address". Gating lives in one place — var supportsVouchers: Bool on ReceiveTokenType — read by both the VM (regenerate on change) and numberOfRowsInSection. Defaults via var defaultVouchers: Int (Offline = 10, else 1). Input validation: 1...30 writes the VM; out-of-range or non-numeric flips the cell to BMFieldCell.error and does not write the VM, so token generation never runs against a bad count. Not persisted.

Address-details + cell row. DetailAddressViewModel appends "Offline transactions left: N" via the existing offline_left_address key when count >= 0. BMAddressCell reuses the previously-hidden expiredLabel outlet to show the count (avoids editing the xib).

Footer info text gating. var supportsOnlineReceive: Bool on ReceiveTokenType — true for .sbbs / .regular / .offline. Per-type caption: .maxPrivacy keeps its existing fee/lockup copy; .sbbs shows receive_description_2 only (no offline path); .regular and .offline show receive_description on an own node, fall back to receive_description_2 otherwise; .publicOffline shows the existing public_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 to QRCodeSmallViewController (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_address accepts never/24h/auto/expired for expiration, but the iOS edit screen had no UI to select Never — the section-2 actions ("Extend" / "Active address") always re-armed to 24h unless duration == 0 was already true. So a 24h address could never be promoted.

Fix. EditAddressViewController section 0 row 2 (the expiry detail row) switched from a read-only BMMultiLinesCell to AddressExpiresCell (already designed for this matrix, advertises the row as tappable). New presentExpiryPicker() pushes the previously-unused .address_expire BMDataPickerViewController (Extend = 24, Never = 0). Picker completion writes viewModel.newAddress directly: duration and isNowActiveDuration go to 0 or Settings.maxAddressDurationSeconds; isNowActive = true, isNowExpired = false, createTime = now. Bridge in AppModel.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 (existing BMDoubleAuthViewController(event: .owner) flow). Owner Key removed from Privacy. New SettingsItemType.node_type = 41 / .node_peers = 40; currentNodeTypeLabel() derives the label from Settings.isNodeProtocolEnabled / connectToRandomNode.

Node Peers screen. NodePeersViewController (BaseTableViewController, grouped, dequeued .subtitle-styled NodePeerCell): 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 three WalletModelDelegate connection callbacks funneled through one refreshFromNetworkChange() helper. Read-only.

Bridge. +[AppModel defaultPeerAddresses] returns the filtered random-peer pool from getDefaultPeers(); +chooseRandomNode rewritten to pick from it (single source of truth, no duplicate filter). New lastConnectionChangedAt stamped in setIsConnected: when the flag flips. iOS doesn't run an embedded node::Node (Mobile Node is enableBodyRequests, not a full node), so no PeerManager rating / inbound peers — the screen stays within what the wallet client knows.

8 new localization keys × 14 .strings files. 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) from EnterWalletPasswordViewController, 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 single Syncing 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 onlyConnect path: incoming onSyncProgressUpdated(done, total) events are stashed into pendingSnapshot instead of redrawing the percent label. When the timer fires (onStabilizationFinished), the VC decides once: open the main page if AppModel.isSynced() is already true or fewer than kTrivialSyncThreshold (= 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 SyncPhase enum (connecting / reconnecting / almostDone / finalizing) with resolveBoundaryPhase(done:total:) returning nil for 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. OpenWalletProgressViewController now implements two previously-unused WalletModelDelegate methods that the bridge has been firing for a while: onNetwotkStartConnecting: sets the "Connecting…" copy while the stabilization timer is still running; onNetwotkStartReconnecting flips to "Reconnecting…" on a mid-sync drop. onNetwotkStatusChange: now also stamps hasConnected = true and clears isReconnecting on the first successful connect so the phase resolver can tell "never connected" apart from "transient drop". The existing phrase != nil && !isNodeProtocolEnabled connect-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.strings files (English placeholder where translation is pending) plus the Localizable.shared.strings accessor.

Deferred — per-phase data from the C++ core. WalletClient::onSyncProgressUpdated(int done, int total) collapses six distinct request types (StateSummary / BodyPack / Body / Utxo / Kernel / Events from wallet/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::SyncDetail struct + new onSyncProgressDetailUpdated virtual + iOS bridge wiring) but requires rebuilding the prebuilt Frameworks/beam-ios-* per BUILD.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 .dapp files from Files / iCloud Drive.

Store query (DAppStore). Loads the bundled dapps_store_app.wasm shader, calls invoke_contract with action=view_publishers,cid=<storeCID> then action=view_dapps,... through the wallet-owned WalletAPIClient. Publisher list cached for the session; unwanted publishers filtered out via a UserDefaults toggle managed by PublishersViewController. BeamX DAO and BeamX DAO Voting always appear as "bundled" entries even when the on-chain store is empty.

Install pipeline (DAppManager). installFromZip stages the SSZipArchive extraction into tmp/dapp-stage-<UUID> first, parses manifest.json, validates the manifest guid against [a-fA-F0-9]{32,64}, then moves the directory into Application Support/dapps/<guid>. Manifest url is normalised through validateManifestURL (rejects .., leading /, anything outside localapp/ / app/). After extraction, sanitizeExtractedTree walks 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 .dapp packages (dao-core-app.dapp, dao-voting-app.dapp) auto-install on first launch via installFromBundle.

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 from pathExtension (application/wasm for .wasm, image/svg+xml for .svg, …), and CORS-permissive headers. Native file:// returns status 0 to XHR and rejects fetch() outright; Beam DApps call Utils.download("./*.wasm") to ship shader bytes into invoke_contract, which can't work otherwise. The handler also injects a tiny <script> shim into the entry HTML that forwards console.log/info/warn/error to a dappLog WebKit message handler so DApp output ends up in the iOS device log. Path-traversal guard refuses anything that resolves outside the DApp root after standardizedFileURL normalisation. The handler is per-WKWebViewConfiguration because WebKit rejects scheme registration on a live configuration; DAOViewController retains it for the lifetime of the load.

Sideloaded .dapp from Files / iCloud Drive. AppDelegate.application(_:open:options:) matches url.pathExtension == "dapp", opens the security-scoped resource, reads the bytes, and runs them through DAppManager.installFromZip(fallbackName: "Sideloaded DApp"). On success the top-most VC pushes MyDAppsViewController.

Wallet-owned WalletAPIClient. A second AppsApiUI instance distinct from the per-DApp one used by DAOViewController. Created once at wallet open (after IPFS config so ClientThread_Create(ipfsnode=true) can resolve the IPFS handle), with call IDs starting at 1_000_000 so result routing in AppModel.sendDAOApiResult: can disambiguate. Consumed by DAppStore (view_publishers / view_dapps), the IPFS download path (ipfs_get), and OraclePriceManager (view_median).

Embedded IPFS config. AppModel.configureEmbeddedIPFS (guarded on BEAM_IPFS_SUPPORT) writes asio_ipfs::config with repo_root = Documents/ipfs-repo before the AppsApi is created. Swarm key is auto-derived in ipfs_imp.cpp from beam::Rules::Network — no explicit configuration needed. WebAPICreator.createApi is flipped to ipfsnode=true so the resulting ApiInitData carries the IPFS service handle (without it, ipfs_get returns ApiError::NotSupported even when the feature is compiled in).

Result routing. Three concurrent API consumers now share AppsApiUI callbacks: the DApp page running in DAOViewController (per-DApp), the wallet-owned client (store / IPFS / oracle), and the legacy DAO path. AppModel.sendDAOApiResult: first asks WalletAPIClient whether 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 .strings files. 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. OraclePriceManager polls invoke_contract with role=manager,action=view_median,cid=<oracleCID> every 60s through the wallet-owned WalletAPIClient. The response carries a Beam-manager-formatted string under result["output"] ({"res": ["val": <u64>,"hEnd": <h>}]} — not valid JSON because of the square brackets with key:value pairs); the manager also returns a direct val field on the top level which the parser picks up first, falling back to a regex over output. Oracle returns price × 1e9; divides into BEAM/USD before normalising to BEAM core's price × Rules::Coin (1e8) writeback.

Writeback. ExchangeManager.shared().currencies is mutated in place — the existing USD/BEAM row's value / realValue / code is updated; if the row doesn't exist yet (cold launch, oracle answers before the centralised feed) a new BMCurrency is appended. Broadcasts onExchangeRatesChange. BMNetworkStatusView only re-renders its "rate not received" suffix on network-status callbacks — the first successful oracle write also nudges onNetwotkStatusChange: 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::onExchangeRates continues to receive remote Currency::USDCurrency::BEAM ticks; the loop now continues past that pair when Settings.isOracleEnabled so 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 by AppsApiUI while 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. BMDataPickerViewController hides 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. AddressViewModel fetches the public offline token via AppModel.getPublicAddress(...) once per process and caches it across instances (static var cachedPublicOfflineAddress). filterAddresses() removes any existing entry with the same walletId and inserts a synthetic BMAddress at index 0 — label "Public offline", duration = 0 (never expires), isContact = true. AddressTableView swipe-actions detect the row via isPublicOfflineEntry and only expose Copy (no Delete / Edit). Tap routes straight to OfflineAddressViewController without going through AddressViewController.

Settings cleanup. SettingsViewModel.SettingsItemType.offline_address and onOfflineAddress() 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 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).

Migration. UIViewController extension helpers (alert(...), confirmAlert(...), confirmAndSkipAlert(...)) now build BMAlertViewController.Action arrays and delegate to BMAlertViewController.present(from:title:message:actions:). Three call sites that previously built UIAlertController directly (MessengerChatViewController.onDeleteChat, AssetSwapDetailsViewController.onCancel, LegacyWalletSendViewController "Activate security mode") are migrated to the extension helpers.

Bugs and fixes

1. WalletDB::init always fails with SQLITE_MISUSE on a clean install

Symptom. Every fresh "Create wallet → Random node → Start" produced "Error: wallet not created" with WalletDB::init failed: sqlite error code=21 and BUG IN CLIENT OF libsqlite3.dylib: vnode unlinked while in use.

Root cause. Symbol conflict: BEAM's bundled libsqlite.a has sqlcipher (SQLITE_HAS_CODEC, sqlite3_key); the iOS system libsqlite3.dylib (pulled in transitively by Pods) does not. The linker was resolving most sqlite3_* symbols against the system lib, so BEAM opened the DB on a connection that doesn't speak sqlcipher and sqlite3_key() then returned SQLITE_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.a to OTHER_LDFLAGS for all six target × config combos. Preflight log now prints sqliteVersion=3.28.0 sqliteHasCodec=1.

2. Stale wallet.db strands the create flow forever

Root cause. createWallet early-returned when WalletDB::isInitialized (just fs::exists) was true. resetWallet had a hadWalletDb gate skipping the unlink when walletDb == nil — but the "Restore or create new wallet" flow calls resetWallet:YES from the password screen before the wallet has been opened, so the file was never deleted.

Fix. Drop the hadWalletDb gate (always unlink wallet.db + -wal / -shm / -journal). Drop the WalletDB::isInitialized early-return in createWallet and defensively unlink wallet.db, wallet.db.private, and sidecars right before WalletDB::init.

3. iOS sandbox blocks SQLite's default temp directory

SQLite falls back to /tmp for journals, which the iOS sandbox forbids. createWallet now sets sqlite3_temp_directory = sqlite3_mprintf("%s", NSTemporaryDirectory()) before WalletDB::init. Logged via the new preflight tmpdir= line.

4. Reactor scope around WalletDB::init

BEAM 7.5.14510's WalletDB::init registers I/O callbacks against the current reactor; reusing the reactor scoped in/out during AppModel.init produced SQLITE_MISUSE on real devices. createWallet now resets and recreates walletReactor, opens an io::Reactor::Scope around WalletDB::init, and only emits onWalledOpened while 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.onSyncProgressUpdated advanced only when total == done && total > 0 && isWaitingRestore. An already-synced node legitimately reports total == done == 0, so the guard was permanently false.

Fix. New condition: isWaitingRestore && (total == done || AppModel.sharedManager().isSynced()). Also delete an unrelated empty if block at the same site.

6. Random-node create flow skipped the sync UI entirely

SelectNodeViewController.onNext for random-node create called openMain() immediately after createWallet() returned, landing the user on a 0-BEAM main view while sync ran invisibly. Now also pushes OpenWalletProgressViewController(password:phrase:) like the mobile-node branch.

7. Create-flow watchdog timer was a no-op for fresh installs

OpenWalletProgressViewController.onTimeOut advanced only when Settings.isChangedNode() was true. On a fresh install the 10s watchdog fired and did nothing, hanging the screen indefinitely if no sync progress arrived. onTimeOut now always calls openMainPage() (already idempotent via isPresented).

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 hideKeyboardWhenTappedAround set tapGesture.cancelsTouchesInView = true, which swallows taps before UITextField can begin editing.

Fix. Flip cancelsTouchesInView to false; add a viewDidAppear override on CreateWalletPasswordViewController that focuses oldPassField / passField inside DispatchQueue.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.mm set only address.address; walletId and label were nil unless ParseParameters happened to populate walletId. OnboardManager.receiveFaucet then called BMAddress.fromAddress(result) whose copy-ctor did [NSString stringWithString:address.walletId].

Fix. Both token-generation callbacks initialize walletId = @"" and label = @"" before the optional parse. BMAddress.fromAddress: guards every stringWithString: against nil and now also copies the address field (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_close CTA 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.

  • ReceiveTokenCell rewritten programmatically (XIB deleted): title row + address details link; full-width middle-truncated address; copy/QR/share button row with fillEqually; conditional SBBS section (separator + value + own copy + italic gray hint); optional bottom hint. Hidden when sbbsAddress is nil/empty or equal to the primary token.
  • ReceiveViewController — registers the programmatic cell, derives the embedded SBBS via getTransactionParameters(token).address for own-node regular/maxprivacy tokens, calls loadFullAssetsList() in viewDidLoad to pre-warm the picker, and now opens the asset picker as a full-screen AssetSearchViewController (new) wrapped in a BaseNavigationController instead of BMPopoverMenu. Cell configured with allowAllAssets = true so the chip stays tappable at 0 non-BEAM assets.
  • New AppModel.loadFullAssetsList bridge → wallet->getAsync()->loadFullAssetsList(). New WalletModel::onFullAssetsListLoaded override fires onAssetInfoChange once the full list is in.
  • ReceiveAddressButtonsCell — share button removed from the XIB (now lives in the address card); a 240×44 copy_address_close CTA is added programmatically and drives the cell's auto-sized height.
  • BMAmountCell gains allowAllAssets: Bool opt-in (currency chip stays enabled regardless of held balance).
  • BMCellProtocol gains @objc optional func onClickCopyAndClose().
  • SendViewController.viewDidAppear no longer triggers viewModel.isNeedFocus.
  • New sbbs_address_hint = "use for CEX withdrawals and mining"; existing sbbs_address_new updated for consistency.

11. Receive screen — confusing payment-mode wording (#593)

Old copy:

Sender will be given a choice between regular and offline payment.

For online payment to complete, you should get online during 12 hours after coins are sent.

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 unused sender_choice key from the Swift accessor and all 14 locale files.

12. Sync screen — double space between label and progress percentage (#589)

en.lproj had "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) and BMAsset (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 == YES and were unarchived through the secure top-level API, but every property inside -initWithCoder: used the non-secure decodeObjectForKey:, falling back to NSKeyedUnarchiver's lenient allow-list.

Fix. -initWithCoder: in both classes switched to decodeObjectOfClass:[NSString class] forKey: for string fields and [NSNumber class] for numeric fields. BMTransaction.m / BMNotification.m / BMCurrency.m use the same pattern but didn't appear in this run's log — flagged as a follow-up audit.

14. BMField KVC-undefined warnings logged on every screen with a text input

Ten xibs set User Defined Runtime Attributes lineColor (UIColor) and lineHeight (NSNumber) on BMField, leftover from an older underline design. BMField never 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.didFinishLaunchingWithOptions read traitCollection.userInterfaceStyle == .dark and passed it to Settings.setDefaultDarkMode(_:), which writes only on first install — so the device's appearance got latched in. Replaced with an unconditional setDefaultDarkMode(false). Existing installs unaffected (setDefaultDarkMode short-circuits once isSetDarkModeKey is 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_pInstance is thread_local. The app installs it on three threads — main, the reactor's worker, and any thread that calls +chooseRandomNode — but OpenWalletProgressViewController.restoreCompleted dispatches AppModel.restore(...) onto a fresh DispatchQueue.global(qos: .background). The first BEAM call in restore: to touch Rules::get() (make_shared<WalletModel> or wallet->start, both synchronous on the calling thread) throws. The existing makeIWTCall workaround 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 the try block. Idempotent (Rules singleton itself is std::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%, onRecoveryProgressUpdated transitions to "Loading wallet" / "Synchronizing with node: 0%" and resets oldProgress = 0. BEAM's WalletDB::ImportRecovery typically fires more than one progress event at completion (99.9% → final done == total flush, sometimes a redundant 100%). Each subsequent callback sets progressView.progress = 1.0 and writes "Restored 100%" again — overwriting the sync label. The if !stopRestore short-circuit then prevents a second chance to re-write.

Fix. onRecoveryProgressUpdated early-returns if stopRestore is 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 showed RecoveryProgress events firing every ~100 µs (~8000/sec) for the entire ~3 min recovery.

Root cause. IWalletDB::IRecoveryProgress::OnProgress(done, total) is called by BEAM's WalletDB::ImportRecovery at sub-1 KB granularity (BEAM's design, not iOS). The bridge in RecoveryProgress.mm did three allocator-heavy things per call: NSLog, a copy of the weak-pointer delegates array, and [delegate onRecoveryProgressUpdated:total:time:] — which immediately did DispatchQueue.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 sends SIGKILL.

Fix. Throttle in OnProgress itself, before any allocation. New int m_lastReportedPercent = -1;. Compute integer percent first; early-return when it hasn't advanced (unless done >= total, so the final completion call always crosses the bridge). Collapses ~8000 callbacks/sec into ~100 deliveries total. The NSLog is 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:

  1. onRecoveryProgressUpdated at 100% sets stopRestore = true, isRestoreFlow = false, isWaitingRestore = true, label = "Synchronizing with node: 0%".
  2. onNodeConnectionChanged 1 fires next.
  3. onNetwotkStatusChange checks !onlyConnect && connected && !isRestoreFlow && phrase != nil && !isNodeProtocolEnabled — step 1 just flipped isRestoreFlow to false, so the gate opens. Schedules openMainPage() after 0.5s.

When the delayed openMainPage() fires, the original ordering inside it ran removeDelegate(self) before the if isWaitingRestore { return } early-return — so the VC was unhooked from delegates while still on screen. Subsequent onSyncProgressUpdated events skipped this VC, and the terminal 1232/1232 event (which would have advanced the screen via fix 5's branch) never reached the Swift handler.

Fix. Reorder openMainPage() so the early-returns run before removeDelegate(self) — the VC only unhooks itself when it's actually leaving the screen. The redundant second removeDelegate further down is idempotent.

20. Asset-info storm at startup — onStatus re-fires getAssetInfo for all 195 assets

Symptom. ~1755 GET ASSET <id> lines in the first ~40s (195 unique IDs × 7–9 bursts). Each request also broadcast onAssetInfoChange to every delegate, saturating the main thread enough that Send/Receive felt unresponsive.

Root cause. Two interacting bugs in WalletModel.mm:

  1. onAssetInfo extracted name into a local but never wrote bmAsset.name = name. The property declared on BMAsset.h stayed empty.
  2. onStatus gated getAssetInfo on if (asset.name == nil || asset.name.isEmpty). Because step 1 never populated name, the gate was always true, so each onStatus re-fired requests for all 195 assets.

Fix. Add bmAsset.name = name; next to the other assignments in onAssetInfo. Wrap the onStatus getAssetInfo call in an m_pendingAssetInfo.insert(asset.assetId).second guard (new std::set<beam::Asset::ID> member; erased on entry to onAssetInfo). Also asset.name = @"BEAM" in the assetId == 0 fast path so BEAM participates in the same invariant. Re-captured log: ≤195 GET ASSET lines 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 with wallet->getAsync()->generateToken(TokenType::RegularNewStyle, bAmount, bAsset, version, false, func) — the 5th param is newAddress. With false, the core calls getDefaultAddressAlways(wa) instead of createAddress. The bridge name was misleading: it was just re-tokenising the same default. The companion generateNewWalletAddressWithBlock: shares false but that's correct for its callers (Send "from" field; faucet binding).

Compounding issue. ReceiveAddressViewModel.generateTokens() is called both from createAddress() completion (once on Receive open) and from amount.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: to true. ReceiveAddressViewModel.generateTokens() .regular and !isOwn cases switch to the synchronous generateRegularAddress:assetId:amount:isPermanentAddress: helper, which fetches the persisted WalletAddress via walletDb->getAddress(m_walletID) and runs GenerateRegularNewToken(...) — re-tokenises the existing address with the new amount instead of creating a new one. currentToken updated to read address?.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.tableFooterView at 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 in SettingsViewController only via adjustVersionFooterIfNeeded() measuring contentSize and mutating footer height — runtime resize loop on every layout pass.

Fix. Lift bottom-anchored content out of the table entirely. New bottomAccessoryView: UIView? on BaseTableViewController: didSet adds/removes from view; viewDidLayoutSubviews lays it out at view.bounds.height - safeAreaInsets.bottom - h and subtracts h + safeAreaInsets.bottom from the table height. keyboardWillShow translates the accessory above the keyboard; keyboardWillHide resets. Honours isHidden.

SettingsViewController — deletes versionFooterMinHeight, adjustVersionFooterIfNeeded(), the viewDidLayoutSubviews override, and 7 tableFooterView assignments. versionView()makeVersionAccessory(). SelectNodeViewController — deletes footerView() (33 lines) and 6 assignments; a single actionButton: BMButton lives in a 70pt accessory; new refreshActionButton() toggles visibility based on isCreateWallet || items[2].selected. PaymentProofDetailViewControllerlazy var footerView becomes a 70pt bottomAccessory. BMDataPickerViewController audited, unchanged. ~50 lines removed, ~12 added.

23. Payment Proof — swipe-down doesn't dismiss the keyboard

Root cause. PaymentProofDetailViewController sets tableView.keyboardDismissMode = .interactive, which requires a drag against scrollable content. The input field lives in tableView.tableHeaderView and there are no rows yet, so contentSize equals bounds. The recent tableView.alwaysBounceVertical = false baseline 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 .interactive something to grab while typing. Every other VC inheriting BaseTableViewController with 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 (system interactivePopGestureRecognizer is unused). gestureRecognizerShouldBegin was a class-name allowlist:

if top is WalletViewController { return false }
else if top is SettingsViewController { return false }   // hits ALL settings sub-pages
else if top is AddressesViewController { return false }
...

SettingsViewController is reused for every settings sub-page (init(type: .general/.privacy/.utilites/.node)), so the gesture was silently denied for every one. Same for AddressesViewController (address detail is also an instance).

Fix. Replace the allowlist with the actual intent — "is this VC the root of its navigation stack?":

if nav.viewControllers.first === top { return false }
if let dao = top as? DAOViewController, dao.app.name.uppercased() == "BEAMX DAO" {
    return false   // WebView pan conflict
}
return true

~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:

  1. SettingsViewController only sets the tight sectionHeaderHeight = 15 / sectionFooterHeight = 15 inside if type == .main. Every other type inherits the system default (~36pt × 2 per section in .grouped style); 3 sections silently add ~120pt.
  2. After fix 22, BaseTableViewController.viewDidLayoutSubviews only subtracted safeAreaInsets.bottom from the table when a bottomAccessoryView was set — the last row rendered behind the home indicator on Face-ID devices.

Fix. BaseTableViewController.viewDidLayoutSubviews initialises bottomReserved with safeAreaInsets.bottom (single-line diff). SettingsViewController lifts the 15pt section-header/footer heights out of the .main branch. Utilities content drops from ~841pt → ~695pt; the page now fits cleanly above the home indicator on iPhone 14, 15, 15 Pro Max.

@Maxnflaxl Maxnflaxl linked an issue May 8, 2026 that may be closed by this pull request
@Maxnflaxl Maxnflaxl self-assigned this May 8, 2026
@Maxnflaxl Maxnflaxl linked an issue May 8, 2026 that may be closed by this pull request
- UI mostly the same
- move SBBS to main receive screen
- requested amount now allows any asset
- update locales
@Maxnflaxl Maxnflaxl linked an issue May 8, 2026 that may be closed by this pull request
@Maxnflaxl Maxnflaxl linked an issue May 8, 2026 that may be closed by this pull request
@Maxnflaxl Maxnflaxl linked an issue May 8, 2026 that may be closed by this pull request
Maxnflaxl added 19 commits May 8, 2026 19:24
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.
Maxnflaxl added 24 commits May 10, 2026 01:01
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant