@notrace/stealth-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Byte/scalar helpers shared across the SDK.
3
+ *
4
+ * Kept dependency-free — no `Buffer`, no `bigint-buffer`. Works in browser,
5
+ * Node 18+, Deno, and Bun without polyfills.
6
+ */
7
+ /** Concatenate any number of byte arrays into a single Uint8Array. */
8
+ declare function concatBytes(...arrs: Uint8Array[]): Uint8Array;
9
+ /** Little-endian unsigned integer decode. */
10
+ declare function bytesToNumberLE(bytes: Uint8Array): bigint;
11
+ /** Little-endian unsigned integer encode to a fixed length. */
12
+ declare function numberToBytesLE(n: bigint, len: number): Uint8Array;
13
+ /** Compare two Uint8Arrays for byte equality. */
14
+ declare function bytesEqual(a: Uint8Array, b: Uint8Array): boolean;
15
+
16
+ /**
17
+ * Minimal base58 (Bitcoin/Solana alphabet) encoder/decoder.
18
+ *
19
+ * Zero-dep. Identical output to `bs58` and `@solana/web3.js` Address encoding.
20
+ */
21
+ /** Encode a byte array to a base58 string. */
22
+ declare function bs58encode(bytes: Uint8Array): string;
23
+ /** Decode a base58 string back to bytes. Throws on invalid input. */
24
+ declare function bs58decode(str: string): Uint8Array;
25
+
26
+ /**
27
+ * Meta-keypair generation and seed derivation.
28
+ *
29
+ * A NoTrace identity is a single ed25519 keypair (the *meta-key*). Its public
30
+ * half is shared with payers — that's enough for them to derive a one-time
31
+ * stealth address. Its private half (the *view+spend scalar*) is what lets the
32
+ * recipient detect and claim incoming payments.
33
+ *
34
+ * Note: this is a non-hierarchical key. If you want separate view-only and
35
+ * spend keys, derive them upstream from your own master secret and pass the
36
+ * spend scalar in only when you need to sign.
37
+ */
38
+ /** A meta-keypair. `seed` and `scalar` are *secret*; only `pub` is shareable. */
39
+ interface MetaKey {
40
+ /** Original 32-byte random seed (kept for backup/export). */
41
+ seed: Uint8Array;
42
+ /** ed25519 scalar derived from the seed (in [0, L)). */
43
+ scalar: bigint;
44
+ /** 32-byte compressed ed25519 public point. Share this with payers. */
45
+ pub: Uint8Array;
46
+ }
47
+ /** Generate a fresh random meta-keypair using `crypto.getRandomValues`. */
48
+ declare function generateMetaKey(): MetaKey;
49
+ /**
50
+ * Deterministically derive a meta-keypair from a 32-byte seed.
51
+ *
52
+ * Performs the standard ed25519 clamping (RFC 8032 §5.1.5) so the scalar lands
53
+ * in a valid range. Same `seed` → same `pub` forever.
54
+ */
55
+ declare function metaFromSeed(seed: Uint8Array): MetaKey;
56
+ /** Recompute the public point for a given scalar. */
57
+ declare function pubFromScalar(scalar: bigint): Uint8Array;
58
+
59
+ /**
60
+ * Stealth-address derivation and recovery.
61
+ *
62
+ * The protocol is ECDH-on-ed25519 with a scalar-tweak — the same shape used by
63
+ * ERC-5564 and Umbra Cash, ported to Solana's native curve. Properties:
64
+ *
65
+ * - Sender derives a stealth address from the recipient's published `meta_pub`.
66
+ * - Sender CANNOT recover the stealth private key (would need `meta_scalar`).
67
+ * - Recipient scans memos and applies their `meta_scalar` to detect payments.
68
+ * - The stealth pub is a normal ed25519 point → a valid Solana address.
69
+ *
70
+ * The "version tag" included in the tweak makes the protocol forward-compatible:
71
+ * a future curve swap or hash change can ship a new tag without breaking
72
+ * existing wallets.
73
+ */
74
+ /** Result of a sender-side derivation. */
75
+ interface SenderDerivation {
76
+ /** The one-time stealth address (32-byte ed25519 pub) to send funds to. */
77
+ stealthPub: Uint8Array;
78
+ /** The ephemeral pub the sender must publish on-chain (e.g. in a memo). */
79
+ ephPub: Uint8Array;
80
+ }
81
+ /**
82
+ * SENDER side: derive a one-time stealth address from the recipient's meta-pub.
83
+ *
84
+ * eph_scalar, eph_pub = fresh ed25519 keypair
85
+ * shared = eph_scalar × meta_pub (DH on ed25519)
86
+ * tweak = SHA512(ver ‖ shared ‖ eph_pub ‖ meta_pub) mod L
87
+ * stealth_pub = meta_pub + tweak × G (point addition)
88
+ *
89
+ * The sender publishes `eph_pub`; only the holder of `meta_scalar` can both
90
+ * recover `stealth_scalar` and produce signatures over it.
91
+ *
92
+ * Safe to call from a hostile environment — no state is persisted.
93
+ */
94
+ declare function deriveStealthSender(metaPubBytes: Uint8Array): SenderDerivation;
95
+ /** Result of a recipient-side recovery — pub matches what the sender derived. */
96
+ interface RecipientRecovery {
97
+ /** The recovered scalar — secret. Use this to sign sweeps. */
98
+ stealthScalar: bigint;
99
+ /** The recovered pub — compare against the on-chain destination. */
100
+ stealthPub: Uint8Array;
101
+ }
102
+ /**
103
+ * RECIPIENT side: given your meta-key and an ephemeral pub from a memo, recover
104
+ * the stealth keypair the sender derived.
105
+ *
106
+ * shared = meta_scalar × eph_pub (same point as sender's)
107
+ * tweak = SHA512(ver ‖ shared ‖ eph_pub ‖ meta_pub) mod L
108
+ * stealth_scalar = (meta_scalar + tweak) mod L
109
+ * stealth_pub = stealth_scalar × G
110
+ *
111
+ * Compare the returned `stealthPub` against the SOL/SPL `destination` field of
112
+ * the transaction containing the memo. A match means the payment is yours.
113
+ */
114
+ declare function recoverStealth(metaScalar: bigint, metaPubBytes: Uint8Array, ephPubBytes: Uint8Array): RecipientRecovery;
115
+
116
+ /**
117
+ * ed25519 signing with a raw scalar (not a seed).
118
+ *
119
+ * Standard ed25519 sign-from-seed first hashes the seed and uses the upper half
120
+ * as a deterministic nonce. Our stealth scalars are *derived*, not seeded — so
121
+ * we use a Schnorr-style construction directly:
122
+ *
123
+ * r = SHA512(prefix ‖ pub ‖ msg) mod L where prefix is 32 fresh bytes
124
+ * R = r × G
125
+ * k = SHA512(R ‖ pub ‖ msg) mod L
126
+ * S = (r + k × scalar) mod L
127
+ * sig = R ‖ S (64 bytes, verifiable as ed25519)
128
+ *
129
+ * The random `prefix` is a hedge against bad randomness leaking the scalar via
130
+ * a colliding `r`. In normal NoTrace use each stealth scalar signs at most one
131
+ * sweep, so even non-hedged would be safe — we hedge anyway.
132
+ */
133
+ /**
134
+ * Produce a 64-byte ed25519 signature using a raw scalar.
135
+ *
136
+ * The resulting signature verifies against `pub = scalar × G` under the
137
+ * standard ed25519 verify algorithm (e.g. `ed25519.verify` in @noble/curves,
138
+ * or Solana's runtime).
139
+ */
140
+ declare function signWithScalar(scalar: bigint, pubBytes: Uint8Array, message: Uint8Array): Uint8Array;
141
+ /** Re-export `ed25519.verify` for callers that don't want to import @noble directly. */
142
+ declare function verify(sig: Uint8Array, message: Uint8Array, pubBytes: Uint8Array): boolean;
143
+
144
+ /**
145
+ * Memo-program encoding for the ephemeral pub.
146
+ *
147
+ * Every NoTrace payment carries a memo of the form `nt1:<bs58_eph_pub>`. The
148
+ * recipient's scanner parses these out of memo-program instructions and feeds
149
+ * them to `recoverStealth` to test for matches.
150
+ *
151
+ * The `nt1:` prefix makes the protocol forward-compatible: a future scheme can
152
+ * use `nt2:` and old wallets will simply skip those memos.
153
+ */
154
+ /** Current memo prefix. Bump when the on-chain wire format changes. */
155
+ declare const MEMO_PREFIX = "nt1:";
156
+ /** Solana's official Memo Program address (v2). Useful for filtering RPC calls. */
157
+ declare const MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
158
+ /**
159
+ * Encode an ephemeral public key into a memo string for on-chain publication.
160
+ * Output is ~48 chars (`nt1:` + 44-char base58).
161
+ */
162
+ declare function encodeMemo(ephPubBytes: Uint8Array): string;
163
+ /**
164
+ * Parse a memo string back into the ephemeral pub. Returns `null` for any
165
+ * memo that doesn't start with the current prefix or fails to decode to 32
166
+ * bytes — so the scanner can pipe every memo through this without try/catch.
167
+ */
168
+ declare function parseMemo(memoString: unknown): Uint8Array | null;
169
+
170
+ /**
171
+ * NoTrace pay-link helpers.
172
+ *
173
+ * A pay-link encodes a recipient's `meta_pub` in the URL fragment of a hosted
174
+ * `/pay` page (default: https://notracesol.xyz/pay). The recipient shares the
175
+ * link; the payer's browser derives a one-time address client-side and signs
176
+ * the transfer through Phantom.
177
+ *
178
+ * Format: `https://<origin>/pay#m=<bs58(meta_pub)>`
179
+ *
180
+ * Using the URL fragment (after `#`) means the meta-pub never reaches the
181
+ * NoTrace server — even the request log can't link payer to recipient.
182
+ */
183
+ /** Build a `/pay` link for a given meta-pub. */
184
+ declare function makePayLink(metaPubBytes: Uint8Array, origin?: string): string;
185
+ /**
186
+ * Parse a NoTrace pay-link and return the recipient meta-pub.
187
+ * Returns `null` for any input that isn't a parseable NoTrace pay-link.
188
+ */
189
+ declare function parsePayLink(url: string): Uint8Array | null;
190
+
191
+ /**
192
+ * Memo-program scanner — find incoming stealth payments addressed to you.
193
+ *
194
+ * Standalone (no `@solana/web3.js` dependency at type-level): the scanner takes
195
+ * a small `RpcLike` interface so it composes with any RPC client — a real
196
+ * `Connection` from web3.js, a mock for tests, or a thin HTTP wrapper.
197
+ *
198
+ * Algorithm per scanned signature:
199
+ *
200
+ * 1. Fetch the parsed tx.
201
+ * 2. Find a Memo-program instruction; extract the memo string.
202
+ * 3. `parseMemo(...)` → 32-byte `eph_pub` (skip if not an `nt1:` memo).
203
+ * 4. `recoverStealth(meta_scalar, meta_pub, eph_pub)` → expected `stealth_pub`.
204
+ * 5. Look for a SystemProgram `transfer` whose `destination` matches; if so,
205
+ * it's a payment to you.
206
+ */
207
+
208
+ /** Minimal shape we need from an RPC client. */
209
+ interface RpcLike {
210
+ /**
211
+ * Return signatures touching `address`, newest first. Slot/blockTime are
212
+ * optional but recommended for ordering and display.
213
+ */
214
+ getSignaturesForAddress(address: string, opts?: {
215
+ limit?: number;
216
+ before?: string;
217
+ until?: string;
218
+ }): Promise<Array<{
219
+ signature: string;
220
+ slot?: number;
221
+ blockTime?: number | null;
222
+ }>>;
223
+ /**
224
+ * Return a parsed transaction. The shape matches web3.js's
225
+ * `getParsedTransaction` for the fields the scanner actually reads.
226
+ */
227
+ getParsedTransaction(signature: string, opts?: {
228
+ maxSupportedTransactionVersion?: number;
229
+ commitment?: string;
230
+ }): Promise<ParsedTransactionLike | null>;
231
+ }
232
+ /** Minimal parsed-tx shape required by the scanner. */
233
+ interface ParsedTransactionLike {
234
+ slot?: number;
235
+ blockTime?: number | null;
236
+ meta?: {
237
+ err?: unknown;
238
+ } | null;
239
+ transaction: {
240
+ message: {
241
+ instructions: Array<ParsedInstructionLike>;
242
+ };
243
+ };
244
+ }
245
+ interface ParsedInstructionLike {
246
+ programId?: {
247
+ toString(): string;
248
+ } | string;
249
+ /** Memo program may surface the memo here as a string. */
250
+ parsed?: string | {
251
+ type?: string;
252
+ info?: {
253
+ destination?: string;
254
+ source?: string;
255
+ lamports?: number;
256
+ } | string;
257
+ };
258
+ /** Raw memo bytes (base58-encoded) if the program is not parsed. */
259
+ data?: string;
260
+ }
261
+ /** A successfully-matched stealth payment. */
262
+ interface StealthPayment {
263
+ /** Transaction signature on Solana. */
264
+ signature: string;
265
+ /** Slot the tx landed in (if the RPC returned it). */
266
+ slot?: number;
267
+ /** Block timestamp in **ms** since epoch (or `null` if RPC didn't supply). */
268
+ blockTimeMs?: number | null;
269
+ /** Lamports received at the stealth address. */
270
+ lamports: number;
271
+ /** Source wallet (the payer). */
272
+ source: string;
273
+ /** Stealth address (base58). */
274
+ stealthPub: string;
275
+ /** Secret stealth scalar — needed to sign sweeps. Treat as private key. */
276
+ stealthScalar: bigint;
277
+ /** Ephemeral pub from the memo (base58). */
278
+ ephPub: string;
279
+ }
280
+ interface ScanOptions {
281
+ /** How many signatures to pull from the memo program per batch. Default 100. */
282
+ limit?: number;
283
+ /** Concurrent `getParsedTransaction` calls. Default 5 (RPC-friendly). */
284
+ concurrency?: number;
285
+ /** Cursor for pagination — fetch sigs older than this signature. */
286
+ before?: string;
287
+ /** Already-scanned signatures to skip. */
288
+ alreadyScanned?: Set<string>;
289
+ }
290
+ /**
291
+ * Scan the memo program for new stealth payments addressed to `meta`.
292
+ *
293
+ * Returns *only the matches*. Updates `alreadyScanned` (if provided) in-place
294
+ * so subsequent calls can resume without re-checking.
295
+ */
296
+ declare function scanForPayments(meta: Pick<MetaKey, "scalar" | "pub">, rpc: RpcLike, opts?: ScanOptions): Promise<StealthPayment[]>;
297
+ /**
298
+ * Check a single signature for a stealth payment to `meta`. Returns the
299
+ * payment if matched, `null` otherwise. Exposed so callers can wire their own
300
+ * scanning loop (e.g. with subscribe/webhook flows instead of polling).
301
+ */
302
+ declare function checkSignature(signature: string, meta: Pick<MetaKey, "scalar" | "pub">, rpc: RpcLike): Promise<StealthPayment | null>;
303
+
304
+ /**
305
+ * @notrace/stealth-sdk
306
+ *
307
+ * Real stealth addresses on Solana. ECDH-derived one-time addresses on
308
+ * ed25519, view-key recoverable, with a Memo-program footprint.
309
+ *
310
+ * Extracted from the production NoTrace wallet (https://notracesol.xyz).
311
+ *
312
+ * Quick start:
313
+ *
314
+ * import {
315
+ * generateMetaKey, deriveStealthSender, recoverStealth,
316
+ * encodeMemo, parseMemo, makePayLink, scanForPayments,
317
+ * } from "@notrace/stealth-sdk";
318
+ *
319
+ * See README.md for end-to-end examples.
320
+ */
321
+
322
+ /** Package version, kept in sync with `package.json`. */
323
+ declare const VERSION = "1.0.0";
324
+
325
+ export { MEMO_PREFIX, MEMO_PROGRAM_ID, type MetaKey, type ParsedInstructionLike, type ParsedTransactionLike, type RecipientRecovery, type RpcLike, type ScanOptions, type SenderDerivation, type StealthPayment, VERSION, bs58decode, bs58encode, bytesEqual, bytesToNumberLE, checkSignature, concatBytes, deriveStealthSender, encodeMemo, generateMetaKey, makePayLink, metaFromSeed, numberToBytesLE, parseMemo, parsePayLink, pubFromScalar, recoverStealth, scanForPayments, signWithScalar, verify };
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Byte/scalar helpers shared across the SDK.
3
+ *
4
+ * Kept dependency-free — no `Buffer`, no `bigint-buffer`. Works in browser,
5
+ * Node 18+, Deno, and Bun without polyfills.
6
+ */
7
+ /** Concatenate any number of byte arrays into a single Uint8Array. */
8
+ declare function concatBytes(...arrs: Uint8Array[]): Uint8Array;
9
+ /** Little-endian unsigned integer decode. */
10
+ declare function bytesToNumberLE(bytes: Uint8Array): bigint;
11
+ /** Little-endian unsigned integer encode to a fixed length. */
12
+ declare function numberToBytesLE(n: bigint, len: number): Uint8Array;
13
+ /** Compare two Uint8Arrays for byte equality. */
14
+ declare function bytesEqual(a: Uint8Array, b: Uint8Array): boolean;
15
+
16
+ /**
17
+ * Minimal base58 (Bitcoin/Solana alphabet) encoder/decoder.
18
+ *
19
+ * Zero-dep. Identical output to `bs58` and `@solana/web3.js` Address encoding.
20
+ */
21
+ /** Encode a byte array to a base58 string. */
22
+ declare function bs58encode(bytes: Uint8Array): string;
23
+ /** Decode a base58 string back to bytes. Throws on invalid input. */
24
+ declare function bs58decode(str: string): Uint8Array;
25
+
26
+ /**
27
+ * Meta-keypair generation and seed derivation.
28
+ *
29
+ * A NoTrace identity is a single ed25519 keypair (the *meta-key*). Its public
30
+ * half is shared with payers — that's enough for them to derive a one-time
31
+ * stealth address. Its private half (the *view+spend scalar*) is what lets the
32
+ * recipient detect and claim incoming payments.
33
+ *
34
+ * Note: this is a non-hierarchical key. If you want separate view-only and
35
+ * spend keys, derive them upstream from your own master secret and pass the
36
+ * spend scalar in only when you need to sign.
37
+ */
38
+ /** A meta-keypair. `seed` and `scalar` are *secret*; only `pub` is shareable. */
39
+ interface MetaKey {
40
+ /** Original 32-byte random seed (kept for backup/export). */
41
+ seed: Uint8Array;
42
+ /** ed25519 scalar derived from the seed (in [0, L)). */
43
+ scalar: bigint;
44
+ /** 32-byte compressed ed25519 public point. Share this with payers. */
45
+ pub: Uint8Array;
46
+ }
47
+ /** Generate a fresh random meta-keypair using `crypto.getRandomValues`. */
48
+ declare function generateMetaKey(): MetaKey;
49
+ /**
50
+ * Deterministically derive a meta-keypair from a 32-byte seed.
51
+ *
52
+ * Performs the standard ed25519 clamping (RFC 8032 §5.1.5) so the scalar lands
53
+ * in a valid range. Same `seed` → same `pub` forever.
54
+ */
55
+ declare function metaFromSeed(seed: Uint8Array): MetaKey;
56
+ /** Recompute the public point for a given scalar. */
57
+ declare function pubFromScalar(scalar: bigint): Uint8Array;
58
+
59
+ /**
60
+ * Stealth-address derivation and recovery.
61
+ *
62
+ * The protocol is ECDH-on-ed25519 with a scalar-tweak — the same shape used by
63
+ * ERC-5564 and Umbra Cash, ported to Solana's native curve. Properties:
64
+ *
65
+ * - Sender derives a stealth address from the recipient's published `meta_pub`.
66
+ * - Sender CANNOT recover the stealth private key (would need `meta_scalar`).
67
+ * - Recipient scans memos and applies their `meta_scalar` to detect payments.
68
+ * - The stealth pub is a normal ed25519 point → a valid Solana address.
69
+ *
70
+ * The "version tag" included in the tweak makes the protocol forward-compatible:
71
+ * a future curve swap or hash change can ship a new tag without breaking
72
+ * existing wallets.
73
+ */
74
+ /** Result of a sender-side derivation. */
75
+ interface SenderDerivation {
76
+ /** The one-time stealth address (32-byte ed25519 pub) to send funds to. */
77
+ stealthPub: Uint8Array;
78
+ /** The ephemeral pub the sender must publish on-chain (e.g. in a memo). */
79
+ ephPub: Uint8Array;
80
+ }
81
+ /**
82
+ * SENDER side: derive a one-time stealth address from the recipient's meta-pub.
83
+ *
84
+ * eph_scalar, eph_pub = fresh ed25519 keypair
85
+ * shared = eph_scalar × meta_pub (DH on ed25519)
86
+ * tweak = SHA512(ver ‖ shared ‖ eph_pub ‖ meta_pub) mod L
87
+ * stealth_pub = meta_pub + tweak × G (point addition)
88
+ *
89
+ * The sender publishes `eph_pub`; only the holder of `meta_scalar` can both
90
+ * recover `stealth_scalar` and produce signatures over it.
91
+ *
92
+ * Safe to call from a hostile environment — no state is persisted.
93
+ */
94
+ declare function deriveStealthSender(metaPubBytes: Uint8Array): SenderDerivation;
95
+ /** Result of a recipient-side recovery — pub matches what the sender derived. */
96
+ interface RecipientRecovery {
97
+ /** The recovered scalar — secret. Use this to sign sweeps. */
98
+ stealthScalar: bigint;
99
+ /** The recovered pub — compare against the on-chain destination. */
100
+ stealthPub: Uint8Array;
101
+ }
102
+ /**
103
+ * RECIPIENT side: given your meta-key and an ephemeral pub from a memo, recover
104
+ * the stealth keypair the sender derived.
105
+ *
106
+ * shared = meta_scalar × eph_pub (same point as sender's)
107
+ * tweak = SHA512(ver ‖ shared ‖ eph_pub ‖ meta_pub) mod L
108
+ * stealth_scalar = (meta_scalar + tweak) mod L
109
+ * stealth_pub = stealth_scalar × G
110
+ *
111
+ * Compare the returned `stealthPub` against the SOL/SPL `destination` field of
112
+ * the transaction containing the memo. A match means the payment is yours.
113
+ */
114
+ declare function recoverStealth(metaScalar: bigint, metaPubBytes: Uint8Array, ephPubBytes: Uint8Array): RecipientRecovery;
115
+
116
+ /**
117
+ * ed25519 signing with a raw scalar (not a seed).
118
+ *
119
+ * Standard ed25519 sign-from-seed first hashes the seed and uses the upper half
120
+ * as a deterministic nonce. Our stealth scalars are *derived*, not seeded — so
121
+ * we use a Schnorr-style construction directly:
122
+ *
123
+ * r = SHA512(prefix ‖ pub ‖ msg) mod L where prefix is 32 fresh bytes
124
+ * R = r × G
125
+ * k = SHA512(R ‖ pub ‖ msg) mod L
126
+ * S = (r + k × scalar) mod L
127
+ * sig = R ‖ S (64 bytes, verifiable as ed25519)
128
+ *
129
+ * The random `prefix` is a hedge against bad randomness leaking the scalar via
130
+ * a colliding `r`. In normal NoTrace use each stealth scalar signs at most one
131
+ * sweep, so even non-hedged would be safe — we hedge anyway.
132
+ */
133
+ /**
134
+ * Produce a 64-byte ed25519 signature using a raw scalar.
135
+ *
136
+ * The resulting signature verifies against `pub = scalar × G` under the
137
+ * standard ed25519 verify algorithm (e.g. `ed25519.verify` in @noble/curves,
138
+ * or Solana's runtime).
139
+ */
140
+ declare function signWithScalar(scalar: bigint, pubBytes: Uint8Array, message: Uint8Array): Uint8Array;
141
+ /** Re-export `ed25519.verify` for callers that don't want to import @noble directly. */
142
+ declare function verify(sig: Uint8Array, message: Uint8Array, pubBytes: Uint8Array): boolean;
143
+
144
+ /**
145
+ * Memo-program encoding for the ephemeral pub.
146
+ *
147
+ * Every NoTrace payment carries a memo of the form `nt1:<bs58_eph_pub>`. The
148
+ * recipient's scanner parses these out of memo-program instructions and feeds
149
+ * them to `recoverStealth` to test for matches.
150
+ *
151
+ * The `nt1:` prefix makes the protocol forward-compatible: a future scheme can
152
+ * use `nt2:` and old wallets will simply skip those memos.
153
+ */
154
+ /** Current memo prefix. Bump when the on-chain wire format changes. */
155
+ declare const MEMO_PREFIX = "nt1:";
156
+ /** Solana's official Memo Program address (v2). Useful for filtering RPC calls. */
157
+ declare const MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
158
+ /**
159
+ * Encode an ephemeral public key into a memo string for on-chain publication.
160
+ * Output is ~48 chars (`nt1:` + 44-char base58).
161
+ */
162
+ declare function encodeMemo(ephPubBytes: Uint8Array): string;
163
+ /**
164
+ * Parse a memo string back into the ephemeral pub. Returns `null` for any
165
+ * memo that doesn't start with the current prefix or fails to decode to 32
166
+ * bytes — so the scanner can pipe every memo through this without try/catch.
167
+ */
168
+ declare function parseMemo(memoString: unknown): Uint8Array | null;
169
+
170
+ /**
171
+ * NoTrace pay-link helpers.
172
+ *
173
+ * A pay-link encodes a recipient's `meta_pub` in the URL fragment of a hosted
174
+ * `/pay` page (default: https://notracesol.xyz/pay). The recipient shares the
175
+ * link; the payer's browser derives a one-time address client-side and signs
176
+ * the transfer through Phantom.
177
+ *
178
+ * Format: `https://<origin>/pay#m=<bs58(meta_pub)>`
179
+ *
180
+ * Using the URL fragment (after `#`) means the meta-pub never reaches the
181
+ * NoTrace server — even the request log can't link payer to recipient.
182
+ */
183
+ /** Build a `/pay` link for a given meta-pub. */
184
+ declare function makePayLink(metaPubBytes: Uint8Array, origin?: string): string;
185
+ /**
186
+ * Parse a NoTrace pay-link and return the recipient meta-pub.
187
+ * Returns `null` for any input that isn't a parseable NoTrace pay-link.
188
+ */
189
+ declare function parsePayLink(url: string): Uint8Array | null;
190
+
191
+ /**
192
+ * Memo-program scanner — find incoming stealth payments addressed to you.
193
+ *
194
+ * Standalone (no `@solana/web3.js` dependency at type-level): the scanner takes
195
+ * a small `RpcLike` interface so it composes with any RPC client — a real
196
+ * `Connection` from web3.js, a mock for tests, or a thin HTTP wrapper.
197
+ *
198
+ * Algorithm per scanned signature:
199
+ *
200
+ * 1. Fetch the parsed tx.
201
+ * 2. Find a Memo-program instruction; extract the memo string.
202
+ * 3. `parseMemo(...)` → 32-byte `eph_pub` (skip if not an `nt1:` memo).
203
+ * 4. `recoverStealth(meta_scalar, meta_pub, eph_pub)` → expected `stealth_pub`.
204
+ * 5. Look for a SystemProgram `transfer` whose `destination` matches; if so,
205
+ * it's a payment to you.
206
+ */
207
+
208
+ /** Minimal shape we need from an RPC client. */
209
+ interface RpcLike {
210
+ /**
211
+ * Return signatures touching `address`, newest first. Slot/blockTime are
212
+ * optional but recommended for ordering and display.
213
+ */
214
+ getSignaturesForAddress(address: string, opts?: {
215
+ limit?: number;
216
+ before?: string;
217
+ until?: string;
218
+ }): Promise<Array<{
219
+ signature: string;
220
+ slot?: number;
221
+ blockTime?: number | null;
222
+ }>>;
223
+ /**
224
+ * Return a parsed transaction. The shape matches web3.js's
225
+ * `getParsedTransaction` for the fields the scanner actually reads.
226
+ */
227
+ getParsedTransaction(signature: string, opts?: {
228
+ maxSupportedTransactionVersion?: number;
229
+ commitment?: string;
230
+ }): Promise<ParsedTransactionLike | null>;
231
+ }
232
+ /** Minimal parsed-tx shape required by the scanner. */
233
+ interface ParsedTransactionLike {
234
+ slot?: number;
235
+ blockTime?: number | null;
236
+ meta?: {
237
+ err?: unknown;
238
+ } | null;
239
+ transaction: {
240
+ message: {
241
+ instructions: Array<ParsedInstructionLike>;
242
+ };
243
+ };
244
+ }
245
+ interface ParsedInstructionLike {
246
+ programId?: {
247
+ toString(): string;
248
+ } | string;
249
+ /** Memo program may surface the memo here as a string. */
250
+ parsed?: string | {
251
+ type?: string;
252
+ info?: {
253
+ destination?: string;
254
+ source?: string;
255
+ lamports?: number;
256
+ } | string;
257
+ };
258
+ /** Raw memo bytes (base58-encoded) if the program is not parsed. */
259
+ data?: string;
260
+ }
261
+ /** A successfully-matched stealth payment. */
262
+ interface StealthPayment {
263
+ /** Transaction signature on Solana. */
264
+ signature: string;
265
+ /** Slot the tx landed in (if the RPC returned it). */
266
+ slot?: number;
267
+ /** Block timestamp in **ms** since epoch (or `null` if RPC didn't supply). */
268
+ blockTimeMs?: number | null;
269
+ /** Lamports received at the stealth address. */
270
+ lamports: number;
271
+ /** Source wallet (the payer). */
272
+ source: string;
273
+ /** Stealth address (base58). */
274
+ stealthPub: string;
275
+ /** Secret stealth scalar — needed to sign sweeps. Treat as private key. */
276
+ stealthScalar: bigint;
277
+ /** Ephemeral pub from the memo (base58). */
278
+ ephPub: string;
279
+ }
280
+ interface ScanOptions {
281
+ /** How many signatures to pull from the memo program per batch. Default 100. */
282
+ limit?: number;
283
+ /** Concurrent `getParsedTransaction` calls. Default 5 (RPC-friendly). */
284
+ concurrency?: number;
285
+ /** Cursor for pagination — fetch sigs older than this signature. */
286
+ before?: string;
287
+ /** Already-scanned signatures to skip. */
288
+ alreadyScanned?: Set<string>;
289
+ }
290
+ /**
291
+ * Scan the memo program for new stealth payments addressed to `meta`.
292
+ *
293
+ * Returns *only the matches*. Updates `alreadyScanned` (if provided) in-place
294
+ * so subsequent calls can resume without re-checking.
295
+ */
296
+ declare function scanForPayments(meta: Pick<MetaKey, "scalar" | "pub">, rpc: RpcLike, opts?: ScanOptions): Promise<StealthPayment[]>;
297
+ /**
298
+ * Check a single signature for a stealth payment to `meta`. Returns the
299
+ * payment if matched, `null` otherwise. Exposed so callers can wire their own
300
+ * scanning loop (e.g. with subscribe/webhook flows instead of polling).
301
+ */
302
+ declare function checkSignature(signature: string, meta: Pick<MetaKey, "scalar" | "pub">, rpc: RpcLike): Promise<StealthPayment | null>;
303
+
304
+ /**
305
+ * @notrace/stealth-sdk
306
+ *
307
+ * Real stealth addresses on Solana. ECDH-derived one-time addresses on
308
+ * ed25519, view-key recoverable, with a Memo-program footprint.
309
+ *
310
+ * Extracted from the production NoTrace wallet (https://notracesol.xyz).
311
+ *
312
+ * Quick start:
313
+ *
314
+ * import {
315
+ * generateMetaKey, deriveStealthSender, recoverStealth,
316
+ * encodeMemo, parseMemo, makePayLink, scanForPayments,
317
+ * } from "@notrace/stealth-sdk";
318
+ *
319
+ * See README.md for end-to-end examples.
320
+ */
321
+
322
+ /** Package version, kept in sync with `package.json`. */
323
+ declare const VERSION = "1.0.0";
324
+
325
+ export { MEMO_PREFIX, MEMO_PROGRAM_ID, type MetaKey, type ParsedInstructionLike, type ParsedTransactionLike, type RecipientRecovery, type RpcLike, type ScanOptions, type SenderDerivation, type StealthPayment, VERSION, bs58decode, bs58encode, bytesEqual, bytesToNumberLE, checkSignature, concatBytes, deriveStealthSender, encodeMemo, generateMetaKey, makePayLink, metaFromSeed, numberToBytesLE, parseMemo, parsePayLink, pubFromScalar, recoverStealth, scanForPayments, signWithScalar, verify };