Skip to content

feat: spending on arkade - #1731

Draft
ovitrif wants to merge 8 commits into
masterfrom
feat/arkade-lightning
Draft

ovitrif wants to merge 8 commits into
masterfrom
feat/arkade-lightning

Conversation

@ovitrif

@ovitrif ovitrif commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Twin: synonymdev/bitkit-ios#1169
Dependency: synonymdev/bitkit-core#183
Refs:

This PR builds the Arkade (Ark Labs) alternative to #1697, so both can be tested on mainnet and compared hands-on. Spending becomes an Arkade balance, Lightning goes through Arkade Intents, and Savings keeps its ldk-node on-chain wallet. The UX matches the Spark draft: the same fallback rule, Funding entry points, transfer flows, Lightning Connections row and detail screen.

Description

  • Runs Arkade's wallet and Lightning client behind ArkadeService, through a Rust binding on arkade-os/rust-sdk (ark-rs) over UniFFI, shared with the iOS twin. Bitkit doesn't embed JS. On mainnet builds only: bitkit-core starts no Lightning solver or LNURL server on Arkade's test network, so dev builds keep Arkade off. CI needs bitkit-core 0.8.0-arkade.1 published from feat: arkade spending in bitkit-core bitkit-core#183; until then this branch builds only against a locally built binding.
  • What ark-rs has for Lightning today:
    • ark-rs 0.12 has VHTLC scripts and a Boltz swap client, but Boltz no longer serves Ark.
    • It has no Arkade Intents client: no Nostr quotes, quote checks, or claim and refund loop.
    • arkade-os/rust-sdk#287 feat(ark-core): VHTLC v2 with emulator covenant leaves (open, 22 Oct) adds the contract the Intents corridor uses.
    • Ark Labs says the Rust SDK is behind the TypeScript one on Intents and has offered a contact for the details. No Rust Intents plan is published.
    • So the binding adds the Intents Lightning client itself: quotes over Nostr, checks, funding, claim and refund. Each step is checked against Ark Labs' TypeScript client, which stays outside the app as a test reference.
  • Derives the Arkade keys from the Bitkit seed and passphrase on BIP86 account 1 (m/86'/0'/1', BIP86). The SDK's default, account 0, would reuse Savings' Taproot keys. There is no new phrase to back up.
  • Routes Lightning receives to Arkade, through Arkade Intents, when no ready channel has the inbound capacity:
    • With an amount, the receive sheet shows a solver hold invoice for exactly that amount. Spending gets it less the solver's fee.
    • Without an amount, the QR's lightning= carries the wallet's Lightning address as an LNURL. Arkade has no amountless invoice.
  • Gives each wallet a Lightning address on Ark Labs' hosted lnurl.arkade.sh (lnurl-server) and registers the wallet's Arkade identity for offline receive. The server then hands a payer a solver hold invoice and claims the payment into the wallet while the app is closed. The address's token derives from the wallet's key, so a restore finds the same address.
  • Routes Lightning sends and their fee estimates to Arkade, through the same Intents client, when no channel has the outbound capacity and the Arkade balance covers the amount. Invoices without an amount are refused, since the swap client can't pay them.
  • Raises Arkade swaps (outcomes) as the node's own payment events and records them as Lightning activity, so the received sheet, send success screen and activity list work for both rails. Payments that reach the wallet outside a swap, such as offline receives, are recorded from the wallet's history.
  • Adds the Arkade balance (settled plus preconfirmed outputs), as feat: add spark via breez sdk #1697 does for Spark, to the Spending balance. Deposits waiting to board show as in transfer to Spending.
  • Transfer to Spending sends Savings on-chain to the wallet's Arkade boarding address. Once it has 1 confirmation, the wallet boards it into the operator's next batch by itself, with no boarding fee on the live operator. The deposit screen shows Waiting, Boarding and Boarded instead of Spark's claim choices.
  • Transfer to Savings offboards the whole Arkade balance to a Savings address in the next batch.
  • Renews outputs before their batch expires (renewal): the wallet renews 3 days before expiry while the app runs, and Ark Labs' delegate (delegate.arkade.money, fee 0) can renew while it is closed, until Ark Labs replaces it with an Arkade Script approach. The Arkade screen shows the Lightning address and the next renewal.
  • Publishes the Lightning address as Bitkit's Paykit btc-lightning-lnurl endpoint when Arkade is the receive rail, so contacts can pay a Spending-only wallet with any amount, offline too. Paykit requests with an amount are paid over Arkade when no channel can carry them.
  • Backs up the Arkade wallet's local state (outputs, contracts, swap records) to VSS as ARKADE_EXIT, exported on start and on every change, encrypted like the other backups. A restore imports it before the wallet opens, then scans the operator for anything newer. The first start on an install also runs that scan, so a wallet restored from the phrase alone finds its outputs and swaps.
  • Dev builds run Arkade on mutinynet, Arkade's test network (networks). Their Savings is on Bitkit's regtest, so Transfer to Spending can't board there.

Out of Scope

  • Running a unilateral exit: the data is backed up and restored, but no screen runs the exit.
  • Push notifications for Arkade receives: receives while closed reach the wallet through the LNURL server, and the app shows them on next open.
  • A Bitkit domain for Lightning addresses: needs Ark Labs' exclusive API key for the domain.
  • Multi-asset receive (USDT, USDC through FixedFloat) that Ark Labs' LNURL instance offers.
  • Arkade-to-Arkade payments, assets and Intents' on-chain routes.
  • Copy and design for Arkade across transfer and receive screens.
  • bitkit-core: the integration lives in the app, beside ldk-node.

Risks for review

  • Developer preview. Arkade Intents is a developer preview with one mainnet solver (beta-solver), pinned from Ark Labs' docs, since the mainnet registry lists none. Ark Labs runs the only operator, the emulator, the relay, the delegate and the LNURL server.
  • Trust model.
    • The operator can't spend outputs without the user.
    • Preconfirmed outputs trust it not to cosign a conflicting spend.
    • Outputs expire with their batch, and after expiry recovery needs the operator.
    • The unilateral exit takes about 7 days (unilateralExitDelay 605,184 s) and the user pays all fees.
  • Claim window. A Lightning receive needs the wallet online to claim it. The solver's invoices expire after 25 minutes, not Bitkit's usual 24 hours. Offline receives go through the LNURL server instead.
  • Fees (live operator and solver card):
    • Lightning in or out: 0.30% plus the solver's minimum (5 sats on a 1,000-sat receive, 47 on 10,000).
    • Offboard: 200 sats per on-chain output, plus 0.2% on outputs younger than 24 hours.
    • Boarding: free.
  • Lightning address host. lnurl.arkade.sh is Ark Labs' own instance, which Ark Labs says Bitkit may use; for a Bitkit domain, they issue an exclusive API key and Bitkit points its domain at their instance.
  • Limits. The solver takes 335 to 200,000 sats per Lightning payment.
  • Churn. Ark Labs' swap client is a release candidate shipping almost daily, and Ark Labs calls the operator alpha. The Rust Intents client must track its wire format.

Paykit's reusable Lightning endpoint

Arkade has no amountless invoice: each solver hold invoice is for one amount and expires after 25 minutes, so a reusable "pay me on Lightning" endpoint needs an LNURL-pay server that makes an invoice per payment. It also claims payments while the app is closed.

  • Ark Labs' answer: Bitkit may use their instance (this PR uses lnurl.arkade.sh), or point a Synonym domain at it via CNAME or A record, with an exclusive API key for issuing addresses on it. No self-hosting is needed.
  • Self-hosting stays an option: lnurl-server is one container with SQLite and a domain. It claims offline receives co-signed by Ark Labs' public emulator, and holds preimages but no keys.
  • To measure: Manual Test 8, paying the Arkade-only contact's endpoint while its app is closed.

Answers from Ark Labs

Kukks (Ark Labs) answered in the Arkade <> Bitkit group on 29 Oct.

  1. Rust Intents client: none upstream yet, since Ark Labs focuses on the TypeScript SDK. They would accept ours upstream, so feat: arkade spending in bitkit-core bitkit-core#183 builds it to be contributed later.
  2. Lightning addresses: Bitkit may use Ark Labs' LNURL instance, or point its own domain at it with a CNAME or A record and issue addresses through an exclusive API key for that domain. That closes the Paykit endpoint gap without self-hosting. Self-hosting stays possible.
  3. Renewal while closed: the delegation service (delegate.arkade.money) is being phased out for a simpler approach on Arkade Script, almost ready. Renewal here sits behind one call (renew, plus the wallet's own pass 3 days before expiry), so it can switch without app changes; this PR invests nothing more in the delegate.
  4. Solvers: the solver code is open source and anyone can run one; Ark Labs runs the beta solver to iterate quickly. Card pinning comes first, then open solver bidding for discovery.
  5. Limits: from dust upwards; the beta solver takes up to about 200,000 sats and will grow.
  6. Recovery: swap state must be backed up beyond the seed. This PR's ARKADE_EXIT backup carries the wallet's whole state, swap records included, and a restore imports it before the wallet starts.
  7. Terms and audits: no terms; Ark Labs runs internal audits across its stack.
  8. Docs: Ark Labs aims to keep docs.arkadeos.com current.
  • Still open, from the code: whether quote amounts are numbers or strings on the wire, whether Nostr kind 24859 is stable, and whether a failed send's lock of about 7 days is intended. These are listed in feat: arkade spending in bitkit-core bitkit-core#183.
  • Their LNURL instance also offers multi-asset receive (Arkade, on-chain, and USDT/USDC through FixedFloat); out of scope here.

Swap-state backup (from Ark Labs' code)

A Lightning swap can't be rebuilt from the seed plus the operator's indexer: @arkade-os/swap only rebuilds Arkade-to-Arkade offers, and its swap contracts have no discovery. So these must be backed up per swap:

  • the swap record: quote id, rfqId, kind and state, the amounts and fee, the solver key, lock.hash, lockupAddress and lockupPkScript, refundLocktime, fundingTxid, and the invoice
  • the claim or refund signer's descriptor
  • the preimage salt (preimageSaltHex, needed in the default static wallet mode) or the preimage itself when stored
  • the lockup contract's parameters: solver key, delays, emulator key and covenant scripts

These come back from the seed and the indexer: the refund key (the wallet's identity key), a preimage derived from an HD descriptor, lockup output state, revealed preimages, and the wallet's own addresses and balance.

The TypeScript SDK has no export or import of swap state, and Ark Labs' wallet dropped swaps from its Nostr backup when it moved to Intents (arkade-os/wallet#908). Bitkit's ARKADE_EXIT backup carries the swap records with all of the above, through export_state in synonymdev/bitkit-core#183.

Renewal after the delegate (from Ark Labs' code)

The replacement for delegate.arkade.money is public but not released:

  • Status:
  • How it works:
    • the wallet registers once, then moves its spendable outputs to a covenant address derived locally from its key and the service's keys, so a restore finds them without the service
    • after that, the service renews them with the emulator's co-signature, taking at most a fee cap from the output
    • swap lockups are not moved, since they keep their own claim and refund paths
  • For Bitkit: nothing is stable enough to build against yet. Renewal stays one call plus the wallet's own pass 3 days before expiry, and delegatee replaces it once feat(widgets): intro and gallery sheet #972 lands and mainnet runs.

Design

N/A — no design available. Arkade reuses the Spark draft's screens.

Preview

Case 1 on the Rust engine (b6092d340 with bitkit-core ac2919b), a mainnet build with a new, unfunded wallet (QR and clock masked): Receive shows the Lightning address, then a 2,000-sat Arkade invoice from the empty wallet, 6 s after the amount is set.

Receive (1)
receive-arkade-rust.mp4

Funded cases follow with the mainnet results.

QA Notes

Setup

  • Mainnet build of this branch against bitkit-core 0.8.0-arkade.1 (or a local build of feat: arkade spending in bitkit-core bitkit-core#183). No key is needed: Arkade, the solver, the delegate and the LNURL server are Ark Labs' public mainnet services. Use small amounts (335 to 200,000 sats per Lightning payment).
  • Dev builds run Arkade on mutinynet. Boarding from Bitkit's regtest Savings can't work there.

Journeys

  • J1: new arkade-transfer-to-spending.xml — with Arkade ready, the transfer buttons and Funding open the Arkade amount and confirm screens
  • J2: updated transfer-unavailable-sheet.xml — now needs Arkade not running, since a ready Arkade replaces the sheet

Manual Tests

  • 1. Mainnet, new wallet, no channels → Receive shows Auto with the Lightning address's LNURL; with an amount, a Lightning invoice for exactly that amount — mainnet services
  • 2. Mainnet, empty wallet, pay its invoice from another wallet → received sheet and activity show it, less the solver fee — mainnet funds
  • 3. Mainnet, close the app, pay its Lightning address → on next open the payment shows in Spending and activity — mainnet funds
  • 4. Mainnet, pay a Lightning invoice from the Arkade balance → paid, activity shows the fee — mainnet funds
  • 5. Mainnet, Transfer to Spending → deposit shows Waiting, then Boarding at 1 confirmation, then Boarded; Spending shows the amount — mainnet funds
  • 6. Mainnet, Transfer to Savings with only an Arkade balance → the balance moves on-chain in the next batch, less 200 sats — mainnet funds
  • 7. Mainnet, restore the wallet on another device → the Arkade balance, the Lightning address and open swaps come back — two devices
  • 8. Mainnet, two Bitkit contacts, one Arkade-only → the other pays its Paykit request over Lightning, any amount, also while it is closed — two devices with Paykit
  • 9. Mainnet, Arkade screen → shows provider Ark Labs, the trust note, the Lightning address and the next renewal — mainnet funds
  • 10. Mainnet, pay an invoice without an amount → refused with an error — any wallet

Automated Checks

  • added ArkadeRepoTest.kt — the operator scan runs on the first start only, a settled boarding marks its deposit boarded, a broadcast deposit shows pending at once, a claimed receive and a paid send raise node events with amount and fee, an amountless invoice is refused, and a backup from another network is skipped while one from this network is imported on the next start
  • updated BackupRepoTest.kt, PrivatePaykitRepoTest.kt, LightningRepoTest.kt, WalletRepoTest.kt, WalletViewModelTest.kt, EditInvoiceVMTest.kt, DeriveBalanceStateUseCaseTest.kt, TransferViewModelTest.kt, AppViewModelSendFlowTest.kt — the Spark draft's cases, run against Arkade

Evidence

  • On the Rust engine, b6092d340 with a local build of bitkit-core ac2919b: a mainnet debug build on an Android 15 emulator, new wallet, no channels, no funds.
    • The Arkade wallet connected on mainnet. Its first start, which scans the operator for the wallet's funds, took about 2.5 minutes.
    • Receive showed the wallet's lnurl.arkade.sh Lightning address as the QR's lightning=.
    • Setting 2,000 sats gave an Arkade invoice for exactly 2,000 sats 6 s later.
    • The Arkade wallet data backed up to VSS (ARKADE_EXIT).
    • just compile, just test and just lint pass against that binding.
  • Measured with Ark Labs' TypeScript client, before the engine moved to Rust. On 9aeb4b416, a store-signed mainnet debug build on an Android 15 emulator with a new wallet and no channels.
    • The Arkade wallet connected to arkade.computer in about 2 s, including its first-start operator scan.
    • Receive showed Auto with the wallet's lnurl.arkade.sh Lightning address as the QR's lightning= LNURL.
    • Setting 2,000 and 5,000 sats gave Lightning invoices for exactly those amounts within 3 s, from an empty wallet.
  • The same TypeScript client, headless against mainnet with a throwaway seed:
    • A 1,000-sat receive quote kept 5 sats as the solver fee (995 to the wallet). With the payer covering the fee instead, 1,000 sats cost the payer 1,006, and 10,000 cost 10,047.
    • The solver's invoices expire after 25 minutes.
    • A payer's LUD-16 callback for 2,000 sats, while the wallet had no session open, returned a 2,000-sat solver hold invoice: offline receive is live on Ark Labs' host.
    • An exported state imported on a fresh origin brought back the same Arkade address, the open swap and the same Lightning address.
  • just compile, just test and just lint pass locally on 9aeb4b416.
  • 1 passed on 9aeb4b416 (Preview).
  • J1 steps 3 to 5 passed on 6dfa8c512 (mainnet emulator, no Savings): Funding showed Transfer from Savings and Use Other Wallet above Manual Setup, and Transfer from Savings opened the Arkade amount screen.

Gaps

  • 2 to 10: not run yet. Each needs mainnet funds, which only the reviewer can move; the PR answers follow from them.
  • VTXO expiry: the live operator doesn't publish its batch expiry, so the first funded output gives the real value.
  • Renewal while closed: the delegate is configured but not yet seen renewing.
  • J1 steps 1 and 2 need a funded Savings balance; J2 not driven yet.

Models used

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

@ovitrif ovitrif self-assigned this Oct 29, 2026
…at/arkade-lightning

# Conflicts:
#	app/src/test/java/to/bitkit/repositories/PrivatePaykitRepoTest.kt
#	app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt

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.

1 participant