Nillion-Ethereum Integration: MetaMask + EIP-712 DID Generation

Technical documentation for Ethereum wallet integration with Nillion using EIP-712 signatures and DIDs.
Overview
Nillion integrates with Ethereum wallets via MetaMask to create passwordless authentication. Ethereum addresses are transformed into did:ethr: format DIDs through EIP-712 typed data signing.
Key Components:
- DID Format:
did:ethr:<ethereum_address> - Signing Standard: EIP-712 typed data
- SDK:
@nillion/nuchandles DID generation - Client Library:
viemfor Ethereum interactions - Storage: React state (runtime), localStorage (tokens), NilDB (profiles)
Authentication Flow
MetaMask Connection → DID Generation → Subscription Check
→ Token Generation → Profile Registration → Storage
Files:
src/context/NillionContext.tsx- Wallet/signing logicsrc/hooks/useInitializeSessionMutation.ts- New session flowsrc/hooks/useLoginMutation.ts- Existing session restorationsrc/context/AuthFlowManager.tsx- Flow orchestration
1. DID Generation
DIDs are generated by the Nillion SDK, not manually constructed.
// Create signer from Web3 wallet
const nucSigner = Signer.fromWeb3({
getAddress: async () => account, // MetaMask address
signTypedData: async (domain, types, message) => {
return walletClient.signTypedData({...});
},
});
// Generate DID
const did = await nucSigner.getDid();
// Result: { didString: "did:ethr:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" }
Location: src/context/NillionContext.tsx (lines 72-101)
2. MetaMask Signing
EIP-712 signatures occur in the signTypedData callback passed to Signer.fromWeb3().
signTypedData: async (domain, types, message) => {
const primaryType = Object.keys(types).find(k => k !== "EIP712Domain");
// Handle chain switching if needed
const domainChainId = Number(domain?.chainId);
if (domainChainId !== activeChainId) {
await metaMaskProvider.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: `0x${domainChainId.toString(16)}` }],
});
}
// Sign via viem
return walletClient.signTypedData({
account,
domain,
types,
primaryType,
message,
});
}
Signing Triggers:
- Root token generation (
refreshRootToken()) - Invocation token creation (
signAndSerialize(signer)) - Builder profile registration
Location: src/context/NillionContext.tsx (lines 74-98)
3. EIP-712 Payload Structure
The Nillion SDK (@nillion/nuc) determines the exact EIP-712 structure. The application receives and passes this payload to MetaMask.
Inferred Structure (based on standard usage):
// Root Token Generation
{
domain: {
name: "Nillion",
version: "1",
chainId: 1 | 11155111 // Mainnet or Sepolia
},
types: {
EIP712Domain: [...],
Token: [
{ name: "subscriber", type: "address" },
{ name: "service", type: "string" },
{ name: "nonce", type: "uint256" }
]
},
message: {
subscriber: "0x742d35Cc...",
service: "nildb",
nonce: 123
}
}
// Invocation Token
await Builder.invocationFrom(rootToken)
.audience(node.id)
.command(NucCmd.nil.db.root)
.signAndSerialize(signer); // Triggers EIP-712 signing
Supported Chains:
- Mainnet (chainId: 1)
- Sepolia (chainId: 11155111)
4. Token Architecture
Token Hierarchy
Root Token (Subscriber Identity)
└─ Invocation Tokens (Per-Operation)
├─ Node-specific (audience: did:key:node_id)
└─ Command-specific (NucCmd.nil.db.*)
Root Token
- Purpose: Proves subscription to Nillion
- Generation: Nilauth service validates signature, returns token
- Format: Base64URL-encoded JWT
- Storage:
localStorageasnillion_rootToken
Invocation Tokens
- Purpose: Authorize specific operations on specific nodes
- Generation: Client-side via
Builder.invocationFrom(rootToken) - Audience: Individual node DIDs (
did:key:z6Mk...) - Storage:
localStorageasnillion_nildbTokens(JSON object)
// Generate invocation token
const invocation = await Builder.invocationFrom(rootToken)
.audience(node.id) // Target node
.command(NucCmd.nil.db.root) // Authorized operation
.signAndSerialize(signer); // EIP-712 sign
Location: src/hooks/useInitializeSessionMutation.ts (lines 39-57)
5. Storage & Persistence
Client-Side Storage
React State (Runtime):
{
signer: NucSigner,
did: "did:ethr:0x...",
wallets: {
isMetaMaskConnected: true,
metaMaskAddress: "0x..."
}
}
localStorage (Persistent):
localStorage.setItem("nillion_rootToken", "eyJhbGci...");
localStorage.setItem("nillion_nildbTokens", JSON.stringify({
"did:key:node1": "eyJhbGci...",
"did:key:node2": "eyJhbGci...",
"did:key:node3": "eyJhbGci..."
}));
NilDB Storage
Builder Profiles:
{
did: "did:ethr:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
name: "Demo Builder"
// Encrypted with Blindfold, replicated across 3 nodes
}
Location: src/hooks/usePersistedConnection.ts, src/hooks/useInitializeSessionMutation.ts
6. Network Architecture
Endpoints
// Nilauth (Authentication Service)
https://nilauth.sandbox.app-cluster.sandbox.nilogy.xyz
// NilDB Nodes (Data Storage)
https://nildb-stg-n1.nillion.network
https://nildb-stg-n2.nillion.network
https://nildb-stg-n3.nillion.network
// Nilchain RPC (Internal to SDK)
http://rpc.testnet.nilchain-rpc-proxy.nilogy.xyz
Client Architecture
Browser
├─ MetaMask (EIP-712 signing)
├─ Nillion SDK (DID generation, tokens)
├─ React App (UI, state management)
└─ HTTP Requests → Nillion Network
├─ Nilauth (subscription check, root token)
└─ NilDB Nodes (profile read/write)
Location: src/config.ts
7. Verification
All verification happens server-side on the Nillion Network.
Nilauth Service:
- Validates EIP-712 signatures on root tokens
- Checks subscription status
- Returns signed root tokens
NilDB Nodes:
- Verify invocation token signatures
- Validate token audience (matches node DID)
- Authorize commands (NucCmd permissions)
What Gets Verified:
- EIP-712 signature validity
- DID format (
did:ethr:0x...) - Subscription status
- Token authenticity
- Token audience/commands
8. Complete Flow Examples
New Session (First-Time Login)
// 1. Connect MetaMask
const account = await metaMaskProvider.request({
method: "eth_requestAccounts"
});
// 2. Generate DID
const signer = Signer.fromWeb3({...});
const did = await signer.getDid();
// Result: "did:ethr:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"
// 3. Check subscription
const subscription = await nillionClient.fetchNodeSubscriberInfo(did.didString);
// 4. Generate root token (triggers EIP-712 signature)
const rootToken = await nillionClient.refreshRootToken({
signer,
service: "nildb"
});
// 5. Generate invocation tokens (one per node)
const invocations = await Promise.all(nodes.map(node =>
Builder.invocationFrom(rootToken)
.audience(node.id)
.command(NucCmd.nil.db.root)
.signAndSerialize(signer)
));
// 6. Register builder profile
await nillionClient.createProfile({
auth: { invocations },
blindfold: { operation: "store" }
});
// 7. Store tokens
localStorage.setItem("nillion_rootToken", rootToken);
localStorage.setItem("nillion_nildbTokens", JSON.stringify(invocations));
Existing Session (Returning User)
// 1. Connect MetaMask
const account = await metaMaskProvider.request({
method: "eth_requestAccounts"
});
// 2. Restore DID from signer
const signer = Signer.fromWeb3({...});
const did = await signer.getDid();
// 3. Load tokens from localStorage
const rootToken = localStorage.getItem("nillion_rootToken");
const nildbTokens = JSON.parse(localStorage.getItem("nillion_nildbTokens"));
// 4. Validate by reading profile
const profile = await nillionClient.readProfile({
auth: { invocations: nildbTokens }
});
// 5. Restore state
setState({ signer, did: did.didString, wallets: {...} });
Location:
- New:
src/hooks/useInitializeSessionMutation.ts - Existing:
src/hooks/useLoginMutation.ts
9. DID Usage Patterns
As Identity
// Subscription lookup
const info = await nillionClient.fetchNodeSubscriberInfo(
"did:ethr:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"
);
// Profile operations
await nillionClient.createProfile({ /* DID embedded in auth */ });
const profile = await nillionClient.readProfile({ /* ... */ });
As Namespace
// Storage key
{
did: "did:ethr:0x742d35Cc...", // Primary key
name: "Demo Builder",
// ... other fields
}
As Cryptographic Proof
// Token claims
{
subscriber: "did:ethr:0x742d35Cc...",
// Signature proves control of Ethereum address
}
10. Error Handling
Chain Mismatch
// Automatically switch chains
if (domainChainId !== activeChainId) {
await metaMaskProvider.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: hex }],
});
}
// Fallback: attempt signature anyway
Duplicate Registration
// NilDB returns 400 on duplicate profile
if (error.code === 400 && error.message.includes("DuplicateEntry")) {
// Gracefully continue - profile already exists
}
Token Expiration
// Refresh root token
const newRootToken = await nillionClient.refreshRootToken({
signer,
service: "nildb"
});
Technical Requirements
Browser Requirements:
- MetaMask installed
- Active Nillion subscription
- Supported Ethereum chain (Mainnet or Sepolia)
Network Access:
- CORS-enabled endpoints
- No direct RPC interaction (handled by MetaMask/SDK)
Security:
- All signatures occur in MetaMask (user approval required)
- Tokens stored client-side (localStorage)
- Profiles encrypted with Blindfold in NilDB
Key Takeaways
- SDK-Driven: Nillion SDK handles DID generation, payload construction, and cryptographic operations
- Client-Side Flow: Entire authentication process runs in browser
- Server-Side Verification: Signature validation occurs on Nillion Network backend
- Token Hierarchy: Root token → invocation tokens → authorized operations
- EIP-712 Standard: Ethereum-native signing for wallet integration
- Storage Strategy: Runtime (React state), persistent (localStorage), distributed (NilDB)
Codebase: https://github.com/geniusyinka/nillion-metamask-login
Network: Nillion Testnet
Chains: Ethereum Mainnet (1), Sepolia (11155111)
