Skip to content

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

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

ovitrif wants to merge 47 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-ios#1145
Dependency: #1697

Stacked on #1697: 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

Wallet B pays wallet A $ 10.00 over Spark (left B, right A, 2x), and A's Home afterwards.

B pays A A's Home
case3-b-to-a.mp4

QA Notes

Setup

  • Two dev (regtest) builds of this branch, 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). Its issuer wallet holds the minted supply in a Breez SDK CLI data dir. To fund a wallet without it, 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

  • updated SparkRepoTest.kt — loads the token balance and transfers (mints left out), scales 6- and 8-decimal tokens to dollar units and back, recognizes Spark inputs, reads an invoice's fixed amount, rejects an invoice for another token

Evidence

At ad0a4b7a8 (the fixed-amount case at 2411e626e), dev build on two Android 16 emulators (A, B) against Lightspark's regtest, 27 Oct:

    1. passed: the CLI issuer paid 100 test dollars to A's invoice; A showed $ 100.00 and Received at once.
  • J1 steps 1 to 6 passed both ways: A paid B $ 25.00 through B's invoice handed over as lightning:<invoice> (A $ 75.00, B $ 25.00), then B paid A $ 10.00 (B $ 15.00, A $ 85.00, recording above). No fee on either side.
  • A Spark invoice from the CLI wallet fixing $ 2.00 opened straight on Confirm with $ 2.00 and paid as it stands.
  • Across platforms with the iOS twin: A paid iOS $ 20.00 and iOS paid A $ 5.00 (A $ 70.00 before the $ 2.00 invoice).
  • J1 step 7 passed: a sats Spark invoice from the CLI wallet stopped on the recipient step with "Enter a valid address for the selected network."
  • The Home tile showed USDT Spark $ 0.00 before funding, and the token metadata (ticker, decimals) loaded from Spark.
  • Round 2 (USDB on mainnet, dollar copy, Convert, To USDT, Paykit usdb-spark-address) at e0eeefa8e: just test and just lint pass; no device run yet. The cases above ran with the earlier regtest test token, "Test Tether USD".
  • Rebased onto bfc43b533 at 3eece748e: just test and just lint pass.
  • Activity, receipts and sends at 74735603d: just test and just lint pass; no device run yet.
  • Mainnet, 27 Oct, on @ovitrif's phones: convert, both USDB sends and the swap to Arbitrum USDT passed on Android 3eece74 / iOS 56d7adf3; USDB activity, details and receipts passed on 2616c13 / 68e07be9 (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.
  • Paying a plain Spark address (no invoice) with the dollar token: untested on device; Bitkit only shows invoices.
  • PIN or biometric check before the payment: untested; both 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 existing code only.

@ovitrif ovitrif self-assigned this Oct 27, 2026
@ovitrif

ovitrif commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 91fa79b: 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.

just test and just lint pass.

@ovitrif

ovitrif commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 74735603d: 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.

just test and just lint pass; 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 3eece74 and iOS 56d7adf3, before this PR's current head; the last ran on the current heads, Android 2616c13 and iOS 68e07be9.

🟢 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
1 of 35 tasks
@greptile-apps greptile-apps Bot mentioned this pull request Oct 27, 2026
3 tasks
@ovitrif
ovitrif force-pushed the feat/breez-spark-lightning branch from 024b6bb to d7be600 Compare October 28, 2026 14:35
@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