惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
S
Secure Thoughts
V2EX - 技术
V2EX - 技术
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
O
OpenAI News
D
DataBreaches.Net
The Cloudflare Blog
L
LangChain Blog
Last Week in AI
Last Week in AI
酷 壳 – CoolShell
酷 壳 – CoolShell
The Register - Security
The Register - Security
MyScale Blog
MyScale Blog
Help Net Security
Help Net Security
Jina AI
Jina AI
T
The Blog of Author Tim Ferriss
T
The Exploit Database - CXSecurity.com
D
Darknet – Hacking Tools, Hacker News & Cyber Security
博客园 - 叶小钗
The Last Watchdog
The Last Watchdog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
www.infosecurity-magazine.com
www.infosecurity-magazine.com
TaoSecurity Blog
TaoSecurity Blog
博客园 - 司徒正美
aimingoo的专栏
aimingoo的专栏
C
Cyber Attacks, Cyber Crime and Cyber Security
A
Arctic Wolf
T
Tenable Blog
Know Your Adversary
Know Your Adversary
Google DeepMind News
Google DeepMind News
量子位
The Hacker News
The Hacker News
AWS News Blog
AWS News Blog
Apple Machine Learning Research
Apple Machine Learning Research
Vercel News
Vercel News
Hacker News: Ask HN
Hacker News: Ask HN
Scott Helme
Scott Helme
小众软件
小众软件
Recent Commits to openclaw:main
Recent Commits to openclaw:main
T
Threat Research - Cisco Blogs
G
GRAHAM CLULEY
H
Heimdal Security Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
博客园_首页
B
Blog
GbyAI
GbyAI
P
Palo Alto Networks Blog
美团技术团队
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - Franky

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
I Built a Midnight dApp Like an ETH Dev. Here's Everything I Got Wrong.
Tushar Pamna · 2026-05-07 · via DEV Community

A brutally honest post-mortem on migrating from a server-managed wallet to the 1AM browser extension on the Midnight Network.

The Decision That Cost Me Two Days

I want to be specific about the moment I made the mistake, because it's a very recognizable moment.

Contracts written. Deployed on preprod. ZK proofs generating correctly through the CLI. I was looking at green checkmarks across the board and thinking: frontend time. The hard part was done.

That's the exact mental state where bad architectural decisions happen. You're riding the momentum of something working, and the last thing you want is to slow down and think carefully about something that feels like plumbing.

So I didn't. I looked at Midnight's SDK, saw a server-side wallet pattern that worked perfectly in Node.js, and made a decision I immediately told myself I'd "clean up later":

For simplicity, I'll just use a single server-side wallet for all transactions.

I've been writing software long enough to know that "clean up later" is a specific kind of lie developers tell themselves. But I told it anyway.

Later arrived. It cost me two days. Here's the full damage report.

What "Server Wallet" Actually Meant

Here's the thing about this mistake: it's completely understandable. If you come from Ethereum, you've probably written backend scripts that interact with contracts using a private key stored in a .env file. Hardhat tasks, deployment scripts, admin functions, server-side signing is normal. Expected, even.

Midnight has an equivalent pattern that works perfectly in Node.js environments. It goes something like this:

import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider';
import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider';
import { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';

const zkConfigProvider = new NodeZkConfigProvider(zkConfigPath);
const proofProvider = httpClientProofProvider('http://127.0.0.1:6300', zkConfigProvider);
const wallet = await WalletFacade(/* ... */);

Enter fullscreen mode Exit fullscreen mode

This is great for scripts. For CLI tools. For testing. It reads ZK artifacts from the filesystem, talks to a local proof server on port 6300, and persists private state using LevelDB.

What it is not is a browser-compatible architecture.

But I didn't think about that. I was in "get it working" mode. I stuffed this into my Next.js backend, created a server action that would sign and submit every transaction with my wallet, and called it done.

This worked. Demos looked fine. The contract calls went through. Nobody watching the demo could tell anything was architecturally broken.

That's the insidious part. The mistake is invisible until you try to ship to real users, and by that point, you've built an entire frontend on top of it.

Why It's Wrong (Beyond the Obvious "Users Can't Sign")

The most glaring problem is one everyone will call out immediately: users aren't signing their own transactions. That's not a dApp. That's a centralized backend with a ZK circuit in the middle. You've recreated a custodial wallet and called it Web3.

But there are deeper, more practical problems.

Problem 1: levelPrivateStateProvider doesn't exist in browsers.

LevelDB is a Node.js library. The moment you try to bundle this in a Next.js client component, you get a wall of webpack errors about missing native modules. You can't polyfill your way out of this.

Problem 2: NodeZkConfigProvider reads from the filesystem.

fs.readFileSync. In a browser. You know where this goes. The ZK artifacts (.verifier files and .bzkir files) need to be fetched over HTTP, not read from disk, but NodeZkConfigProvider doesn't know how to do that.

Problem 3: You're depending on a local proof server.

The httpClientProofProvider calls http://127.0.0.1:6300. That's a proof server that needs to be running locally. Your users don't have one. Your Vercel deployment definitely doesn't. And even if you ran it remotely, you'd immediately hit CORS hell trying to call it from the browser.

Problem 4: You hold all the keys.

Every transaction goes through your server wallet. Your seed phrase. Your keys. If your server goes down, the dApp breaks. If you rotate keys, every active session breaks. You've become the single point of failure for a supposedly trustless application.

I knew all of this, in theory. I just thought I'd "clean it up later."

Later arrived.

The Migration: What Actually Has to Change

The 1AM wallet is a browser extension for Midnight, think MetaMask but for ZK-native transactions. It injects a window.midnight['1am'] object and handles proof generation, transaction balancing, and signing internally.

To migrate, I had to rip out four things and replace them all.

1. ZK Config Provider

Before: NodeZkConfigProvider reading from filesystem.

After: FetchZkConfigProvider loading artifacts over HTTP.

const baseUrl = new URL('/contract/collection', window.location.origin).toString();
const zkConfigProvider = new FetchZkConfigProvider(baseUrl, window.fetch.bind(window));

Enter fullscreen mode Exit fullscreen mode

This expects your compiled artifacts to be served statically. The provider constructs URLs like:

  • {baseUrl}/keys/{circuitId}.verifier
  • {baseUrl}/zkir/{circuitId}.bzkir

So I had to compile my contracts and drop the output into public/contract/collection/. Then verify they were actually accessible:

curl http://localhost:3000/contract/collection/keys/mint.verifier

Enter fullscreen mode Exit fullscreen mode

If that returns HTML, you've got a path problem. If it returns binary data, you're good.

The gotcha I hit: I had my artifacts in the right directory but wrong structure. The provider expects keys/ and zkir/ subdirectories with specific naming. My files were dumped flat into the folder. This gave me a cryptic Failed to fetch ZK config for circuit 'mint' error that took me two hours to trace back to a missing subdirectory.

Two hours. For a missing folder. The error message gave no indication that the problem was directory structure, it just said fetch failed. This is a category of debugging that ages you.

2. Proof Provider

Before: httpClientProofProvider pointing to a local proof server.

After: The wallet's own proving capability.

const api = await wallet1AM.connect(networkId);
const provingProvider = await api.getProvingProvider(zkConfigProvider);

const proofProvider = {
  async proveTx(unprovenTx: any, _config: any) {
    const { CostModel } = await import('@midnight-ntwrk/ledger-v8');
    return unprovenTx.prove(provingProvider, CostModel.initialCostModel());
  },
};

Enter fullscreen mode Exit fullscreen mode

The 1AM extension bundles its own ZK prover. You don't need a proof server running anywhere. This is the cleanest part of the migration, once you have a connected wallet API, proof generation just works.

The dynamic import on CostModel is necessary. The ledger-v8 package ships a WASM module that Next.js tries to bundle at build time, and it fails. Dynamic import() defers it to runtime in the browser where WebAssembly is available. You'll still see warnings in your build logs about async/await not being supported in the target environment, those are non-blocking, but they're loud and they'll worry you the first time you see them.

I spent an embarrassing stretch of time trying to silence those warnings before accepting they were cosmetic. The build was fine. The app ran fine. I was just pattern-matching on red text in terminals. Move on faster than I did.

3. Private State Provider

Before: levelPrivateStateProvider with LevelDB persistence.

After: An in-memory map with contract address scoping.

function createPrivateStateProvider() {
  let contractAddressScope = '';
  const stateStore = new Map<string, any>();

  return {
    setContractAddress(address: string) {
      contractAddressScope = address;
    },
    async set(privateStateId: string, state: any) {
      stateStore.set(`${contractAddressScope}:${privateStateId}`, state);
    },
    async get(privateStateId: string) {
      return stateStore.get(`${contractAddressScope}:${privateStateId}`) ?? null;
    },
    // signing key methods...
  };
}

Enter fullscreen mode Exit fullscreen mode

The scoping by contract address is not optional. If you deploy multiple contracts in the same session without scoping, private state bleeds between them. I deployed two collection contracts during testing and couldn't figure out why the second one was behaving like the first, private state was resolving to the wrong contract's data. This one took longer to diagnose than I'd like to admit, because "state leaking between contracts" isn't where your brain goes first when a minting flow misbehaves.

The honest trade-off: this is ephemeral. Page reload wipes it. For production you'd want IndexedDB or syncing with the wallet's own private state store. That's a future problem. Right now, scoped in-memory state is correct enough to ship and test against real users.

4. Transaction Balancing and Submission

This is the most finicky part and where most of the debugging time went.

Before: WalletFacade.balanceTx() returned a Transaction object directly.

After: api.balanceUnsealedTransaction() returns a hex string. You have to deserialize it yourself.

const walletProvider: WalletProvider = {
  balanceTx: async (tx: any) => {
    const txHex = toHex(tx.serialize());
    const balanced = await api.balanceUnsealedTransaction(txHex);

    // The wallet returns { tx: "hexstring" } — you must deserialize
    const { Transaction } = await import('@midnight-ntwrk/ledger-v8');
    const bytes = new Uint8Array(
      balanced.tx.match(/.{2}/g).map((b: string) => parseInt(b, 16))
    );
    return Transaction.deserialize('signature', 'proof', 'binding', bytes);
  },
  getCoinPublicKey: () => shieldedAddress.shieldedCoinPublicKey,
  getEncryptionPublicKey: () => shieldedAddress.shieldedEncryptionPublicKey,
};

Enter fullscreen mode Exit fullscreen mode

The error I got before adding the deserialization step was Error: balanceUnsealedTransaction returned invalid result. Not "type mismatch," not "expected Transaction object, received string." Just "invalid result." I stared at that error for longer than I should have before realizing the wallet was returning hex and I was passing it somewhere that expected a typed object.

Good error messages would have made this a 5-minute fix. It was not a 5-minute fix.

For submission:

const midnightProvider: MidnightProvider = {
  submitTx: async (tx: any) => {
    const txHex = toHex(tx.serialize());
    const result = await api.submitTransaction(txHex);

    // Handle multiple possible return shapes across wallet versions
    return (typeof result === 'string' 
      ? result 
      : result?.transactionId || result?.id) || '';
  },
};

Enter fullscreen mode Exit fullscreen mode

The defensive extraction on transactionId || id exists because different versions of the 1AM extension return different shapes. You'll only discover which one you have when your transaction ID comes back as undefined in production.

The Race Condition Nobody Warns You About

Browser extensions load asynchronously. Your app boots, checks window.midnight['1am'], finds nothing, and reports "wallet not found", even though the extension is installed and working.

You need to poll:

const DETECT_TIMEOUT_MS = 6000;
const DETECT_INTERVAL_MS = 300;

const startedAt = Date.now();
const intervalId = setInterval(() => {
  const wallet = (window as any).midnight?.['1am'];

  if (wallet) {
    clearInterval(intervalId);
    // proceed with connection
    return;
  }

  if (Date.now() - startedAt >= DETECT_TIMEOUT_MS) {
    clearInterval(intervalId);
    // show "install wallet" message
  }
}, DETECT_INTERVAL_MS);

Enter fullscreen mode Exit fullscreen mode

Six seconds is generous but reasonable. The extension usually loads within one or two. If it's not there after six, the user genuinely doesn't have it installed.

Also support Lace wallet as a fallback, window.midnight.mnLace, and consider a seed phrase input mode for CLI users and recovery scenarios. Real users will be less technical than you. Having multiple connection paths matters.

The Full Diff: What the Architecture Actually Looks Like Now

The server wallet model had everything in a single Node.js process. One wallet, one proof server, one private state database. Simple to reason about, impossible to scale to real users.

The browser wallet model distributes responsibility correctly:

User's Browser
├── 1AM Extension
│   ├── User's private keys (never leaves extension)
│   ├── Built-in ZK prover
│   └── Transaction signing
│
└── Your Next.js App
    ├── FetchZkConfigProvider (loads artifacts from /public)
    ├── In-memory PrivateStateProvider (scoped per contract)
    └── WalletProvider / MidnightProvider (delegates to extension)

Enter fullscreen mode Exit fullscreen mode

The app never touches private keys. The user signs everything. The proof server is gone. The LevelDB dependency is gone. CORS is gone.

What I'd Tell Myself If I Could Go Back

Don't prototype with server wallets. The migration cost is real. Every abstraction you build around the server wallet pattern has to be unwound. If I'd started with the 1AM integration from day one, even just stubbed out, the final code would have been cleaner and I'd have caught the ZK artifact path issues during initial development rather than during migration.

Read the provider interfaces, not the implementations. The SDK exports clean interfaces for WalletProvider, MidnightProvider, ProofProvider. If I'd read those first instead of copying the working server-side implementations, I'd have understood earlier that these are swappable, and that swapping them is the whole point.

Browser-first is table stakes. You know this from Ethereum. MetaMask was non-negotiable in 2019. Apply the same instinct to Midnight immediately, don't let the novelty of ZK circuits make you forget the basics.

Scope your private state by contract address on day one. The fix is a single string prefix. The debugging session when you skip it is not.

The Honest Part Nobody Writes

Here's the thing I kept thinking about while doing this migration:

The error messages in Midnight's SDK are often terrible. Not "could be better", genuinely unhelpful in ways that send you in the wrong direction. balanceUnsealedTransaction returned invalid result tells you nothing about what was actually invalid. Failed to fetch ZK config for circuit 'mint' gives you zero signal about whether the problem is the URL, the directory structure, the file format, or something else entirely.

This is a real cost. Not a dealbreaker, but a real cost, and one that compounds. Every cryptic error is an hour of debugging that a better message would have made five minutes. When you're building alone at 3 AM and you've been staring at the same error for three hours, "invalid result" starts to feel personal.

I'm writing this down because the Midnight team is actively building, they read what builders publish, and error message quality is a low-effort, high-leverage improvement. The underlying architecture is sound. The ZK primitives are genuinely interesting. But right now, the gap between "this works in CLI" and "this works in a user-facing browser app" is navigated almost entirely by developer pain, and it doesn't have to be.

If this post exists in the Midnight documentation six months from now as a reference for the migration path, that's a good outcome. The patterns above are correct. The architecture works. The error messages that led me to those patterns were not helpful, and I'd rather name that honestly than wrap this up with a cheerful "but overall it was worth it!"

It was worth it. The honest version of that statement includes everything above.

Tushar Pamnani — building at Sevryn Labs. Find me on X or GitHub. Writing about Midnight at dev.to/tusharpamnani.

If you're starting your Midnight journey and want to skip some of these walls, reach out. The repos are open.