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
- User is authenticated and has a wallet (Login + Create a Wallet)
@nerochain/mpc-sdkinstalled
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
| State | UI |
|---|---|
| Signing in progress | Button disabled, text changes to "Signing..." |
| Signature complete | Hex signature displayed |
| Transaction sent | Transaction hash displayed after confirmation |
| Device not trusted | Message prompting device verification |
| Network error | Error message from the hook's error property |
Hooks reference
| Hook method | Purpose |
|---|---|
signMessage(msg) | EIP-191 personal message signature |
signTypedData(domain, types, primaryType, value) | EIP-712 structured data signature |
isSigning | true while any signing operation is in progress |
error | Last error from a signing operation, cleared on next call |