@arkade-os/boltz-swap 0.2.20 → 0.3.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,17 +1,19 @@
1
- # Lightning Swaps
1
+ # Arkade Swaps
2
2
 
3
- > Integrate Lightning Network with Arkade using Submarine Swaps
3
+ > Lightning and chain swaps for Arkade using Boltz
4
4
 
5
- Arkade provides seamless integration with the Lightning Network through Boltz submarine swaps, allowing users to move funds between Arkade and Lightning channels.
5
+ `@arkade-os/boltz-swap` provides seamless integration with the Lightning Network and Bitcoin on-chain through Boltz swaps, allowing users to move funds between Arkade, Lightning, and Bitcoin.
6
6
 
7
7
  ## Overview
8
8
 
9
- The `BoltzSwapProvider` library extends Arkade's functionality by enabling:
9
+ The library enables four swap types:
10
10
 
11
- 1. **Lightning to Arkade swaps** - Receive funds from Lightning payments into your Arkade wallet
12
- 2. **Arkade to Lightning swaps** - Send funds from your Arkade wallet to Lightning invoices
11
+ 1. **Lightning to Arkade** - Receive funds from Lightning payments into your Arkade wallet
12
+ 2. **Arkade to Lightning** - Send funds from your Arkade wallet to Lightning invoices
13
+ 3. **ARK to BTC** - Move funds from Arkade to a Bitcoin on-chain address
14
+ 4. **BTC to ARK** - Move funds from Bitcoin on-chain into your Arkade wallet
13
15
 
14
- This integration is built on top of the Boltz submarine swap protocol, providing a reliable and secure way to bridge the gap between Arkade and the Lightning Network.
16
+ Built on top of the Boltz swap protocol with automatic background monitoring via SwapManager.
15
17
 
16
18
  ## Installation
17
19
 
@@ -21,11 +23,11 @@ npm install @arkade-os/sdk @arkade-os/boltz-swap
21
23
 
22
24
  ## Basic Usage
23
25
 
24
- ### Initializing the Lightning Swap Provider
26
+ ### Initializing
25
27
 
26
28
  ```typescript
27
29
  import { Wallet, SingleKey } from '@arkade-os/sdk';
28
- import { ArkadeLightning, BoltzSwapProvider } from '@arkade-os/boltz-swap';
30
+ import { ArkadeSwaps, BoltzSwapProvider } from '@arkade-os/boltz-swap';
29
31
 
30
32
  // Create an identity
31
33
  const identity = SingleKey.fromHex('your_private_key_in_hex');
@@ -36,53 +38,59 @@ const wallet = await Wallet.create({
36
38
  arkServerUrl: 'https://mutinynet.arkade.sh',
37
39
  });
38
40
 
39
- // Initialize the Lightning swap provider
41
+ // Initialize the swap provider
40
42
  const swapProvider = new BoltzSwapProvider({
41
43
  apiUrl: 'https://api.boltz.mutinynet.arkade.sh',
42
44
  network: 'mutinynet',
43
45
  referralId: 'arkade', // optional
44
46
  });
45
47
 
46
- // Create the ArkadeLightning instance
47
- const arkadeLightning = new ArkadeLightning({
48
+ // Create the ArkadeSwaps instance
49
+ const swaps = new ArkadeSwaps({
48
50
  wallet,
49
51
  swapProvider,
50
- // Optional: enable SwapManager with defaults
52
+ // Optional: enable SwapManager for background monitoring
51
53
  // swapManager: true,
52
54
  });
53
55
  ```
54
56
 
55
- ### ServiceWorkerWallet with IndexDB
57
+ **SwapRepository**: Swap storage is pluggable via `SwapRepository`. By default, `ArkadeSwaps` uses an IndexedDB-backed repository in browser contexts. You can inject your own repository (for tests, Node.js, or custom storage) via the `swapRepository` option. Custom implementations must set `readonly version = 1` to match the interface — TypeScript will error when the version is bumped, signaling a required update.
56
58
 
57
- ```typescript
58
- import { ServiceWorkerWallet, SingleKey, RestArkProvider, RestIndexerProvider } from '@arkade-os/sdk';
59
- import { IndexedDBStorageAdapter } from '@arkade-os/sdk/storage';
60
-
61
- // Create your identity
62
- const identity = SingleKey.fromHex('your_private_key_hex');
63
- // Or generate a new one:
64
- // const identity = SingleKey.fromRandomBytes();
65
-
66
- // Configure IndexedDB storage adapter for ServiceWorker
67
- const storage = new IndexedDBStorageAdapter('arkade-service-worker-wallet', 1);
59
+ Platform-specific repositories are available as subpath exports:
68
60
 
69
- const wallet = await ServiceWorkerWallet.setup({
70
- serviceWorkerPath: '/service-worker.js',
71
- arkServerUrl: 'https://mutinynet.arkade.sh',
72
- identity,
73
- storage, // Pass the IndexedDB storage adapter
74
- });
61
+ ```typescript
62
+ // SQLite (React Native / Node.js)
63
+ import { SQLiteSwapRepository } from '@arkade-os/boltz-swap/repositories/sqlite';
75
64
 
76
- // Must provide external providers for ServiceWorkerWallet (it doesn't have them)
77
- const arkadeLightning = new ArkadeLightning({
78
- wallet: serviceWorkerWallet,
79
- arkProvider: new RestArkProvider('https://mutinynet.arkade.sh'),
80
- indexerProvider: new RestIndexerProvider('https://mutinynet.arkade.sh'),
81
- swapProvider,
82
- });
65
+ // Realm (React Native)
66
+ import { RealmSwapRepository, BoltzRealmSchemas } from '@arkade-os/boltz-swap/repositories/realm';
83
67
  ```
84
68
 
85
- **Storage Adapters**: The Arkade SDK provides various storage adapters for different environments. For ServiceWorker environments, use `IndexedDBStorageAdapter`. For more storage options and adapters, see the [Arkade SDK storage adapters documentation](https://github.com/arkade-os/ts-sdk).
69
+ > [!WARNING]
70
+ > If you previously used the v1 `StorageAdapter`-based repositories, migrate
71
+ > data into the new IndexedDB repositories before use. You can use
72
+ > `getMigrationStatus` from `@arkade-os/sdk` to check whether migration is
73
+ > needed before running it:
74
+ >
75
+ > ```typescript
76
+ > import {
77
+ > IndexedDbSwapRepository,
78
+ > migrateToSwapRepository
79
+ > } from '@arkade-os/boltz-swap'
80
+ > import { getMigrationStatus } from '@arkade-os/sdk'
81
+ > import { IndexedDBStorageAdapter } from '@arkade-os/sdk/adapters/indexedDB'
82
+ >
83
+ > // if you used a different name for the DB, use your own here
84
+ > const oldStorage = new IndexedDBStorageAdapter('arkade-service-worker', 1)
85
+ >
86
+ > const status = await getMigrationStatus('wallet', oldStorage)
87
+ > if (status !== 'not-needed') {
88
+ > await migrateToSwapRepository(oldStorage, new IndexedDbSwapRepository())
89
+ > }
90
+ > ```
91
+
92
+ Existing data stays in the old DB (e.g. `arkade-service-worker`) until you run the migration once.
93
+ After `migrateToSwapRepository`, the IndexedDB-backed SwapRepository is used going forward.
86
94
 
87
95
  ## Background Swap Monitoring (SwapManager)
88
96
 
@@ -92,14 +100,14 @@ By default, you must manually monitor each swap and act on their state. **SwapMa
92
100
 
93
101
  ```typescript
94
102
  // Option 1: Enable with defaults
95
- const arkadeLightning = new ArkadeLightning({
103
+ const swaps = new ArkadeSwaps({
96
104
  wallet,
97
105
  swapProvider,
98
106
  swapManager: true, // Simple boolean to enable with defaults
99
107
  });
100
108
 
101
109
  // Option 2: Enable with custom config
102
- const arkadeLightning = new ArkadeLightning({
110
+ const swaps = new ArkadeSwaps({
103
111
  wallet,
104
112
  swapProvider,
105
113
  swapManager: {
@@ -116,14 +124,11 @@ const arkadeLightning = new ArkadeLightning({
116
124
  },
117
125
  });
118
126
 
119
- // If autostart is false, manually start monitoring
120
- // (autostart is true by default, so this is only needed if you set it to false)
121
- if (config.swapManager?.autostart === false) {
122
- await arkadeLightning.startSwapManager();
123
- }
127
+ // If autoStart is false, manually start monitoring
128
+ await swaps.startSwapManager();
124
129
 
125
130
  // Create swaps - they're automatically monitored!
126
- const invoice = await arkadeLightning.createLightningInvoice({ amount: 50000 });
131
+ const invoice = await swaps.createLightningInvoice({ amount: 50000 });
127
132
  // User can navigate to other pages - swap completes in background
128
133
  ```
129
134
 
@@ -134,8 +139,9 @@ const invoice = await arkadeLightning.createLightningInvoice({ amount: 50000 });
134
139
  - **Fallback polling** with exponential backoff if WebSocket fails
135
140
  - **Auto-claim/refund** executes when status allows
136
141
  - **Resumes on app reopen** - loads pending swaps, polls latest status, executes refunds if expired
137
- - **⚠️ Requires app running** - stops when app closes (service worker support planned)
138
- - If swaps expire while app is closed, refunds execute automatically on next app launch
142
+ - **Default `ArkadeSwaps` requires the app running** - monitoring stops when the app/tab closes
143
+ - For browser background monitoring, use `ServiceWorkerArkadeLightning`
144
+ - If swaps expire while closed, refunds execute automatically on next app launch (unless claimed/refunded by your background runtime)
139
145
 
140
146
  ### Configuration Options
141
147
 
@@ -159,7 +165,7 @@ const invoice = await arkadeLightning.createLightningInvoice({ amount: 50000 });
159
165
  onSwapUpdate: (swap, oldStatus) => {},
160
166
  onSwapCompleted: (swap) => {},
161
167
  onSwapFailed: (swap, error) => {},
162
- onActionExecuted: (swap, action) => {}, // 'claim' or 'refund'
168
+ onActionExecuted: (swap, action) => {}, // 'claim', 'refund', 'claimArk', 'claimBtc', 'refundArk', 'signServerClaim'
163
169
  onWebSocketConnected: () => {},
164
170
  onWebSocketDisconnected: (error?) => {},
165
171
  }
@@ -168,84 +174,67 @@ const invoice = await arkadeLightning.createLightningInvoice({ amount: 50000 });
168
174
 
169
175
  ### Event Subscription
170
176
 
171
- SwapManager supports flexible event subscription - you can add/remove listeners dynamically instead of just providing them in config:
177
+ SwapManager supports flexible event subscription - you can add/remove listeners dynamically:
172
178
 
173
179
  ```typescript
174
- const arkadeLightning = new ArkadeLightning({
180
+ const swaps = new ArkadeSwaps({
175
181
  wallet,
176
182
  swapProvider,
177
183
  swapManager: true,
178
184
  });
179
185
 
180
- const manager = arkadeLightning.getSwapManager();
186
+ const manager = swaps.getSwapManager();
181
187
 
182
188
  // Subscribe to events using on* methods (returns unsubscribe function)
183
189
  const unsubscribe = manager.onSwapUpdate((swap, oldStatus) => {
184
190
  console.log(`Swap ${swap.id}: ${oldStatus} → ${swap.status}`);
185
- // Update UI based on swap status
186
191
  });
187
192
 
188
193
  // Subscribe to completed events
189
194
  manager.onSwapCompleted((swap) => {
190
195
  console.log(`Swap ${swap.id} completed!`);
191
- showNotification(`Payment completed!`);
192
196
  });
193
197
 
194
198
  // Subscribe to failures
195
199
  manager.onSwapFailed((swap, error) => {
196
200
  console.error(`Swap ${swap.id} failed:`, error);
197
- showErrorDialog(error.message);
198
201
  });
199
202
 
200
- // Subscribe to actions (claim/refund)
203
+ // Subscribe to actions (claim/refund/claimArk/claimBtc/refundArk/signServerClaim)
201
204
  manager.onActionExecuted((swap, action) => {
202
205
  console.log(`Executed ${action} for swap ${swap.id}`);
203
206
  });
204
207
 
205
208
  // WebSocket events
206
- manager.onWebSocketConnected(() => {
207
- console.log('Connected to swap updates');
208
- });
209
-
210
- manager.onWebSocketDisconnected((error) => {
211
- console.log('Disconnected from swap updates', error);
212
- });
209
+ manager.onWebSocketConnected(() => console.log('Connected'));
210
+ manager.onWebSocketDisconnected((error) => console.log('Disconnected', error));
213
211
 
214
212
  // Unsubscribe when no longer needed (e.g., component unmount)
215
- // unsubscribe();
213
+ unsubscribe();
216
214
 
217
- // Or use off* methods to remove specific listeners
218
- const listener = (swap) => console.log('completed', swap.id);
219
- manager.onSwapCompleted(listener);
220
- // Later...
221
- manager.offSwapCompleted(listener);
215
+ // Or use off* methods to remove a specific listener
216
+ manager.offSwapUpdate(myListener);
222
217
  ```
223
218
 
224
- **Benefits of dynamic event subscription:**
225
- - Add/remove listeners at any time
226
- - Multiple listeners per event type
227
- - Easy cleanup when components unmount
228
- - No need to restart SwapManager to change handlers
229
-
230
219
  ### Cleanup (Disposable Pattern)
231
220
 
232
- ArkadeLightning implements the Disposable pattern for automatic cleanup:
221
+ ArkadeSwaps implements the Disposable pattern for automatic cleanup:
233
222
 
234
223
  ```typescript
235
224
  // Option 1: Manual cleanup
236
- const arkadeLightning = new ArkadeLightning({ wallet, swapProvider });
225
+ const swaps = new ArkadeSwaps({ wallet, swapProvider });
237
226
  // ... use it
238
- await arkadeLightning.dispose(); // Stops SwapManager and cleans up
227
+ await swaps.dispose(); // Stops SwapManager and cleans up
239
228
 
240
229
  // Option 2: Automatic cleanup with `await using` (TypeScript 5.2+)
241
230
  {
242
- await using arkadeLightning = new ArkadeLightning({
231
+ await using swaps = new ArkadeSwaps({
243
232
  wallet,
244
233
  swapProvider,
245
234
  swapManager: { autoStart: true },
246
235
  });
247
236
 
248
- // Use arkadeLightning...
237
+ // Use swaps...
249
238
 
250
239
  } // SwapManager automatically stopped when scope exits
251
240
  ```
@@ -254,11 +243,11 @@ await arkadeLightning.dispose(); // Stops SwapManager and cleans up
254
243
 
255
244
  ```typescript
256
245
  // Stop background monitoring
257
- await arkadeLightning.stopSwapManager();
246
+ await swaps.stopSwapManager();
258
247
 
259
248
  // Check manager stats
260
- const manager = arkadeLightning.getSwapManager();
261
- const stats = manager?.getStats();
249
+ const manager = swaps.getSwapManager();
250
+ const stats = await manager?.getStats();
262
251
  console.log(`Monitoring ${stats.monitoredSwaps} swaps`);
263
252
  console.log(`WebSocket connected: ${stats.websocketConnected}`);
264
253
  ```
@@ -268,15 +257,14 @@ console.log(`WebSocket connected: ${stats.websocketConnected}`);
268
257
  When SwapManager is enabled, you can subscribe to updates for specific swaps to show progress in your UI:
269
258
 
270
259
  ```typescript
271
- const result = await arkadeLightning.createLightningInvoice({ amount: 50000 });
260
+ const result = await swaps.createLightningInvoice({ amount: 50000 });
272
261
 
273
262
  // Subscribe to this specific swap's updates
274
- const manager = arkadeLightning.getSwapManager();
263
+ const manager = swaps.getSwapManager();
275
264
  const unsubscribe = manager.subscribeToSwapUpdates(
276
265
  result.pendingSwap.id,
277
266
  (swap, oldStatus) => {
278
267
  console.log(`Swap ${swap.id}: ${oldStatus} → ${swap.status}`);
279
- // Update UI based on status
280
268
  if (swap.status === 'transaction.mempool') {
281
269
  showNotification('Payment detected in mempool!');
282
270
  } else if (swap.status === 'invoice.settled') {
@@ -294,20 +282,15 @@ const unsubscribe = manager.subscribeToSwapUpdates(
294
282
  Even with SwapManager enabled, you can still wait for specific swaps to complete:
295
283
 
296
284
  ```typescript
297
- const result = await arkadeLightning.createLightningInvoice({ amount: 50000 });
285
+ const result = await swaps.createLightningInvoice({ amount: 50000 });
298
286
 
299
287
  // This blocks until the swap completes, but SwapManager handles the monitoring
300
288
  try {
301
- const { txid } = await arkadeLightning.waitAndClaim(result.pendingSwap);
289
+ const { txid } = await swaps.waitAndClaim(result.pendingSwap);
302
290
  console.log('Payment claimed successfully:', txid);
303
291
  } catch (error) {
304
292
  console.error('Payment failed:', error);
305
293
  }
306
-
307
- // Benefits of delegating to SwapManager:
308
- // No race conditions - manager coordinates with manual calls
309
- // User can navigate away - swap completes in background
310
- // Automatic refund on failure - no manual error handling needed
311
294
  ```
312
295
 
313
296
  ### Without SwapManager (Manual Mode)
@@ -316,123 +299,123 @@ If SwapManager is not enabled, you must manually monitor swaps:
316
299
 
317
300
  ```typescript
318
301
  // Create invoice
319
- const result = await arkadeLightning.createLightningInvoice({ amount: 50000 });
302
+ const result = await swaps.createLightningInvoice({ amount: 50000 });
320
303
 
321
304
  // MUST manually monitor - blocks until complete
322
- await arkadeLightning.waitAndClaim(result.pendingSwap);
305
+ await swaps.waitAndClaim(result.pendingSwap);
323
306
  // User must stay on this page - navigating away stops monitoring
324
307
  ```
325
308
 
326
- ## Checking Swap Limits
327
-
328
- Before creating Lightning invoices or sending payments, you can check the minimum and maximum swap amounts supported by the Boltz service. This is useful to validate that your invoice amount is within the acceptable range.
309
+ ## Expo / React Native
329
310
 
330
- ```typescript
331
- // Get current swap limits (in satoshis)
332
- const limits = await arkadeLightning.getLimits();
311
+ Expo/React Native cannot run a long-lived Service Worker, and background work is executed by the OS for a short window (typically every ~15+ minutes). To enable best-effort background claim/refund for swaps, use `ExpoArkadeLightning` plus a background task defined at global scope.
333
312
 
334
- if (limits) {
335
- console.log('Minimum swap amount:', limits.min, 'sats');
336
- console.log('Maximum swap amount:', limits.max, 'sats');
313
+ ### Prerequisites
337
314
 
338
- // Example: Validate invoice amount before creating
339
- const invoiceAmount = 50000; // 50,000 sats
315
+ - Install Expo background task dependencies:
340
316
 
341
- if (invoiceAmount < limits.min) {
342
- console.error(`Amount ${invoiceAmount} is below minimum ${limits.min} sats`);
343
- } else if (invoiceAmount > limits.max) {
344
- console.error(`Amount ${invoiceAmount} is above maximum ${limits.max} sats`);
345
- } else {
346
- console.log('Amount is within valid range');
347
- // Safe to proceed with creating invoice or payment
348
- }
349
- } else {
350
- console.log('Unable to fetch limits - no swap provider configured');
351
- }
317
+ ```bash
318
+ npx expo install expo-task-manager expo-background-task
319
+ npx expo install @react-native-async-storage/async-storage expo-secure-store
320
+ npx expo install expo-crypto
321
+ npx expo install expo-sqlite && npm install indexeddbshim
352
322
  ```
353
323
 
354
- ### Validating Lightning Invoice Amounts
355
-
356
- ```typescript
357
- import { decodeInvoice } from '@arkade-os/boltz-swap';
324
+ - If you rely on the default IndexedDB-backed repositories in Expo, call `setupExpoDb()` **before any SDK/boltz-swap import**:
358
325
 
359
- // Decode an incoming Lightning invoice to check its amount
360
- const invoice = 'lnbc500u1pj...'; // Lightning invoice string
361
- const decodedInvoice = decodeInvoice(invoice);
326
+ ```ts
327
+ import { setupExpoDb } from "@arkade-os/sdk/adapters/expo-db";
362
328
 
363
- console.log('Invoice amount:', decodedInvoice.amountSats, 'sats');
364
-
365
- // Check if the invoice amount is within swap limits
366
- const limits = await arkadeLightning.getLimits();
367
-
368
- if (limits && decodedInvoice.amountSats >= limits.min && decodedInvoice.amountSats <= limits.max) {
369
- // Amount is valid for swaps
370
- const paymentResult = await arkadeLightning.sendLightningPayment({
371
- invoice: invoice,
372
- });
373
- console.log('Payment successful!');
374
- } else {
375
- console.error('Invoice amount is outside supported swap limits');
376
- }
329
+ setupExpoDb();
377
330
  ```
378
331
 
379
- ## Checking Swap Fees
332
+ - Expo requires a `crypto.getRandomValues()` polyfill for cryptographic operations:
380
333
 
381
- You can check the fee to pay for different swap amounts supported by the Boltz service.
382
- This is useful to validate the user is willing to pay the fees.
383
-
384
- ```typescript
385
- // Get current swap fees
386
- const fees: FeesResponse | null = await arkadeLightning.getFees();
387
- if (!fees) throw new Error('something went wrong');
388
-
389
- const calcSubmarineSwapFee = (satoshis: number): number => {
390
- if (!satoshis) return 0;
391
- const { percentage, minerFees } = fees.submarine;
392
- return Math.ceil((satoshis * percentage) / 100 + minerFees);
393
- };
394
-
395
- const calcReverseSwapFee = (satoshis: number): number => {
396
- if (!satoshis) return 0;
397
- const { percentage, minerFees } = fees.reverse;
398
- return Math.ceil((satoshis * percentage) / 100 + minerFees.claim + minerFees.lockup);
399
- };
334
+ ```ts
335
+ import * as Crypto from "expo-crypto";
336
+ if (!global.crypto) global.crypto = {} as any;
337
+ global.crypto.getRandomValues = Crypto.getRandomValues;
400
338
  ```
401
339
 
402
- ## Checking Swap Status
403
-
404
- **With SwapManager:** Status updates are automatic via events - no manual checking needed.
405
-
406
- **Without SwapManager (manual mode):**
407
- ```typescript
408
- const response = await arkadeLightning.getSwapStatus('swap_id');
409
- console.log('swap status = ', response.status);
340
+ ### 1) Define the background task (global scope)
341
+
342
+ `TaskManager.defineTask()` must be called at module scope before React mounts.
343
+
344
+ ```ts
345
+ // App entry point (e.g., _layout.tsx) — GLOBAL SCOPE
346
+ import AsyncStorage from "@react-native-async-storage/async-storage";
347
+ import * as SecureStore from "expo-secure-store";
348
+ import { SingleKey } from "@arkade-os/sdk";
349
+ import { AsyncStorageTaskQueue } from "@arkade-os/sdk/worker/expo";
350
+ import { IndexedDbSwapRepository } from "@arkade-os/boltz-swap";
351
+ import { defineExpoSwapBackgroundTask } from "@arkade-os/boltz-swap/expo";
352
+
353
+ const swapTaskQueue = new AsyncStorageTaskQueue(AsyncStorage, "ark:swap-queue");
354
+ const swapRepository = new IndexedDbSwapRepository();
355
+
356
+ defineExpoSwapBackgroundTask("ark-swap-poll", {
357
+ taskQueue: swapTaskQueue,
358
+ swapRepository,
359
+ identityFactory: async () => {
360
+ const key = await SecureStore.getItemAsync("ark-private-key");
361
+ if (!key) throw new Error("Missing private key in SecureStore");
362
+ return SingleKey.fromHex(key);
363
+ },
364
+ });
410
365
  ```
411
366
 
412
- ## Storage
413
-
414
- This library automatically stores pending swaps using the wallet's built-in contract repository. All swap data is persisted automatically and can be retrieved using the following methods:
367
+ ### 2) Set up `ExpoArkadeLightning` (component/provider)
368
+
369
+ Use an `IWallet` implementation that provides `arkProvider` and `indexerProvider` (for example `ExpoWallet` from `@arkade-os/sdk/wallet/expo`, or `Wallet.create()` with `ExpoArkProvider` / `ExpoIndexerProvider`).
370
+
371
+ ```ts
372
+ import AsyncStorage from "@react-native-async-storage/async-storage";
373
+ import { ExpoWallet } from "@arkade-os/sdk/wallet/expo";
374
+ import { AsyncStorageTaskQueue } from "@arkade-os/sdk/worker/expo";
375
+ import { BoltzSwapProvider } from "@arkade-os/boltz-swap";
376
+ import { ExpoArkadeLightning } from "@arkade-os/boltz-swap/expo";
377
+
378
+ // Used by ExpoWallet's background task (defined via @arkade-os/sdk/wallet/expo)
379
+ const walletTaskQueue = new AsyncStorageTaskQueue(AsyncStorage, "ark:wallet-queue");
380
+
381
+ const wallet = await ExpoWallet.setup({
382
+ identity, // same identity used by identityFactory()
383
+ arkServerUrl: "https://mutinynet.arkade.sh",
384
+ storage: { walletRepository, contractRepository },
385
+ background: {
386
+ taskName: "ark-wallet-poll",
387
+ taskQueue: walletTaskQueue,
388
+ foregroundIntervalMs: 20_000,
389
+ minimumBackgroundInterval: 15,
390
+ },
391
+ });
415
392
 
416
- ```typescript
417
- // Get all pending submarine swaps (those waiting for Lightning payment)
418
- const pendingPaymentsToLightning = await arkadeLightning.getPendingSubmarineSwaps();
393
+ const swapProvider = new BoltzSwapProvider({
394
+ apiUrl: "https://api.boltz.mutinynet.arkade.sh",
395
+ network: "mutinynet",
396
+ });
419
397
 
420
- // Get all pending reverse swaps (those waiting for claim)
421
- const pendingPaymentsFromLightning = await arkadeLightning.getPendingReverseSwaps();
398
+ const arkLn = await ExpoArkadeLightning.setup({
399
+ wallet,
400
+ swapProvider,
401
+ swapRepository, // must match the one used in defineExpoSwapBackgroundTask
402
+ background: {
403
+ taskName: "ark-swap-poll",
404
+ taskQueue: swapTaskQueue, // must match the one used in defineExpoSwapBackgroundTask
405
+ foregroundIntervalMs: 20_000,
406
+ minimumBackgroundInterval: 15,
407
+ },
408
+ });
422
409
 
423
- // Get complete swap history (both completed and pending)
424
- const swapHistory = await arkadeLightning.getSwapHistory();
410
+ await arkLn.createLightningInvoice({ amount: 1000 });
425
411
  ```
426
412
 
427
- **Note**: All swap data is automatically persisted and retrieved through the wallet's contract repository. No additional storage configuration is required.
428
-
429
413
  ## Receiving Lightning Payments
430
414
 
431
415
  To receive a Lightning payment into your Arkade wallet:
432
416
 
433
417
  ```typescript
434
- // Create a Lightning invoice that will deposit funds to your Arkade wallet
435
- const result = await arkadeLightning.createLightningInvoice({
418
+ const result = await swaps.createLightningInvoice({
436
419
  amount: 50000, // 50,000 sats
437
420
  description: 'Payment to my Arkade wallet',
438
421
  });
@@ -443,9 +426,6 @@ console.log('Lightning Invoice:', result.invoice);
443
426
  console.log('Payment Hash:', result.paymentHash);
444
427
  console.log('Pending swap', result.pendingSwap);
445
428
  console.log('Preimage', result.preimage);
446
-
447
- // The invoice can now be shared with the payer
448
- // When paid, funds will appear in your Arkade wallet
449
429
  ```
450
430
 
451
431
  ### Monitoring Incoming Lightning Payments
@@ -453,18 +433,15 @@ console.log('Preimage', result.preimage);
453
433
  **With SwapManager (recommended):**
454
434
  ```typescript
455
435
  // SwapManager handles monitoring and claiming automatically
456
- // Just listen to events for UI updates
457
- const result = await arkadeLightning.createLightningInvoice({ amount: 50000 });
458
- // Payment will be claimed automatically when received
436
+ const result = await swaps.createLightningInvoice({ amount: 50000 });
437
+ // Payment will be claimed automatically when received
459
438
  ```
460
439
 
461
440
  **Without SwapManager (manual mode):**
462
441
  ```typescript
463
442
  // You must manually monitor - blocks until payment is received
464
- const receivalResult = await arkadeLightning.waitAndClaim(result.pendingSwap);
465
- console.log('Receival successful!');
443
+ const receivalResult = await swaps.waitAndClaim(result.pendingSwap);
466
444
  console.log('Transaction ID:', receivalResult.txid);
467
- // ⚠️ User must stay on this page - navigating away stops monitoring
468
445
  ```
469
446
 
470
447
  ## Sending Lightning Payments
@@ -478,7 +455,7 @@ const invoiceDetails = decodeInvoice('lnbc500u1pj...');
478
455
  console.log('Invoice amount:', invoiceDetails.amountSats, 'sats');
479
456
 
480
457
  // Send payment - returns immediately after creating swap
481
- const paymentResult = await arkadeLightning.sendLightningPayment({
458
+ const paymentResult = await swaps.sendLightningPayment({
482
459
  invoice: 'lnbc500u1pj...',
483
460
  });
484
461
 
@@ -488,18 +465,172 @@ console.log('Payment initiated:', paymentResult.txid);
488
465
 
489
466
  **Without SwapManager (manual mode):**
490
467
  ```typescript
491
- // Blocks until payment completes or fails
492
- const paymentResult = await arkadeLightning.sendLightningPayment({
468
+ const paymentResult = await swaps.sendLightningPayment({
493
469
  invoice: 'lnbc500u1pj...',
494
470
  });
495
471
 
496
- console.log('Payment successful!');
497
472
  console.log('Amount:', paymentResult.amount);
498
473
  console.log('Preimage:', paymentResult.preimage);
499
474
  console.log('Transaction ID:', paymentResult.txid);
500
- // ⚠️ If payment fails, you must manually handle refund (see Error Handling)
501
475
  ```
502
476
 
477
+ ## Chain Swaps
478
+
479
+ Chain swaps move funds between Arkade and Bitcoin on-chain via Boltz.
480
+
481
+ #### Amounts
482
+
483
+ When creating a swap, and because there are fees to be paid, you must define one and only one type of amount:
484
+ - senderLockAmount: sender will send this exact amount, receiver will receive less (amount - fees)
485
+ - receiverLockAmount: receiver will receive this exact amount, sender needs to send more (amount + fees)
486
+
487
+ ### ARK to BTC
488
+
489
+ Send funds from your Arkade wallet to a Bitcoin address:
490
+
491
+ ```typescript
492
+ // Create the swap
493
+ const result = await swaps.arkToBtc({
494
+ btcAddress: 'bc1q...',
495
+ senderLockAmount: 100000,
496
+ feeSatsPerByte: 2, // optional, defaults to 1
497
+ });
498
+
499
+ console.log('Pay to ARK address:', result.arkAddress);
500
+ console.log('Amount to pay:', result.amountToPay, 'sats');
501
+
502
+ // Wait for BTC to be claimed
503
+ // If you use swapManager, this step is not needed
504
+ const { txid } = await swaps.waitAndClaimBtc(result.pendingSwap);
505
+ console.log('BTC claimed:', txid);
506
+ ```
507
+
508
+ If the swap fails, refund your ARK funds:
509
+
510
+ ```typescript
511
+ await swaps.refundArk(result.pendingSwap);
512
+ ```
513
+
514
+ ### BTC to ARK
515
+
516
+ Receive funds from Bitcoin into your Arkade wallet:
517
+
518
+ ```typescript
519
+ // Create the swap
520
+ const result = await swaps.btcToArk({
521
+ receiverLockAmount: 100000,
522
+ feeSatsPerByte: 2, // optional, defaults to 1
523
+ });
524
+
525
+ console.log('Pay to BTC address:', result.btcAddress);
526
+ console.log('Amount to pay:', result.amountToPay, 'sats');
527
+
528
+ // Wait for ARK to be claimed
529
+ // If you use swapManager, this step is not needed
530
+ const { txid } = await swaps.waitAndClaimArk(result.pendingSwap);
531
+ console.log('ARK claimed:', txid);
532
+ ```
533
+
534
+ ### Chain Swap Fees and Limits
535
+
536
+ ```typescript
537
+ // Get chain swap fees
538
+ const chainFees = await swaps.getFees('ARK', 'BTC');
539
+ console.log('Percentage:', chainFees.percentage);
540
+ console.log('Server miner fee:', chainFees.minerFees.server);
541
+ console.log('User claim fee:', chainFees.minerFees.user.claim);
542
+ console.log('User lockup fee:', chainFees.minerFees.user.lockup);
543
+
544
+ // Get chain swap limits
545
+ const chainLimits = await swaps.getLimits('ARK', 'BTC');
546
+ console.log('Min:', chainLimits.min, 'sats');
547
+ console.log('Max:', chainLimits.max, 'sats');
548
+ ```
549
+
550
+ ### Renegotiating Quotes
551
+
552
+ If the amount sent to the swap is different from the expected, renegotiate it:
553
+
554
+ ```typescript
555
+ const newAmount = await swaps.quoteSwap(pendingSwap.id);
556
+ console.log('Updated amount:', newAmount);
557
+ ```
558
+
559
+ ## Checking Swap Limits
560
+
561
+ Before creating swaps, check the supported amount range:
562
+
563
+ ```typescript
564
+ // Lightning swap limits
565
+ const limits = await swaps.getLimits();
566
+ console.log('Min:', limits.min, 'sats');
567
+ console.log('Max:', limits.max, 'sats');
568
+
569
+ // Chain swap limits
570
+ const chainLimits = await swaps.getLimits('ARK', 'BTC');
571
+ ```
572
+
573
+ ### Validating Lightning Invoice Amounts
574
+
575
+ ```typescript
576
+ import { decodeInvoice } from '@arkade-os/boltz-swap';
577
+
578
+ const invoice = 'lnbc500u1pj...';
579
+ const decoded = decodeInvoice(invoice);
580
+ console.log('Invoice amount:', decoded.amountSats, 'sats');
581
+
582
+ const limits = await swaps.getLimits();
583
+ if (decoded.amountSats >= limits.min && decoded.amountSats <= limits.max) {
584
+ await swaps.sendLightningPayment({ invoice });
585
+ }
586
+ ```
587
+
588
+ ## Checking Swap Fees
589
+
590
+ ```typescript
591
+ // Lightning fees
592
+ const fees = await swaps.getFees();
593
+
594
+ const calcSubmarineSwapFee = (satoshis: number): number => {
595
+ const { percentage, minerFees } = fees.submarine;
596
+ return Math.ceil((satoshis * percentage) / 100 + minerFees);
597
+ };
598
+
599
+ const calcReverseSwapFee = (satoshis: number): number => {
600
+ const { percentage, minerFees } = fees.reverse;
601
+ return Math.ceil((satoshis * percentage) / 100 + minerFees.claim + minerFees.lockup);
602
+ };
603
+
604
+ // Chain fees
605
+ const chainFees = await swaps.getFees('ARK', 'BTC');
606
+ ```
607
+
608
+ ## Checking Swap Status
609
+
610
+ **With SwapManager:** Status updates are automatic via events - no manual checking needed.
611
+
612
+ **Without SwapManager (manual mode):**
613
+ ```typescript
614
+ const response = await swaps.getSwapStatus('swap_id');
615
+ console.log('swap status = ', response.status);
616
+ ```
617
+
618
+ ## Storage
619
+
620
+ All swap data is persisted automatically and can be retrieved:
621
+
622
+ ```typescript
623
+ // Get pending swaps by type
624
+ const pendingSubmarineSwaps = await swaps.getPendingSubmarineSwaps();
625
+ const pendingReverseSwaps = await swaps.getPendingReverseSwaps();
626
+ const pendingChainSwaps = await swaps.getPendingChainSwaps();
627
+
628
+ // Get complete swap history (all types, sorted by creation date)
629
+ const swapHistory = await swaps.getSwapHistory();
630
+ ```
631
+
632
+ **Note**: If IndexedDB is not available (e.g., in Node.js), provide a custom `swapRepository` implementation.
633
+
503
634
  ## Error Handling
504
635
 
505
636
  **With SwapManager:** Refunds are handled automatically - listen to `onSwapFailed` event for notifications.
@@ -516,35 +647,82 @@ import {
516
647
  InvoiceFailedToPayError,
517
648
  InsufficientFundsError,
518
649
  TransactionFailedError,
650
+ isPendingSubmarineSwap,
651
+ isPendingChainSwap,
519
652
  } from '@arkade-os/boltz-swap';
520
653
 
521
654
  try {
522
- await arkadeLightning.sendLightningPayment({
655
+ await swaps.sendLightningPayment({
523
656
  invoice: 'lnbc500u1pj...',
524
657
  });
525
658
  } catch (error) {
526
659
  if (error instanceof InvoiceExpiredError) {
527
- console.error('The invoice has expired. Please request a new one.');
660
+ console.error('The invoice has expired.');
528
661
  } else if (error instanceof InvoiceFailedToPayError) {
529
662
  console.error('The provider failed to pay the invoice.');
530
663
  } else if (error instanceof InsufficientFundsError) {
531
664
  console.error('Not enough funds available:', error.message);
532
665
  } else if (error instanceof NetworkError) {
533
- console.error('Network issue. Please try again later:', error.message);
666
+ console.error('Network issue:', error.message);
534
667
  } else if (error instanceof SchemaError) {
535
- console.error('Invalid response from API. Please try again later.');
668
+ console.error('Invalid response from API.');
536
669
  } else if (error instanceof SwapExpiredError) {
537
670
  console.error('The swap has expired.');
538
671
  } else if (error instanceof TransactionFailedError) {
539
- console.error('Transaction failed. Please try again later');
672
+ console.error('Transaction failed.');
540
673
  } else {
541
674
  console.error('Unknown error:', error);
542
675
  }
543
676
 
544
677
  // Manual refund (only needed without SwapManager)
545
678
  if (error.isRefundable && error.pendingSwap) {
546
- const refundResult = await arkadeLightning.refundVHTLC(error.pendingSwap);
547
- console.log('Refund claimed:', refundResult.txid);
679
+ if (isPendingChainSwap(error.pendingSwap)) {
680
+ await swaps.refundArk(error.pendingSwap);
681
+ } else if (isPendingSubmarineSwap(error.pendingSwap)) {
682
+ await swaps.refundVHTLC(error.pendingSwap);
683
+ }
548
684
  }
549
685
  }
550
686
  ```
687
+
688
+ ## Type Guards
689
+
690
+ ```typescript
691
+ import {
692
+ isPendingReverseSwap,
693
+ isPendingSubmarineSwap,
694
+ isPendingChainSwap,
695
+ isSubmarineSwapRefundable,
696
+ isChainSwapClaimable,
697
+ isChainSwapRefundable,
698
+ isSubmarineFinalStatus,
699
+ isReverseFinalStatus,
700
+ isChainFinalStatus,
701
+ } from '@arkade-os/boltz-swap';
702
+
703
+ // Discriminate swap types
704
+ if (isPendingReverseSwap(swap)) { /* Lightning → Arkade */ }
705
+ if (isPendingSubmarineSwap(swap)) { /* Arkade → Lightning */ }
706
+ if (isPendingChainSwap(swap)) { /* ARK ↔ BTC chain */ }
707
+
708
+ // Check swap state
709
+ if (isChainSwapClaimable(swap)) { /* ready to claim */ }
710
+ if (isChainSwapRefundable(swap)) { /* can be refunded */ }
711
+ ```
712
+
713
+ ### Releasing
714
+
715
+ ```bash
716
+ # Release new version (will prompt for version patch, minor, major)
717
+ pnpm release
718
+
719
+ # You can test release process without making changes
720
+ pnpm release:dry-run
721
+
722
+ # Cleanup: checkout version commit and remove release branch
723
+ pnpm release:cleanup
724
+ ```
725
+
726
+ ## License
727
+
728
+ MIT