npm package: @pedro-7/experiment-2
Description: A production-ready TypeScript package that brings Boltz submarine-swaps to Arkade.
Author: Arkade-OS
Latest version: 0.2.20

# Lightning Swaps

> Integrate Lightning Network with Arkade using Submarine Swaps

Arkade provides seamless integration with the Lightning Network through Boltz submarine swaps, allowing users to move funds between Arkade and Lightning channels.

## Overview

The `BoltzSwapProvider` library extends Arkade's functionality by enabling:

1. **Lightning to Arkade swaps** - Receive funds from Lightning payments into your Arkade wallet
2. **Arkade to Lightning swaps** - Send funds from your Arkade wallet to Lightning invoices

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.

## Installation

```bash
npm install @arkade-os/sdk @arkade-os/boltz-swap
```

## Basic Usage

### Initializing the Lightning Swap Provider

```typescript
import { Wallet, SingleKey } from '@arkade-os/sdk';
import { ArkadeLightning, BoltzSwapProvider } from '@arkade-os/boltz-swap';

// Create an identity
const identity = SingleKey.fromHex('your_private_key_in_hex');

// Initialize your Arkade wallet
const wallet = await Wallet.create({
  identity,
  arkServerUrl: 'https://mutinynet.arkade.sh',
});

// Initialize the Lightning swap provider
const swapProvider = new BoltzSwapProvider({
  apiUrl: 'https://api.boltz.mutinynet.arkade.sh',
  network: 'mutinynet',
  referralId: 'arkade', // optional
});

// Create the ArkadeLightning instance
const arkadeLightning = new ArkadeLightning({
  wallet,
  swapProvider,
  // Optional: enable SwapManager with defaults
  // swapManager: true,
});
```

### ServiceWorkerWallet with IndexedDB

```typescript
import { ServiceWorkerWallet, SingleKey, RestArkProvider, RestIndexerProvider } from '@arkade-os/sdk';
import { IndexedDBStorageAdapter } from '@arkade-os/sdk/adapters/indexedDB';

// Create your identity
const identity = SingleKey.fromHex('your_private_key_hex');
// Or generate a new one:
// const identity = SingleKey.fromRandomBytes();

const wallet = await ServiceWorkerWallet.setup({
  serviceWorkerPath: '/service-worker.js',
  arkServerUrl: 'https://mutinynet.arkade.sh',
  identity,
  // Optional: can use custom repositories, uses IndexedDB by default
});

// Must provide external providers for ServiceWorkerWallet (it doesn't have them)
const arkadeLightning = new ArkadeLightning({
  wallet: serviceWorkerWallet,
  arkProvider: new RestArkProvider('https://mutinynet.arkade.sh'),
  indexerProvider: new RestIndexerProvider('https://mutinynet.arkade.sh'),
  swapProvider,
});
```

**SwapRepository**: Swap storage is pluggable via `SwapRepository`. In ServiceWorker environments, the default is `IndexedDB`. For other options, see the [Arkade SDK Repositories documentation](https://github.com/arkade-os/ts-sdk).

> [!WARNING]
> If you previously used the v1 `StorageAdapter`-based repositories, migrate
> data into the new IndexedDB repositories before use:
>
> ```typescript
> import {
>   IndexedDbSwapRepository,
>   migrateToSwapRepository
> } from '@arkade-os/boltz-swap'
> import { IndexedDBStorageAdapter } from '@arkade-os/sdk/adapters/indexedDB'
>
> // if you used a different name for the DB, use your own here
> const oldStorage = new IndexedDBStorageAdapter('arkade-service-worker', 1)
> await migrateToSwapRepository(oldStorage, new IndexedDbSwapRepository())
> ```

Existing data stays in the old DB (e.g. `arkade-service-worker`) until you run the migration once.
After `migrateToSwapRepository`, the IndexedDB-backed SwapRepository is used going forward.

## Background Swap Monitoring (SwapManager)

By default, you must manually monitor each swap and act on their state. **SwapManager** enables autonomous background processing - swaps complete automatically while the app is running. When the app reopens, it automatically resumes pending swaps.

### Enable SwapManager

```typescript
// Option 1: Enable with defaults
const arkadeLightning = new ArkadeLightning({
  wallet,
  swapProvider,
  swapManager: true, // Simple boolean to enable with defaults
});

// Option 2: Enable with custom config
const arkadeLightning = new ArkadeLightning({
  wallet,
  swapProvider,
  swapManager: {
    autoStart: false, // Set to false to manually call startSwapManager() later
    // Events for UI updates (optional, can also use on/off methods)
    events: {
      onSwapCompleted: (swap) => {
        console.log(`Swap ${swap.id} completed!`);
      },
      onSwapUpdate: (swap, oldStatus) => {
        console.log(`${swap.id}: ${oldStatus} → ${swap.status}`);
      },
    },
  },
});

// If autostart is false, manually start monitoring
// (autostart is true by default, so this is only needed if you set it to false)
if (config.swapManager?.autostart === false) {
  await arkadeLightning.startSwapManager();
}

// Create swaps - they're automatically monitored!
const invoice = await arkadeLightning.createLightningInvoice({ amount: 50000 });
// User can navigate to other pages - swap completes in background
```

### How It Works

- **Single WebSocket** monitors all swaps (not one per swap)
- **Automatic polling** after WebSocket connects/reconnects
- **Fallback polling** with exponential backoff if WebSocket fails
- **Auto-claim/refund** executes when status allows
- **Resumes on app reopen** - loads pending swaps, polls latest status, executes refunds if expired
- **⚠️ Requires app running** - stops when app closes (service worker support planned)
  - If swaps expire while app is closed, refunds execute automatically on next app launch

### Configuration Options

```typescript
  // Simple boolean to enable with defaults
  swapManager: true,

  // OR custom configuration
  swapManager: {
    enableAutoActions: true,        // Auto claim/refund (default: true)
    autoStart: true,                // Auto-start on init (default: true)
    pollInterval: 30000,            // Failsafe poll every 30s when WS active (default)
    reconnectDelayMs: 1000,         // Initial WS reconnect delay (default)
    maxReconnectDelayMs: 60000,     // Max WS reconnect delay (default)
    pollRetryDelayMs: 5000,         // Initial fallback poll delay (default)
    maxPollRetryDelayMs: 300000,    // Max fallback poll delay (default)

    // Optional: provide event listeners in config
    // (can also use on/off methods dynamically - see Event Subscription section)
    events: {
      onSwapUpdate: (swap, oldStatus) => {},
      onSwapCompleted: (swap) => {},
      onSwapFailed: (swap, error) => {},
      onActionExecuted: (swap, action) => {},  // 'claim' or 'refund'
      onWebSocketConnected: () => {},
      onWebSocketDisconnected: (error?) => {},
    }
  }
```

### Event Subscription

SwapManager supports flexible event subscription - you can add/remove listeners dynamically instead of just providing them in config:

```typescript
const arkadeLightning = new ArkadeLightning({
  wallet,
  swapProvider,
  swapManager: true,
});

const manager = arkadeLightning.getSwapManager();

// Subscribe to events using on* methods (returns unsubscribe function)
const unsubscribe = manager.onSwapUpdate((swap, oldStatus) => {
  console.log(`Swap ${swap.id}: ${oldStatus} → ${swap.status}`);
  // Update UI based on swap status
});

// Subscribe to completed events
manager.onSwapCompleted((swap) => {
  console.log(`Swap ${swap.id} completed!`);
  showNotification(`Payment completed!`);
});

// Subscribe to failures
manager.onSwapFailed((swap, error) => {
  console.error(`Swap ${swap.id} failed:`, error);
  showErrorDialog(error.message);
});

// Subscribe to actions (claim/refund)
manager.onActionExecuted((swap, action) => {
  console.log(`Executed ${action} for swap ${swap.id}`);
});

// WebSocket events
manager.onWebSocketConnected(() => {
  console.log('Connected to swap updates');
});

manager.onWebSocketDisconnected((error) => {
  console.log('Disconnected from swap updates', error);
});

// Unsubscribe when no longer needed (e.g., component unmount)
// unsubscribe();
```

**Benefits of dynamic event subscription:**
- Add/remove listeners at any time
- Multiple listeners per event type
- Easy cleanup when components unmount
- No need to restart SwapManager to change handlers

### Cleanup (Disposable Pattern)

ArkadeLightning implements the Disposable pattern for automatic cleanup:

```typescript
// Option 1: Manual cleanup
const arkadeLightning = new ArkadeLightning({ wallet, swapProvider });
// ... use it
await arkadeLightning.dispose(); // Stops SwapManager and cleans up

// Option 2: Automatic cleanup with `await using` (TypeScript 5.2+)
{
  await using arkadeLightning = new ArkadeLightning({
    wallet,
    swapProvider,
    swapManager: { autoStart: true },
  });

  // Use arkadeLightning...

} // SwapManager automatically stopped when scope exits
```

### Manual Control

```typescript
// Stop background monitoring
await arkadeLightning.stopSwapManager();

// Check manager stats
const manager = arkadeLightning.getSwapManager();
const stats = manager?.getStats();
console.log(`Monitoring ${stats.monitoredSwaps} swaps`);
console.log(`WebSocket connected: ${stats.websocketConnected}`);
```

### Per-Swap UI Hooks

When SwapManager is enabled, you can subscribe to updates for specific swaps to show progress in your UI:

```typescript
const result = await arkadeLightning.createLightningInvoice({ amount: 50000 });

// Subscribe to this specific swap's updates
const manager = arkadeLightning.getSwapManager();
const unsubscribe = manager.subscribeToSwapUpdates(
  result.pendingSwap.id,
  (swap, oldStatus) => {
    console.log(`Swap ${swap.id}: ${oldStatus} → ${swap.status}`);
    // Update UI based on status
    if (swap.status === 'transaction.mempool') {
      showNotification('Payment detected in mempool!');
    } else if (swap.status === 'invoice.settled') {
      showNotification('Payment received!');
    }
  }
);

// Clean up when component unmounts
// unsubscribe();
```

### Blocking with SwapManager

Even with SwapManager enabled, you can still wait for specific swaps to complete:

```typescript
const result = await arkadeLightning.createLightningInvoice({ amount: 50000 });

// This blocks until the swap completes, but SwapManager handles the monitoring
try {
  const { txid } = await arkadeLightning.waitAndClaim(result.pendingSwap);
  console.log('Payment claimed successfully:', txid);
} catch (error) {
  console.error('Payment failed:', error);
}

// Benefits of delegating to SwapManager:
// No race conditions - manager coordinates with manual calls
// User can navigate away - swap completes in background
// Automatic refund on failure - no manual error handling needed
```

### Without SwapManager (Manual Mode)

If SwapManager is not enabled, you must manually monitor swaps:

```typescript
// Create invoice
const result = await arkadeLightning.createLightningInvoice({ amount: 50000 });

// MUST manually monitor - blocks until complete
await arkadeLightning.waitAndClaim(result.pendingSwap);
// User must stay on this page - navigating away stops monitoring
```

## Checking Swap Limits

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.

```typescript
// Get current swap limits (in satoshis)
const limits = await arkadeLightning.getLimits();

if (limits) {
  console.log('Minimum swap amount:', limits.min, 'sats');
  console.log('Maximum swap amount:', limits.max, 'sats');

  // Example: Validate invoice amount before creating
  const invoiceAmount = 50000; // 50,000 sats

  if (invoiceAmount < limits.min) {
    console.error(`Amount ${invoiceAmount} is below minimum ${limits.min} sats`);
  } else if (invoiceAmount > limits.max) {
    console.error(`Amount ${invoiceAmount} is above maximum ${limits.max} sats`);
  } else {
    console.log('Amount is within valid range');
    // Safe to proceed with creating invoice or payment
  }
} else {
  console.log('Unable to fetch limits - no swap provider configured');
}
```

### Validating Lightning Invoice Amounts

```typescript
import { decodeInvoice } from '@arkade-os/boltz-swap';

// Decode an incoming Lightning invoice to check its amount
const invoice = 'lnbc500u1pj...'; // Lightning invoice string
const decodedInvoice = decodeInvoice(invoice);

console.log('Invoice amount:', decodedInvoice.amountSats, 'sats');

// Check if the invoice amount is within swap limits
const limits = await arkadeLightning.getLimits();

if (limits && decodedInvoice.amountSats >= limits.min && decodedInvoice.amountSats <= limits.max) {
  // Amount is valid for swaps
  const paymentResult = await arkadeLightning.sendLightningPayment({
    invoice: invoice,
  });
  console.log('Payment successful!');
} else {
  console.error('Invoice amount is outside supported swap limits');
}
```

## Checking Swap Fees

You can check the fee to pay for different swap amounts supported by the Boltz service.
This is useful to validate the user is willing to pay the fees.

```typescript
// Get current swap fees
const fees: FeesResponse | null = await arkadeLightning.getFees();
if (!fees) throw new Error('something went wrong');

const calcSubmarineSwapFee = (satoshis: number): number => {
  if (!satoshis) return 0;
  const { percentage, minerFees } = fees.submarine;
  return Math.ceil((satoshis * percentage) / 100 + minerFees);
};

const calcReverseSwapFee = (satoshis: number): number => {
  if (!satoshis) return 0;
  const { percentage, minerFees } = fees.reverse;
  return Math.ceil((satoshis * percentage) / 100 + minerFees.claim + minerFees.lockup);
};
```

## Checking Swap Status

**With SwapManager:** Status updates are automatic via events - no manual checking needed.

**Without SwapManager (manual mode):**
```typescript
const response = await arkadeLightning.getSwapStatus('swap_id');
console.log('swap status = ', response.status);
```

## Storage

This library stores swaps via a **SwapRepository**. By default, `ArkadeLightning` uses an IndexedDB-backed repository in browser contexts, so swap data persists across reloads. You can inject your own repository (for tests, Node.js, or custom storage) via the `swapRepository` option.

```typescript
const arkadeLightning = new ArkadeLightning({
  wallet,
  swapProvider,
  // Optional: override storage
  // swapRepository: myCustomSwapRepository,
});
```

All swap data is persisted automatically and can be retrieved using the following methods:

```typescript
// Get all pending submarine swaps (those waiting for Lightning payment)
const pendingPaymentsToLightning = await arkadeLightning.getPendingSubmarineSwaps();

// Get all pending reverse swaps (those waiting for claim)
const pendingPaymentsFromLightning = await arkadeLightning.getPendingReverseSwaps();

// Get complete swap history (both completed and pending)
const swapHistory = await arkadeLightning.getSwapHistory();
```

**Note**: If IndexedDB is not available (e.g., in Node.js), provide a custom `swapRepository` implementation.

## Receiving Lightning Payments

To receive a Lightning payment into your Arkade wallet:

```typescript
// Create a Lightning invoice that will deposit funds to your Arkade wallet
const result = await arkadeLightning.createLightningInvoice({
  amount: 50000, // 50,000 sats
  description: 'Payment to my Arkade wallet',
});

console.log('Receive amount:', result.amount);
console.log('Expiry (seconds):', result.expiry);
console.log('Lightning Invoice:', result.invoice);
console.log('Payment Hash:', result.paymentHash);
console.log('Pending swap', result.pendingSwap);
console.log('Preimage', result.preimage);

// The invoice can now be shared with the payer
// When paid, funds will appear in your Arkade wallet
```

### Monitoring Incoming Lightning Payments

**With SwapManager (recommended):**
```typescript
// SwapManager handles monitoring and claiming automatically
// Just listen to events for UI updates
const result = await arkadeLightning.createLightningInvoice({ amount: 50000 });
// Payment will be claimed automatically when received 
```

**Without SwapManager (manual mode):**
```typescript
// You must manually monitor - blocks until payment is received
const receivalResult = await arkadeLightning.waitAndClaim(result.pendingSwap);
console.log('Receival successful!');
console.log('Transaction ID:', receivalResult.txid);
// ⚠️ User must stay on this page - navigating away stops monitoring
```

## Sending Lightning Payments

**With SwapManager (recommended):**
```typescript
import { decodeInvoice } from '@arkade-os/boltz-swap';

// Validate invoice first
const invoiceDetails = decodeInvoice('lnbc500u1pj...');
console.log('Invoice amount:', invoiceDetails.amountSats, 'sats');

// Send payment - returns immediately after creating swap
const paymentResult = await arkadeLightning.sendLightningPayment({
  invoice: 'lnbc500u1pj...',
});

console.log('Payment initiated:', paymentResult.txid);
// SwapManager monitors in background and handles refunds if payment fails
```

**Without SwapManager (manual mode):**
```typescript
// Blocks until payment completes or fails
const paymentResult = await arkadeLightning.sendLightningPayment({
  invoice: 'lnbc500u1pj...',
});

console.log('Payment successful!');
console.log('Amount:', paymentResult.amount);
console.log('Preimage:', paymentResult.preimage);
console.log('Transaction ID:', paymentResult.txid);
// ⚠️ If payment fails, you must manually handle refund (see Error Handling)
```

## Error Handling

**With SwapManager:** Refunds are handled automatically - listen to `onSwapFailed` event for notifications.

**Without SwapManager (manual mode):** You must handle errors and execute refunds manually:

```typescript
import {
  SwapError,
  SchemaError,
  NetworkError,
  SwapExpiredError,
  InvoiceExpiredError,
  InvoiceFailedToPayError,
  InsufficientFundsError,
  TransactionFailedError,
} from '@arkade-os/boltz-swap';

try {
  await arkadeLightning.sendLightningPayment({
    invoice: 'lnbc500u1pj...',
  });
} catch (error) {
  if (error instanceof InvoiceExpiredError) {
    console.error('The invoice has expired. Please request a new one.');
  } else if (error instanceof InvoiceFailedToPayError) {
    console.error('The provider failed to pay the invoice.');
  } else if (error instanceof InsufficientFundsError) {
    console.error('Not enough funds available:', error.message);
  } else if (error instanceof NetworkError) {
    console.error('Network issue. Please try again later:', error.message);
  } else if (error instanceof SchemaError) {
    console.error('Invalid response from API. Please try again later.');
  } else if (error instanceof SwapExpiredError) {
    console.error('The swap has expired.');
  } else if (error instanceof TransactionFailedError) {
    console.error('Transaction failed. Please try again later');
  } else {
    console.error('Unknown error:', error);
  }

  // Manual refund (only needed without SwapManager)
  if (error.isRefundable && error.pendingSwap) {
    const refundResult = await arkadeLightning.refundVHTLC(error.pendingSwap);
    console.log('Refund claimed:', refundResult.txid);
  }
}
```

### Releasing

```bash
# Release new version (will prompt for version patch, minor, major)
pnpm release

# You can test release process without making changes
pnpm release:dry-run

# Cleanup: checkout version commit and remove release branch
pnpm release:cleanup
```

## License

MIT