NERO
Guides

Sign Messages & Transactions

Sign personal messages, EIP-712 typed data, and send on-chain transactions using MPC threshold signatures

Sign Messages & Transactions

Once a user has a wallet, you can sign messages and send transactions. Every signing operation uses the MPC threshold protocol — the private key is never reconstructed on any single machine.

Prerequisites

Sign a personal message

signMessage() produces an EIP-191 personal_sign signature. Pass a plain string and receive a hex-encoded signature.

import { useNeroWallet } from "@nerochain/mpc-sdk/react";
import { useState } from "react";

function SignMessageDemo() {
  const { signMessage, isSigning, error } = useNeroWallet();
  const [message, setMessage] = useState("Hello NERO!");
  const [signature, setSignature] = useState<string | null>(null);

  const handleSign = async () => {
    try {
      setSignature(null);
      const sig = await signMessage(message);
      setSignature(sig);
    } catch {
      // error state is surfaced via the hook's `error` property
    }
  };

  return (
    <div>
      <input
        type="text"
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        placeholder="Message to sign"
      />
      <button onClick={handleSign} disabled={isSigning}>
        {isSigning ? "Signing..." : "Sign Message"}
      </button>

      {signature && (
        <pre style={{ wordBreak: "break-all" }}>{signature}</pre>
      )}
      {error && <p style={{ color: "red" }}>{error.message}</p>}
    </div>
  );
}

Sign EIP-712 typed data

signTypedData() signs structured data according to the EIP-712 standard. It takes four arguments: domain, types, primary type, and the message value.

import { useNeroWallet } from "@nerochain/mpc-sdk/react";
import { useState } from "react";

function SignTypedDataDemo() {
  const { signTypedData, isSigning, error } = useNeroWallet();
  const [signature, setSignature] = useState<string | null>(null);

  const handleSign = async () => {
    try {
      setSignature(null);
      const sig = await signTypedData(
        { name: "MyDapp", version: "1", chainId: 1559 },
        { Action: [{ name: "content", type: "string" }] },
        "Action",
        { content: "Approve transfer" },
      );
      setSignature(sig);
    } catch {
      // error state is surfaced via the hook's `error` property
    }
  };

  return (
    <div>
      <button onClick={handleSign} disabled={isSigning}>
        {isSigning ? "Signing..." : "Sign Typed Data (EIP-712)"}
      </button>

      {signature && (
        <pre style={{ wordBreak: "break-all" }}>{signature}</pre>
      )}
      {error && <p style={{ color: "red" }}>{error.message}</p>}
    </div>
  );
}

The domain object typically includes your dApp name, version, and chain ID. Types define the structure of the data being signed. The primary type identifies which type definition is the top-level message.

Send a transaction with ethers.js

The SDK exposes an EIP-1193 provider that plugs into ethers.js (or viem). Use useNeroMpcAuth to access the underlying SDK instance, then wrap its provider in a BrowserProvider.

import { useNeroMpcAuth, useNeroWallet } from "@nerochain/mpc-sdk/react";
import { BrowserProvider, parseEther } from "ethers";
import { useState } from "react";

function SendTransactionDemo() {
  const { sdk } = useNeroMpcAuth();
  const { wallet, isSigning, error } = useNeroWallet();
  const [txHash, setTxHash] = useState<string | null>(null);
  const [sending, setSending] = useState(false);

  const handleSend = async () => {
    if (!sdk) return;
    setSending(true);
    try {
      setTxHash(null);
      const provider = new BrowserProvider(sdk.getProvider());
      const signer = await provider.getSigner();

      const tx = await signer.sendTransaction({
        to: "0xRecipientAddress",
        value: parseEther("0.01"),
      });

      const receipt = await tx.wait();
      setTxHash(receipt?.hash ?? tx.hash);
    } catch (err) {
      console.error("Transaction failed:", err);
    } finally {
      setSending(false);
    }
  };

  return (
    <div>
      <p>From: {wallet?.eoaAddress}</p>
      <button onClick={handleSend} disabled={sending || isSigning}>
        {sending ? "Sending..." : "Send 0.01 NERO"}
      </button>

      {txHash && <p>Confirmed: {txHash}</p>}
      {error && <p style={{ color: "red" }}>{error.message}</p>}
    </div>
  );
}

signer.sendTransaction() triggers the MPC co-signing protocol under the hood. The SDK handles the threshold signature, assembles the final ECDSA signature, and broadcasts the signed transaction.

Handle device verification errors

If a signing operation fails with a DEVICE_NOT_TRUSTED error, the user's device needs to be verified before signing can proceed. Check for this error code and prompt the user accordingly.

import { useNeroWallet } from "@nerochain/mpc-sdk/react";

function SignWithDeviceCheck() {
  const { signMessage, isSigning, error } = useNeroWallet();

  const isDeviceError =
    error &&
    ((error as { code?: string }).code === "DEVICE_NOT_TRUSTED" ||
      error.message.includes("device"));

  const handleSign = async () => {
    try {
      await signMessage("Hello NERO!");
    } catch {
      // error state is surfaced via the hook's `error` property
    }
  };

  if (isDeviceError) {
    return (
      <div>
        <p>This device needs verification before signing.</p>
        <p>
          Check your email for a verification link, or verify through
          the NERO dashboard.
        </p>
      </div>
    );
  }

  return (
    <button onClick={handleSign} disabled={isSigning}>
      {isSigning ? "Signing..." : "Sign Message"}
    </button>
  );
}

Complete example

A full component combining personal signatures, typed data, and transaction sending:

import {
  useNeroMpcAuth,
  useNeroUser,
  useNeroWallet,
} from "@nerochain/mpc-sdk/react";
import { BrowserProvider, parseEther } from "ethers";
import { useState } from "react";

function SigningPage() {
  const { sdk } = useNeroMpcAuth();
  const { user, isAuthenticated } = useNeroUser();
  const { wallet, hasWallet, signMessage, signTypedData, isSigning, error } =
    useNeroWallet();
  const [result, setResult] = useState<Record<string, unknown> | null>(null);

  if (!isAuthenticated || !hasWallet) {
    return <p>Please log in and generate a wallet first.</p>;
  }

  const handlePersonalSign = async () => {
    try {
      setResult(null);
      const sig = await signMessage("Hello NERO!");
      setResult({ type: "personal_sign", message: "Hello NERO!", signature: sig });
    } catch {
      // error surfaced via hook
    }
  };

  const handleTypedSign = async () => {
    try {
      setResult(null);
      const sig = await signTypedData(
        { name: "ExampleDapp", version: "1", chainId: 1559 },
        { Action: [{ name: "content", type: "string" }] },
        "Action",
        { content: "Approve transfer" },
      );
      setResult({ type: "eip712", signature: sig });
    } catch {
      // error surfaced via hook
    }
  };

  const handleSendTx = async () => {
    if (!sdk) return;
    try {
      setResult(null);
      const provider = new BrowserProvider(sdk.getProvider());
      const signer = await provider.getSigner();
      const tx = await signer.sendTransaction({
        to: "0xRecipientAddress",
        value: parseEther("0.001"),
      });
      const receipt = await tx.wait();
      setResult({ type: "transaction", hash: receipt?.hash ?? tx.hash });
    } catch (err) {
      setResult({
        type: "transaction",
        error: err instanceof Error ? err.message : String(err),
      });
    }
  };

  return (
    <div>
      <p>Wallet: {wallet?.eoaAddress}</p>

      <div style={{ display: "flex", gap: 8, marginTop: 16 }}>
        <button onClick={handlePersonalSign} disabled={isSigning}>
          {isSigning ? "Signing..." : "Sign Message"}
        </button>
        <button onClick={handleTypedSign} disabled={isSigning}>
          {isSigning ? "Signing..." : "Sign Typed Data"}
        </button>
        <button onClick={handleSendTx} disabled={isSigning}>
          {isSigning ? "Sending..." : "Send Transaction"}
        </button>
      </div>

      {error && <p style={{ color: "red" }}>{error.message}</p>}
      {result && <pre>{JSON.stringify(result, null, 2)}</pre>}
    </div>
  );
}

What the user sees

StateUI
Signing in progressButton disabled, text changes to "Signing..."
Signature completeHex signature displayed
Transaction sentTransaction hash displayed after confirmation
Device not trustedMessage prompting device verification
Network errorError message from the hook's error property

Hooks reference

Hook methodPurpose
signMessage(msg)EIP-191 personal message signature
signTypedData(domain, types, primaryType, value)EIP-712 structured data signature
isSigningtrue while any signing operation is in progress
errorLast error from a signing operation, cleared on next call

On this page