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

# ECDSA

> Control a smart account with standard Ethereum keys.

export const VmCodeGroup = ({children, caption, label = "Virtual machine"}) => {
  const [vm, setVm] = useState("evm");
  const [open, setOpen] = useState(false);
  const selectorRef = useRef(null);
  const triggerRef = useRef(null);
  const menuRef = useRef(null);
  const focusIndex = useRef(0);
  const options = [{
    value: "evm",
    title: "EVM",
    icon: "/images/networks/ethereum.svg"
  }, {
    value: "solana",
    title: "Solana",
    icon: "/images/networks/solana.svg"
  }];
  if (typeof caption !== "string" || caption.trim() === "") {
    throw new Error("VmCodeGroup requires a non-empty caption.");
  }
  const selected = options.find(option => option.value === vm);
  const blocks = (Array.isArray(children) ? children : [children]).filter(child => typeof child !== "string" || child.trim() !== "");
  if (blocks.length !== 2 || !blocks.every(block => typeof block?.props?.language === "string" && block.props.numberOfLines !== undefined) || !options.every(option => blocks.filter(block => block.props.filename === option.title).length === 1)) {
    throw new Error("VmCodeGroup requires two fenced code blocks titled EVM and Solana, with no other content.");
  }
  useEffect(() => {
    const path = window.location.pathname;
    const selection = window.__rhinestoneDocsVm;
    if (selection?.path === path) setVm(selection.vm);
    const onChange = event => {
      if (event.detail.path === path) {
        setVm(event.detail.vm);
        setOpen(false);
      }
    };
    window.addEventListener("rhinestone-docs-vm", onChange);
    return () => window.removeEventListener("rhinestone-docs-vm", onChange);
  }, []);
  useEffect(() => {
    if (!open) return;
    menuRef.current?.querySelectorAll("button")[focusIndex.current]?.focus();
    const dismiss = event => {
      if (!selectorRef.current?.contains(event.target)) setOpen(false);
    };
    document.addEventListener("pointerdown", dismiss);
    return () => document.removeEventListener("pointerdown", dismiss);
  }, [open]);
  const selectVm = value => {
    const selection = {
      path: window.location.pathname,
      vm: value
    };
    window.__rhinestoneDocsVm = selection;
    window.dispatchEvent(new CustomEvent("rhinestone-docs-vm", {
      detail: selection
    }));
    triggerRef.current?.focus();
  };
  const openMenu = index => {
    focusIndex.current = index;
    setOpen(true);
    menuRef.current?.querySelectorAll("button")[index]?.focus();
  };
  const handleMenuKey = event => {
    const buttons = Array.from(menuRef.current.querySelectorAll("button"));
    const current = buttons.indexOf(document.activeElement);
    let next;
    if (event.key === "ArrowDown") next = (current + 1) % buttons.length;
    if (event.key === "ArrowUp") next = (current + buttons.length - 1) % buttons.length;
    if (event.key === "Home" || event.key.toLowerCase() === "e") next = 0;
    if (event.key === "End" || event.key.toLowerCase() === "s") next = buttons.length - 1;
    if (next !== undefined) {
      event.preventDefault();
      buttons[next].focus();
    }
    if (event.key === "Tab") setOpen(false);
  };
  return <div className="vm-code-group">
      <div className="vm-code-group-header">
        <span className="vm-code-caption" title={caption}>
          {caption}
        </span>
        <div className="vm-code-selector" ref={selectorRef} onBlur={event => {
    if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
  }} onKeyDown={event => {
    if (event.key === "Escape" && open) {
      event.preventDefault();
      setOpen(false);
      triggerRef.current?.focus();
    }
  }}>
          <button type="button" className="vm-code-trigger" ref={triggerRef} aria-label={`${label}: ${selected.title}`} aria-haspopup="menu" aria-expanded={open} onClick={() => open ? setOpen(false) : openMenu(options.indexOf(selected))} onKeyDown={event => {
    if (event.key === "ArrowDown" || event.key === "ArrowUp") {
      event.preventDefault();
      openMenu(event.key === "ArrowDown" ? 0 : options.length - 1);
    }
  }}>
            <img className="vm-code-icon" src={selected.icon} alt="" width="16" height="16" />
            <span>{selected.title}</span>
            <svg className="vm-code-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
              <path d="m7 10 5 5 5-5" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
            </svg>
          </button>
          {open && <div className="vm-code-menu" role="menu" aria-label={label} ref={menuRef} onKeyDown={handleMenuKey}>
              {options.map(option => <button type="button" role="menuitemradio" aria-checked={vm === option.value} tabIndex={-1} className="vm-code-option" key={option.value} onClick={() => selectVm(option.value)}>
                  <img className="vm-code-icon" src={option.icon} alt="" width="16" height="16" />
                  <span>{option.title}</span>
                  {vm === option.value && <svg className="vm-code-check" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                      <path d="m5 12 4 4L19 6" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
                    </svg>}
                </button>)}
            </div>}
        </div>
      </div>
      {options.map(option => {
    const block = blocks.find(block => block.props.filename === option.title);
    const CodeBlock = block.type;
    return <div key={option.value} className="vm-code-group-panel" data-vm={option.value} hidden={option.value !== vm}>
            <CodeBlock {...block.props} filename={undefined} />
          </div>;
  })}
    </div>;
};

Choose ECDSA for server wallets, agents, existing wallet clients, or any integration that already produces Ethereum signatures. You are responsible for securing and recovering the key.

## Configure an ECDSA owner

Initialize the SDK and a viem secp256k1 account once for either VM:

```ts theme={null}
import { RhinestoneSDK } from '@rhinestone/sdk'
import type { Hex } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'

const apiKey = process.env.RHINESTONE_API_KEY
const privateKey = process.env.OWNER_PRIVATE_KEY

if (!apiKey || !privateKey) {
  throw new Error('RHINESTONE_API_KEY and OWNER_PRIVATE_KEY are required')
}

const owner = privateKeyToAccount(privateKey as Hex)
const rhinestone = new RhinestoneSDK({
  auth: { mode: 'apiKey', apiKey },
})

```

Use the signer as the account owner:

<VmCodeGroup caption="Configure an ECDSA owner">
  ```ts EVM theme={null}
  // Defaults to the Ownable Validator with a threshold of one.
  const account = await rhinestone.createAccount({
    evm: {
      owners: {
        type: 'ecdsa',
        accounts: [owner],
      },
    },
  })
  ```

  ```ts Solana theme={null}
  import { createSolanaSwigId } from '@rhinestone/sdk/solana'

  // Persist id and swig; deploy the Swig before funding its wallet.
  // Deployment requires the owner's secp256k1 public key.
  const { id, swig } = createSolanaSwigId()
  const account = await rhinestone.createAccount({
    solana: { swig, owner: { type: 'ecdsa', account: owner } },
  })
  ```
</VmCodeGroup>

Follow [account configuration](/wallets/custom-signer/account-setup/create-an-account) for deployment and restoration. For remote signing requirements, see [Custom signing](/wallets/custom-signer/signing/custom-signing).

<Warning>
  Never expose a private key in client code, logs, or source control. Use a key management system for production server signers.
</Warning>

## Use more than one owner

For EVM, use [multisig](/wallets/custom-signer/account-setup/native-signers/multisig) when several ECDSA owners should control one account. Use [multi-factor authentication](/wallets/custom-signer/account-setup/native-signers/multi-factor-authentication) when you need different validator types, such as ECDSA and passkeys.

## Manage EVM owners

Use the SDK actions to change an account after deployment:

* [`addOwner`](/wallets/custom-signer/sdk-reference/actions/ecdsa/add-owner)
* [`removeOwner`](/wallets/custom-signer/sdk-reference/actions/ecdsa/remove-owner)
* [`changeThreshold`](/wallets/custom-signer/sdk-reference/actions/ecdsa/change-threshold)

## Manage Solana authorities

An authorized `all` or `manageAuthority` role can add or remove one non-root key per transaction. Continue the Solana setup above with a separate local account `additionalOwner` (or pass its secp256k1 public key, never its EVM address):

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

const addAuthority = {
  destination: {
    chain: solanaDevnet,
    authority: addEcdsaKey(additionalOwner, { permission: 'allButManageAuthority' }),
  },
}
const removeAuthority = {
  destination: {
    chain: solanaDevnet,
    authority: removeEcdsaKey(additionalOwner),
  },
}
```

Choose permissions explicitly:

* `allButManageAuthority`: spend and run instructions, but not change authorities.
* `all`: spend, run instructions, and manage non-root authorities.
* `manageAuthority`: manage non-root authorities, but not spend or run instructions. It can grant `all` to another key, so it still confers takeover power.

Send one change through the [shared transaction lifecycle](/transactions/multichain/send-a-transaction). Authority changes are always sponsored, including rent for a new role; do not add `source` or `sponsored` fields. Settle any previous submission before retrying. `getAuthorityStatus(addAuthority)` returns `applied` only for the exact requested permission; `conflict` is not ready.

The root is permanent. Removing the root, a missing key, or the last authority-management role is refused. Multiple roles are not threshold multisig or MFA. Keep local signer configuration synchronized with the roles onchain; see [Solana support](/wallets/custom-signer/solana-support).


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