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

# Recover an account

> Rotate ECDSA or passkey owners with configured guardians.

Recovery builds the calls required to add the new owners, update the threshold, and remove old owners. Submit every returned call as a separate guardian-signed UserOperation in the order returned.

Before starting, confirm that recovery is [set up](/wallets/custom-signer/recovery/set-up) on this chain and that the SDK has a [bundler](/wallets/custom-signer/configuration/erc-4337). Guardian signers cannot authorize `prepareTransaction`, `signMessage`, or `signTypedData`.

<Tabs>
  <Tab title="ECDSA owners">
    ```ts theme={null}
    import { recoverEcdsaOwnership } from "@rhinestone/sdk/actions/recovery";

    const recoveryCalls = await recoverEcdsaOwnership({
      accountAddress: rhinestoneAccount.getAddress(),
      chain,
      config: rhinestoneAccount.config,
      newOwners: {
        type: "ecdsa",
        accounts: [newOwnerAccount],
      },
    });

    for (const call of recoveryCalls) {
      const result = await rhinestoneAccount.sendUserOperation({
        chain,
        calls: [call],
        signers: {
          type: "guardians",
          guardians: [guardianAccountA, guardianAccountB],
        },
      });
      await rhinestoneAccount.waitForExecution(result);
    }
    ```

    `newOwners` is the complete target owner set. Existing owners omitted from it are removed.
  </Tab>

  <Tab title="Passkey owners">
    The passkey validator stores credential IDs, not recoverable public-key coordinates. When enrolling each credential, persist its public-key coordinates, `pubKeyX` and `pubKeyY`, in your application. They contain no secret material. Recovery needs the complete persisted set; it does not need the lost passkey's private key or authenticator.

    ```ts theme={null}
    import { recoverPasskeyOwnership } from "@rhinestone/sdk/actions/recovery";

    // Load every enrolled credential's persisted public-key coordinates.
    const currentCredentials = await loadPasskeyCoordinates(
      rhinestoneAccount.getAddress(),
    );

    const recoveryCalls = await recoverPasskeyOwnership({
      accountAddress: rhinestoneAccount.getAddress(),
      chain,
      config: rhinestoneAccount.config,
      currentCredentials,
      newOwners: {
        type: "passkey",
        accounts: [newPasskeyAccount],
      },
    });

    for (const call of recoveryCalls) {
      const result = await rhinestoneAccount.sendUserOperation({
        chain,
        calls: [call],
        signers: {
          type: "guardians",
          guardians: [guardianAccountA, guardianAccountB],
        },
      });
      await rhinestoneAccount.waitForExecution(result);
    }
    ```

    `newOwners` is the complete target credential set. Existing passkey owners omitted from it are removed.

    <Warning>
      Pass every installed credential in `currentCredentials`, not only the
      credential being replaced. A partial set can make recovery try to add an
      existing credential and revert with `CredentialAlreadyExists`. If you did not
      persist the public-key coordinates at enrollment, they cannot be recovered
      on-chain.
    </Warning>
  </Tab>
</Tabs>

## Ordering and signing constraints

* Send exactly one returned recovery call per UserOperation. The recovery validator authorizes one account `execute` call, so batching the array is rejected on-chain.
* Preserve the returned order and wait for each UserOperation before sending the next. Additions run before threshold changes and removals to keep the validator configuration valid throughout the rotation.
* Pass at least the configured guardian threshold. Every guardian in the `guardians` array is asked to sign.
* Recovery is complete only after the final call lands. Old and new owners can both remain valid during the sequence.
* If a call fails, do not regenerate a different sequence against stale state. Read the account state again and rebuild the remaining recovery calls.

## Nexus accounts

For Nexus with Ownable V0, pass the same module address used by the account configuration:

```ts theme={null}
const recoveryCalls = await recoverEcdsaOwnership({
  accountAddress: rhinestoneAccount.getAddress(),
  chain,
  config: rhinestoneAccount.config,
  newOwners: {
    type: "ecdsa",
    accounts: [newOwnerAccount],
    module: ownableV0Address,
  },
});
```

Ownable V0 does not support legible EIP-712 signing. The SDK uses its personal-sign fallback for typed data and intents.
