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

> Recover EVM owners with guardians or add a Solana passkey with a recovery key.

EVM guardian 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 EVM recovery, 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('evm'),
      chain,
      config: rhinestoneAccount.config.evm,
      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('evm'),
    );

    const recoveryCalls = await recoverPasskeyOwnership({
      accountAddress: rhinestoneAccount.getAddress('evm'),
      chain,
      config: rhinestoneAccount.config.evm,
      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.

## Use the recovered account

After the final recovery call is confirmed, create a new SDK handle with the complete replacement `evm.owners` configuration, including its threshold and the same validator module. The old handle still uses the old signers.

Preserve the original account address and deployment metadata in `evm.initData` using the [existing-account setup](/wallets/custom-signer/configuration/bring-your-own-account). Changing owners without attaching the original identity can derive a different account. An address alone is sufficient only when the account is already deployed on every chain you will use; retain the original factory and initialization data for deployment on additional chains.

Recovery changes ownership only on the chain where the calls execute. Verify the owner state on each chain before using the replacement signers there.

## EVM 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('evm'),
  chain,
  config: rhinestoneAccount.config.evm,
  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.

## Solana: install a replacement passkey

Use the recovery key [enrolled beforehand](/wallets/custom-signer/recovery/set-up#solana-enroll-a-recovery-key) with `manageAuthority` or `all`. This is an authority-management procedure, not guardian recovery. Reattach the saved `swig` with the recovery signer; changing the local owner does not change on-chain roles or derive a replacement wallet.

The example continues your shared authenticated `rhinestone` instance, saved `swig`, viem secp256k1 `recoverySigner`, and newly created viem WebAuthn `newPasskey`. Keep the recovery key inside its signer. Network availability depends on your environment; `solanaDevnet` is illustrative.

```ts theme={null}
import { addPasskey, solanaDevnet } from '@rhinestone/sdk/solana'

const recoveryAccount = await rhinestone.createAccount({
  solana: {
    swig,
    owner: { type: 'ecdsa', account: recoverySigner },
  },
})
const installReplacement = {
  destination: {
    chain: solanaDevnet,
    authority: addPasskey(newPasskey, { permission: 'all' }),
  },
}

const before = await recoveryAccount.getAuthorityStatus(installReplacement)
if (before.status === 'conflict') {
  throw new Error('Replacement key has a different authority permission')
}
if (before.status === 'notApplied') {
  const prepared = await recoveryAccount.prepareTransaction(installReplacement)
  const signed = await recoveryAccount.signTransaction(prepared)
  await recoveryAccount.waitForExecution(
    await recoveryAccount.submitTransaction(signed),
  )
}

const after = await recoveryAccount.getAuthorityStatus(installReplacement)
if (after.status !== 'applied') {
  throw new Error('Replacement authority is not applied')
}

const recoveredAccount = await rhinestone.createAccount({
  solana: {
    swig,
    owner: { type: 'passkey', account: newPasskey },
  },
})
```

`all` grants spending and authority-management power. Choose `allButManageAuthority` instead only if the replacement should spend without managing keys and you retain a separate authority manager. These permissions are not session budgets or expiry policies.

The role check verifies the requested key and permission, not every aspect of account readiness. Before retrying an uncertain submission, reconcile its intent status through the shared lifecycle. Use the new handle for subsequent signing; the old handle still uses the recovery key.

Installing a replacement does not remove any old role. An authorized manager can use `removePasskey` or `removeEcdsaKey` from `@rhinestone/sdk/solana` in a separate authority transaction to remove a non-root key. The permanent root and the last authority manager cannot be removed. If the lost or compromised passkey was the root, this procedure cannot revoke it; migrate assets to a new wallet when compromise is suspected.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.