WDK-compatible Bitcoin wallet manager/account implementation built on top of @arkade-os/sdk, with optional Lightning support through @arkade-os/boltz-swap.
Implemented:
- WDK
WalletManagerintegration (getAccount,getAccountByPath,dispose) - WDK account methods for send/sign/verify/quote and read-only conversion
- Destination auto-detection for Ark address, BTC address, and BOLT11 invoices
- LNURL/Lightning-address helpers (
fetchInvoice, limits, callback resolution) - Utility exports for address detection, BIP21 parsing/encoding, fees, and formatting
- Three account types via index: boarding (0), offchain (1), lightning (2)
- Lightning receive via
createLightningInvoice()(HRPC → Boltz swap) - Lightning send via auto-detection of BOLT11 invoices in
sendTransaction() - Transaction history for arkade networks via
getTransactionHistory()(HRPC → SDK) - Arkade balance fetching via direct REST calls to Ark indexer and Esplora
TODO (known gaps in current implementation):
getFeeRates()currently returns placeholder values (normal: 0n,fast: 0n)- Lightning swap lifecycle helpers are not implemented yet (needs evaluation whether these are needed):
waitForLightningPayment,getPendingLightningReceives,getPendingLightningSends,getSwapHistory,getLightningLimits,getLightningFees - Transaction routing enum includes
EMAIL, but email payments are not implemented - BIP21 helpers are implemented, but
sendTransaction/quoteSendTransactioncurrently expect direct destination values (Ark/BTC/BOLT11), not a BIP21 URI
The wallet manager exposes three account indices, all sharing the same underlying @arkade-os/sdk wallet instance:
| Index | AddressType | Purpose |
|---|---|---|
| 0 | boarding |
On-chain BTC deposit address (funds enter the Ark) |
| 1 | offchain |
Ark protocol address (VTXO-to-VTXO transfers) |
| 2 | lightning |
Lightning via Boltz swaps (no static address; uses invoice generation) |
getAddress() returns an empty string for Lightning (index 2). The UI should detect this and present an amount-input + invoice-generation flow instead of a static QR code.
arkade-wdk/
├── src/
│ ├── lib/ # address, bip21, bolt11, lnurl, fees, formatting, send routing
│ ├── wallet-manager-arkade.ts # WDK wallet manager implementation
│ ├── wallet-account-arkade.ts # WDK account + read-only account implementations
│ └── index.ts # package exports
├── packages/
│ ├── pear-wrk-wdk/ # submodule: bare-kit worklet runtime (HRPC schema + handlers)
│ └── wdk-react-native-provider/# submodule: React Native provider (WDK service, contexts, UI wiring)
├── examples/
│ └── wdk-starter-react-native/ # submodule: Expo example app
├── patches/
│ ├── pear-wrk-wdk.patch
│ ├── wdk-react-native-provider.patch
│ └── wdk-starter-react-native.patch
└── scripts/
├── setup-dev.js # local dev setup helper
├── apply-patches.js # apply ./patches to each submodule
└── generate-patches.js # regenerate ./patches from submodule diffs
npm install @arkade-os/wdk @tetherto/wdk@arkade-os/sdk is not required as a direct install for normal usage of this adapter.
It is pulled transitively by @arkade-os/wdk.
If your app imports @arkade-os/sdk directly, add it explicitly to your app dependencies.
For local monorepo development with submodules and links:
npm run setup:devimport WdkManager from '@tetherto/wdk'
import WalletManagerArkade from '@arkade-os/wdk'
const seedPhrase = 'your twelve word seed phrase here'
const wdk = new WdkManager(seedPhrase)
wdk.registerWallet('bitcoin', WalletManagerArkade, {
arkServerUrl: 'https://arkade.computer',
swapProviderUrl: 'https://api.ark.boltz.exchange', // optional: enables Lightning methods
})
const account = await wdk.getAccount('bitcoin', 0)
const arkAddress = await account.getAddress()
const balance = await account.getBalance()
const quote = await account.quoteSendTransaction({
to: arkAddress,
value: 1000n,
})
const tx = await account.sendTransaction({
to: arkAddress,
value: 1000n,
})
console.log({ balance, quoteFee: quote.fee, txid: tx.hash })Create Lightning invoice (enabled only when swapProviderUrl is configured):
const { invoice, paymentHash } = await account.createLightningInvoice(50_000, 'Payment for coffee')
console.log(invoice) // BOLT11 invoice stringPay to Lightning address / LNURL:
import { fetchInvoice, isLightningAddress } from '@arkade-os/wdk'
if (isLightningAddress('user@wallet.com')) {
const invoice = await fetchInvoice('user@wallet.com', 1000, 'tip')
await account.sendTransaction({ to: invoice, value: 1000n })
}WalletAccountArkade exposes the underlying SDK wallet as account.wallet for operations not covered by the WDK interface:
const detailedBalance = await account.wallet.getBalance() // { total, offchain, onchain }
const history = await account.getTransactionHistory()class WalletManagerArkade extends WalletManager {
// inherited from @tetherto/wdk-wallet WalletManager
static getRandomSeedPhrase(wordCount?: 12 | 24): string
static isValidSeedPhrase(seedPhrase: string): boolean
constructor(seed: string | Uint8Array, config?: ArkadeWalletConfig)
getAccount(index?: number): Promise<WalletAccountArkade>
getAccountByPath(path: string): Promise<WalletAccountArkade>
getFeeRates(): Promise<{ normal: bigint; fast: bigint }>
dispose(): void
}class WalletAccountArkadeReadOnly {
readonly index: number
readonly path: string
readonly keyPair: { publicKey: Uint8Array }
getAddress(): Promise<string> // returns '' for lightning accounts
getBalance(): Promise<bigint>
getTransactionHistory(): Promise<ArkTransaction[]>
verify(message: string, signature: string): Promise<boolean>
getTransactionReceipt(hash: string): Promise<unknown | null>
getTokenBalance(tokenAddress: string): Promise<bigint> // always 0n for Bitcoin
quoteSendTransaction(tx: Transaction): Promise<{ fee: bigint }>
quoteTransfer(options: TransferOptions): Promise<{ fee: bigint }> // throws (not applicable)
}class WalletAccountArkade extends WalletAccountArkadeReadOnly {
readonly keyPair: { publicKey: Uint8Array; privateKey: Uint8Array | null }
readonly wallet: IWallet
readonly arkadeLightning: ArkadeLightning | null
sendTransaction(tx: Transaction): Promise<{ hash: string; fee: bigint }>
quoteSendTransaction(tx: Transaction): Promise<{ fee: bigint }>
transfer(options: TransferOptions): Promise<TransferResult> // throws (not applicable)
sign(message: string): Promise<string>
toReadOnlyAccount(): Promise<WalletAccountArkadeReadOnly>
dispose(): void
createLightningInvoice(amount: number, description?: string): Promise<{ invoice: string; paymentHash: string }>
}Address:
decodeArkAddressisArkAddressisBTCAddressisLightningInvoice
Transaction routing:
detectTransactionTypequoteSendsendTransactionType
BIP21:
isBip21decodeBip21encodeBip21
BOLT11:
decodeInvoiceisValidInvoice
LNURL / Lightning address:
isLnUrlisLightningAddressisValidLnUrlgetCallbackUrlcheckLnUrlConditionsfetchInvoicefetchArkAddressgetLnUrlLimitsextractRecipientFromMetadata
Fees and formatting:
calculateOffchainFeecalculateOnchainFeecalculateLightningFeefromSatoshistoSatoshisformatSatsformatSatsWithCommasprettyNumber
import type { ArkadeWalletConfig } from '@arkade-os/wdk'
const config: ArkadeWalletConfig = {
arkServerUrl: 'https://arkade.computer',
swapProviderUrl: 'https://api.ark.boltz.exchange',
}ArkadeWalletConfig includes @arkade-os/sdk wallet config fields (except identity) plus swapProviderUrl.
Minimum Arkade configuration is arkServerUrl or arkProvider.
The normal WDK path for balances is: RN provider -> HRPC getAddressBalance -> worklet -> SDK wallet.getBalance(). For arkade networks, the SDK's internal Esplora URL defaults to http://localhost:3000 (regtest), which is unreachable from an Android device. Non-arkade chains (BTC, EVM, TON, etc.) don't hit this problem because their balances are fetched via the WDK indexer at wdk-api.tether.io directly from RN's fetch, never through the worklet.
The current workaround in wdk-react-native-provider calls the Ark indexer and Esplora REST APIs directly from the RN side:
- Offchain/Lightning balance:
GET ${arkServerUrl}/v1/indexer/vtxos?scripts=${pkScript}&spendableOnly=true - Boarding balance:
GET ${esploraUrl}/address/${addr}/utxo
This involves an inline bech32m decoder (arkAddressToPkScript) to extract the pkScript from the Ark address without adding a dependency. Once the SDK's Esplora URL is configurable or auto-detected correctly, these workarounds can be removed and the standard HRPC getAddressBalance path can be used for arkade too.
Unlike balances, transaction history goes through the full HRPC path: RN provider -> HRPC getTransactionHistory -> worklet -> SDK wallet.getTransactionHistory(). The SDK returns ArkTransaction[] which is serialized as JSON through HRPC and mapped to the provider's Transaction interface on the RN side.
The packages/ and examples/ directories are git submodules, each with their own repository. Changes flow inside-out: commit within the submodule first, then commit the updated submodule reference in the parent.
# 1. Make changes inside a submodule
cd packages/wdk-react-native-provider
# ... edit files ...
git add -A && git commit -m "your change"
git push
# 2. Back in the parent repo, commit the updated submodule pointer
cd ../..
git add packages/wdk-react-native-provider
git commit -m "Update wdk-react-native-provider submodule"Repeat for each submodule that changed (packages/pear-wrk-wdk, examples/wdk-starter-react-native).
git submodule update --init --recursiveOr use the setup script which also builds and links:
npm run setup:devBecause the submodules are upstream repos, local modifications are tracked as patch files under ./patches/ rather than as commits in forks. This keeps the parent repo's changes visible without requiring write access to the upstream repos.
node scripts/apply-patches.jsRun with --check to verify patches apply cleanly without modifying the working tree:
node scripts/apply-patches.js --checknode scripts/generate-patches.jsThis compares each submodule's working tree against origin/main and overwrites the corresponding file in ./patches/. Commit the updated patch files to the parent repo.
Specify a different base ref with --base:
node scripts/generate-patches.js --base origin/v2# 1. Edit files inside the submodule
cd packages/wdk-react-native-provider
# ... edit files ...
# 2. Regenerate the patch from the parent repo root
cd ../..
node scripts/generate-patches.js
# 3. Commit the updated patch to the parent repo
git add patches/wdk-react-native-provider.patch
git commit -m "Update wdk-react-native-provider patch"After editing provider source, regenerate bundles and type definitions before committing:
cd packages/wdk-react-native-provider
npm run prepare # runs gen:secret-manager-bundle + gen:worker-bundle + bob buildThis re-bundles the worklet (picking up any HRPC schema changes from pear-wrk-wdk) and type-checks with the stricter bob build settings.
npm install
npm run build
npm run dev
npm run lint
npm run formatTesting:
npm testMIT