/**
* useStealthAddress - Mobile-optimized stealth address hook
*
* React Native version with secure storage and native clipboard support.
*
* @example
* ```tsx
* import { useStealthAddress } from '@sip-protocol/react-native'
*
* function ReceiveScreen() {
* const {
* metaAddress,
* stealthAddress,
* isGenerating,
* regenerate,
* copyToClipboard,
* saveToKeychain,
* } = useStealthAddress('solana')
*
* return (
*
* Share: {metaAddress}
*
* Copy
*
*
* Save Securely
*
*
* )
* }
* ```
*/
import { useState, useEffect, useCallback } from 'react'
import { copyToClipboard as nativeCopyToClipboard } from '../utils/clipboard'
import { SecureStorage } from '../storage/secure-storage'
/**
* Supported chain IDs (matches @sip-protocol/types ChainId)
*/
export type SupportedChainId =
| 'ethereum'
| 'solana'
| 'near'
| 'bitcoin'
| 'polygon'
| 'arbitrum'
| 'optimism'
| 'base'
| 'bsc'
| 'avalanche'
| 'cosmos'
| 'aptos'
| 'sui'
| 'polkadot'
| 'tezos'
/**
* Stealth meta-address structure from SDK
*/
interface StealthMetaAddressResult {
metaAddress: {
chain: string
spendingKey: string
viewingKey: string
}
spendingPrivateKey: string
viewingPrivateKey: string
}
/**
* Stealth address generation result from SDK
*/
interface StealthAddressResult {
stealthAddress: {
address: string
}
ephemeralPublicKey: string
}
/**
* Options for useStealthAddress hook
*/
export interface UseStealthAddressOptions {
/** Auto-save to secure storage on generation */
autoSave?: boolean
/** Require biometrics to access stored keys */
requireBiometrics?: boolean
/** Wallet identifier for storage (default: 'default') */
walletId?: string
}
/**
* Return type for useStealthAddress hook
*/
export interface UseStealthAddressReturn {
/** Encoded meta-address for sharing */
metaAddress: string | null
/** One-time stealth address */
stealthAddress: string | null
/** Spending private key (for claiming) */
spendingPrivateKey: string | null
/** Viewing private key (for scanning) */
viewingPrivateKey: string | null
/** Whether generation is in progress */
isGenerating: boolean
/** Error if any occurred */
error: Error | null
/** Generate a new stealth address */
regenerate: () => void
/** Copy stealth address to clipboard */
copyToClipboard: () => Promise
/** Save keys to secure storage */
saveToKeychain: () => Promise
/** Load keys from secure storage */
loadFromKeychain: () => Promise
/** Clear error state */
clearError: () => void
}
/**
* Mobile-optimized stealth address hook
*
* @param chain - Target blockchain
* @param options - Hook options
*/
export function useStealthAddress(
chain: SupportedChainId,
options: UseStealthAddressOptions = {}
): UseStealthAddressReturn {
const { autoSave = false, requireBiometrics = false, walletId = 'default' } = options
const [metaAddress, setMetaAddress] = useState(null)
const [stealthAddress, setStealthAddress] = useState(null)
const [spendingPrivateKey, setSpendingPrivateKey] = useState(null)
const [viewingPrivateKey, setViewingPrivateKey] = useState(null)
const [isGenerating, setIsGenerating] = useState(false)
const [error, setError] = useState(null)
// Generate keys on mount
useEffect(() => {
let cancelled = false
const generate = async () => {
setIsGenerating(true)
try {
// Dynamic import SDK functions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sdk: any = await import('@sip-protocol/sdk')
// Generate meta-address with keys (handles Ed25519 vs secp256k1 internally)
const generateStealthMetaAddress = sdk.generateStealthMetaAddress as (
chain: string
) => StealthMetaAddressResult
const metaAddressData = generateStealthMetaAddress(chain)
if (cancelled) return
const encodeStealthMetaAddress = sdk.encodeStealthMetaAddress as (
metaAddress: { chain: string; spendingKey: string; viewingKey: string }
) => string
const encoded = encodeStealthMetaAddress(metaAddressData.metaAddress)
setMetaAddress(encoded)
setSpendingPrivateKey(metaAddressData.spendingPrivateKey)
setViewingPrivateKey(metaAddressData.viewingPrivateKey)
// Generate initial stealth address
const generateStealthAddress = sdk.generateStealthAddress as (
metaAddress: { chain: string; spendingKey: string; viewingKey: string }
) => StealthAddressResult
const stealthData = generateStealthAddress(metaAddressData.metaAddress)
if (cancelled) return
setStealthAddress(stealthData.stealthAddress.address)
setError(null)
// Auto-save if enabled
if (autoSave && !cancelled) {
await SecureStorage.setSpendingKey(walletId, metaAddressData.spendingPrivateKey, {
requireBiometrics,
})
await SecureStorage.setViewingKey(walletId, metaAddressData.viewingPrivateKey, {
requireBiometrics,
})
await SecureStorage.setMetaAddress(walletId, encoded, { requireBiometrics })
}
} catch (err) {
if (cancelled) return
const error = err instanceof Error ? err : new Error('Failed to generate stealth addresses')
setError(error)
setMetaAddress(null)
setStealthAddress(null)
setSpendingPrivateKey(null)
setViewingPrivateKey(null)
} finally {
if (!cancelled) {
setIsGenerating(false)
}
}
}
generate()
return () => {
cancelled = true
}
}, [chain, autoSave, requireBiometrics, walletId])
// Regenerate stealth address
const regenerate = useCallback(() => {
if (!metaAddress) return
setIsGenerating(true)
// Use setTimeout to avoid blocking UI
setTimeout(async () => {
try {
const parts = metaAddress.split(':')
if (parts.length < 4) {
throw new Error('Invalid meta-address format')
}
const [, chainId, spendingKey, viewingKey] = parts
const metaAddressObj = {
chain: chainId,
spendingKey: spendingKey.startsWith('0x') ? spendingKey : `0x${spendingKey}`,
viewingKey: viewingKey.startsWith('0x') ? viewingKey : `0x${viewingKey}`,
}
// Dynamic import SDK
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sdk: any = await import('@sip-protocol/sdk')
const generateStealthAddress = sdk.generateStealthAddress as (
metaAddress: { chain: string; spendingKey: string; viewingKey: string }
) => StealthAddressResult
const stealthData = generateStealthAddress(metaAddressObj)
setStealthAddress(stealthData.stealthAddress.address)
setError(null)
} catch (err) {
const error = err instanceof Error ? err : new Error('Failed to regenerate stealth address')
setError(error)
} finally {
setIsGenerating(false)
}
}, 0)
}, [metaAddress])
// Copy to clipboard (native)
const copyToClipboard = useCallback(async (): Promise => {
if (!stealthAddress) return false
const success = await nativeCopyToClipboard(stealthAddress)
if (!success) {
setError(new Error('Failed to copy to clipboard'))
} else {
setError(null)
}
return success
}, [stealthAddress])
// Save to secure storage
const saveToKeychain = useCallback(async (): Promise => {
if (!spendingPrivateKey || !viewingPrivateKey || !metaAddress) {
setError(new Error('No keys to save'))
return false
}
try {
await SecureStorage.setSpendingKey(walletId, spendingPrivateKey, { requireBiometrics })
await SecureStorage.setViewingKey(walletId, viewingPrivateKey, { requireBiometrics })
await SecureStorage.setMetaAddress(walletId, metaAddress, { requireBiometrics })
setError(null)
return true
} catch (err) {
const error = err instanceof Error ? err : new Error('Failed to save to keychain')
setError(error)
return false
}
}, [spendingPrivateKey, viewingPrivateKey, metaAddress, walletId, requireBiometrics])
// Load from secure storage
const loadFromKeychain = useCallback(async (): Promise => {
try {
const storedMeta = await SecureStorage.getMetaAddress(walletId, { requireBiometrics })
const storedSpending = await SecureStorage.getSpendingKey(walletId, { requireBiometrics })
const storedViewing = await SecureStorage.getViewingKey(walletId, { requireBiometrics })
if (!storedMeta || !storedSpending || !storedViewing) {
setError(new Error('No keys found in keychain'))
return false
}
setMetaAddress(storedMeta)
setSpendingPrivateKey(storedSpending)
setViewingPrivateKey(storedViewing)
setError(null)
return true
} catch (err) {
const error = err instanceof Error ? err : new Error('Failed to load from keychain')
setError(error)
return false
}
}, [walletId, requireBiometrics])
// Clear error
const clearError = useCallback(() => {
setError(null)
}, [])
return {
metaAddress,
stealthAddress,
spendingPrivateKey,
viewingPrivateKey,
isGenerating,
error,
regenerate,
copyToClipboard,
saveToKeychain,
loadFromKeychain,
clearError,
}
}