Skip to content

feat: arkade spending in bitkit-core - #183

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

ovitrif wants to merge 11 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. Wiring into the apps follows.

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

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. Which is canonical, and will rust-sdk change?
  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.

Gaps

  • Funding, claiming and refunding with real money are not run here. They are the apps' mainnet tests.
  • The bindings are not yet released for the apps.

Models used

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

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