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

# Withdraw modal

> Let users withdraw tokens from any account to any supported chain. Your app executes the transfer.

The `WithdrawModal` component handles outbound transfers. The user selects a destination chain and token, enters a recipient and an amount, and the modal registers the account that routes the funds — then asks your app to perform one transfer.

The modal never holds a key and never moves funds itself, so it works with any account model: an EOA, a smart account, an embedded or in-app wallet, or a relayer.

## Basic usage

```tsx theme={null}
import { WithdrawModal } from "@rhinestone/deposit-modal/withdraw";
import "@rhinestone/deposit-modal/styles.css";

<WithdrawModal
  isOpen={isOpen}
  onClose={() => setIsOpen(false)}
  accountAddress="0xACCOUNT_HOLDING_THE_FUNDS"
  backendUrl={process.env.NEXT_PUBLIC_DEPOSIT_PROXY_URL}
  sourceChain={8453}
  sourceToken="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
  targetChain={10}
  targetToken="0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"
  onSendTransaction={async ({ chainId, token, amount, to }) => ({
    txHash: await myWallet.sendTransfer({ chainId, token, amount, to }),
  })}
  onLifecycle={(event) => {
    if (event.type === "complete") {
      console.log("Withdrawal complete:", event.destinationTxHash);
    }
  }}
/>
```

`accountAddress` is the account holding the funds. The modal reads its balance and stops the user sending to themselves; it never transacts from it.

## Executing the transfer

`onSendTransaction` is required. The modal calls it once, when the user confirms on the review screen, with everything needed for a single transfer:

```ts theme={null}
onSendTransaction: (request: WithdrawTransferRequest) => Promise<{ txHash: Hex }>

interface WithdrawTransferRequest {
  chainId: number;  // source chain to send from
  token: Address;   // source token; NATIVE_TOKEN_ADDRESS for the native asset
  amount: bigint;   // in the token's base units
  to: Address;      // send here
}
```

Two rules, both silent-failure modes if missed:

<Warning>
  **Send to `to`, and nothing else.** It is the Rhinestone account that receives the funds and bridges them on to the recipient — except on a same-route withdrawal (source chain and token identical to the target), where the modal skips the bridge and `to` is the recipient directly. Substituting an address of your own either bypasses the bridge or strands the funds.

  **Return the on-chain transaction hash.** Progress is tracked by looking the deposit up by that hash. An ERC-4337 wallet must await the receipt and return the bundled transaction hash, **not** the userOp hash — returning the wrong one leaves the modal waiting on a withdrawal that already succeeded.
</Warning>

Reject the promise to surface a failure in the modal. The user can retry from the review screen.

### If your funds are in a Safe

A 1/1 Safe signs with `personal_sign` over the `SafeTx` hash, relayed by whoever pays the gas. Build the EIP-712 `SafeTx`, sign it, and submit `execTransaction`:

```tsx theme={null}
onSendTransaction={async ({ chainId, token, amount, to }) => {
  const safeTx = await buildSafeTransaction({ chainId, token, amount, to });

  const signature = await provider.request({
    method: "personal_sign",
    params: [safeTx.safeTxHash, ownerAddress],
  });

  // Adjust v for Safe's eth_sign verification
  const v = parseInt(signature.slice(-2), 16);
  const adjusted = signature.slice(0, -2) + (v + 4).toString(16);

  const { txHash } = await relaySafeTransaction(safeTx, adjusted);
  return { txHash };
}}
```

<Note>
  Safe's `eth_sign` path requires adding 4 to the `v` value of a `personal_sign` signature. This adjustment is specific to Safe's signature verification — see the [Safe docs](https://docs.safe.global/advanced/smart-account-signatures#eth_sign-signature) for details.
</Note>

To keep gas sponsored, relay the signed transaction through `POST /safe/withdraw` on your proxy rather than submitting `execTransaction` from the user's wallet. Note that a relayed transaction cannot use Safe's pre-validated (`v = 1`) signature shortcut: that only validates when `msg.sender` is the owner, and for a relayed call the sender is the relayer.

## Props reference

### Required

| Prop                | Type                                                             | Description                                                              |
| ------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `isOpen`            | `boolean`                                                        | Controls modal visibility                                                |
| `onClose`           | `() => void`                                                     | Called when the user closes the modal                                    |
| `accountAddress`    | `Address`                                                        | Account holding the funds being withdrawn                                |
| `sourceChain`       | `Chain \| number`                                                | Chain where the account holds funds                                      |
| `sourceToken`       | `Address`                                                        | Token to withdraw                                                        |
| `onSendTransaction` | `(request: WithdrawTransferRequest) => Promise<{ txHash: Hex }>` | Performs the transfer and returns its on-chain hash                      |
| `backendUrl`        | `string`                                                         | Your [backend proxy](/deposits/widget/backend), which holds your API key |

### Transfer

| Prop            | Type              | Default       | Description                                          |
| --------------- | ----------------- | ------------- | ---------------------------------------------------- |
| `targetChain`   | `Chain \| number` | `sourceChain` | Seeds the destination chain; the user can change it  |
| `targetToken`   | `Address`         | `sourceToken` | Seeds the token to receive; the user can change it   |
| `recipient`     | `Address`         | —             | Pre-fills the delivery address; the user can edit it |
| `defaultAmount` | `string`          | —             | Pre-filled withdrawal amount                         |

<Note>
  `targetChain` and `targetToken` are optional — they seed the form, and the user can
  pick any supported destination. Omit them to open on a same-chain, same-token
  withdrawal.
</Note>

### Account

| Prop            | Type      | Default | Description                                    |
| --------------- | --------- | ------- | ---------------------------------------------- |
| `forceRegister` | `boolean` | `false` | Re-register even if the account already exists |

### Backend

| Prop      | Type        | Default        | Description                                                                                                    |
| --------- | ----------- | -------------- | -------------------------------------------------------------------------------------------------------------- |
| `rpcUrls` | `RpcUrlMap` | Chain defaults | Per-chain RPC overrides keyed by EVM chain id. Used for balance reads; chains left unset use their default RPC |

### Display

| Prop                  | Type                   | Default | Description                                                         |
| --------------------- | ---------------------- | ------- | ------------------------------------------------------------------- |
| `inline`              | `boolean`              | `false` | Render without modal overlay                                        |
| `closeOnOverlayClick` | `boolean`              | `true`  | Close modal on backdrop click                                       |
| `className`           | `string`               | —       | CSS class for the modal container                                   |
| `theme`               | `DepositModalTheme`    | —       | [Theme configuration](/deposits/widget/customization#theme)         |
| `uiConfig`            | `DepositModalUIConfig` | —       | [UI configuration](/deposits/widget/customization#ui-configuration) |
| `debug`               | `boolean`              | `false` | Enable debug logging                                                |

### Callbacks

| Prop          | Type                                      | Description                                                                                                                               |
| ------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `onReady`     | `() => void`                              | Modal initialized                                                                                                                         |
| `onLifecycle` | `(event: WithdrawLifecycleEvent) => void` | [Lifecycle event](/deposits/widget/status-tracking#onlifecycle) — switch on `event.type` (`connected`, `submitted`, `complete`, `failed`) |
| `onError`     | `(data: ErrorEventData) => void`          | Error at any stage                                                                                                                        |
| `onEvent`     | `(event: WithdrawAnalyticsEvent) => void` | [Analytics event](/deposits/widget/status-tracking#analytics)                                                                             |

`WithdrawLifecycleEvent` has no `balance-changed` or `smart-account-changed` variants, and its payload types differ from the deposit union: `txHash` is `Hex`, `sourceChain` is always a `number`, `sourceToken` / `targetToken` are `Address`, and `"submitted"` carries an `accountAddress`.
