> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rhinestone.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# History and recovery

> Let users review deposits and recover eligible failures from the deposit or withdraw modal.

Available from `@rhinestone/deposit-modal` v0.15.0, history is the self-service recovery path for both host modals. It lists deposits for the account in context and can recover an eligible failure when you provide `signRecovery`.

<Tabs>
  <Tab title="Deposit modal">
    ```tsx theme={null}
    <DepositModal
      // ...required props
      recipient={recipient}
      signRecovery={({ typedData, signer, chainId }) =>
        getRecoveryWallet({ address: signer, chainId }).signTypedData(typedData)
      }
    />
    ```

    History lists deposits for the configured `recipient`.
  </Tab>

  <Tab title="Withdraw modal">
    ```tsx theme={null}
    <WithdrawModal
      // ...required props
      recipient={withdrawalDestination}
      signRecovery={({ typedData, signer, chainId }) =>
        getRecoveryWallet({ address: signer, chainId }).signTypedData(typedData)
      }
    />
    ```

    History follows the currently valid withdrawal destination, not `accountAddress`. Changing the destination re-scopes the list and status badge. An invalid or absent destination has no history entry point. Provide `signRecovery` only when your app can sign for destinations the user selects.
  </Tab>
</Tabs>

## Where history appears

The history entry point is available before a transaction and on terminal success or failure screens. It is hidden while the modal is signing, submitting, polling, or showing an external provider. Opening history during a flow preserves the underlying form or step, so Back returns the user to it.

The entry-point badge summarizes the visible account:

| Badge       | Meaning                                                                    |
| ----------- | -------------------------------------------------------------------------- |
| `needs_you` | At least one failed row can be recovered. Takes precedence over `progress` |
| `progress`  | At least one row is pending or processing                                  |
| `none`      | Neither condition applies                                                  |

From v0.17.0, a pending or processing row also carries an
[arrival estimate](/deposits/widget/status-tracking#arrival-estimates): the expected
duration, or delay framing once the wait runs long. It is computed from the row's
creation time, so it is the same after a reload, on another device, and for a deposit
started outside the modal. A row with no served estimate renders as before.

Without `signRecovery`, history remains available but read-only. The transaction-hash fallback is hidden and the badge does not use `needs_you` because the integration cannot offer recovery.

Your [proxy](/deposits/widget/backend#required-routes) must forward `GET /deposits` for lookup and `POST /deposits/recover` for signed recovery. These are existing routes; history adds no backend handler or header.

## Recover from a history row

An eligible failed row opens a review, signing, and outcome flow. The refund destination starts as the deposit recipient, remains editable, and is covered by the signature.

Recovery is offered only when all of these conditions hold:

* The raw deposit status is `failed` or `rejected`.
* Both source and destination are EVM chains.
* The deposit names a valid recipient matching the account whose history is open.
* Your integration supplies `signRecovery` for that recipient.

Completed, pending, processing, non-EVM, recipient-less, and otherwise ineligible rows remain visible without a recovery action.

## Recover by transaction hash

When signing is enabled, history also offers transaction-hash lookup for a recoverable deposit omitted from the list, including a spam-filtered deposit. The lookup is scoped to the account whose history is open—even in the withdraw modal. A hash that belongs only to another account returns no match.

Recovering an unlisted deposit does not add it to the history list afterward.

The package offers no cross-account lookup. A support case that must find another account's deposit goes through the [dashboard workflow](/deposits/troubleshooting).

## Signing contract

`signRecovery` is optional on `DepositModal` and `WithdrawModal` and has the exported `SignRecovery` type:

```ts theme={null}
import type { SignRecovery } from "@rhinestone/deposit-modal";

const signRecovery: SignRecovery = async ({ typedData, signer, chainId }) => {
  const wallet = getRecoveryWallet({ address: signer, chainId });
  return wallet.signTypedData(typedData);
};
```

`getRecoveryWallet` represents your app's account lookup. Select the wallet by both `signer` and `chainId`, then use the signing API expected by its SDK. The callback receives the complete typed data, so do not reconstruct it.

| Field         | Type                  | Description                                                             |
| ------------- | --------------------- | ----------------------------------------------------------------------- |
| `typedData`   | `TypedDataDefinition` | EIP-712 `RecoverDeposit` data naming the deposit and refund destination |
| `signer`      | `Address`             | Deposit recipient whose signature the service verifies                  |
| `chainId`     | `number`              | Deposit destination chain, where the recipient's verifier lives         |
| `depositId`   | `string`              | Deposit being recovered                                                 |
| `destination` | `Address`             | Editable refund destination covered by the signature                    |

`chainId` is **not** the source chain from which funds are refunded. Use it to select the verifier context for a smart account. This matters for ERC-1271 and ERC-6492 accounts whose deployment or owners differ by chain; EOA signatures are unaffected.

Return whatever the recipient verifies:

| Recipient                | Return value                                            |
| ------------------------ | ------------------------------------------------------- |
| EOA                      | Raw 65-byte EIP-712 signature                           |
| Deployed smart account   | Signature accepted by ERC-1271 `isValidSignature`       |
| Undeployed smart account | Account signature wrapped with ERC-6492 deployment data |

A raw smart-account owner signature is usually insufficient. Sign through the account SDK so it produces the wallet's expected ERC-1271 or ERC-6492 format.

An ERC-6492 wrapper carries the account's factory and factory data, so an account that does not exist on chain yet can still be verified — which matters, since a deposit can fail before the user's account is ever deployed.

```ts theme={null}
import { SignatureErc6492 } from "ox/erc6492";

const signRecovery: SignRecovery = async ({ typedData }) => {
  const signature = await account.signTypedData(typedData);
  if (await account.isDeployed()) return signature;
  return SignatureErc6492.wrap({
    to: factory,        // the factory that will deploy the account
    data: factoryData,  // the calldata that deploys it
    signature,
  });
};
```

`ox` is already a dependency of viem, so this adds nothing to your install.

### What the user signs

```ts theme={null}
domain:      { name: "Rhinestone Deposit Recovery", version: "1" }
primaryType: "RecoverDeposit"
types: { RecoverDeposit: [
  { name: "depositId",   type: "uint256" },
  { name: "destination", type: "address" },
]}
```

Two fields, both meaningful to the person signing: which deposit, and where the money goes.

**The destination is inside the signature**, which is the property worth understanding. The service cannot pay anywhere other than the address the user signed for, so a compromised page cannot redirect the funds — and your success screen can state where they went without trusting a response to echo it back.

<Warning>
  A signature stays valid until the deposit is claimed — there is no expiry. Replay is
  already closed, because claiming moves the deposit out of `failed`/`rejected` and a
  second attempt is rejected. But do not persist a signature: treat it as
  single-use and discard it once the request returns.
</Warning>

## Handling failures

A failed recovery response carries a machine-readable `code`, exported as `RecoveryErrorCode`. Switch on that rather than the HTTP status — the code is the contract, and the correct advice differs between codes that share a status.

| `code`                           | Means                                                                             | Retry?                                     |
| -------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------ |
| `DEPOSIT_NOT_RECOVERABLE`        | Not in `failed`/`rejected` — often already claimed                                | No                                         |
| `RECOVERY_UNSUPPORTED`           | The deposit has no recipient to verify against, or a non-EVM chain on either side | No                                         |
| `SIGNATURE_INVALID`              | The recipient's verifier rejected these bytes                                     | No — signing again the same way won't help |
| `VERIFICATION_UNAVAILABLE`       | The on-chain check couldn't run                                                   | Yes, and without re-signing                |
| `REFUND_RECONCILIATION_REQUIRED` | Funds may be in flight; needs an operator                                         | **No**                                     |
| `REFUND_FAILED`                  | The transfer didn't complete                                                      | Yes                                        |

History recovery maps these to copy and to whether it offers a retry, so you get this for free. Handle them yourself only if you drive your own UI.

<Warning>
  Never retry `REFUND_RECONCILIATION_REQUIRED`. The funds may already be moving, and
  the deposit needs an operator either way — contact support with the deposit.
</Warning>

## When the recipient can't sign

Some recipients have no key to sign with: a wallet the user has lost, or a recipient your app controls rather than the user. For those, `@rhinestone/deposit-modal/server` exports `createRefundHandler`, which authorizes on your say-so instead of a signature — you decide who the caller is, and it verifies the deposit belongs to them before spending your API key.

```ts theme={null}
// app/api/refund/route.ts
import { createRefundHandler } from "@rhinestone/deposit-modal/server";

export const POST = createRefundHandler({
  apiKey: process.env.RHINESTONE_API_KEY!,
  async authorize(request) {
    const session = await getSession(request);
    return session ? { depositRecipient: session.depositRecipient } : null;
  },
});
```

`authorize` returns the deposit `recipient` the caller owns, or `null` to reject with 401. Add `refundDestination` to pin where the money goes rather than letting the request choose. `depositRecipient` never decides the destination — the handler lists deposits settling to it and rejects a `txHash` that isn't among them.

It returns a `(Request) => Promise<Response>`, so it mounts in any fetch-based runtime: Next.js route handlers, Hono, `Bun.serve`, Cloudflare Workers.

<Warning>
  Import from `@rhinestone/deposit-modal/server` only — it holds your API key and must
  never reach the browser. Neither modal calls this route; drive your own UI against it.
</Warning>

<Warning>
  Never refund to an exchange deposit address. Exchanges don't credit arbitrary
  incoming transfers, so the funds **may be lost**. This is why recovery never defaults
  the destination to the deposit's sender.
</Warning>

## Host callbacks and analytics

History navigation emits the [deposit or withdraw history events](/deposits/widget/status-tracking#history-analytics) in the current host session. Starting recovery creates a correlated claim session, and its `ClaimAnalyticsEvent` values arrive through the host modal's `onEvent` callback:

```ts theme={null}
DepositModalProps["onEvent"]
// DepositAnalyticsEvent | ClaimAnalyticsEvent | AnalyticsIngestFailureEvent

WithdrawModalProps["onEvent"]
// WithdrawAnalyticsEvent | ClaimAnalyticsEvent | AnalyticsIngestFailureEvent
```

History does not add claim events to `onLifecycle`. `enableAnalyticsIngest={false}` disables Rhinestone collection but does not change what `onEvent` receives, and analytics delivery never blocks recovery.

## Other recovery models

Choose the authorization model that matches the caller:

* **User self-service:** use account-scoped history with `signRecovery`.
* **Recipient cannot sign:** authorize an operator flow on your server with [`createRefundHandler`](#when-the-recipient-cant-sign). It is not called by either modal and must never be exposed as a browser proxy route.
