Repository navigation
feat: add usdt wallet and paykit payments - #884
Conversation
22ca2e2 to
647d480
Compare
2605af0 to
47f079f
Compare
|
talosmachina
left a comment
There was a problem hiding this comment.
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:
resolveUsdtPaymentRoutethrows when the flag is off. The send scanner routes only in the unrestricted scope. Every caller re-checksisSendPresentedand 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.sendclearspaymentStartedonly 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.
unsignedPayloadchecks that the endpoint's raw payload decodes to that same address. - PIN bypass:
UsdtSendViewand 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.
There was a problem hiding this comment.
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
Doubleon the send path, and a link'suint256round-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()andconfirmPayment()guard onbusy/submitted, coresendis 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 fromv2.5.0stay 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.
There was a problem hiding this comment.
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.5with a BTC equivalent and no unit toggle; Continue gives…&uint256=3500000; Details show the same0x…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 keepsuint256=3500000. - Send: a pasted
ethereum:link withuint256=10001opens the amount screen at$ 0.010001with the recipient carried to manual entry; a seventh decimal is refused; wrong chain, wrong token, a repeateduint256, 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.
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.
pubky-auth/share-receiving-details.xml — needs the watch-only authorization fixture.
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.
|
@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 |
This comment has been minimized.
This comment has been minimized.
jvsena42
left a comment
There was a problem hiding this comment.
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 cachedcorebefore anything else, and the credential read moved intoinitializeWallet, so the idle refresh no longer touches the keychain. The cache is dropped inwipe(), 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
Run Tests, Run Integration Tests and build-local were still running at this head when I posted this.
piotr-iohk
left a comment
There was a problem hiding this comment.
@ben-kaufman please resolve the merge conflicts. QA review has not been performed for this request.
jvsena42
left a comment
There was a problem hiding this comment.
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:
shouldAutomaticallyPaykeeps the PR'sconversion == niland master's dropped.lightningcondition. USDT cannot reach the Bitcoin single-swipe path (requiresAssetSelectionroots it at Amount →.usdt).- The hardware fallback now uses the Bitcoin on-chain amount.
- SwipeButton
isSubmittedreplacesisConfirmedwith 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.
piotr-iohk
left a comment
There was a problem hiding this comment.
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.
|
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 |
|
@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. |
|
@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: |
ovitrif
left a comment
There was a problem hiding this comment.
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-*.sqliteexists,backupSnapshot()throwsNotConfiguredand every wallet backup fails, Bitcoin and Paykit state included; restoring a backup withusdtWalletin 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.18to0.8.0-rc6in 2.6.0 (project.pbxproj), a release candidate in a store build.
|
Addressed the new review in 767789a and checked the same paths on Android:
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. |
piotr-iohk
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
prefersUsdtonly sets the initial rail.- Recovery clears
paymentStartedonly after a successfulstoredTransferread returns nothing. It is gated onsending, 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_paymentswas 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
left a comment
There was a problem hiding this comment.
utAck - read summary of code changes parsed by AI
Closes #919
This PR adds USDT0 on Arbitrum One to Bitkit, with fees paid in USDT.
Uses Core
0.8.0-rc6and Paykitrc74. 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/usdtby 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
UIDesignRequiresCompatibilityflag. 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
Design
Bitkit - Handoff v62. The current USDT designs are in Refactor v63.
USDT introduction, Settings and send network picker.
Home and USDT wallet/activity.
Receive QR, networks, amount, fees and received.
Send amount, review, loading and success, balance menu.
Sent summary, sent technical details, received summary and received technical details.
Authorization, generic authorization, payment request and request details.
Request creation, subscription creation, Earn introduction and authorization completion. Requests group available Bitcoin rails and USDT in outlined chips; new subscriptions select one asset family.
Subscription overview: one monthly total aligned with Active and Created.
Receiving pending, delivering summary, delivering technical details, receiving/refund details, cross-network deposits and refund confirmation.
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 to0.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.mdandDocs/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
0af48b7used bitkit-docker #27 at5d63e3aand #28 at0395571, 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
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.swiftandHomeWalletBalanceTests.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.swiftandUsdtPaymentLinkTests.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.