Skip to content

feat: add usdt wallet and paykit payments - #1432

Open
ben-kaufman wants to merge 63 commits into
masterfrom
feat/usdt-pr-ready-20261006
Open

ben-kaufman wants to merge 63 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 #1466

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-ios#884. 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.

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

Earn uses the same three-line English headline and unavailable-address message as iOS.

Includes master bcc3185f6, including LDK rc74 and Paykit rc74. 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.

Local validation: compile and all 4,182 unit tests pass with rc6; the final affected 544-test rerun also passes. The latest recovery, offline-backup and fee-warning changes received independent source review; these UI journeys have not been rerun for this fix batch. All three committed native bridge UI tests pass on the final tree, covering receiving/delivering details, explorer/history actions, summary-first deposit navigation, comma-locale refund confirmation, cancellation and terminal-state eligibility. Refund-address focus and typed text are preserved across provider-status updates. Unit tests also cover stale responses after Back and refund acknowledgment. Earlier isolated fixture checks also covered activity status icons. All six new bridge frames were checked against Figma using isolated native fixtures; no payment or refund was submitted. Full Detekt diagnostics have no additions against the preceding PR head. USD request and subscription display uses locale-aware grouping and exactly two decimals, including trailing zeros; stored payment values are unchanged. Three native UI tests exercise the production saved-state/navigation helpers for payment progress/result retention across configuration recreation, returning to Wallet after process death during or after submission, unpaid draft restoration, fresh payment-link navigation and clickable secondary-button identifiers. These ran in an isolated app without funded transactions or permission grants. Prior native fixtures cover the supplied request/subscription and Earn designs, both asset families, opt-in/decline and receiving-details completion.

The full Detekt report contains 1,399 findings and no new violations from this update. Comparison with master confirms the same existing rule sites; 11 earlier PR diagnostics have increased metrics in methods that already exceeded their limits on master. Unrelated baseline warnings are preserved. Journey XML validates. Funded payment and remote authorization flows were not repeated for these fixes.

Companion E2E #280 handles the USDT intro before requiring the visible balance. Both mobile workflows select that companion branch for Appium tests.

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.

  • J10 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 updated create-and-propose.xml — subscription methods follow the selected denomination.

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

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

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

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

  • J9 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.

  • J12 new usdt-bridge-amount-errors.xml — rejected send quotes and receive estimates show the bridge-cost error, block review/QR and recover with a valid quote.

  • 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

  • updated UsdtViewModelTest.kt — quote reuse within a send flow, changed inputs/wallets, expiry, refresh errors, deducted bridge-cost warnings, retained recovery after submission-deadline expiry and invalidation after dismissal or submission.

  • updated PaykitUsdtPaymentRepoTest.kt, PaykitUsdtPaymentRepoRecoveryTest.kt, UsdtRepoTest.kt and AppViewModelSendFlowTest.kt — payment preparation proceeds during background receipt verification, concurrent attempts and restored state remain protected, expired quotes fail before submission, and contact sends pause background work until completion or failure.

  • added UsdtBridgeDetailsTest.kt — native receiving/delivery screens, real networks, explorer/history access, refund form focus and text across status updates, localized confirmation, cancellation and terminal-state eligibility.

  • updated UsdtDepositViewModelTest.kt — current provider status in history, nonblocking background refresh, refund eligibility and stale-result rejection after navigation or refund acknowledgment.

  • added PaykitAmountViewTest.kt — locale-aware two-decimal USD display, grouping, single-cent and maximum-value boundaries.

  • expanded amount, request, proof, endpoint, review and navigation tests — stable unit changes, fixed external denominations, proof-free retry binding release, malformed-period isolation, optional endpoint failures, hardware-only routing, retained submission state and required Paykit authorization.

  • added UsdtPaymentNavigationTest.kt — actual Compose saved-state restoration and fresh same/different payment-link navigation.

  • updated SubscriptionsScreenTest.kt — combines monthly amounts at the wallet rate and avoids a partial total when the rate is unavailable.

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

  • updated AppViewModelSendFlowTest.kt — disabled clipboard filtering, explicit enable/cancel behavior, contact/request cleanup and effective Paykit payment methods. Also checks quoted USDT preselection at zero, insufficient, exactly rounded and surplus balances.

  • updated UsdtPreferencesRepoTest.kt, UsdtRepoTest.kt and SettingsViewModelTest.kt — protected funds, pending deposits, settlement, reset, intro dismissal and avoiding gateway refresh for unused opted-out wallets.

  • updated Paykit repository tests — receiving methods and published endpoints follow effective USDT availability.

  • ran native emulator presentation checks with isolated rendering fixtures — authorization, protected Settings, enable modal, network picker, amount layout and confirmation details. 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.

@ben-kaufman
ben-kaufman force-pushed the feat/usdt-pr-ready-20261006 branch from 190743f to 378b76e Compare October 7, 2026 04:27
Base automatically changed from codex/paykit-shared-runtime-local-20260930 to master October 7, 2026 17:17
@ben-kaufman
ben-kaufman force-pushed the feat/usdt-pr-ready-20261006 branch 4 times, most recently from 707d8aa to 2667c7f Compare October 12, 2026 02:23
Comment thread app/src/test/java/to/bitkit/repositories/PaykitPaymentProofRepoTest.kt Outdated
Comment thread app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Fixed
Comment thread app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Fixed
Comment thread app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment thread app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt
Comment thread app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt
Comment thread app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt
Comment thread app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt
Comment thread app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt
Comment thread app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt
Comment thread app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt
@ben-kaufman
ben-kaufman marked this pull request as ready for review October 12, 2026 14:08
@greptile-apps

greptile-apps Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 3/5

[Critical impact] Adds USDT wallet and payment processing with external service integration.

Fix Android test compilation and the fixed-invoice review mismatch before merging.

Findings

  1. P1 Android tests cannot compile ▶
  2. P1 Review and payment amounts differ ▶
  3. P2 Contact payment drafts get replaced ▶
  4. P2 Unchanged receipts trigger backups ▶

Summary

Adds an Arbitrum USDT wallet and integrates it with Paykit payments.

  • Bitkit adds an Arbitrum USDT wallet with its own balance and activity.
  • USDT payments use shared send and receive screens with exact amounts.
  • Paykit requests and subscriptions can use USDT, Bitcoin, or dollars.
  • Paykit shows whether a USDT payment is verified, late, or short.
  • Earn authorization can include an optional USDT receiving address.

Acknowledged scope and QA notes:

  • ben-kaufman explicitly listed translation completion as out of scope.
  • ben-kaufman explicitly acknowledged remaining bridge/refund acceptance, full VSS restore, signed callbacks, Locks and Shop testing as open or deferred QA work.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Contact or payment link] --> B[Amount entry]
  B --> C[Payment review]
  C --> D[Local approval]
  D --> E[USDT wallet]
  E --> F[Encrypted wallet backup]
  F --> G[Submit payment]
  G --> H[Track transfer]
  H --> I[Send Paykit proof]
  I --> J[Receiver checks receipt]
  J --> K[Request status and activity]
Loading

Reviews (1) · Last reviewed commit: "fix: preserve payment review and contact..." · Reviewed by Greptile

Comment thread app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
Comment thread app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment thread app/src/main/java/to/bitkit/repositories/PaykitUsdtPaymentRepo.kt 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 scanning, a USDT companion claim in Pubky auth approval, and USDT state in the wallet backup, on bitkit-core 0.8.0-rc2. Reviewed dc540ee, oversized tier (158 files, about 11.7k added lines): I read only the money, signing, auth-claim, backup and scan-routing paths. The UI screens, strings and the rest were not reviewed.

What I checked, and the candidates I ruled out

Read: UsdtService.kt, UsdtRepo.kt, PaykitUsdtPaymentRepo.kt, the USDT paths of AppViewModel.kt (sendPaykitUsdt, authorizeUsdtExecution, handleUsdtPaymentInput, launchScan, openPaykitEndpoints), PubkyAuthClaimCodec.kt, PaykitSdkService.approveAuthWithCompanionClaim, BackupRepo.getWalletBackupDataBytes, and in bitkit-core v0.8.0-rc2: usdt/wallet.rs (send, recover_pending), usdt/backup.rs, usdt/payment_request.rs, usdt/proof.rs.

Ruled out

  • Signing a changed request: authorizeUsdtExecution runs inside core's authorize callback and re-checks the active context, the reviewed amount, the payment terms and their validity window before anything is signed.
  • Broadcast without a backup: core persists the backup snapshot before broadcasting, on both the send and the recovery path, so a signed operation never leaves the device unbacked.
  • Foreign chain or token in a scanned request: usdt_parse_payment_request rejects anything but chain 42161 with the USDT contract, and rejects unknown or repeated query keys. The app routes it only with the flag on, no hardware wallet active, and no USDT send already open.
  • Companion claim payload swap: approveAuthWithCompanionClaim re-parses the claim from the URL, requires the same claim type, and validates the payload before the Paykit key is added, and adds it only when the claim asks for Paykit access.
  • PIN bypass: the USDT send and deposit flows check PIN-for-payments the same way the bitcoin send does.
  • Backup during an unfinished restore: getWalletBackupDataBytes refuses while a Paykit restore is pending.

Not done: no local build. On this track an unrequested review does not run Gradle on a PR branch, 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.

Comment thread app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt
Comment thread app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt
Comment thread app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt
ovi-reviewer[bot]

This comment was marked as resolved.

@ovi-reviewer

ovi-reviewer Bot commented Oct 12, 2026

Copy link
Copy Markdown

@ben-kaufman build error

@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.

Requesting changes at 3b6dbdf: 2 MEDIUM and 1 LOW inline, each with a failing test. The device gate has not run; it waits for these.

build is also red at this head. The failure is on the merge with master: tests added there by #1457 (AppViewModelSendFlowTest.kt, around lines 7739–7862 of the merged file) still pass amountSats, which this PR replaces. Merging master and updating those call sites clears it.

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, PaykitAmount.quotedTo uses BigDecimal and rounds up; no Double on a payment path.
  • Payment links: core pins chain 42161 and the USDT token, rejects unknown or repeated keys and checksums the recipient; review and send use the same quote id; ethereum: links are refused while a hardware send sheet is open.
  • Send: confirm/send guard on busy, submitted and quote id; core send is idempotent per quote id and refuses a second pending transfer; a backup failure after signing leaves one pending operation that is rebroadcast.
  • Paykit: the payable amount is recomputed and must equal the reviewed terms at pay time on the Bitcoin rails (hasMismatchedIncomingPaymentRequest) and in prepare/authorizeUsdtExecution for USDT; deadlines use the earlier of the request deadline and the quote expiry, including on-chain recovery after relaunch; a quoted initial subscription payment cannot skip amount review; one paymentId cannot satisfy two requests.
  • Persisted formats from v2.5.0: PendingPaykitPaymentProof only gains a defaulted field and an additive enum value.
  • Address sharing: the USDT row renders whenever the claim asks for it, unknown or duplicate claim items are rejected, the address is re-read and compared at approval, and authorize re-parses and compares the request with what was displayed.
  • Wipe order and the Trezor stored-key mapping.
  • The copies of #1460 and #1461 carried here match those PRs' code.

Question, not a finding: from a hardware-wallet Send sheet, Contacts → a contact with a USDT endpoint reaches openPaykitEndpoints (AppViewModel.kt:3792) before the hardware guard (:3930), and prefersUsdt picks USDT when the seed wallet has no on-chain balance, which is the normal state for a hardware-only user. The payment would then come from the seed wallet's USDT. The review is the USDT one, so it is not hidden, but ethereum: links are refused in the same sheet (:3889). Is that path intended?

perf: relevant (a 30 s refresh loop with history from the Home card, 10 s while pending; the repo mutex is held across the refresh; a wallet backup upload before every USDT send). Not measured yet.

⚠️ Device gate: not run — it follows once the findings below are answered. 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 iOS side is reviewed at synonymdev/bitkit-ios#884.

Comment thread app/src/main/java/to/bitkit/repositories/PaykitUsdtPaymentRepo.kt Outdated
Comment thread app/src/main/java/to/bitkit/repositories/PaykitUsdtPaymentRepo.kt Outdated
Comment thread app/src/main/java/to/bitkit/repositories/PaykitUsdtPaymentRepo.kt Outdated
@ben-kaufman
ben-kaufman force-pushed the feat/usdt-pr-ready-20261006 branch from 3b6dbdf to 6a2f645 Compare October 12, 2026 16:11
@ben-kaufman

Copy link
Copy Markdown
Contributor Author

@jvsena42 Fixed the three findings and synchronized with current master, including the new minimum-payment tests. The final build, instrumentation-source compilation and all 4,065 unit tests pass; the complete lint report has no introduced findings. Both apps now use published Core 0.8.0-rc3, which also enforces the approved USDT deadline immediately before submission and during recovery.

On the Contacts question: that opens the shared Paykit method selector. It can select USDT when the seed wallet has no Bitcoin balance, and the USDT review makes that source explicit before approval. It uses the seed wallet, not hardware signing. The raw ethereum: link guard protects an already active hardware-send flow from replacement. I've left this behavior unchanged in this update.

The fresh-wallet receive check also passed: Back preserves both an amountless QR and an earlier committed amount. Funded/provider-dependent acceptance cases remain listed separately in the PR.

@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 b6bcdbb: the three findings from my last review are fixed and the delta has no new HIGH or MEDIUM. Device result follows.

  • Restart double pay: hasBinding now falls back to the stored attempts (PaykitUsdtPaymentRepo.kt:315-317) and fails closed when the store is unreadable; covered by started payment remains bound after restart before reconciliation. The stored fallback is not filtered by wallet or identity, which can only over-bind.
  • iOS backup decode: requestId, binding and billingPeriod have = null defaults (:75-77); wallet backup restores contact attempts with omitted optional fields decodes a whole WalletBackupV1. Backups still write explicit nulls.
  • Cancelled send: the reset runs in NonCancellable and only after the journal read succeeds with no transfer (:226-237); the cancellation is still rethrown.

Read in the delta (6a2f6451c and the rebase onto 97923d8):

  • A fixed Lightning invoice amount is locked on Spending from the amount screen and the fee selector, and submit rejects a mismatch (AppViewModel.kt:5667). Switching Spending → Savings keeps the invoice amount rather than an earlier typed one; what is shown is what is paid.
  • USDT request payments pass the earlier of the request deadline and the quote expiry to core 0.8.0-rc3, which checks it before signing and again before broadcast.
  • USDT links are ignored while a contact payment is open; the receive amount is a draft until Continue.
  • The dust check on the Lightning-to-on-chain fallback is master's code kept through the rebase.

The hardware Send sheet question is answered in the author's comment (pays from the seed wallet, the review names the source); nothing further from me.

CI at this head: build-local, lint and detekt pass; build and the e2e shards were still running when I posted this.

perf: relevant (refresh loop from the Home card, a keychain read plus state decode in hasBinding for each unbound request check). Not measured yet.

Device gate (partial): b6bcdbb — not run yet; pending usdt-wallet-navigation.xml, usdt-receive-amount-and-units.xml and the direct-link steps of usdt-payment-links-and-scans.xml on an emulator without USDT funds, plus the idle refresh measurement.

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Regtest APK

Built from 12dfd8a (run).

Download bitkit-dev-debug universal APK (expires in 30 days).

ovi-reviewer[bot]

This comment was marked as outdated.

@ben-kaufman

Copy link
Copy Markdown
Contributor Author

Applied the activity-search fix also raised in the iOS review, in 3f44eec: searching for u, us or t no longer lists every USDT transfer. Full USDT (case-insensitive) and matching recipient/transaction fragments still work. The regression test, all 4,156 unit tests, compilation and lint checks pass with no new lint findings. Updated the shared opt-in walkthrough to distinguish the iOS sheet from Android’s Cancel-only alert.

@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: Follow-up review of 3f44eec since 77545b8, using baseline bc-be8931d7-1a05-445c-b0ee-01ed77242cb9. This pass inspected USDT activity search and the updated opt-in dismissal wording. Inherited baseline coverage remains for USDT opt-in and link gating, send amount and MAX, Paykit USDT execution, request pricing, Pubky receiving-address consent, deposit refund and activity status, hardware-wallet rejection of non-Bitcoin targets, and iOS enable-prompt and activity-title parity at 810858ae.

No new actionable code findings.

Activity search keeps a USDT row when the query is empty, equals USDT in any case, or occurs in the recipient or transaction hash. Queries such as u, us, and t no longer match every row. bitkit-ios#884 at b522b8f1 uses the same predicate. The opt-in journey matches the Android alert: Cancel and Enable, with outside taps and Back ignored. The documented core version remains 0.8.0-rc6.

UsdtActivityFilterTest.kt was inspected and not executed. bitkit-e2e-tests#280 at fd0b8d1e does not assert this search. The send shard on this revision failed during regtest setup before Appium started.

Recommended before device testing: MAX still depends on the gateway deployment named in the pull request, synonymdev/bitkit-usdt-service@1456181. This review did not inspect that service revision.

Device testing: not performed in this review.

@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 6, the delta 1ccb284..3f44eec: c51f3bd (cross-asset review and recovery), b1fbfb8 (USD formatting), f2f766a and d3acbb6 (bridge design), 77545b8 (docs), 3f44eec (exact USDT activity search; read, no findings). The device run used a build of 77545b8; 3f44eec only changes activity search.

The code review of the delta found nothing above LOW and nothing in c51f3bd that changes funds handling. Checked and clean:

  • The fixed-price fallback for other currencies is shown and paid as the same sats, and the rate is never applied twice.
  • canReleaseBinding releases a request only for FAILED/REPLACED attempts with no proof.
  • Pending navigation pops the whole graph, so Back can't land on Confirm again.
  • USDT stays out of the hardware wallet path.
  • The UsdtViewModel fail-closed guards hold.

b1fbfb8 fixes my USD formatting LOW (thread resolved, verified on device).

Requesting changes for the MEDIUM inline: a Paykit USDT contact payment from this emulator failed twice with an expired quote after a 1.5–3 minute spinner, while the same payment from iOS took 21 s. It predates the latest push but came with this PR's Paykit USDT payments; the round-4 gate drove only plain USDT sends, so my earlier approvals missed it.

Also inline: one functional LOW and three design LOWs. The design LOWs come from re-checking the linked frames against the round-1 baselines; only 49861-363168 changed in a way that affects code. They are non-blocking.

Not raised:

  • "5." in a USDT amount: I suspected it skipped normalisation before the quote, but on device it quotes as $5.00, so dropped.
  • The Home headline shows "—" when the paykit rate is older than 10 minutes. Both platforms do this on purpose; there is no Figma state for it, worth a design note.

Device gate: 77545b8 — cross-tested against the iOS PR build (gater4 ↔ Gate58, usdt-fixture):

  • Incoming USD request presents with $1.00.
  • The Received USDT celebration is deferred while Cross-Network Deposits is open and shows after Back.
  • "5." quotes correctly.
  • Paykit USDT contact payment failed twice (MEDIUM).

Not run:

  • ⚠️ payment-requests/fixed-price-bitcoin.xml and subscriptions/create-and-propose.xml — not run yet, still pending.
  • ⚠️ usdt-bridge-details.xml refund flow — not run yet, still pending.
  • ⚠️ A FAILED USDT attempt re-offering Bitcoin — not run; there is no way to fail a transfer on the fixture.

perf: Paykit USDT pay, swipe to failure 112–187 s; iOS swipe to submit 21 s.

Skipped — covered by CI at 77545b8: unit tests, detekt.

Comment thread app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment thread app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositsScreen.kt Outdated
Comment thread app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositsScreen.kt Outdated
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 3f44eec..7df6209: 4f3ab97 (unblock USDT contact payments), f5dc875 (docs), 00a8e5d (localized refund amounts), 7df6209 (tests).

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

Code review, Fable, of the reconcile rewrite. Checked and clean:

  • Snapshots are taken under the mutex, network work runs outside it, and merges re-read under the lock, keyed by binding state.
  • No lost update, resurrected binding, dropped proof or double pay against concurrent prepare/send/setPaymentStarted/restoreBackup.
  • requireValidForSubmission checks expiry at three points.
  • isSubmittingPaymentRequest now covers the whole contact payment, so background backups and SDK sends defer.

Device gate: 7df6209, cross-tested with the iOS PR build (Gate58 → gater4, usdt-fixture):

  • Paykit USDT contact payment, two runs. Continue → Confirm 9 s (was 85 s), no quote expiry.
    • Run 1: swipe → result screen 83 s; BackupUnavailable at 22:59:32, then core submitted the persisted transfer at 23:04:46.
    • Run 2: swipe → eth_sendUserOperation 39 s, "USDT Sent" at 46 s.
    • One payment per run (details on the MEDIUM thread).
  • Labels: "AVAILABLE $46.00" with USDT selected.
  • Deposits: the detail opens on the summary (Status, Receiving network Arbitrum One, Sender network, Provider status), and Details shows "Request refund" / "Hide details".
  • ⚠️ The Android refund-dialog amount was not driven, because the field kept closing (the LOW). It rests on usdtOverviewAmount and the German-locale assertion in UsdtBridgeDetailsTest.

perf: Continue → Confirm 9 s (was 85 s); swipe → submit 39 s (run 2), 6.5 min (run 1, BackupUnavailable recovery). Opening a contact payment (Contact › Pay → Amount) took 106 s here against 24 s on iOS; that predates this delta and is part of the Paykit open path.

Skipped — covered by CI: unit tests, detekt.

@ben-kaufman
ben-kaufman force-pushed the feat/usdt-pr-ready-20261006 branch from 64f4d4c to dcbf816 Compare October 15, 2026 01:13

@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: Follow-up review of dcbf816 since 3f44eec, using the previous QA review. This pass inspected quote reuse, the five-second submission buffer, contact-payment submission locking, deposit-refund refresh and localized amounts, activity status icon colors, and the subscription acceptance journey wording. Inherited baseline coverage remains for activity search, opt-in dismissal, opt-in and link gating, MAX and bridge-cost behavior, request pricing, Pubky receiving-address consent, hardware-wallet rejection of non-Bitcoin targets, and iOS enable-prompt and activity-title parity at 810858ae.

No new actionable code findings.

Reused quotes stay bound to the sender, amount, recipient, and network, and only while more than five seconds of life remain. A quote inside that buffer is rejected before core submission, including after authorization. Contact USDT sends hold the payment-submission lock until prepare and send finish, including when there is no incoming request. Background deposit refresh does not mark the screen busy, so an in-progress refund address is kept while the provider status can still update.

Source inspection only. UsdtViewModelTest, UsdtRepoTest, UsdtDepositViewModelTest, PaykitUsdtPaymentRepoTest, and AppViewModelSendFlowTest were read and not executed. Device testing was not performed. Earlier unresolved design notes on the history-sync line, the cross-network deposits entry, subscription rows, and the Earn intro remain on their existing threads.

@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 423b74a. 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. iOS has the matching fix.

The new boundary test and all 4,176 unit tests pass. Compile passes and the full Detekt report has no additions against the preceding head. The shared payment-request journey is updated. No funded payments were repeated. @piotr-iohk ready for another look.

@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 (display) and some LOWs. No funds-safety issue found: double-pay across rails and restarts, quote reuse, locking order, and backup shape against the iOS twin all hold. Already-raised threads are not repeated.

Other LOWs:

  • USDT preference doesn't gate gateway traffic: UsdtHomeRefreshEffect (HomeScreen.kt:~616) and backupSnapshot() run on Env.isUsdtEnabled alone, so every wallet derives its USDT address, creates usdt-<addr>.sqlite and polls the gateway every 30 s, including users who declined USDT (address and IP visible to the gateway). Because every wallet then has that file, clearing the RPC/bundler URLs (the documented switch-off) makes UsdtService.backupSnapshot() throw NotConfigured and every WALLET backup fail, not only USDT users'.
  • "No Thanks" on the USDT intro can't complete for a wallet that already holds USDT with the preference undecided: setUsdtEnabled(false) hits checkCanDisable and throws, so the sheet stays (UsdtTimedSheet.kt, ContentView.kt:~661).
  • Receipt and refund amounts are rounded to cents (usdtOverviewAmount, HALF_UP at 2 dp): a $5.00 request paid with 4.996 USDT reads "Underpaid · 5.00 USDT", and a 1.234567 USDT refund confirms as 1.23.
  • usdCents in UsdtDepositReceiveScreen.kt:~286 joins locale grouping with a hard-coded "." (de_DE shows "$10.000.00"); and the Receive "Contacts" button resets amount and note but keeps the previous draft's accepted endpoints (ReceiveSheet.kt:~193).

}

private fun usdtHeadlineSats(balance: ULong?, visible: Boolean, rate: PaykitExchangeRate?): Long? =
if (visible && balance != 0uL) balance?.let { usdtDisplaySats(usdtFormatAmount(it), rate) } else 0L

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.

MEDIUM: the Home total balance shows "—" for a USDT-enabled user whenever the USDT balance is unknown

usdtHeadlineSats returns null when balance is null (only exact 0 maps to 0), and line 686 sets isUnavailable = usdtSats == null, so both headline rows become "—". UsdtRepo.state.balance stays null until wallet.balance() succeeds, so a cold start offline, or a slow or down gateway, blanks the Bitcoin total even with 0 USDT held and the Bitcoin balances known. A USD rate older than 10 minutes does the same for users holding USDT. Suggest treating unknown USDT as 0 in the headline, or showing the Bitcoin total with a small "USDT unavailable" marker.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I would keep the unavailable combined total here: an unknown USDT balance is not a zero balance, and showing only Bitcoin as the total can understate the wallet. Savings and Spending still show their known balances in the individual cards. The same rule applies on iOS. A labelled partial total would be a reasonable design alternative, but I have kept the current truthful unavailable state and left this discussion open.

Comment thread app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtViewModel.kt Outdated
@ben-kaufman

Copy link
Copy Markdown
Contributor Author

Addressed the new review in 12dfd8a and checked the matching iOS paths:

  • Unused opted-out wallets no longer create USDT storage or poll on Home/payment preparation/backup. Existing journals retain recovery, and local backup/restore works without network configuration.
  • No Thanks completes for an undecided wallet while balance/pending-operation protection remains in force.
  • Receipt and refund approval values retain up to six decimals; deposit estimate cents use the locale separator.
  • Receive → Contacts clears the previous draft's accepted endpoints.
  • Bridge-cost warnings include the deduction from the recipient amount; recorded deadline failures show expiry while retaining recovery.
  • Interrupted Paykit attempts can recover when Core has no operation and no send is active.

The unavailable combined Home total is intentional; I explained that on its thread.

Validation: compile and all 4,182 unit tests pass, plus a final 544-test affected rerun. The full Detekt report has the same 1,399 existing diagnostics and no additions. Independent recovery/quality review is clean. Journey specs match iOS; 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: Follow-up review of 12dfd8a since 423b74a, baseline bc-836efa4d-0d5c-407e-a13e-0426a838e9e2. The comparison base and merge base are still bcc3185. This pass covered the recovery and payment-detail delta; the quoted-request, quote-reuse, deposit, subscription, search, opt-in, MAX, request, Pubky, and hardware coverage from that baseline is unchanged and inherited.

No new actionable code findings.

No new actionable code findings in the follow-up. A missing local transfer now clears paymentStarted and can release the Paykit binding, including after a failed send whose history read is empty. That matches the updated recovery tests and the iOS change at 767789a: a pending or confirmed local transfer still keeps the binding, and a failed history read does not release it. Unused wallets skip gateway refresh and do not open USDT storage for backup. Bridge warnings add the deducted bridge amount to the network fee. A recorded send that misses its submission deadline stays on that quote, shows the expired state, and does not send again.

The open Home-total comment on 423b74a still matches this head: a visible USDT balance that is unknown, or a positive balance without a fresh rate, blanks the combined headline. Savings and Spending stay available, and the matching iOS test treats that headline as intentional. This delta does not change it.

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

Suggested additional test cases

  • Android, 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.
  • Android, 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.

This branch has not been deployed

No deployments
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

8 participants