Skip to content

feat: add usdt on spark via breez sdk - #1145

Closed
ovitrif wants to merge 10 commits into
feat/breez-spark-lightningfrom
feat/usdt-spark
Closed

ovitrif wants to merge 10 commits into
feat/breez-spark-lightningfrom
feat/usdt-spark

Conversation

@ovitrif

@ovitrif ovitrif commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Twin: synonymdev/bitkit-android#1710
Dependency: #1128

Stacked on #1128: base this PR on feat/breez-spark-lightning and merge it after that PR.

This PR adds a dollar balance held on Spark, next to the Arbitrum USDT wallet, so two Bitkit wallets can pay each other dollars over Spark through the Breez SDK.

Description

  • Adds a dollar wallet on Spark holding USDB, Brale's dollar token: a Home tile, a wallet screen with its balance and activity, receive and send, reusing the USDT wallet's header, illustration, rows, number pad, swipe and PIN check. The copy names it for what it is (USDB, dollars on Spark) with its own _spark strings in every locale.
  • Receives through a Spark invoice that names the token, so a payer scanning it knows which asset to send.
  • Sends to a Spark invoice for that token or to a Spark address, for free; an invoice for sats or another token is refused at the recipient step, and an invoice that fixes its amount is paid as it stands.
  • Routes a scanned or deep-linked Spark address or invoice to the dollar send flow.
  • Adds Convert: get dollars from the wallet's own sats or bitcoin from its dollars, through Flashnet, as a self-transfer the SDK prices first (ConversionOptions on a payment to the wallet's own Spark address).
  • Adds To USDT on mainnet: moves dollars to Bitkit's own Arbitrum USDT wallet through Orchestra (crossChainConfig, a route to Arbitrum USDT that accepts USDB).
  • Publishes usdb-spark-address on Paykit with the wallet's Spark address, with the Spending endpoints, while Spark holds dollars; paying a contact who publishes it, while you hold Spark dollars, opens the dollar send flow with the address filled in.
  • Counts the dollar balance in the Home total, like Arbitrum USDT.
  • Pins the token by identifier per network (Env.sparkUsdTokenId), since anyone can issue a token with any ticker: USDB on mainnet, a 6-decimal "Test USDB" on regtest.
  • Blocks a conversion to dollars that the Spending balance can't cover, maps the SDK's insufficient-funds error to a clear message, and shows the Flashnet fee in dollars.
  • Shows every dollar send, receive, conversion and move to USDT in Home and All Activity, each with a detail screen: amount in dollars, fee, status, time, the invoice paid, the transaction id, and for a conversion the sats it spent or returned.
  • Shows the Received sheet when dollars arrive in the foreground, and a notification when they arrive in the background; a sent payment ends on a success sheet with Details instead of a processing state.
  • Accepts a Spark invoice from the scanner, the clipboard and deep links, and shows the receive invoice as an uppercase QR, which encodes in the denser alphanumeric mode and scans more easily.
  • Syncs Spark when the app returns to the foreground and every 5 s while the receive screen is open, so a payment received while backgrounded shows at once.

Out of Scope

  • Bridging the other way, from Arbitrum USDT into Spark dollars (Orchestra receive into the Spark wallet).
  • Paykit payment requests, subscriptions and proofs for Spark dollars: they keep their existing assets until a Spark proof type exists, so usdb-spark-address stays out of accepted-method lists and allowances.
  • Stable Balance (automatic conversion of received sats): stays off, since it would turn the Spending balance into dollars.

Design

N/A — no design available.

Preview

Android pays iOS $ 20.00, then iOS pays Android $ 5.00 over Spark (2x; iOS and Android side by side).

Android pays iOS iOS pays Android
case4-android-to-ios.mp4
case5-ios-to-android.mp4

QA Notes

Setup

  • Two Debug (regtest) builds of this branch on simulators, A and B, onboarded with a new wallet each; Spark connects to Lightspark's hosted regtest with no extra setup.

  • The regtest test token is btknrt1wrr0qtshpyuzrx5p0mvd605dnwvfv3dcpfexqgfzpgl3rykdacsq6p8z6l ("Test USDB", USDB, 6 decimals), the same as on Android. To fund a wallet without its issuer wallet, build the Breez CLI (breez/spark-sdk tag 0.27.2, cargo build -p cli --release), create a token of your own (issuer create-token "Test USDB" USDB 6 -f 1000000000000000, issuer mint-token 1000000000000) and set its identifier as Env.sparkUsdTokenId for regtest in a local build.

  • Mainnet: Convert in the dollar wallet turns Spending sats into USDB; no other funding is needed.

Journeys

  • J1: new spark-usd-bitkit-to-bitkit.xml — two Bitkits pay each other dollars over Spark, free, and a sats invoice is refused

Manual Tests

  • 1. Pay 100 test dollars from the token issuer's CLI wallet to A's receive QR (pay -r <invoice> -a 100000000) → A's tile and wallet show $ 100.00 and a Received row — the token issuer wallet is not in the Capabilities table
  • 2. Convert sats to USDB (mainnet): the USDB balance shows the dollars bought, at Flashnet's pool fee
  • 3. Send USDB Pixel to iPhone (mainnet): arrives with no fee
  • 4. Send USDB iPhone to Pixel (mainnet): arrives with no fee
  • 5. Swap USDB to Arbitrum USDT (mainnet): Received USDT
  • 6. USDB activity, details and receipts (mainnet): sends, receives and conversions show in Home and All Activity with details, a send ends on a success sheet, a receive shows the Received sheet or a notification

Automated Checks

  • added SparkUsdTests.swift — recognizes Spark inputs, scales 6- and 8-decimal tokens to dollar units and back, pins a token on regtest only

Evidence

At 7583370, Debug build on an iOS 26.5 simulator (B) paired with the Android twin's dev build on an emulator (A), against Lightspark's regtest, 27 Oct:

  • The Home tile read USDT Spark $ 0.00 right after onboarding, with the token metadata loaded from Spark.
  • J1 steps 3 to 6 passed across platforms: Android A paid iOS B's invoice $ 20.00 (B showed $ 20.00 and Received), then iOS B pasted A's invoice into Send, entered 5 and swiped, and A received $ 5.00 (B $ 15.00, A $ 70.00). No fee on either side.
  • The unit suite at f25edfe4 (the same code plus the deep-link route, the fixed-amount fix and the mainnet token): 2906 tests, SparkUsdTests passed; one failure, SendConfirmationLifecycleTests.testCustomFeeWalletSwitchSurvivesFeeScreenNavigation, which fails the same way at the base a9aa36fd.
  • Round 2 (USDB on mainnet, dollar copy, Convert, To USDT) at a0905411: unit suite as above, with only that pre-existing failure; Paykit usdb-spark-address at 9cbd37ce: the same suite result. No device run yet. The cases above ran with the earlier regtest test token.
  • Rebased onto 9ddd96bf at 56d7adf3 (one conflict in SparkManager, both sides kept): builds clean; the unit suite (2909 tests) has only the failure it also has on base.
  • Activity, receipts and sends at c65593c5: builds; the unit suite (2909 tests) has only the failure it also has on base; no device run yet.
  • Mainnet, 27 Oct, on @ovitrif's phones: convert, both USDB sends and the swap to Arbitrum USDT passed on Android 3eece748e / iOS 56d7adf; USDB activity, details and receipts passed on 2616c13b1 / 68e07be (QA comment).

Gaps

  • Convert and To USDT: untested. Flashnet has no regtest liquidity for a test token (Breez runs its conversion tests on mainnet only) and Orchestra serves mainnet only, so the phones are the first run.
  • Paykit usdb-spark-address: unit-tested builds only; publishing and paying a contact are not run on a device.
  • iOS to iOS, J1 step 7 (sats invoice refused), the fixed-amount invoice and the lightning: deep-link route: not run on iOS; Android covered them.
  • Manual test 1 on iOS: not run separately; B was funded by the Android wallet instead of the CLI.
  • PIN or biometric check before the payment: untested; the test wallets had no PIN.
  • Restore from seed bringing the balance and activity back: untested.
  • The background notification, scanning a Spark invoice QR and the foreground sync: not checked on their own on a device.

Models used

  • Planning/scoping: claude-opus-5-5 (reasoning: Not exposed)
  • Implementation: claude-opus-5-5 (reasoning: Not exposed), claude-sonnet-5-5 (reasoning: high)
  • Review: Not performed

Metadata note: the session exposes the model id but not its reasoning setting; the Sonnet worker read the Android code only.

@ovitrif

ovitrif commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed e2959f2: fixes from the first mainnet Convert run.

  • Get dollars stops before the swipe when Spending can't cover it. With 0 sats on Spark, or a quoted sats cost above the Spending balance, it shows "Insufficient Spending Balance". An insufficient-funds error from the SDK maps to the same message instead of "try again".
  • The conversion fee shows in dollars. Flashnet reports feePaidAssetIn in USDB base units in both directions. Checked against its simulate endpoint on the BTC/USDB pool (LP fee 5 bps, no host fee): 2,504 sats in gives 1,600, which is $0.0016. The SDK passes that through as ConversionEstimate.fee and documents it as sats, so the quote showed about $0.0008 as "801 sats". There is no flat minimum.

Builds clean. The unit suite (2909 tests) has one failure, SendConfirmationLifecycleTests.testCustomFeeWalletSwitchSurvivesFeeScreenNavigation, which also fails on the base branch.

@ovitrif

ovitrif commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed c65593c5: Spark dollar payments now behave like the other payments, after the first mainnet Convert and USDB send.

  • Every dollar send, receive, conversion and move to USDT shows in Home and All Activity, with a detail screen: dollars, fee, status, time, invoice, transaction id, and the sats side of a conversion.
  • Received sheet when dollars arrive in the foreground, a notification in the background, and a success sheet after a send.
  • The scanner, clipboard and deep links accept Spark invoices; the receive QR is uppercase so it scans more easily.
  • Spark syncs on app resume and every 5 s on the receive screen.

Builds; the unit suite has only the failure it also has on base; not yet run on a device.

@ovitrif

ovitrif commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator Author

QA:

Tested on a Pixel 10 Pro and an iPhone, on mainnet, on 27 Oct. The first four cases ran on Android 3eece748e and iOS 56d7adf, before this PR's current head; the last ran on the current heads, Android 2616c13b1 and iOS 68e07be.

🟢 Convert sats to USDB

Convert on the Pixel failed until Spark held Lightning sats, then bought $ 2.00 of USDB for 2,504 sats with Flashnet's 0.05% pool fee. The quote showed the fee as 801 USDB base units ($ 0.0008). An insufficient Spending balance now stops the conversion with a clear message, and the fee shows in dollars.


🟢 Send USDB Pixel to iPhone

The Pixel paid the iPhone $ 1.00 USDB, and it arrived with no fee on either side.


🟢 Send USDB iPhone to Pixel

The iPhone paid the Pixel $ 0.50 USDB, and it arrived with no fee on either side.


🟢 Swap USDB to Arbitrum USDT

At 15:55 CET, To USDT on the Pixel moved USDB through Orchestra, and the Pixel received $ 1.41 USDT. The amount sent wasn't recorded, so this bridge's fee is unknown.


🟢 USDB activity, details and receipts

Checked by @ovitrif on both phones at about 16:30 CET: USDB sends, receives and conversions show in Home and All Activity with their details, a send ends on a success sheet, and a receive shows the Received sheet. The same $ 0.50 USDB payment from 14:41 shows its dollar amount, sats side, status, $ 0.00 fee, invoice and transaction id on both phones.

Received on the PixelSent from the iPhone

@ovitrif ovitrif mentioned this pull request Oct 27, 2026
34 tasks
@ovitrif

ovitrif commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: Bitkit keeps its current USDT0 implementation and is not adding dollars on Spark.

@ovitrif ovitrif closed this Oct 28, 2026
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.

1 participant