npm package: @sahilw/dust-stealth
Description: Dust Protocol stealth address operations — ERC-5564/ERC-6538 ECDH stealth addresses on secp256k1
Author: Dust Protocol
Latest version: 0.1.0
Deprecated: Moved to dust-stealth (unscoped). Install: npm i dust-stealth

# @dust/stealth

[![npm version](https://img.shields.io/npm/v/@dust/stealth)](https://www.npmjs.com/package/@dust/stealth)

Stealth address generation and scanning for the Dust Protocol, implementing ERC-5564 and ERC-6538. Provides key management, one-time address generation, blockchain scanning, the `.dust` human-readable name system, HD claim address derivation, and EIP-7702 delegation support.

## Installation

```bash
npm install @dust/stealth ethers
```

## Quick Start

```typescript
import { ethers } from 'ethers'
import {
  deriveStealthKeyPairFromSignatureAndPin,
  formatStealthMetaAddress,
  generateStealthAddress,
  scanAnnouncements,
} from '@dust/stealth'

// Recipient: derive keys from wallet signature + PIN
const provider = new ethers.providers.JsonRpcProvider('https://sepolia.drpc.org')
const signer = provider.getSigner()
const signature = await signer.signMessage(STEALTH_KEY_DERIVATION_MESSAGE)
const keys = await deriveStealthKeyPairFromSignatureAndPin(signature, '123456')
const metaAddress = formatStealthMetaAddress(keys)

// Sender: generate a one-time stealth address
const parsed = parseStealthMetaAddress(metaAddress)
const { stealthAddress, ephemeralPublicKey, viewTag } = generateStealthAddress(parsed, 11155111)

// Recipient: scan for incoming payments
const results = await scanAnnouncements(provider, keys, fromBlock, 'latest', undefined, 11155111)
for (const r of results) {
  if (r.isMatch) console.log('Found payment at', r.announcement.stealthAddress)
}
```

## API Reference

### Key Management

#### `STEALTH_KEY_DERIVATION_MESSAGE`

The canonical message to request the wallet signature from users. Present this exact string in your signing prompt.

```typescript
import { STEALTH_KEY_DERIVATION_MESSAGE } from '@dust/stealth'
// 'Sign to activate your Private Wallet.\n\n...'
```

#### `generateStealthKeyPair(): StealthKeyPair`

Generate a random stealth key pair. Use `deriveStealthKeyPairFromSignature` or `deriveStealthKeyPairFromSignatureAndPin` for production — this is for testing.

#### `deriveStealthKeyPairFromSignature(signature: string): StealthKeyPair`

Deterministically derive spending and viewing keys from a wallet signature alone. Entropy is computed via `keccak256(signature)` with domain separation.

```typescript
import { deriveStealthKeyPairFromSignature } from '@dust/stealth'

const keys = deriveStealthKeyPairFromSignature(signature)
```

#### `deriveStealthKeyPairFromSignatureAndPin(signature: string, pin: string): Promise<StealthKeyPair>`

Derive keys from a wallet signature and PIN. Uses PBKDF2 with 100,000 iterations (v2 salts). This is the recommended derivation for users.

```typescript
import { deriveStealthKeyPairFromSignatureAndPin } from '@dust/stealth'

const keys = await deriveStealthKeyPairFromSignatureAndPin(signature, '123456')
```

#### `formatStealthMetaAddress(keys: StealthKeyPair, chain?: string): string`

Encode keys as a stealth meta-address URI: `st:eth:0x<spending><viewing>`.

```typescript
const metaAddress = formatStealthMetaAddress(keys)
// 'st:eth:0x02abc...def02xyz...uvw'
```

#### `parseStealthMetaAddress(uri: string): StealthMetaAddress`

Parse a stealth meta-address URI. Throws if the format is invalid or either public key is invalid.

#### `isValidCompressedPublicKey(key: string): boolean`

Returns `true` if `key` is a valid 33-byte compressed secp256k1 public key (`02`/`03` prefix, 66 hex chars).

#### `getPublicKeyFromPrivate(privateKey: string, compressed?: boolean): string`

Derive the public key from a private key.

#### `decompressPublicKey(compressedKey: string): { x: string; y: string } | null`

Return the uncompressed `(x, y)` coordinates. Returns `null` if the key is invalid.

---

### Address Generation (ERC-5564)

#### `generateStealthAddress(meta: StealthMetaAddress, chainId?: number): GeneratedStealthAddress`

Generate a one-time stealth address for a recipient's meta-address. Uses ECDH over secp256k1.

On EIP-7702 chains (Ethereum Sepolia), the stealth address is the EOA itself. On other chains, it is the CREATE2 account address derived from `accountFactory`.

```typescript
import { parseStealthMetaAddress, generateStealthAddress } from '@dust/stealth'

const meta = parseStealthMetaAddress('st:eth:0x...')
const { stealthAddress, stealthEOAAddress, ephemeralPublicKey, viewTag } = generateStealthAddress(meta, 11155111)

// stealthAddress  — where to send funds
// ephemeralPublicKey — announce this on-chain so the recipient can scan
// viewTag — 1-byte tag for fast scanning
```

#### `computeStealthPrivateKey(spendingPrivateKey: string, viewingPrivateKey: string, ephemeralPublicKey: string): string`

Compute the private key controlling a stealth address. Call this after scanning confirms a match.

```typescript
import { computeStealthPrivateKey } from '@dust/stealth'

const stealthPrivateKey = computeStealthPrivateKey(
  keys.spendingPrivateKey,
  keys.viewingPrivateKey,
  announcement.ephemeralPublicKey,
)
```

#### `verifyStealthAddress(ephemeralPublicKey: string, spendingPublicKey: string, expectedAddress: string, viewingPrivateKey: string): boolean`

Verify that an announced stealth address matches the recipient's keys. Used inside the scanner.

#### `computeViewTag(viewingPrivateKey: string, ephemeralPublicKey: string): string`

Compute the 1-byte view tag from the ECDH shared secret. Mismatching view tags prune 255/256 announcements during scanning without revealing the private key.

#### `computeStealthWalletAddress(ownerEOA: string, chainId?: number): string`

Compute the CREATE2 stealth wallet address for a given EOA owner.

#### `computeStealthAccountAddress(ownerEOA: string, chainId?: number): string`

Compute the CREATE2 ERC-4337 account address for a given EOA owner.

#### `computeLegacyStealthWalletAddress(ownerEOA: string, chainId?: number): string`

Compute the legacy wallet CREATE2 address (for backwards compatibility with older announcements).

#### `computeLegacyStealthAccountAddress(ownerEOA: string, chainId?: number): string`

Compute the legacy account CREATE2 address.

#### `getAddressFromPrivateKey(privateKey: string): string`

Return the checksummed Ethereum address for a private key.

---

### Transaction Signing

#### `signWalletDrain(stealthPrivateKey, walletAddress, to, chainId, nonce?): Promise<string>`

Sign a drain message for a StealthWallet (CREATE2) contract: `keccak256(walletAddress, to, nonce, chainId)`.

#### `signWalletExecute(stealthPrivateKey, walletAddress, to, value, data, chainId, nonce?): Promise<string>`

Sign an execute message for a StealthWallet contract.

#### `signUserOp(userOpHash: string, stealthPrivateKey: string): Promise<string>`

Sign an ERC-4337 UserOperation hash for an ERC-4337 stealth account.

---

### Announcement Scanner

#### `scanAnnouncements(provider, keys, fromBlock, toBlock?, announcerAddress?, chainId?): Promise<ScanResult[]>`

Scan on-chain `Announcement` events and return all that match the recipient's keys. Uses chunked queries to respect chain-specific RPC block range limits and retries with exponential backoff.

View tag filtering eliminates 255/256 of announcements before full ECDH computation.

```typescript
import { ethers } from 'ethers'
import { scanAnnouncements } from '@dust/stealth'

const provider = new ethers.providers.JsonRpcProvider('https://sepolia.drpc.org')
const results = await scanAnnouncements(
  provider,
  keys,
  10251347,  // fromBlock (deployment block)
  'latest',
  undefined, // uses chain default announcer
  11155111,
)

for (const { announcement, stealthPrivateKey, walletType } of results) {
  console.log('Stealth address:', announcement.stealthAddress)
  console.log('Wallet type:', walletType) // 'eoa' | 'create2' | 'account' | 'eip7702'
}
```

#### `scanAnnouncementsViewOnly(provider, viewingPrivateKey, spendingPublicKey, fromBlock, toBlock?, announcerAddress?, chainId?): Promise<StealthAnnouncement[]>`

Scan without revealing the spending private key. Returns matching announcements but not the stealth private key. Use for watch-only wallets.

#### `getAnnouncementCount(provider, fromBlock, toBlock?, announcerAddress?, chainId?): Promise<number>`

Count total announcements in a block range (does not filter by recipient).

---

### ERC-6538 Registry

The ERC-6538 registry maps wallet addresses to stealth meta-addresses on-chain.

#### `getRegistryForChain(chainId?: number): string`

Get the registry contract address for a chain.

#### `registerStealthMetaAddress(signer, metaAddress, schemeId?, registryAddress?): Promise<string>`

Register a meta-address for the signer's account. Returns the transaction hash.

```typescript
import { registerStealthMetaAddress } from '@dust/stealth'

const txHash = await registerStealthMetaAddress(signer, 'st:eth:0x...')
```

#### `registerStealthMetaAddressOnBehalf(signer, registrant, metaAddress, signature, schemeId?, registryAddress?): Promise<string>`

Register a meta-address on behalf of another address using an EIP-712 signature from that address.

#### `lookupStealthMetaAddress(provider, address, schemeId?, registryAddress?): Promise<string | null>`

Look up the registered meta-address for any address. Returns `null` if not registered.

#### `getRegistryNonce(provider, address, registryAddress?): Promise<ethers.BigNumber>`

Get the current EIP-712 nonce for an address.

#### `signRegistration(signer, metaAddress, nonce, chainId, schemeId?, registryAddress?): Promise<string>`

Sign an EIP-712 registration message for use with `registerStealthMetaAddressOnBehalf`.

#### `isRegistered(provider, address, schemeId?, registryAddress?): Promise<boolean>`

Returns `true` if the address has a registered meta-address.

#### `formatBytesToUri(bytes: string, chain?: string): string`

Convert raw registry bytes to the `st:eth:0x...` URI format.

---

### .dust Name Registry

Human-readable names resolving to stealth meta-addresses. Names are registered on the canonical naming chain (Ethereum Sepolia) and resolved there when the active chain has no name registry.

#### `NAME_SUFFIX`

`'.dust'` — the suffix appended to all names.

#### `normalizeName(name: string): string`

Lowercase and trim a name.

#### `stripNameSuffix(name: string): string`

Remove `.dust` suffix if present.

#### `formatNameWithSuffix(name: string): string`

Ensure a name ends with `.dust`.

#### `isValidName(name: string): boolean`

Returns `true` if the name is 1–32 alphanumeric/hyphen/underscore characters.

#### `isStealthName(input: string): boolean`

Returns `true` if the input looks like a `.dust` name or valid name without suffix.

#### `registerStealthName(signer, name, metaAddress, registryAddress?): Promise<string>`

Register a `.dust` name pointing to a meta-address. Throws if the name is invalid.

```typescript
import { registerStealthName } from '@dust/stealth'

const txHash = await registerStealthName(signer, 'alice.dust', 'st:eth:0x...')
```

#### `resolveStealthName(provider, name, chainId?): Promise<string | null>`

Resolve a name to its raw meta-address bytes. Tries the active chain first, then falls back to the canonical chain. Returns `null` if not registered.

#### `isNameAvailable(provider, name, chainId?): Promise<boolean | null>`

Check if a name is available to register. Returns `null` if the registry is not configured.

#### `getNameOwner(provider, name, chainId?): Promise<string | null>`

Get the owner address of a registered name. Returns `null` if unregistered.

#### `getNamesOwnedBy(provider, address, chainId?): Promise<string[]>`

List all names owned by an address.

#### `updateNameMetaAddress(signer, name, newMetaAddress, registryAddress?): Promise<string>`

Update the meta-address a name points to.

#### `transferStealthName(signer, name, newOwner, registryAddress?): Promise<string>`

Transfer ownership of a name.

#### `discoverNameByMetaAddress(provider, metaAddressHex, deployer): Promise<string | null>`

Reverse-lookup a name from its meta-address bytes. Searches names owned by `deployer` across all configured chains.

---

### PIN Functions

The PIN module provides key derivation with versioned salt schemes and AES-256-GCM PIN encryption.

#### `validatePin(pin: string): { valid: boolean; error?: string }`

Validate a 6-digit numeric PIN string.

#### `encryptPin(pin: string, signature: string): Promise<string>`

Encrypt a PIN with AES-256-GCM, keyed from the wallet signature via PBKDF2. Returns a hex-encoded `iv || ciphertext`.

#### `decryptPin(encrypted: string, signature: string): Promise<string>`

Decrypt a previously encrypted PIN.

#### Seed derivation functions

| Function | Salt | Use |
|----------|------|-----|
| `deriveSpendingSeed(sig, pin)` | `'Dust Spend Authority v2'` | Current (v2) |
| `deriveViewingSeed(sig, pin)` | `'Dust View Authority v2'` | Current (v2) |
| `deriveClaimSeed(sig, pin)` | `'Dust Claim Authority v2'` | Current (v2) |
| `deriveSpendingSeedV0(sig, pin)` | SHA-512 `'Dust Spend Authority'` | Legacy (v0) |
| `deriveViewingSeedV0(sig, pin)` | SHA-512 `'Dust View Authority'` | Legacy (v0) |
| `deriveClaimSeedV0(sig, pin)` | SHA-512 `'Dust Claim Authority'` | Legacy (v0) |
| `deriveSpendingSeedV1(sig, pin)` | PBKDF2 `'Dust Spend Authority'` | Legacy (v1) |
| `deriveViewingSeedV1(sig, pin)` | PBKDF2 `'Dust View Authority'` | Legacy (v1) |
| `deriveClaimSeedV1(sig, pin)` | PBKDF2 `'Dust Claim Authority'` | Legacy (v1) |

---

### HD Claim Addresses

Deterministic addresses derived from a wallet signature for receiving stealth claims.

#### `CLAIM_ADDRESS_DERIVATION_MESSAGE`

The canonical message for the wallet signature request.

#### `deriveSeedFromSignature(signature: string): string`

Compute `keccak256(signature)` as a 32-byte hex seed.

#### `deriveClaimAddressAtIndex(seed: string, index: number): DerivedClaimAddress`

Derive the claim address at a specific index from a seed.

#### `deriveClaimAddresses(signature: string, count: number): DerivedClaimAddress[]`

Derive `count` claim addresses from a signature (no PIN).

#### `deriveSeedFromSignatureAndPin(signature: string, pin: string): Promise<string>`

Derive a seed using PBKDF2 with PIN (v2 derivation).

#### `deriveClaimAddressesWithPin(signature: string, pin: string, count: number): Promise<DerivedClaimAddress[]>`

Derive `count` claim addresses using the PIN-based seed.

#### `getNextClaimAddress(signature: string, usedAddresses: string[]): DerivedClaimAddress`

Find the first unused claim address (checks up to index 1000).

#### `verifyClaimAddressDerivation(signature: string, address: string, maxIndex?: number): number`

Verify that an address belongs to the derived set. Returns the index or `-1`.

```typescript
interface DerivedClaimAddress {
  address: string
  privateKey: string
  path: string      // 'stealth/N'
  index: number
  label?: string
}
```

---

### EIP-7702 Delegation

EIP-7702 lets a stealth EOA delegate its execution logic to the `StealthSubAccount7702` contract on supported chains (currently Ethereum Sepolia).

#### `signDrain7702(stealthPrivateKey, stealthAddress, to, nonce, chainId): Promise<string>`

Sign a drain message: `keccak256(abi.encode(stealthAddress, to, nonce, chainId))`.

#### `signInitialize7702(stealthPrivateKey, stealthAddress, ownerAddress, chainId): Promise<string>`

Sign an initialize message: `keccak256(abi.encode(stealthAddress, owner, chainId))`.

#### `buildSignedAuthorization(stealthPrivateKey: string, chainId: number): Promise<Record<string, unknown>>`

Build a signed EIP-7702 authorization using viem. The consuming application must have `viem` installed.

---

### Relayer Client

The stealth relayer handles gasless claim forwarding for stealth wallets.

#### `createRelayerClient(config: RelayerClientConfig): RelayerClient`

Create a relayer client.

```typescript
import { createRelayerClient } from '@dust/stealth'

const relayer = createRelayerClient({ baseUrl: 'https://relayer.example.com' })

const healthy = await relayer.checkHealth()
const info = await relayer.getInfo(11155111)

const job = await relayer.submitWithdraw(
  stealthAddress,
  signature,
  recipientAddress,
  ownerAddress,
  11155111,
)

const status = await relayer.waitForJobCompletion(job.jobId)
console.log('TX:', status?.txHash)
```

```typescript
interface RelayerClientConfig {
  baseUrl: string
}

interface RelayerClient {
  checkHealth(chainId?: number): Promise<boolean>
  getInfo(chainId?: number): Promise<RelayerInfo | null>
  calculateFee(stealthAddress: string, chainId?: number): Promise<FeeCalculation | null>
  submitWithdraw(stealthAddress, signature, recipient, owner, chainId?): Promise<WithdrawResponse>
  getJobStatus(jobId: string, chainId?: number): Promise<JobStatus | null>
  waitForJobCompletion(jobId, maxAttempts?, intervalMs?, chainId?): Promise<JobStatus | null>
}

interface RelayerInfo {
  relayerAddress: string
  balance: string
  feeBps: number
  minFee: string
  chainId: number
}

interface FeeCalculation {
  balance: string
  fee: string
  amountAfterFee: string
  feeBps: number
}

interface WithdrawResponse {
  jobId: string
  status: 'pending' | 'processing' | 'completed' | 'failed'
  message: string
  fee: string
  amountAfterFee: string
}
```

---

### Contract ABIs

Pre-typed ABIs for on-chain interactions:

```typescript
import {
  STEALTH_WALLET_FACTORY_ABI,
  STEALTH_ACCOUNT_FACTORY_ABI,
  ENTRY_POINT_ABI,
  DUST_PAYMASTER_ABI,
  STEALTH_SUB_ACCOUNT_7702_ABI,
} from '@dust/stealth'
```

## Types

```typescript
interface StealthKeyPair {
  spendingPrivateKey: string
  spendingPublicKey: string
  viewingPrivateKey: string
  viewingPublicKey: string
}

interface StealthMetaAddress {
  prefix: string          // 'eth'
  spendingPublicKey: string
  viewingPublicKey: string
  raw: string             // 'st:eth:0x...'
}

interface GeneratedStealthAddress {
  stealthAddress: string        // where to send funds
  stealthEOAAddress: string     // underlying EOA
  ephemeralPublicKey: string    // announce on-chain
  viewTag: string               // 1-byte hex for fast filtering
  stealthPublicKey: string      // compressed public key of the stealth EOA
}

interface ScanResult {
  announcement: StealthAnnouncement
  stealthPrivateKey: string
  isMatch: boolean
  privateKeyVerified?: boolean
  derivedAddress?: string
  walletType?: 'eoa' | 'create2' | 'account' | 'eip7702'
  announcedTokenAddress?: string | null
  announcedTokenAmount?: string | null
  announcedChainId?: number | null
}
```

## License

MIT