Skip to main content

One post tagged with "ethereum"

View All Tags

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

· 6 min read
Nillion Team
Nillion Development Team

Understanding nilDB Encryption

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/nuc handles DID generation
  • Client Library: viem for 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 logic
  • src/hooks/useInitializeSessionMutation.ts - New session flow
  • src/hooks/useLoginMutation.ts - Existing session restoration
  • src/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: localStorage as nillion_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: localStorage as nillion_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

  1. SDK-Driven: Nillion SDK handles DID generation, payload construction, and cryptographic operations
  2. Client-Side Flow: Entire authentication process runs in browser
  3. Server-Side Verification: Signature validation occurs on Nillion Network backend
  4. Token Hierarchy: Root token → invocation tokens → authorized operations
  5. EIP-712 Standard: Ethereum-native signing for wallet integration
  6. 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)