Skip to content

feat: add usdt wallet and paykit payments - #884

Merged
ben-kaufman merged 65 commits into
masterfrom
feat/usdt-pr-ready-20261006
Oct 15, 2026
Merged

ben-kaufman merged 65 commits into
masterfrom
feat/usdt-pr-ready-20261006

Conversation

@ben-kaufman

@ben-kaufman ben-kaufman commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Closes #919

This PR adds USDT0 on Arbitrum One to Bitkit, with fees paid in USDT.

Uses Core 0.8.0-rc6 and Paykit rc74. Other platform: synonymdev/bitkit-android#1432. MAX requires deployment of the matching gateway update in synonymdev/bitkit-usdt-service@1456181 (MAX gas estimation, bridge status handling and actionable amount errors).

Description

  • Makes USDT opt-in and off by default, with the introduction and Settings switch. Disabled USDT is hidden from authorization and automatic clipboard detection, while Bitcoin/Lightning remain available. Explicit USDT links and scans use the enable modal and resume the original flow; declining cancels quietly. Funds or pending operations keep the Settings switch on and greyed out. Settled history with an unavailable balance preserves history access without re-enabling receiving options. Disabling requires a fresh zero balance and no pending operations.

  • Adds USDT balances, direct send/receive, Home activity and transaction details. The Home balance shares the existing two-column grid with hardware wallets. Entry and payment calculations retain six-decimal precision; displayed summaries and payment review round to cents, keeping nonzero sub-cent amounts visible. Verified receipt values and refund approvals retain up to six decimals.

  • Implements the supplied USDT Home, receive, send, activity and shared authorization designs using existing typography, keypad, rows, sheets, review, authentication and swipe controls. Uses USDT teal (#009393) throughout.

  • Adds MAX with a fee reserve, an explicit send-network pill, and a balance menu for supported payment sources. Plain EVM addresses keep the selected network; supported payment URIs provide the network. Network changes retain valid recipients and manually entered amounts; MAX is cleared so it can be recalculated for the selected route. New links cannot replace an active send. Returning to an unchanged payment review reuses its valid quote within the same send flow; input changes, expiry and submission require a fresh quote.

  • Separates transaction summaries from technical details, including verified sender addresses when known. Sent totals include actual fees. Pending, transferring, failed and refunded states preserve their actual status, provider references and support actions. The bridge layouts include full-screen Cross-Network Deposits, source-network refund instructions and a native confirmation with localized amounts. Deposit history remains accessible before funds arrive, with explorer and support actions retained.

  • Supports Paykit contact payments, requests and subscription installments with asset/rail-specific rates, payment deadlines and verified ERC-20 proofs. Review values the payable amount at Bitkit's own rate; request details retain the original denomination. Mixed-currency dollar requests initially select USDT only when the balance covers the quoted USDT amount, including rail-specific pricing and rounding; otherwise they start with Bitcoin. The payer can still change methods before submission. Bitkit-created subscriptions use one asset family. The Monthly Cost overview combines active BTC/USD/USDT amounts into one total valued at Bitkit's rate, while payment terms remain unchanged. Subscription review names the accepted payment methods and uses teal for USDT-only requests.

  • Distinguishes submitted proofs from verified receipts, including underpaid and late payments. Direct incoming transfers stay unattributed unless verified request evidence identifies the contact. Pending payments and attribution recover through encrypted VSS backups. Independent proof failures do not block later payments; an unsubmitted request binding is released only after checking the current Core journal, and is checked again after restart or restore.

  • Adds reusable Orchestra deposit addresses and outbound bridging through USDT0 or Orchestra, choosing the best eligible quote before approval. Cross-network review distinguishes the amount sent from the recipient estimate and keeps maximum debit and the details toggle visible above the swipe, with scrollable provider and fee details. Same-network review aligns its paired dividers and places the fee note below Hide Details. Already funded payments remain with their original provider.

  • Adds optional USDT receiving-address sharing on the Earn introduction, with the choice and privacy disclosure visible above the action buttons. Authorization retains the requester's identity and permissions; completion confirms whether Bitcoin, USDT or both were shared. With USDT off, combined authorization returns Bitcoin details only and USDT-address-only authorization is unavailable. When USDT is enabled, sharing its address requires explicit consent. Payment requests share the existing requester and invoice-note components.

  • Preserves fixed-amount Lightning invoices across method changes and displays available fee estimates. Bitcoin and USDT retries retain the originally approved payment terms and submission deadline.

  • Keeps unused opted-out wallets from creating USDT storage or polling the gateway on Home or during backup. Existing journals retain payment/proof recovery; local backup and restore remain available when USDT network configuration is disabled.

  • Uses bitkit.to/v1/usdt by default, with local build overrides. Recognized USDT fee-payment failures explain that a smaller payment may be needed.

  • Retains the existing system appearance on iOS 26 through the app-wide UIDesignRequiresCompatibility flag. The shared sheet wrapper also subtracts the bottom safe area from its fixed detent, since SwiftUI adds that inset; this applies to Bitcoin and USDT sheets.

Out of Scope

  • Translation completion; new strings use the agreed English fallback.
  • Cross-network Paykit payments, automatic recurring payments and USDT purchases in Earn.

Design

Bitkit - Handoff v62. The current USDT designs are in Refactor v63.

Incoming Activity shows the verified Arbitrum receipt and sender. Unknown original sender networks are omitted from incoming Activity; bridge-network details remain in Deposit Activity because batched deposits are not correlated to individual receipts. The new bridge activity, provider deposit history and refund layouts reuse the existing amount headers, detail rows, activity cards and buttons. Deposit history remains reachable before an Arbitrum receipt exists. Delayed, failed, refunding, refunded and support-needed states adapt these layouts with truthful provider status and conditional actions. Cross-network payment review and history loading/error variants still use the existing components and disclosures. Exact payment values, explicit networks and truthful provider states take precedence over inconsistent sample data.

Earn retains the existing-account privacy disclosure raised in Figma thread #999: sharing a USDT address exposes that account's balance and history. This text, the USDT-only introduction and the loading/unavailable variants do not yet have dedicated frames. Decorative artwork adapts to keep the disclosure and actions readable; these states are not claimed as exact Figma matches.

QA Notes

Integrates master through e40db967, including LDK rc74, Paykit rc74 and the first Bitcoin subscription payment flow. The shared subscription review preserves USDT method choices and styling; converted payments still require review. Core is pinned to 0.8.0-rc6; production release acceptance must include the matching Core release. The integration preserves proposal stream IDs, durable proof delivery, prepared Bitcoin fees and the existing USDT conversion/recovery rules.

Release acceptance remains open for the remaining live bridge routes, recovery faults, cross-platform VSS restore and Locks payment/unlock. Shop testing is deferred. MAX needs the gateway deployment noted above. Configuration and funded checks are in Docs/USDT.md and Docs/USDT-QA.md. Local bridge fixtures simulate delivery and use fixed paymaster prices; they do not establish live provider or fee coverage.

Historical fixture provenance: the review report for 0af48b7 used bitkit-docker #27 at 5d63e3a and #28 at 0395571, merged locally onto Docker main. This identifies that earlier review environment; it does not establish acceptance of the current PR head or live bridging.

Local validation: the broad run passed 2,556 of 2,557 local unit tests. Its sole stale refund-rounding assertion was updated for exact amounts; the affected 65 tests then passed on the final code. The six existing live/fixture-dependent suites remain excluded. The latest recovery, offline-backup and fee-warning changes received independent source review; these UI journeys have not been rerun for this fix batch. The earlier broader CI-selected run passed 2,562 tests with four fixture-dependent skips. The refund test checks both dot and comma locales, masking, network and full address. The earlier 31 focused wallet, activity, routing and sheet tests also pass. Incoming USDT receipts replace Receive through the shared queue once; they queue while deposits, send or other sheets are open and wait for receipt-sheet dismissal before showing the next receipt. The Receive replacement and duplicate-dismissal behavior are also unit-tested. Background deposit refresh keeps refund editing enabled and rejects stale results after selection or recovery changes. A native fixture confirmed the keyboard stayed open across delayed refreshes; exact-text automation was inconclusive because the simulator input tool injected incorrect characters. This queue and dismissal ordering are unit-tested; native presentation checks cover status icons, summary-first deposits, details and list formatting, not an incoming-payment event during deposit navigation. All six new bridge frames were compared with Figma using native fixtures. Receive/network/history, activity/history, Back, the prefilled support form, scrolling, refund confirmation and terminal-state eligibility were checked in the simulator. PIN/biometric callback behavior was source-reviewed; no live refund was submitted. USD display uses locale-aware grouping and exactly two decimals, including trailing zeros; stored payment values are unchanged. The earlier 355 focused payment, routing, recovery, amount and Home tests passed. In an isolated simulator using en_BR, USD entry, review and the recipient row all show the comma separator, and returning from review retains the amount. Classic BTC uses the dot separator and also retains its amount. Automatic USDT clipboard prompts are suppressed while a direct or contact payment is open. Existing automated coverage includes wallet protection, backup/binding, proof reconciliation and authorization. Native fixtures cover the supplied request, subscription and Earn designs, consent states and larger text; they do not establish live settlement or remote authorization.

SwiftFormat reports 43 existing findings in 11 files byte-identical to master; none are in the changed files. Translation validation has no errors and uses the approved English fallback. Funded payments were not repeated for these fixes. The companion E2E change handles the optional USDT intro before waiting for the wallet balance.

Journeys

  • J16 new usdt-bridge-details.xml — receiving and delivering layouts, provider deposit history, summary/details navigation, deferred incoming celebrations, a single celebration when replacing Receive, status icons, uninterrupted refund-address editing during polling, exact localized refund confirmation and cancellation, terminal states, explorer access and history access before a receipt.

  • J15 new usdt-payment-restoration.xml — unpaid draft restoration, payment progress across Android recreation, returning to Wallet after process death during or after submission, a fresh empty Send and explicit link reopening; uses local test funds only.

  • J14 new usdt-opt-in.xml — introduction choice, protected on/disabled Settings, explicit-link enablement, quiet cancellation and no gateway polling for unused opted-out wallets. Other USDT journeys now explicitly enable USDT first.

  • J12 new usdt-bridge-amount-errors.xml — rejected send quotes and receive estimates show an actionable bridge-cost message; increasing the amount opens review or a receive QR. Exercised with a local rejection fixture and unfunded live success responses.

  • J11 updated hardware-wallet/send-onchain.xml — open the funding-source menu and explicitly select the paired wallet.

  • J1 new paykit-quoted-usdt.xml — quoted currencies and methods, localized two-decimal USD amounts across review, sent screens, cards and details, Back navigation, the available USDT balance label and unpaid dismissal.

  • J2 new share-receiving-details.xml — enabled consent, disabled combined Bitcoin/USDT authorization, USDT-only unavailability and address-loading failures and USDT becoming enabled while authorization is open.

  • J3 new lightning-fee-review.xml — fixed Lightning amounts and available fee estimates.

  • J4 new onchain-single-activity.xml — one Bitcoin activity through synchronization, confirmation and restart.

  • J5 updated create-and-propose.xml — subscription methods follow the selected denomination.

  • J6 new usdt-bridge-support.xml — refund/support details and copied provider references.

  • J7 new usdt-payment-links-and-scans.xml — enabled/disabled clipboard detection, explicit links/scans, exact amounts, dismissal across navigation and preserving an active send.

  • J8 new usdt-receive-amount-and-units.xml — dollar entry, BTC equivalent, explicit networks and discarding unconfirmed edits.

  • J9 new usdt-wallet-navigation.xml — Home tile placement with odd/even hardware-wallet counts, wallet activity and fresh receive sessions.

  • J10 new usdt-send-review-and-details.xml — quote reuse, changed-payment and expiry checks, deducted bridge-cost warnings, MAX, network changes, payment-source menu, long balance/network labels, bridge details visibility and technical transaction details.

  • J13 updated subscriptions/review-and-subscribe.xml — USDT-only subscriptions preserve their locked payment and use teal; the overview combines mixed currencies into one wallet-valued monthly total.

Manual Tests

  • 1 Pay a contact, request and subscription installment → verify receipt, attribution and restart recovery — funded Arbitrum accounts and controlled billing clock not in Capabilities.
  • 2 Deposit and bridge through each enabled route → verify destination, amount, refunds and references — funded cross-network accounts and provider controls not in Capabilities.
  • 3 Interrupt backup, retry and restore on the other platform → a recorded payment stays pending without another approval, then recovers without duplication — controlled VSS outages and cross-platform restore not in Capabilities. Also check an interrupted pre-submission Paykit attempt with no Core journal entry can be retried, and a deadline expiring during backup displays expiry while retaining the recorded operation.
  • 4 Review quoted BTC/USD/USDT and same-asset requests at controlled market rates → payable terms remain fixed and wallet valuation uses its own rate — controlled rates and receipt fixtures not in Capabilities.

Automated Checks

  • added UsdtPreparedQuoteTests.swift — exact payment/wallet matching and expiry-buffer boundaries for quote reuse, plus fee warnings that include deductions from the bridged amount.

  • updated PaykitAmountTests.swift — locale-aware two-decimal USD display, grouping, single-cent and maximum-value boundaries.

  • added PaykitAmountEntryConversionTests.swift and HomeWalletBalanceTests.swift — stable repeated unit changes, trailing-decimal entry and unavailable USDT valuations.

  • expanded request, backup, wallet, contact and LNURL coverage — fixed external denominations, proof-free retry binding release, optional endpoint failures, hardware-only routing and exact fixed LNURL amounts.

  • updated PaykitPaymentRequestServiceTests.swift — combines monthly amounts at the wallet rate and verifies unchanged proof reconciliation does not trigger another backup. Also checks quoted USDT preselection at zero, insufficient, exactly rounded and surplus balances.

  • updated PubkyAuthApprovalSheetTests.swift — enabled/disabled consent, Bitcoin-only and combined payloads, required USDT unavailability, late enablement and preserving optional consent during authorization.

  • updated ClipboardPromptValidatorTests.swift and UsdtPaymentLinkTests.swift — disabled USDT filtering, preserved Bitcoin/Lightning detection and explicit payment links.

  • updated UsdtWalletManagerTests.swift — balance and pending-operation protection, settlement, restore/reset, refund eligibility, exact localized refund confirmation, offline backup/restore without network configuration, unused-wallet storage gating, hidden balances and queued receipt presentation, including replacing Receive once and avoiding duplicate celebration after dismissal.

  • updated SheetViewModelTests.swift — incoming receipts can replace Receive but do not interrupt other sheets.

  • updated PaykitPaymentStateBackupTests.swift — payment binding protection across reconciliation, restore, unreadable persistence and wallet identities, and recovery of interrupted sends only when Core has no operation and no send is active.

  • updated UsdtDisplayTests.swift — exact localized input and amount formatting.

  • ran native simulator presentation checks with isolated rendering fixtures — authorization, protected Settings, enable modal, network picker, confirmation details and decimal formatting. Also checked explicit-link Enable/Cancel in the normal app. Fixtures do not prove live settlement or service authorization.

Matching send, Paykit and hardware-wallet Appium updates: synonymdev/bitkit-e2e-tests#280. Both mobile workflows select that companion branch automatically; the full Appium payment flows remain for CI.

@greptile-apps

greptile-apps Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Critical impact] Adds USDT wallet and payment processing with new backend endpoints.

Fix the fixed-Lightning amount mismatch before merging so users pay the amount shown in review.

Findings

  1. P1 Payment exceeds reviewed amount ▶
  2. P2 Bitcoin Receive stays loading ▶
  3. P2 Receive tests stop early ▶

Summary

Adds an Arbitrum USDT wallet with fees paid in USDT.

  • Bitkit users can send, receive, and track USDT from a dedicated wallet.
  • USDT payment links and scans open the existing send flow.
  • Paykit requests can use USDT with payment terms and verified receipts.
  • Paykit subscriptions can set payment methods for their amount type.
  • Encrypted wallet backups include USDT and Paykit payment records.
  • Pubky authorization can include optional USDT receiving details.
  • Bitcoin sends keep one activity entry as wallet sync catches up.

Acknowledged scope and QA:

  • ben-kaufman records remaining bridge, refund/recovery, full VSS restore, signed address-sharing callback and Locks acceptance as open QA. Shop testing is deferred.
  • ben-kaufman marks translation completion out of scope and accepts English fallback.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Contact or payment request] --> B[Choose payment method and review amount]
  B --> C[Bitcoin payment]
  B --> D[USDT quote and approval]
  D --> E[Save request binding]
  E --> F[Back up signed payment]
  F --> G[Submit USDT payment]
  G --> H[Reconcile transfer]
  H --> I[Send Paykit proof]
  I --> J[Receiver verifies receipt]
Loading

Reviews (1) · Last reviewed commit: "fix: keep one activity per bitcoin send" · Reviewed by Greptile

Comment thread Bitkit/Utilities/PaymentNavigationHelper.swift
Comment thread Bitkit/Views/Wallets/Receive/ReceiveQr.swift Outdated
Comment thread BitkitUITests/UsdtWalletUITests.swift Outdated

@talosmachina talosmachina left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No findings. Adds a USDT (Arbitrum) wallet with Paykit USDT payments, ethereum: request routing from scan, clipboard and deep links, a USDT companion claim in Pubky auth approval, and USDT state in the wallet backup, on bitkit-core 0.8.0-rc2. Reviewed d7b72f3, oversized tier (131 files, about 6.9k added lines): I read only the money, auth-claim, backup and routing paths. The views, strings and the rest were not reviewed.

What I checked, and the candidates I ruled out

Read: UsdtWalletManager.swift, PaykitUsdtPaymentService.swift, PubkyAuthRequest.swift (claim payload and encode), the companion-approval path of PubkyService.swift, resolveUsdtPaymentRoute and its four callers (ScannerManager main and send scans, MainNavView clipboard and deep link), AppViewModel.requiresLightningNode, Env.configValue and the USDT build settings in project.pbxproj, and in bitkit-core v0.8.0-rc2: usdt/wallet.rs, usdt/backup.rs, usdt/payment_request.rs, usdt/proof.rs.

Ruled out

  • USDT route with the feature off: resolveUsdtPaymentRoute throws when the flag is off. The send scanner routes only in the unrestricted scope. Every caller re-checks isSendPresented and the navigation path after the sheet hides.
  • Foreign chain or token: core's parser accepts only chain 42161 with the USDT contract, and rejects unknown or repeated query keys.
  • Payment marked unstarted after a real broadcast: PaykitUsdtPaymentService.send clears paymentStarted only when core holds no stored transfer for the quote. Core persists signed operations before broadcasting.
  • Companion claim with a different USDT address: the endpoint is re-fetched and must equal the address the user approved. unsignedPayload checks that the endpoint's raw payload decodes to that same address.
  • PIN bypass: UsdtSendView and the deposit history check PIN-for-payments like the bitcoin send does.
  • Debug/Release endpoint drift: both configurations carry the same four USDT URLs.

Not done: no build, because iOS does not build on Linux, so this is reasoned from the code. CI was still pending at review time.

Merge confidence: 3/5, the oversized-tier ceiling. The reviewed paths are clean, but most of the diff was not read.

ovi-reviewer[bot]

This comment was marked as resolved.

@jvsena42 jvsena42 left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Code scan at d7b72f3: no HIGH or MEDIUM in the iOS code, 3 LOW inline. Not approving yet: Run Tests is red at this head on a UI test this PR adds (UsdtWalletUITests.testReceiveAndInvalidPaymentStaySeparateFromBitcoin), which other reviewers have already raised, and the device journeys are still to run.

The PR is large, so the review was split into three areas (USDT wallet, Paykit payments and backup, authorization and the rest). What held up on reading, with core checked at v0.8.0-rc2:

  • Amounts: the keypad caps at 6 decimals, parsing is integer-only with checked arithmetic, no Double on the send path, and a link's uint256 round-trips exactly.
  • Payment links: core pins chain 42161 and the USDT token, rejects unknown or repeated keys and checksums the recipient; the app passes only recipient and amount, and review and send use the same quote. Hardware-wallet scans stay Bitcoin-only (SendOptionsView.swift:22, ScannerManager.swift:191).
  • Double submit: send() and confirmPayment() guard on busy/submitted, core send is idempotent per quote id and refuses a second pending transfer.
  • Paykit: the payable amount is re-derived and must equal the reviewed terms at pay time for both Bitcoin and USDT; deadlines use the earlier of the request deadline and the quote expiry, including recovery after relaunch; in-flight protection reads the keychain, so a restart does not reopen a USDT-bound request to Bitcoin; one transfer cannot satisfy two requests.
  • Address sharing: the USDT row is shown whenever the claim asks for it, the address is re-read and compared at approval, declining omits the field and the rest proceeds, and the approval is bound to the request displayed.
  • Keychain: one new entry (paykit_usdt_payments), device-only, nothing seed-derived. Trezor stored keys from v2.5.0 stay byte-identical.
  • The copy of #916 carried here is identical to that PR's head.

One cross-platform item, confirmed with a test on the Android side: this build omits nil requestId, binding and billingPeriod from a backed-up USDT attempt, and Android cannot decode a backup with those keys missing, so an iOS backup holding any one-time or request-less USDT payment fails to restore there. The finding and the fix are on the Android model: synonymdev/bitkit-android#1432 (comment). Nothing has to change here unless you prefer to encode explicit nulls on this side.

perf: relevant (a 30 s foreground refresh loop with history, 10 s with a pending transfer, plus 10 s loops on receive and details, and a VSS upload before every USDT send). Not measured yet.

Device gate (partial): d7b72f3 — not run yet. Driven so far on an iPhone 17 simulator (debug build, no USDT funds): Home shows the USDT row, the wallet and Receive open on Arbitrum One, and the Back behaviour in the first inline note. Pending: usdt-wallet-navigation.xml, usdt-receive-amount-and-units.xml, usdt-payment-links-and-scans.xml and share-receiving-details.xml as far as they go without funds, and the refresh rate idle on Home. usdt-bridge-support.xml and the paid steps of paykit-quoted-usdt.xml need mainnet USDT and provider history, which I do not have.

The build I ran carries a one-expression local change (second inline note); nothing else differs from the head.

Comment thread Bitkit/Views/Wallets/UsdtReceiveView.swift
Comment thread Bitkit/Views/Wallets/UsdtDepositHistoryView.swift Outdated
Comment thread journeys/paykit-quoted-usdt.xml Outdated
ovi-reviewer[bot]

This comment was marked as outdated.

ovi-reviewer[bot]

This comment was marked as outdated.

jvsena42
jvsena42 previously approved these changes Oct 12, 2026

@jvsena42 jvsena42 left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving at 8ed632b with 1 LOW open (inline). It can be fixed here or moved to a follow-up issue; a reply on the thread saying which is enough.

8ed632b answers my three earlier notes, and I read the rest of the commit: the fixed Lightning amount is now locked on Spending in the amount screen and on review, and submit rejects an amount that differs from the invoice (SendConfirmationView.swift:883-901), which closes the editable-invoice case raised on the old head; USDT sends pass the earlier of the request deadline and the quote expiry to core (0.8.0-rc3, one commit, enforcing that deadline before signing and before broadcast); USDT links and scans are ignored while a contact payment is open.

Device gate: 8ed632b — journeys: usdt-receive-amount-and-units.xml 7 of 9 actions passed (not checked: the Share sheet text, and the last action, which needs an existing USDT activity row), usdt-payment-links-and-scans.xml and usdt-wallet-navigation.xml driven in part (below); perf: idle on Home the mnemonic is read from the keychain 14 times in 190 s after launch (0 after launch on a build without this PR), 1 ACTIVITY and 2 WALLET backups in the same window, no gateway errors; Figma: not compared; Android: not run, synonymdev/bitkit-android#1432 has changes requested.

iPhone 17 simulator (iOS 26.5), debug build, USDT balance 0:

  • Receive: "Receive Dollars", the USDT tab and "Network: Arbitrum One"; 3.5 shows $ 3.5 with a BTC equivalent and no unit toggle; Continue gives …&uint256=3500000; Details show the same 0x… address; Savings shows "Receive Bitcoin" and USDT comes back with the Arbitrum QR.
  • Back from the amount page: Edit → 5 → Back leaves the QR without uint256; with 3.5 committed, Edit → 9 → Back keeps uint256=3500000.
  • Send: a pasted ethereum: link with uint256=10001 opens the amount screen at $ 0.010001 with the recipient carried to manual entry; a seventh decimal is refused; wrong chain, wrong token, a repeated uint256, a short recipient and a Bitcoin address each give "Enter a valid address for the selected network." and no amount screen.
  • The head builds on Xcode 26.5 without the local change I needed before.

⚠️ Not run: usdt-bridge-support.xml, the funded parts of USDT-QA.md, and the "entire balance" action of the links journey — they need mainnet USDT and provider history, which I do not have.
⚠️ Not run: pubky-auth/share-receiving-details.xml — needs the watch-only authorization fixture.
⚠️ Not run: the new lightning-fee-review.xml actions (a contact publishing a fixed 10,000-sat invoice next to USDT), the ethereum: deep-link variants through the system URL handler at this head (driven by paste instead), and paykit-quoted-usdt.xml.

Skipped — covered by CI at 8ed632b: unit tests, integration tests, build (green). e2e-tests-local was still running when I posted this.

Comment thread Bitkit/Managers/UsdtWalletManager.swift Outdated
@ben-kaufman

Copy link
Copy Markdown
Contributor Author

@jvsena42 Updated the shared J5 journey on both platforms: Savings has the fresh Bitcoin QR; Spending shows a Lightning QR when receive liquidity is available, otherwise the Spending receive setup. The unfunded setup screen was expected behavior, so no payment or funding was needed to fix this expectation.

This update also includes current master's migration-retry changes. The combined regression run passes 623 tests, with only the 12 existing destructive cleanup tests skipped. The unavailable funded/provider/share checks remain open; they are not counted as passes.

@ovi-reviewer

This comment has been minimized.

jvsena42
jvsena42 previously approved these changes Oct 12, 2026

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving at 0951822. Since my last approval the branch merged master (the migration retry changes from #852, which I reviewed there) and added one commit that fixes the LOW I left; no finding from me is open.

  • wallet() now returns the cached core before anything else, and the credential read moved into initializeWallet, so the idle refresh no longer touches the keychain. The cache is dropped in wipe(), and a wallet change rebuilds the manager with the session.
  • Measured on the simulator, idle on Home for 200 s: 5 mnemonic reads, all within 13 s of launch, none afterwards (14 after launch at 8ed632b). USDT still loads: the Home row and the Arbitrum receive QR are there.

Device gate: 0951822 — journeys: unchanged from my review at 8ed632b (no journey code path changed; the one journey edit is wording); perf: 0 mnemonic reads per minute idle after launch (4.4 at 8ed632b). The ⚠️ not-run list from that review still applies.

Run Tests, Run Integration Tests and build-local were still running at this head when I posted this.

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@ben-kaufman please resolve the merge conflicts. QA review has not been performed for this request.

jvsena42
jvsena42 previously approved these changes Oct 14, 2026

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Round 7, the delta b522b8f..587c542: f555905 (USDT receipt queue and deposit-detail alignment), 397b355 (master merge with #881 conflict resolution), c348e73 (localized refund amounts), 99fbda1 (tests), 587c542 (MainNavView split).

Approving: nothing above LOW. One new LOW inline, non-blocking; fix it here or in a follow-up issue.

Code review, Fable, including --remerge-diff of 397b355 against both parents. Checked and clean:

  • shouldAutomaticallyPay keeps the PR's conversion == nil and master's dropped .lightning condition. USDT cannot reach the Bitcoin single-swipe path (requiresAssetSelection roots it at Amount → .usdt).
  • The hardware fallback now uses the Bitcoin on-chain amount.
  • SwipeButton isSubmitted replaces isConfirmed with the same rendering.
  • No wrong-asset, double-pay or auto-start-without-review path.
  • The receipt queue defers on Cross-Network Deposits, sheets, replacement and send, and never emits for outgoing or refund rows.

Build: 99fbda1 failed to compile here with Xcode 26.5: MainNavView.swift:421: the compiler is unable to type-check this expression in reasonable time, reproduced with the machine idle. 587c542 (the split) builds with stock settings. The device run below used 99fbda1 built with raised solver limits; 587c542 only reorganises the same modifiers into computed properties.

Device gate: 99fbda1 (behaviour-equivalent to 587c542):

  • USDT received while Cross-Network Deposits is open: nothing on that screen, one celebration after Back.
  • USDT received while USDT Receive is open: the celebration shows twice (the LOW).
  • Deposits:
    • The detail opens on the summary (Status, Receiving network, Sender network); Details shows "Request refund" / "Hide details".
    • Rows read "$3,00" with no space.
    • Confirm Refund reads "Request refund for 3,50 USDT deposit?".

perf: nothing relevant in this delta.

Skipped — covered by CI: unit tests, still pending at 587c542.

Comment thread Bitkit/Views/Wallets/UsdtReceiveView.swift Outdated

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Scope: Reviewed the full PR diff against its merge base, at 4caa785e.

No new actionable code findings.

Quote reuse matches the Android follow-up at dcbf816: same sender, amount, recipient, and network, and only with more than five seconds of life. Deposit refresh bails while refund confirmation or PIN entry is active and does not clear the refund address for the same deposit. Paykit USDT send marks the attempt started and clears that flag when authorization fails or core has no stored transfer. Request amounts are parsed before pricing. The funding-source menu uses the same AssetButton-switch and SendFundingSources identifiers as Android.

iOS leaves the final expiry rejection inside sendWithDeadline rather than rejecting the five-second buffer in the app before that call. That path was not shown to broadcast a different amount. Source inspection only; the USDT and Paykit unit tests were read and not executed. Device testing was not performed. The same unresolved design notes as Android, including the history-sync line, cross-network deposits entry, subscription rows, and Earn intro, remain on their existing threads.

Suggested additional test cases

  • iOS, USDT send review with a quote that has just over five seconds of life: start PIN or biometric authorization and let the quote fall inside that buffer before authorization finishes. Expected: no broadcast, and the review shows the expired-quote error.

Related primary: synonymdev/bitkit-android#1432.

@BitcoinErrorLog

Copy link
Copy Markdown
Contributor

Thanks Ben. The Shop is moving to one "Pay with Paykit" option, so a single request will offer both BTC and USDT, and I checked that this PR already handles it: it parses both endpoints, lets the buyer switch rails with the fixed rate, and sends the USDT proof back to the server that issued the request. Nothing here blocks.

One small UX note, not blocking: on a USD-priced request, USDT is preselected whenever the buyer has USDT enabled, even with a zero USDT balance (Android AppViewModel.kt:3388-3397, iOS AppViewModel.swift:48). The buyer then sees "insufficient USDT" and has to switch to Bitcoin by hand. Could the preselection pick USDT only when the USDT balance covers the payable amount, and fall back to Bitcoin otherwise?

@ovitrif

ovitrif commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

@BitcoinErrorLog thanks for checking it against the single "Pay with Paykit" option. Acknowledged: with the Shop on Paykit, USDT via Paykit from this PR is in scope for Bitkit 2.6.0 and goes into RC2.

@ben-kaufman

Copy link
Copy Markdown
Contributor Author

@BitcoinErrorLog fixed in 49033c7. For a dollar request offering Bitcoin and USDT, USDT now starts selected only when its balance covers the exact payable USDT amount; otherwise Bitcoin starts selected. This uses the request’s quoted rail rate and atomic rounding. USDT-only requests, existing bindings, manual switching and final fee checks are preserved. Android has the matching fix.

The new boundary test and 2,554 local unit tests pass; format diagnostics have no additions. The shared payment-request journey is updated. No funded payments were repeated.

@piotr-iohk I checked the authorization timing suggestion against Core rc6: sendWithDeadline rejects a quote with five seconds or less remaining before RPC/signing, then rechecks before signing. The app clears the cached quote before submission and shows the expired-quote error on failure. The quote-reuse boundary tests pass; I have not run the delayed PIN/biometric scenario on a device, so that remains a useful device QA check. No duplicate app-side expiry guard was added.

@ovitrif ovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code review from our side, no device runs: one MEDIUM below, plus a few LOWs. Nothing above MEDIUM, and no loss-of-funds path found. Already-raised threads are not repeated.

LOWs:

  • Wallet backup is coupled to USDT config: with USDT disabled in a build (the documented switch-off) while a usdt-*.sqlite exists, backupSnapshot() throws NotConfigured and every wallet backup fails, Bitcoin and Paykit state included; restoring a backup with usdtWallet in such a build aborts the whole restore (BackupService.swift:925, :356, UsdtWalletManager.swift:176-185). Worth a plan for the hotfix case.
  • bitkit-core moves from 0.5.18 to 0.8.0-rc6 in 2.6.0 (project.pbxproj), a release candidate in a store build.

Comment thread Bitkit/Services/PaykitUsdtPaymentService.swift
Comment thread Bitkit/Info.plist
Comment thread Bitkit/Views/Sheets/Sheet.swift
@ben-kaufman

Copy link
Copy Markdown
Contributor Author

Addressed the new review in 767789a and checked the same paths on Android:

  • Recover interrupted Paykit attempts only when Core has no operation and no send is active; retain recorded payments and proof protection.
  • Keep local USDT backup/restore working when network configuration is disabled, and avoid creating or polling unused opted-out wallets.
  • Include deducted bridge costs in fee warnings, show submission-deadline expiry, and preserve exact receipt/refund amounts.

The rc6 dependency is intentional for this integration. The matching Core release remains part of production release acceptance; this PR does not make that release decision. The two shared iOS appearance changes are now explicit in the description.

Validation: 2,556 tests passed in the broad local run; its only failure was a stale refund-rounding assertion. After updating it, all 65 affected tests passed on the final code. Six existing live/fixture suites were excluded. Full SwiftFormat diagnostics match the existing baseline with no findings in changed files. Independent recovery/quality review is clean. Journey specs were updated on both platforms; no funded payment or native journey run was repeated for this batch.

@ben-kaufman
ben-kaufman requested a review from ovitrif October 15, 2026 09:33

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Scope: Reviewed the full PR diff against its merge base e40db96, at 767789a. No prior iOS baseline was supplied.

No new actionable code findings.

No new actionable code findings. Send and Paykit paths recheck the reviewed amount, recipient, and payment terms before broadcast. A missing local transfer releases the Paykit binding; a pending or confirmed transfer keeps it. Cancellation during send does not clear paymentStarted, and the next reconcile releases that binding only when history is still empty. Unused wallets do not poll the gateway or create USDT storage for backup. The home headline is blank until a visible USDT balance and a fresh rate exist; testTotalIsUnavailableUntilVisibleUsdtHasABalanceAndFreshRate encodes that, and the USDT tile still shows its own amount.

Compared with bitkit-android#1432 at 12dfd8a for this recovery commit: fee totals, exact refund formatting, intro dismissal with funds, and binding release match. Asset binaries and the localization catalog were not compared to Figma.

Validation: source and unit tests were read, not executed. Device testing: not performed in this review.

Suggested additional test cases

  • iOS, funded wallet whose USDT choice is still undecided: tap No Thanks. The intro does not return, the USDT tile stays, and Settings cannot turn USDT off while a balance or pending transfer remains.
  • iOS, Paykit contact payment: let the submission deadline expire after the wallet has stored the transfer. The sheet stays on that quote, shows the expired message, and a second confirm does not send again.

Related primary: synonymdev/bitkit-android#1432.

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Round 8, the delta 587c542..767789a: a33d891 (single receipt presenter, refund state), 4caa785 (reuse valid quotes), 49033c7 (prefer funded USDT for quoted requests), 767789a (process-death recovery, fee warning with bridge cost, deadline-expired messaging).

Approving: nothing above LOW.

Code review, Fable. Checked and clean:

  • Quote reuse is keyed on sender, amount, recipient and destination, with a 5 s margin. It is cleared on send, MAX and disappear; embedded Paykit never reuses.
  • prefersUsdt only sets the initial rail.
  • Recovery clears paymentStarted only after a successful storedTransfer read returns nothing. It is gated on sending, with a generation check after the only await, and a recorded transfer still governs the binding.
  • The fee-warning math saturates and doesn't double-count USDT0 fees. "Request expired" is shown only for SubmissionDeadlineExpired, which never broadcasts.

ovitrif's MEDIUM on PaykitUsdtPaymentService.swift is fixed in 767789a. His two LOWs (Info.plist compatibility flag, shared sheet detent) have author replies and stay with him.

Device gate: 767789a (installed build), cross-tested with the Android PR build (gater4 → Gate58, usdt-fixture):

  • Process death mid-send: swipe 10:09:50, app terminated at 10:10:04.856, 0.3 s after paykit_usdt_payments was written the second time. After relaunch, core broadcast the recorded transfer once at 10:11:46 (payer 12.00 → 7.32), and a new payment to the same contact reached Confirm. No stuck state, no second debit (I did not pay the second one).
  • Receive sheet celebration (my LOW): one "Received USDT $1,00", none after dismiss (verified at 49033c7).

perf: nothing relevant in this delta.

Skipped — covered by CI at 767789a: build-local and validate green, unit and integration tests still pending.

@ovitrif ovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utAck - read summary of code changes parsed by AI

@ben-kaufman
ben-kaufman enabled auto-merge (squash) October 15, 2026 10:43
@ben-kaufman
ben-kaufman disabled auto-merge October 15, 2026 11:54
@ben-kaufman
ben-kaufman merged commit afb5793 into master Oct 15, 2026
18 of 20 checks passed
@ben-kaufman
ben-kaufman deleted the feat/usdt-pr-ready-20261006 branch October 15, 2026 11:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: hold, send and receive usdt in bitkit

6 participants