Skip to content

feat: pubky swaps as paykit routes - #182

Draft
ovitrif wants to merge 9 commits into
masterfrom
feat/pubky-swaps-route
Draft

ovitrif wants to merge 9 commits into
masterfrom
feat/pubky-swaps-route

Conversation

@ovitrif

@ovitrif ovitrif commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Refs:

Description

  • Adds a pubky_swaps module so that a Paykit payment routes through a Pubky Swaps provider when no balance the wallet holds reaches an endpoint the contact published. The user never picks the swap; the router does.
  • Adds a wallet-local router following Paykit Capabilities §9.1: it pays directly when it can, otherwise turns each trusted provider's offer into swap/1 edges, pre-ranks them by indicative fee (base + ppm + onchain, enforced minimum), asks the cheapest three for a firm quote and picks the smallest debit for the exact amount the contact must receive.
  • Adds the first route, a reverse swap: the wallet pays a hold invoice over Lightning, the provider locks bitcoin on chain, and bitkit-core claims it straight to the contact's on-chain address. The lock covers the wallet's own claim fee, so the contact receives exactly the amount asked for.
  • Checks everything the provider sends before the app pays: the quote against the request, the contract against the quote and the chain tip, the HTLC rebuilt from the wallet's own keys, and the hold invoice's hash, amount and expiry. These are pubky-swap's own swap-common validators.
  • Finds providers in the caller's candidate list, usually the user's contacts: each is asked for its live offer over iroh pubky-swap/direct/1 at once, with 20 s to answer, and those that run no provider are skipped. iroh needs no homeserver on either side.
  • Derives the wallet's provider identity, claim keys and preimages from the seed, under domain strings of their own. Swap records hold the contract and the key index, never a secret.
  • Raises the Android Bindgen job's timeout from 60 to 90 minutes: master already takes 50 to 55 minutes, and iroh pushes the four-ABI build past the limit.
  • New exports: pubky_swaps_route_paykit_payment, pubky_swaps_commit_route, pubky_swaps_advance, pubky_swaps_provider_edges.

The apps that use this are synonymdev/bitkit-android#1732 and synonymdev/bitkit-ios#1170, both on the prerelease 0.8.0-swaps.2 or its predecessor 0.8.0-swaps.1.

Cross-platform impact

  • New dependencies on Android and iOS: swap-common (git, coreyphillips/pubky-swap at 8686702, unaudited), iroh 1.2.0 and electrum-client 0.25. The library size change against a release build is not measured yet. cargo check passes for aarch64-apple-ios.
  • The Swift package links Network.framework: iroh reads network interfaces through it, and apps that link BitkitCore without it fail with undefined nw_* symbols.
  • No existing export changes.

What it needs to ship

  • Providers: mainnet Pubky Swaps providers with Lightning and on-chain liquidity, and a way to choose them. Today the app passes a configured list; Paykit Capabilities discovery (descriptors, follow graph, curated lists) is not built.
  • swap/1 in Paykit: Capability Descriptors published by providers, and paykit-cap.* negotiation over Encrypted Links. That needs the additive paykit-rs SDK API for extension kinds (Design review: Paykit Capabilities, a general capability and intent layer for Paykit peers (answers Paykit Intents question 8) pubky/paykit-rs#229, §17). pubky-swap signs nothing at main, so quotes and contracts are not attributable yet.
  • Backup of swap state: swap records are not in the wallet backup, and there is no seed-only scan to rebuild them. An app restart between paying and claiming leaves the claim to a later pubky_swaps_advance call; the app does not resume it yet. Funds stay safe through the timeouts: the provider cancels its hold invoice, or refunds its own lock.
  • Fees: the quote covers the provider's service fee, its on-chain cost and the wallet's claim fee. The wallet's claim fee rate is fixed at 2 sat/vB in the Android caller and should come from Bitkit's fee estimates. Lightning routing fees to the provider are on top.
  • Out of scope here: submarine routes (savings to a Lightning-only contact), USDT (Pubky Swaps is Bitcoin and Lightning only), auto-pay route selection, and cooperative signing.

Preview

The Android twin's journey: a wallet with only a spending balance pays a contact who accepts only on-chain bitcoin. The router finds a Pubky Swaps provider among the wallet's contacts, the confirm screen shows the route, and the contact receives exactly 20,000 sats on chain (2x speed, regtest).

swaps-route-android2x.mp4

QA Notes

Setup

  • A regtest Pubky Swaps provider: coreyphillips/pubky-swap at 8686702, built with cargo build --release -p swap-provider --features full, with one local change so it answers over iroh without signing in to a homeserver:
    -        "file" => Transport::from_recovery_file(&identity.value, &identity.passphrase).await?,
    -        _ => Transport::from_recovery_phrase(&identity.value, Some(&identity.passphrase)).await?,
    +        "file" => Transport::unsigned_from_recovery("file", &identity.value, &identity.passphrase)?,
    +        _ => Transport::unsigned_from_recovery("phrase", &identity.value, &identity.passphrase)?,
    (swap-provider/src/lib.rs, in run).
  • Run it against synonymdev/bitkit-docker's bitcoind, electrs and LND: swap-provider --network regtest --directions submarine,reverse --min-amount 10000 --max-amount 1000000 --base-fee 100 --fee-ppm 2500 --confirmations 1 --lnd-address https://127.0.0.1:23009 --lnd-cert <bitkit-docker>/lnd/tls.cert --lnd-macaroon <bitkit-docker>/lnd/data/chain/bitcoin/regtest/admin.macaroon --electrum-url tcp://127.0.0.1:60001 --data-dir <dir>, with PUBKY_SWAP_RECOVERY_PHRASE__FILE and PUBKY_SWAP_WALLET_MNEMONIC__FILE pointing at two throwaway BIP39 phrases. Fund the provider's first BIP84 address and mine six blocks. --doctor must pass.

Automated Checks

  • added keys.rs and router.rs unit tests: seed derivation, edge ranking, fee formula, endpoint preference
  • added client.rs live test (ignored by default): route, quote, commit, contract and hold-invoice checks against a running regtest provider. Run with PUBKY_SWAPS_PROVIDER=<pubky> PUBKY_SWAPS_ELECTRUM=tcp://127.0.0.1:60001 PUBKY_SWAPS_DESTINATION=<bcrt1 address> cargo test --lib pubky_swaps::client::live_tests -- --ignored
  • ran cargo fmt --check, cargo clippy --locked --lib --tests, cargo test --lib modules::pubky_swaps, cargo check --lib --target aarch64-apple-ios: pass
  • ran the live test against the provider above: pass
  • ran the full journey through the Android twin on a regtest emulator at 3fd9e9d, with the provider found only among the wallet's contacts: the provider logged hold invoice accepted, HTLC funded and client claimed; hold invoice settled, and the claim paid exactly 20,000 sats to the contact's address (one input from the HTLC, one output)

@ovitrif ovitrif self-assigned this Oct 29, 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