@notrace/stealth-sdk
v1.0.0
Published
Real stealth addresses on Solana. ECDH-derived one-time addresses on ed25519, view-key recoverable, with a Memo-program footprint. Extracted from the production NoTrace wallet.
Maintainers
Readme
@notrace/stealth-sdk
Real stealth addresses on Solana. ECDH-derived, view-key recoverable, with a Memo-program footprint.
Extracted from the production NoTrace wallet. Zero relayers, zero mixers, zero on-chain program — just a fresh one-time address per payment, derived in the payer's browser. The recipient scans the Memo program with a view-key and finds their payments locally.
[!NOTE] This is the same code that ships in the NoTrace web wallet. Browser, Node 18+, Deno and Bun are all supported. No
Buffer, no globals.
Install
npm install @notrace/stealth-sdk
# or
pnpm add @notrace/stealth-sdk
# or
yarn add @notrace/stealth-sdk@solana/web3.js is an optional peer dep — only the scanner needs it (and
even there, the scanner takes a small RpcLike interface, so you can wire in
your own client).
Quick start
Recipient — generate identity, share a pay-link
import { generateMetaKey, makePayLink } from "@notrace/stealth-sdk";
const meta = generateMetaKey();
// Persist `meta.seed` somewhere encrypted — that's your whole identity.
const link = makePayLink(meta.pub);
// → "https://notracesol.xyz/pay#m=4mNp9q8K…"
// Share `link` anywhere. Nobody learns who the recipient is.Payer — derive a one-time address and send
import { deriveStealthSender, encodeMemo, parsePayLink } from "@notrace/stealth-sdk";
const metaPub = parsePayLink(linkFromRecipient)!;
const { stealthPub, ephPub } = deriveStealthSender(metaPub);
// Build a Solana tx:
// 1. SystemProgram.transfer → stealthPub (base58)
// 2. Memo instruction with the body `encodeMemo(ephPub)` (e.g. "nt1:4mNp…")
// Sign + send with Phantom/web3.js. The recipient's scanner will find it.Recipient — scan for incoming payments
import { Connection } from "@solana/web3.js";
import { scanForPayments, metaFromSeed } from "@notrace/stealth-sdk";
const meta = metaFromSeed(savedSeed);
const rpc = new Connection("https://solana-rpc.publicnode.com", "confirmed");
const payments = await scanForPayments(meta, rpc as any, { limit: 200 });
for (const p of payments) {
console.log(p.lamports / 1e9, "SOL @", p.stealthPub, "from", p.source);
}Recipient — sweep funds out of a stealth address
import { signWithScalar, verify } from "@notrace/stealth-sdk";
// `payment.stealthScalar` from the scanner is the secret you need.
const message = transferTx.serializeMessage(); // web3.js
const sig = signWithScalar(payment.stealthScalar, transferTx.feePayer!.toBytes(), message);
transferTx.addSignature(transferTx.feePayer!, Buffer.from(sig));
// Optionally double-check:
console.assert(verify(sig, message, transferTx.feePayer!.toBytes()));
await rpc.sendRawTransaction(transferTx.serialize());How it works
NoTrace is an ERC-5564-style stealth-address scheme ported to ed25519:
SENDER RECIPIENT
────── ─────────
eph_scalar, eph_pub ← fresh keypair meta_scalar, meta_pub ← persistent
shared = eph_scalar × meta_pub shared = meta_scalar × eph_pub
tweak = SHA512(ver ‖ shared ‖ tweak = SHA512(ver ‖ shared ‖
eph_pub ‖ meta_pub) mod L eph_pub ‖ meta_pub) mod L
stealth_pub = meta_pub + tweak × G stealth_scalar = (meta_scalar + tweak) mod L
stealth_pub = stealth_scalar × GThe shared point is identical on both sides because eph_scalar × (meta_scalar
× G) = meta_scalar × (eph_scalar × G). The sender knows stealth_pub (a valid
Solana address) but not stealth_scalar — they can't spend the funds they
just sent. Only the recipient, applying their meta_scalar to the
sender-published eph_pub, recovers the spend scalar.
The protocol is self-contained: no Solana program needed. The wire format is a
~48-character Memo (nt1:<base58_eph_pub>) attached to a plain SystemProgram
transfer.
API reference
| Export | Returns | Notes |
| --- | --- | --- |
| generateMetaKey() | MetaKey | Fresh meta-keypair. Persist .seed. |
| metaFromSeed(seed) | MetaKey | Deterministic from a 32-byte seed. |
| pubFromScalar(scalar) | Uint8Array(32) | scalar × G. |
| deriveStealthSender(metaPub) | { stealthPub, ephPub } | Call once per payment. |
| recoverStealth(metaScalar, metaPub, ephPub) | { stealthScalar, stealthPub } | Recipient-side. |
| signWithScalar(scalar, pub, msg) | Uint8Array(64) | ed25519 sig from raw scalar. |
| verify(sig, msg, pub) | boolean | Standard ed25519 verify. |
| encodeMemo(ephPub) | string | nt1:<base58_eph_pub>. |
| parseMemo(s) | Uint8Array(32) \| null | Returns null for non-NoTrace memos. |
| makePayLink(metaPub, origin?) | string | https://…/pay#m=<base58>. |
| parsePayLink(url) | Uint8Array(32) \| null | Accepts #m= or ?m= form. |
| scanForPayments(meta, rpc, opts?) | StealthPayment[] | Polls the Memo program. |
| checkSignature(sig, meta, rpc) | StealthPayment \| null | Single-tx variant. |
| bs58encode / bs58decode | — | Zero-dep base58. |
Constants: MEMO_PREFIX ("nt1:"), MEMO_PROGRAM_ID, VERSION.
Security notes
- The seed is the only secret. Lose
meta.seedand you lose all incoming funds. Back it up encrypted, the same way you'd treat any private key. - The signing path uses a hedged Schnorr nonce
(
r = SHA512(random_prefix ‖ pub ‖ msg)), so bad randomness can't leak the scalar through a collidingr. Each stealth scalar typically signs just one sweep tx, but the hedge costs nothing. - The
nt1:memo prefix is intentional — a future protocol revision can shipnt2:without breaking old wallets, which will simply skip those memos. - Stealth addresses are real ed25519 points; they're indistinguishable on-chain from any other Solana address.
License
MIT — see LICENSE.
Built by NoTrace. Issues, PRs, and audits welcome.
