Skip to content

feat: arkade spending in bitkit-core - #183

Closed
ovitrif wants to merge 23 commits into
masterfrom
feat/arkade-spending
Closed

ovitrif wants to merge 23 commits into
masterfrom
feat/arkade-spending

Conversation

@ovitrif

@ovitrif ovitrif commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Refs:

This PR adds Arkade (Ark Labs) to bitkit-core as src/modules/arkade, the engine behind Spending on Arkade in synonymdev/bitkit-android#1731 and synonymdev/bitkit-ios#1169. It builds on arkade-os/rust-sdk 0.12 and is exposed to both apps through UniFFI. Bitkit doesn't embed JS, and rust-sdk has no Arkade Intents Lightning client, so this module adds one.

Draft. The wallet, the Lightning address and the Intents Lightning client are all in this head, and both app drafts run on it. The prerelease 0.8.0-arkade.2 (tag v0.8.0-arkade.2) carries it to their CI.

Description

  • Wallet (ArkadeClient on ark-client 0.12, Esplora chain source):
    • Starts from the Bitkit seed and passphrase on BIP86 account 1 (m/86'/0'/1'), so no key is shared with Savings (account 0).
    • Covers balance, outputs with batch expiry, history, boarding address, and boarding confirmed deposits.
    • Renews outputs before expiry (by itself; Ark Labs' delegate is being retired) and offboards to an on-chain address.
    • Exports and imports its state for the apps' VSS backup.
    • Runs a background chores loop that boards and renews about once a minute.
    • Sends events to a foreign listener (with_foreign), like the Boltz module.
  • Lightning address on Ark Labs' lnurl.arkade.sh (protocol of ArkLabsHQ/lnurl-server):
    • Finds the address the wallet owns or claims a random one, then registers the wallet's Arkade identity for offline receive.
    • The session token is derived from the identity exactly as @arkade-os/lnurl-client derives it, so a restore finds the same address.
  • Arkade Intents Lightning client (src/modules/arkade/intents, exported as arkade_receive, arkade_quote_pay, arkade_pay and arkade_swaps, with swap events on the listener):
    • Quotes over Nostr (kind 24859, NIP-44) from the mainnet beta-solver.
    • Receive: a hold invoice for an exact amount, verified, then claimed into the wallet.
    • Send: funds the lockup, detects the solver's claim, and refunds after refund_locktime.
    • Persists swap records, including them in the state backup.
    • Uses the corridor's VHTLC v2 with emulator covenant leaves, ported from arkade-os/rust-sdk#287 until it merges.
  • Vendors ark-core 0.12.0 with one patch: it builds the 2-of-2 leaf as <operator> CHECKSIGVERIFY <owner> CHECKSIG, while Ark Labs' TypeScript SDK and the live operator use owner first. Without the patch, the same keys give different Arkade and boarding addresses than the TypeScript wallet.
  • Networks: mainnet against Ark Labs' services, mutinynet (wallet only), and a regtest network that takes its endpoints from the app (operator, Esplora, LNURL server, delegate, solver key and relays), for a local arkade-regtest stack.
  • Dependencies: ark-client and ark-core 0.12 with ark-client's sqlite feature off, so SQLite is linked once (bitkit-core's rusqlite). Stage B adds nostr-sdk 0.43, with default features off plus nip44.

Out of Scope

  • src/modules/arkade: a unilateral exit runner.
  • A Synonym-run lnurl-server.
  • Asset swaps, on-chain corridors and published RFQs.

Upstream

Ark Labs (Kukks, 29 Oct) confirms rust-sdk has no Arkade Intents client and would accept one upstream. The Intents client here is written to be contributed to arkade-os/rust-sdk later: it takes the wallet through a small trait, keeps one ark-core, and is checked against the TypeScript SDK.

Answers that shape this PR (Ark Labs, 29 Oct)

  • Renewal: the delegation service is being phased out for an Arkade Script approach. So renew and the chores loop stay the only renewal path, and nothing more depends on delegate.arkade.money.
  • Recovery: swap state must be backed up beyond the seed. export_state and import_state carry swap records, salts included.
  • Lightning addresses: Ark Labs' LNURL instance may be used, also behind a Synonym domain with an exclusive API key.
  • Limits: the beta solver goes up to about 200,000 sats.

Still open for Ark Labs

  1. Key order in ark-core. ark-core 0.12 orders the 2-of-2 leaf as operator then owner, unlike the TypeScript SDK and the live operator. Ark Labs asked for a fix upstream: fix(ark-core): match the ts sdk's vtxo and boarding addresses arkade-os/rust-sdk#288 (a failing test, then the fix). How wallets made with the old order move over is left to them; the vendored patch here goes once it merges.
  2. Wire format. Should quote amounts be numbers or strings? The solver sends numbers, while the TypeScript comments say strings. Is the provisional kind 24859 stable?
  3. Swap secrets. In the default static wallet mode, a swap's preimage comes from the identity key plus a salt stored in the swap record. Will walletMode: "auto" move to HD, and with which derivation?
  4. Failed send lock. A failed send stays locked about 7 days (refund_locktime about 612,000 s ahead) unless the solver pushes the cooperative refund. Is that intended?

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 #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 #972 lands and mainnet runs.

Preview

N/A — a library change.

QA Notes

  • cargo build and cargo test must pass, and so must build_android.sh and build_ios.sh.
  • Results are checked against Ark Labs' TypeScript SDK as the oracle, with no money moved:
    • Arkade and boarding addresses, the Lightning address token and the claim public key for a throwaway mnemonic (src/modules/arkade/fixtures).
    • Intents: the swap preimage, payment hash, claim key, and receive and send lockup addresses, which also equal the solver's own.
  • Ignored mainnet tests (cargo test -- --ignored), which move no money:
    • Start a fresh wallet, claim its Lightning address, and find it again after an export and import.
    • Intents: a receive quote of 1,000 sats nets 995 with a hold invoice whose hash is ours; a send quote for a 1,000-sat invoice locks 1,004.
  • Ignored regtest test against a local arkade-regtest stack, with test money (regtest_tests.rs; the header says how to run it). Passed on 7a6a4e0:
    • Receive into an empty wallet: a 20,000-sat invoice, paid by the stack's LND, claimed in 10 s; 19,900 sats arrived.
    • Send: a 5,000-sat invoice paid for 5,021 in 8 to 10 s.
    • Offline receive: with the wallet stopped, a payment to its Lightning address; lnurl-server claimed it, and the restored wallet found 2,985 sats of 3,000 within 2 s.
    • Restore from the phrase and the backup in a fresh directory: the same addresses, the same Lightning address, the balance and the paid send.

Gaps

  • Real money: funding, claiming and refunding run on regtest here; mainnet stays the apps' test.
  • A failed send's refund is not run: the solver's refund locktime matures on median-time-past, about 2 hours or more on regtest's seconds-typed locks.

Models used

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

Mainnet reads go to mempool.space and move to blockstream.info when it times out, refuses the connection or answers with an error; the explorer that answers stays in use. A timeout or refused connection is no longer retried on the same server, and a read gives up after 10 s instead of 30.
The history watch and chores read each boarding address every few seconds, past what public explorers allow per IP (blockstream.info: 700 an hour). Spent outputs and confirmed transactions are now cached for good, the tip and address histories for 60 s, the history watch runs every 30 s instead of 15, a rate-limited explorer is skipped instead of retried, and mempool.emzy.de is a third explorer.
The operator refused every boarding from the test Pixel with INVALID_INTENT_TIMERANGE (proof of ownership not yet valid): ark-client 0.12 makes a batch intent valid from the device's current second, and that phone runs about 2 s ahead. A vendored ark-client makes the intent valid from a minute earlier. Boarding, renewal and offboarding all join batches this way.
@ovitrif

ovitrif commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator Author

Closing this PR: after a full mainnet QA run on Android and iOS, Bitkit goes with Spark for Lightning.

Arkade worked end to end with real funds (LN send and receive, Lightning address, transfers to and from Savings), but it needs too much built and run on our side: chain access through block explorers, our own LN swap client and the activity history. The branch stays for reference.

@ovitrif ovitrif closed this Nov 3, 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