---
title: "Unilateral exit & status"
description: "Choose between a cooperative leave and a unilateral exit, start one with useWalletExit or useWalletExitBatch, and track it with the status and summary hooks."
canonical: https://wavelength.lightning.engineering/guides/unilateral-exit/
---

> Docs index: https://wavelength.lightning.engineering/llms.txt

[Wavelength](/)›Guides›Unilateral exit & status

# Unilateral exit & status

5 min read

Guides

Web SDK

Every VTXO leaves Ark one of two ways: a cooperative leave the operator settles in the next round, or a unilateral exit you force on-chain yourself. [`useWalletExit`](/reference/wavelength-react/#useWalletExit) starts one outpoint; [`useWalletExitBatch`](/reference/wavelength-react/#useWalletExitBatch) starts several at once. [`useWalletExitPlan`](/reference/wavelength-react/#useWalletExitPlan), [`useWalletExitStatus`](/reference/wavelength-react/#useWalletExitStatus), and [`useWalletExits`](/reference/wavelength-react/#useWalletExits) preview and track them. `sweepWallet` has no dedicated hook yet, so reach it through [`useWalletEngine()`](/reference/wavelength-react/#useWalletEngine).client, the escape hatch every granular hook is built on.

## Cooperative or unilateral?

Try cooperative first. It settles in roughly one round instead of waiting out a CSV timelock, and it costs nothing beyond the round’s normal fees. It’s also the default: calling `exit({ outpoint })` with no `forceUnrollAck` is a cooperative leave.

Reach for unilateral only when the operator is unreachable or unresponsive, or a cooperative attempt already failed and you need a way out regardless of that. It’s trustless, since it needs no cooperation from the operator, but it is slower and costs real sats: it materializes a recovery transaction tree on-chain, then waits out the VTXO’s CSV timelock before you can sweep the funds. Preview the cost with `getExitPlan` before committing (see below). See [Leaving Ark](/concepts/leaving-ark/) for the full decision writeup, including what each path looks like in the activity feed.

Unilateral is a separate, explicit branch, not something the SDK falls back to on your behalf: a cooperative failure rejects the call and never silently starts a unilateral exit underneath you. Starting one requires the exported `FORCE_UNROLL_ACK` constant:

```ts
import { FORCE_UNROLL_ACK } from '@lightninglabs/wavelength-react';

await client.exit({ outpoint, forceUnrollAck: FORCE_UNROLL_ACK });
```

`FORCE_UNROLL_ACK` exists so the unilateral path is something you opt into on purpose, not something a typo or a copy-pasted request can trigger by accident. `destination` (cooperative) and `forceUnrollAck` (unilateral) are mutually exclusive on `ExitRequest`; you pick the branch up front.

## Preview with getExitPlan

Before starting a unilateral exit, call `client.getExitPlan({ outpoints })` (or the equivalent [`useWalletExitPlan`](/reference/wavelength-react/#useWalletExitPlan) hook) to preview funding requirements for each VTXO. Show the `plans` summary to the user so they can make an informed decision before broadcasting.

Pass `confTarget` (in blocks) to influence the fee rate used for the recommended-funding estimate, the same way you would for other fee-sensitive calls.

```tsx
import { useState } from 'react';
import { useWalletEngine } from '@lightninglabs/wavelength-react';
import type { GetExitPlanResult } from '@lightninglabs/wavelength-react';

function ExitPlan({ outpoints }: { outpoints: string[] }) {
  const { client } = useWalletEngine();
  const [plan, setPlan] = useState<GetExitPlanResult | null>(null);

  const preview = async () => {
    // confTarget is optional; omit it to use the client's default fee estimate.
    const exitPlan = await client.getExitPlan({ outpoints, confTarget: 6 });
    setPlan(exitPlan);
  };

  return (
    <div>
      <button onClick={preview}>Preview exit</button>
      {plan && (
        <p>
          Can start: {String(plan.canStart)}. Recommended funding:{' '}
          {plan.totalRecommendedFundingSat} sats.
        </p>
      )}
    </div>
  );
}
```

If `canStart` is `false`, check each `ExitPlanEntry.infeasibilityReason`: a plain funding shortfall is fixable by sending sats to `fundingAddress`, but `sweep_below_dust` and `uneconomical` are structural and no amount of funding helps. [`isExitInfeasibilityFundable`](/reference/wavelength-core/#isExitInfeasibilityFundable) makes that split for you, so you can show a “fund your wallet” affordance for the fixable case and a plain “can’t exit this VTXO” message for the rest.

## Start a single exit

`useWalletExit` follows the same throw-and-capture convention as the other mutation hooks: an `exit` action plus `exitPending`/`exitError`/`exitData`. Do not call it twice for the same outpoint; check [`useWalletExitStatus`](#track-status-and-summary) first if the user may have already started an exit in a previous session.

```tsx
import { FORCE_UNROLL_ACK, useWalletExit } from '@lightninglabs/wavelength-react';

function StartExit({ outpoint }: { outpoint: string }) {
  const { exit, exitPending, exitError } = useWalletExit();

  const startExit = () =>
    exit({ outpoint, forceUnrollAck: FORCE_UNROLL_ACK });

  return (
    <>
      <button disabled={exitPending} onClick={startExit}>
        Start exit
      </button>
      {exitError && <p role="alert">{exitError.message}</p>}
    </>
  );
}
```

`exitData` resolves with an [`ExitResult`](/reference/wavelength-core/#ExitResult) whose `path` discriminates the outcome. Which fields are populated depends on `path`:

| Field              | Populated when                                                      |
| ------------------ | ------------------------------------------------------------------- |
| `queuedOutpoints`  | `path === 'cooperative'`                                            |
| `created`          | `path` is `'unilateral'` or `'unilateral_fallback'`                 |
| `actorID`          | `path` is `'unilateral'` or `'unilateral_fallback'`                 |
| `cooperativeError` | retained for source compatibility; not populated by the current RPC |

`unilateral_fallback` is also retained for source compatibility, but the current RPC never returns it: cooperative failures reject instead of silently entering that legacy branch.

## Batch exits with useWalletExitBatch

Exiting more than one outpoint by hand means calling `exit()` in a loop and handling funding contention yourself, since exit funding is never reserved: an earlier start can consume the fee inputs a later one needs. `useWalletExitBatch` does that work for you. It resolves once every outpoint has **started**, not completed; a unilateral exit keeps running for hours or days after that. On the unilateral path it re-plans between starts, so it notices a funding conflict before attempting the next exit rather than after, and it stops cleanly (rather than retrying) if the daemon rejects a start mid-batch.

```tsx
import { useWalletExitBatch } from '@lightninglabs/wavelength-react';

function ExitAll({ outpoints }: { outpoints: string[] }) {
  const { exitBatch, exitBatchPending, exitBatchError, exitBatchEvents } =
    useWalletExitBatch();

  const startAll = () => exitBatch({ mode: 'unilateral', outpoints, confTarget: 6 });

  return (
    <div>
      <button disabled={exitBatchPending} onClick={startAll}>
        Exit all
      </button>
      <ul>
        {exitBatchEvents.map((event, i) => (
          <li key={i}>
            {event.type}
            {'outpoint' in event ? `: ${event.outpoint}` : ''}
          </li>
        ))}
      </ul>
      {exitBatchError && <p role="alert">{exitBatchError.message}</p>}
    </div>
  );
}
```

Pass `{ mode: 'cooperative', outpoints, destination? }` instead to queue every outpoint into the next cooperative round; the cooperative path has no funding-contention re-plan step, since it never touches the backing wallet. Once the call settles, read `exitBatchData.started` for what ran, `.skipped` for outpoints that already had a job running, `.remaining` for what never started, and `.stoppedBy` for why, if the batch stopped early.

## Track status and summary

[`useWalletExitStatus(outpoint, opts?)`](/reference/wavelength-react/#useWalletExitStatus) fetches on mount and whenever `outpoint` changes, and defaults to the cheap, phase-only call. Pass `{ detailed: true }` when you need the recovery-tree progress, the CSV maturity countdown, and a fee breakdown; that’s a live round-trip, so leave it off for a plain status label. Pass `{ pollMs }` to poll while the component stays mounted: polling is opt-in and runs with no visibility/focus gating, stopping only on unmount, so there’s nothing to tear down yourself.

```tsx
import { useWalletExitStatus } from '@lightninglabs/wavelength-react';

function ExitStatus({ outpoint }: { outpoint: string }) {
  const { status, statusPending, statusError } = useWalletExitStatus(outpoint, {
    detailed: true,
    pollMs: 5000,
  });

  if (statusPending && !status) return <p>Loading...</p>;
  if (statusError) return <p role="alert">{statusError.message}</p>;
  if (!status?.found) return <p>No exit job for this outpoint.</p>;
  if (status.status === 'failed') return <p role="alert">{status.lastError}</p>;

  return (
    <p>
      {status.status}
      {status.cSV && !status.cSV.mature && ` - ${status.cSV.blocksRemaining} blocks to maturity`}
    </p>
  );
}
```

[`useWalletExits()`](/reference/wavelength-react/#useWalletExits) reads the wallet-wide portfolio instead of one outpoint: every in-progress exit plus the combined amount, fees, and net recoverable. It fetches on mount and refetches whenever wallet activity changes, so a completed exit drops out of the summary without a manual refresh.

```tsx
import { useWalletExits } from '@lightninglabs/wavelength-react';

function ExitSummary() {
  const { summary, summaryPending } = useWalletExits();

  if (summaryPending && !summary) return <p>Loading...</p>;
  if (!summary || summary.totalExits === 0) return null;

  return (
    <p>
      {summary.totalExits} exit(s) in progress, ~{summary.totalEstNetRecoveredSat} sats recoverable.
    </p>
  );
}
```

## Sweep the backing wallet

Once claimable outputs are ready, call `client.sweepWallet()` with `broadcast: false` first to preview, then `broadcast: true` with a `destinationAddress` to consolidate funds on-chain. `broadcast` defaults to `false` when omitted, so a call without it is always a preview. `sweepWallet` also accepts optional `feeRateSatPerVByte` and `confTarget` overrides for the sweep transaction’s fee; both have sensible defaults when omitted, so most callers can leave them unset. The preview result’s `estimatedFeeSat` and `netAmountSat` are useful to show the user what a broadcast would cost and net before they commit to it. If `preview.canBroadcast` is `false`, do not attempt the broadcast call; show `preview.failureReason` to explain why instead.

```tsx
import { useWalletEngine } from '@lightninglabs/wavelength-react';

function Sweep() {
  const { client } = useWalletEngine();

  const sweep = async () => {
    const preview = await client.sweepWallet({
      destinationAddress: 'bc1q...',
      broadcast: false,
    });

    if (!preview.canBroadcast) {
      console.error(preview.failureReason);
      return;
    }

    await client.sweepWallet({
      destinationAddress: 'bc1q...',
      broadcast: true,
    });
  };

  return <button onClick={sweep}>Sweep to on-chain wallet</button>;
}
```
