Skip to content

NoTraceSol/stealth-sdk

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@notrace/stealth-sdk

Real stealth addresses on Solana. ECDH-derived, view-key recoverable, with a Memo-program footprint.

npm License ed25519


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 — no relayer, no mixer.

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

Verified on Bun 1.1.x, Deno 2.x, and Node 18+ — no runtime-specific shims.

@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 × G

The 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.seed and 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 colliding r. 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 ship nt2: 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.

About

developer SDK + CLI for integrating NoTrace stealth payments

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors