> ## 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.

# Callbacks and error handling

> Handle widget lifecycle callbacks, arrival estimates, and errors in your app.

The modal emits every state transition through a single `onLifecycle` callback.
You switch on `event.type` to update your UI, trigger backend processes, or log
analytics. New event variants can be added without changing the prop surface.

These callbacks are browser UI signals, not proof of server-side settlement: they stop when the modal closes. Use [webhooks](/deposits/headless/processing-and-tracking/webhooks) on your server for work that must happen regardless, such as crediting a balance or sending a receipt.

## Deposit lifecycle

```mermaid actions={false} theme={null}
sequenceDiagram
    participant App as Your app
    participant Modal as Deposit modal
    participant Chain as Blockchain

    Modal->>App: onReady
    Note over Modal: User picks a funding method
    Modal->>App: onLifecycle "connected" (wallet only)
    Note over Modal: User selects chain, token, amount
    Modal->>Chain: Submit deposit tx
    Modal->>App: onLifecycle "submitted"
    Note over Chain: Bridge in progress
    Chain-->>Modal: Funds arrive on target chain
    Modal->>App: onLifecycle "complete"
```

1. The modal initializes and fires `onReady`
2. If the user funds from a wallet, `"connected"` fires with the EOA `address` and
   the `smartAccount` the deposit lands on
3. The user selects a source chain, token, and amount, then confirms
4. The modal submits the transaction on the source chain and emits `"submitted"`
5. The bridge routes funds to the target chain. Once they arrive, the modal
   emits `"complete"`

If the bridge fails after submission, `"failed"` is emitted instead of
`"complete"`.

<Warning>
  `"connected"` fires only for wallet funding. QR transfer, fiat on-ramp and exchange
  connect involve no wallet, so it never fires — use `onReady` if you need a "flow
  started" signal.
</Warning>

## Arrival estimates

From `@rhinestone/deposit-modal` v0.17.0, the modal states how long a deposit is
expected to take and changes its framing when the wait runs long.

The estimate is served by the deposit service — with the quote, and again on each
read of an in-flight deposit — and is derived from recent completed deposits on the
same route, falling back to the route's own indicative estimate. It is an
expectation, never a deadline, guarantee, or SLA. It is resolved per request, so two
reads of the same deposit can return different numbers, and it is omitted whenever no
estimate is available.

Nothing about your integration changes. The estimate rides the existing
`POST /quotes/preview` and `GET /deposits` routes your
[proxy](/deposits/overview/widget/backend-setup#required-routes) already forwards — there is no new
prop, proxy route, or host configuration.

### While the deposit is processing

The pending screen shows one of three treatments, each replacing the previous one in
place rather than accompanying it:

| Treatment   | Shown                                                                                                                      |
| ----------- | -------------------------------------------------------------------------------------------------------------------------- |
| Expectation | `Usually arrives in ~5 min`, while the wait is normal                                                                      |
| Delayed     | Past the served soft threshold: the wait is longer than usual and completes automatically                                  |
| Escalated   | Past the served escalated threshold: the wait is much longer than usual, and [`PROCESS_TIMEOUT`](#codes) reaches `onError` |

Crossing a threshold drops the expectation instead of restating an estimate the
deposit has already exceeded. The treatments only advance — a phase change or a late
quote never walks the framing back. The estimate itself is latched for the widget
session the first time it is seen, so a quote that resolves late can supply the
numbers for the first time but can never change numbers the user has already been
shown.

### Before submission

The review and QR screens state the expected duration end to end rather than the
bridging leg alone, using the served estimate and falling back to the route's fill
time when none was served. Sub-minute estimates read in seconds (`~7 seconds`)
instead of collapsing to "less than a minute".

### In deposit history

An in-flight row in the [history panel](/deposits/overview/widget/history-and-recovery) carries
the same three treatments, computed from the deposit's creation time against the
served thresholds. History reads them from `GET /deposits` rather than from the
current session, so the expectation survives a reload and is the same on another
device or for a deposit started elsewhere.

### When no estimate is served

Estimates are optional and route-dependent, and a read that cannot resolve one simply
omits it. In that case the modal shows no expectation and no delay framing, and
behaves exactly as it did before v0.17.0. The withdraw modal's pending screen is
served no estimate at all, so it is unchanged.

See [tracking a deposit through the API](/deposits/headless/processing-and-tracking/track-deposits#response) for
the field on the deposit read, and the
[`GET /deposits` reference](/api-reference/deposit-service/utilities/list-deposits)
for its schema.

## onLifecycle

`onLifecycle` receives a discriminated union — `DepositLifecycleEvent` on
`<DepositModal>`, `WithdrawLifecycleEvent` on `<WithdrawModal>`. The two are
similar but not identical; see [withdraw events](#withdraw-events) for the
differences.

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

<DepositModal
  // ...required props
  onLifecycle={(event: DepositLifecycleEvent) => {
    switch (event.type) {
      case "connected":
        console.log("smart account", event.smartAccount);
        break;
      case "submitted":
        console.log("source tx", event.txHash, "on", event.sourceChain);
        break;
      case "complete":
        console.log("done", event.destinationTxHash, event.amount);
        break;
      case "failed":
        console.error("failed", event.txHash, event.error);
        break;
      case "balance-changed":
        setBalance(event.totalUsd);
        break;
      case "smart-account-changed":
        setSmartAccount({ evm: event.evm, solana: event.solana });
        break;
    }
  }}
/>
```

### Deposit events

| `event.type`              | Fields                                                                                                                                                                                                                                                                 | Description                                       |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| `"connected"`             | `address: Address`, `smartAccount: Address`                                                                                                                                                                                                                            | A wallet was connected as the funding source      |
| `"submitted"`             | `txHash: string`, `sourceChain: ChainId \| "unknown"`, `amount: string`, `sourceDecimals?: number`, `amountUsd?: string`                                                                                                                                               | Deposit transaction submitted on the source chain |
| `"complete"`              | `txHash: string`, `destinationTxHash?: string`, `amount: string`, `destinationAmount?: string`, `sourceChain: ChainId \| "unknown"`, `sourceToken?: string`, `sourceDecimals?: number`, `amountUsd?: string`, `targetChain: number \| "solana"`, `targetToken: string` | Tokens arrived on the target chain                |
| `"failed"`                | `txHash: string`, `error?: string`                                                                                                                                                                                                                                     | Bridge or transfer failed after submission        |
| `"balance-changed"`       | `totalUsd: number`                                                                                                                                                                                                                                                     | The user's total portfolio balance (USD) changed  |
| `"smart-account-changed"` | `evm: Address \| null`, `solana: string \| null`                                                                                                                                                                                                                       | The resolved smart account addresses changed      |

`amount` is in the source token's base units — divide by `sourceDecimals` to display
it. `sourceDecimals` is omitted when the token isn't recognised, which happens for a
QR deposit of an unlisted token.

`destinationAmount`, when present, is the amount delivered in the destination token's base units. Use it to reconcile a [checkout payment](/deposits/overview/widget/widget-configuration#checkout-payments); `amount` is the source amount sent.

`amountUsd` is the USD value as entered in the modal. It is omitted for flows with no
amount input: QR transfer, fiat on-ramp, and exchange connect.

<Warning>
  `sourceChain: "unknown"` is deposit-only. When a webhook-detected deposit
  arrives without chain or token information, `sourceChain` is `"unknown"` and
  `sourceToken` is `undefined` — handle this branch so you don't pick the wrong
  explorer URL.
</Warning>

### Withdraw events

`WithdrawLifecycleEvent` carries the same `type` values minus `"balance-changed"`
and `"smart-account-changed"`. Its `txHash` is `Hex`, `sourceChain` is always a
`number`, `sourceToken` / `targetToken` are `Address`, and `"submitted"` adds an
`accountAddress: Address` field. `"connected"` means the deposit account for the
chosen target is registered and fundable, not that a wallet connected.

## onReady

Fires once when the modal is initialized and ready for interaction. No payload.

```tsx theme={null}
onReady={() => console.log("modal ready")}
```

## onError

Fires on errors at any stage — wallet connection, transaction signing, bridge
setup — that prevent the deposit from being submitted. Distinct from the
`"failed"` lifecycle event, which covers failures after the source transaction
confirms.

```tsx theme={null}
onError={(data) => console.error(`[${data.code}] ${data.message}`)}
```

| Field     | Type                  | Description              |
| --------- | --------------------- | ------------------------ |
| `message` | `string`              | Error description        |
| `code`    | `string \| undefined` | Error code, if available |

### Codes

| `code`                              | Meaning                                                                                                                                                      |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `WITHDRAW_MISSING_SEND_TRANSACTION` | `<WithdrawModal>` opened without an `onSendTransaction` function                                                                                             |
| `WITHDRAW_REGISTER_FAILED`          | Registering the withdrawal's deposit account failed                                                                                                          |
| `WITHDRAW_FLOW_ERROR`               | The withdrawal failed after the form was submitted                                                                                                           |
| `SWAPPED_CONNECT_EXCHANGES_FAILED`  | The exchange list could not be fetched                                                                                                                       |
| `PROCESS_TIMEOUT`                   | Advisory: the deposit passed the served escalated threshold and is [taking much longer than usual](#arrival-estimates). Not a failure — processing continues |

`PROCESS_TIMEOUT` fires at most once per deposit. From v0.17.0 it fires at the served
escalated threshold, five to ten minutes in practice, and keeps its previous fixed
ten-minute threshold when no estimate is served. It does not fire for same-route
transfers, and the withdraw modal keeps the fixed threshold.

Errors without a code carry only `message`. For bridge-level codes, see [deposit processing error codes](/deposits/headless/processing-and-tracking/deposit-processing#error-codes).

## Error handling

| Stage               | Signal                   | Typical causes                                                                                                              |
| ------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| Wallet connection   | `onError`                | User rejected connection, network error                                                                                     |
| Transaction signing | `onError`                | User rejected transaction, insufficient gas                                                                                 |
| After submission    | `onLifecycle` `"failed"` | Bridge failure, timeout, price deviation. A `PROCESS_TIMEOUT` on `onError` is advisory and does not mean the deposit failed |
| Any stage           | `onError`                | Unexpected errors                                                                                                           |

After the source chain transaction confirms, the deposit service may
[retry automatically](/deposits/headless/processing-and-tracking/deposit-processing#retries) before the
`"failed"` event fires.
