Signing Messages
Sign personal messages (EIP-191) and EIP-712 typed data using threshold ECDSA
Signing Messages
The Nero MPC SDK signs messages using threshold cryptography where the full private key is never reconstructed on the client device during normal operation. Both DKLS (2-party threshold ECDSA) and Pedersen (DKG with ERC-4337) protocols are supported.
Dual-Protocol Signing Architecture
| Protocol | Implementation | Account Type |
|---|---|---|
dkls | DKLSClient | EOA (Externally Owned Account) |
pedersen | SmartWallet via SigningClient | Smart Account (ERC-4337) |
The protocol selection in your SDK configuration determines which signing path is taken for all operations.
Personal Messages (EIP-191)
Message signing implements the Ethereum personal_sign standard, which prefixes messages with "\x19Ethereum Signed Message:\n" + len(message) before hashing. Both protocols support raw string and Uint8Array message inputs.
const message = "Hello, NERO!";
const signature = await sdk.signMessage(message);DKLS path: Converts the message to a string if it's a Uint8Array, then delegates to DKLSClient.signMessage().
Pedersen path: Uses SmartWallet.signMessage(), which internally coordinates with the backend using a SigningClient.
Typed Data (EIP-712)
Sign structured data with domain separation:
const typedData = {
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
],
Transfer: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" },
],
},
primaryType: "Transfer",
domain: {
name: "MyApp",
version: "1",
chainId: 689,
},
message: {
to: "0x1234...",
amount: "1000000000000000000",
},
};
const signature = await sdk.signTypedData(typedData);DKLS path: Delegates to DKLSClient.signTypedData(), which performs EIP-712 encoding before generating the signature.
Pedersen path: Uses SmartWallet.signTypedData(), following the same MPC coordination pattern as message signing but with EIP-712 encoding.
Signature Format
All signing methods return signatures as hex-encoded strings in the format 0x<r><s><v>.
Signatures conform to the Signature interface:
| Field | Description |
|---|---|
r | First 32 bytes of the signature |
s | Second 32 bytes of the signature |
v | Recovery identifier |
fullSignature | Complete hex-encoded signature string |
EIP-1193 Provider Integration
The SDK exposes an EIP-1193 compatible provider for signing through standard Web3 libraries:
const provider = sdk.getProvider();
// personal_sign: converts hex or UTF-8 messages to bytes
await provider.request({
method: "personal_sign",
params: [message, address],
});
// eth_signTypedData_v4: parses JSON typed data
await provider.request({
method: "eth_signTypedData_v4",
params: [address, JSON.stringify(typedData)],
});With ethers.js
import { BrowserProvider } from "ethers";
const ethersProvider = new BrowserProvider(sdk.getProvider());
const signer = await ethersProvider.getSigner();
const signature = await signer.signMessage("Hello, NERO!");With viem
import { createWalletClient, custom } from "viem";
const client = createWalletClient({
transport: custom(sdk.getProvider()),
});Error Handling
| Error Code | Scenario | Thrown By |
|---|---|---|
NO_WALLET | Signing attempted before wallet creation | SDK signing method guards |
INVALID_NONCE_COMMITMENT | Backend provided malformed nonce | SigningClient validation |
INVALID_PARTIAL_SIGNATURE | Backend partial signature verification failed | SigningClient verification logic |
Security Model
- No Full Key Reconstruction: During signing operations, the full private key is never reconstructed on the client. Only partial signatures are computed locally.
- Share Protection: Client shares are stored encrypted in
IndexedDBStorageorMemoryStorageusing a device-specific key.
The threshold signature protocol ensures that neither party alone can forge signatures without the other's participation, maintaining non-custodial security while enabling responsive UX through client-side partial signature generation.