@sip-protocol/react 0.1.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,188 @@
1
+ import { useState, useCallback, useContext } from 'react'
2
+ import { SIP, type SIPConfig } from '@sip-protocol/sdk'
3
+ import { useSIPContext } from '../providers/sip-provider'
4
+
5
+ /**
6
+ * Return type for useSIP hook
7
+ */
8
+ export interface UseSIPReturn {
9
+ /** SIP client instance (null if not initialized or no provider) */
10
+ client: SIP | null
11
+ /** Whether the client is ready to use */
12
+ isReady: boolean
13
+ /** Error during initialization (if any) */
14
+ error: Error | null
15
+ /** Manually initialize the SIP client (only for standalone usage) */
16
+ initialize: (config: SIPConfig) => Promise<void>
17
+ }
18
+
19
+ /**
20
+ * useSIP - Main hook for accessing SIP client
21
+ *
22
+ * Provides access to the SIP client instance from SIPProvider, along with
23
+ * initialization state and error handling. Can also be used standalone without
24
+ * a provider by calling `initialize()`.
25
+ *
26
+ * **Usage with SIPProvider (recommended):**
27
+ * ```tsx
28
+ * import { SIPProvider, useSIP } from '@sip-protocol/react'
29
+ *
30
+ * function App() {
31
+ * return (
32
+ * <SIPProvider config={{ network: 'testnet' }}>
33
+ * <MyComponent />
34
+ * </SIPProvider>
35
+ * )
36
+ * }
37
+ *
38
+ * function MyComponent() {
39
+ * const { client, isReady } = useSIP()
40
+ *
41
+ * if (!isReady || !client) {
42
+ * return <div>Loading...</div>
43
+ * }
44
+ *
45
+ * // Use client.createIntent(), client.getQuotes(), etc.
46
+ * }
47
+ * ```
48
+ *
49
+ * **Standalone usage (without provider):**
50
+ * ```tsx
51
+ * function MyComponent() {
52
+ * const { client, isReady, initialize, error } = useSIP()
53
+ *
54
+ * useEffect(() => {
55
+ * initialize({ network: 'testnet' }).catch(console.error)
56
+ * }, [])
57
+ *
58
+ * if (error) {
59
+ * return <div>Error: {error.message}</div>
60
+ * }
61
+ *
62
+ * if (!isReady || !client) {
63
+ * return <div>Initializing...</div>
64
+ * }
65
+ *
66
+ * return <div>Ready!</div>
67
+ * }
68
+ * ```
69
+ *
70
+ * @returns Object with client, isReady, error, and initialize function
71
+ *
72
+ * @example Basic usage with provider
73
+ * ```tsx
74
+ * import { useSIP } from '@sip-protocol/react'
75
+ *
76
+ * function MyComponent() {
77
+ * const { client, isReady } = useSIP()
78
+ *
79
+ * if (!isReady || !client) {
80
+ * return <div>Loading...</div>
81
+ * }
82
+ *
83
+ * // Use client methods
84
+ * const handleCreateIntent = async () => {
85
+ * const intent = await client.createIntent({ ... })
86
+ * }
87
+ * }
88
+ * ```
89
+ *
90
+ * @example With error handling
91
+ * ```tsx
92
+ * function MyComponent() {
93
+ * const { client, isReady, error } = useSIP()
94
+ *
95
+ * if (error) {
96
+ * return <div>Failed to initialize: {error.message}</div>
97
+ * }
98
+ *
99
+ * if (!isReady || !client) {
100
+ * return <div>Initializing SIP client...</div>
101
+ * }
102
+ *
103
+ * return <div>Ready to use SIP!</div>
104
+ * }
105
+ * ```
106
+ *
107
+ * @example Standalone initialization
108
+ * ```tsx
109
+ * function MyComponent() {
110
+ * const { client, isReady, initialize } = useSIP()
111
+ *
112
+ * const handleInit = async () => {
113
+ * try {
114
+ * await initialize({
115
+ * network: 'mainnet',
116
+ * mode: 'production',
117
+ * intentsAdapter: { jwtToken: 'xxx' }
118
+ * })
119
+ * } catch (err) {
120
+ * console.error('Init failed:', err)
121
+ * }
122
+ * }
123
+ *
124
+ * return (
125
+ * <button onClick={handleInit} disabled={isReady}>
126
+ * {isReady ? 'Initialized' : 'Initialize SIP'}
127
+ * </button>
128
+ * )
129
+ * }
130
+ * ```
131
+ */
132
+ export function useSIP(): UseSIPReturn {
133
+ // State for standalone usage (without provider)
134
+ const [standaloneClient, setStandaloneClient] = useState<SIP | null>(null)
135
+ const [standaloneReady, setStandaloneReady] = useState(false)
136
+ const [standaloneError, setStandaloneError] = useState<Error | null>(null)
137
+
138
+ // Try to get context from SIPProvider
139
+ let providerContext
140
+ try {
141
+ providerContext = useSIPContext()
142
+ } catch {
143
+ // Not inside SIPProvider, use standalone mode
144
+ providerContext = null
145
+ }
146
+
147
+ const standaloneInitialize = useCallback(async (config: SIPConfig) => {
148
+ // Prevent re-initialization if already initialized
149
+ if (standaloneClient && standaloneReady) {
150
+ console.warn('SIP client already initialized. Call will be ignored.')
151
+ return
152
+ }
153
+
154
+ try {
155
+ setStandaloneError(null)
156
+ setStandaloneReady(false)
157
+
158
+ const newClient = new SIP(config)
159
+ setStandaloneClient(newClient)
160
+ setStandaloneReady(true)
161
+ } catch (err) {
162
+ const error = err instanceof Error ? err : new Error(String(err))
163
+ setStandaloneError(error)
164
+ setStandaloneReady(false)
165
+ throw error
166
+ }
167
+ }, [standaloneClient, standaloneReady])
168
+
169
+ // If we have a provider context, use it
170
+ if (providerContext) {
171
+ return {
172
+ client: providerContext.client,
173
+ isReady: true, // Provider always provides ready client
174
+ error: null, // Provider throws on error, doesn't expose it
175
+ initialize: async () => {
176
+ console.warn('initialize() called but SIPProvider is already providing a client. This call will be ignored.')
177
+ },
178
+ }
179
+ }
180
+
181
+ // Otherwise, return standalone state
182
+ return {
183
+ client: standaloneClient,
184
+ isReady: standaloneReady,
185
+ error: standaloneError,
186
+ initialize: standaloneInitialize,
187
+ }
188
+ }
@@ -0,0 +1,184 @@
1
+ import { useState, useEffect, useCallback } from 'react'
2
+ import {
3
+ generateStealthMetaAddress,
4
+ generateStealthAddress,
5
+ generateEd25519StealthMetaAddress,
6
+ generateEd25519StealthAddress,
7
+ encodeStealthMetaAddress,
8
+ isEd25519Chain,
9
+ } from '@sip-protocol/sdk'
10
+ import type { ChainId } from '@sip-protocol/types'
11
+
12
+ /**
13
+ * useStealthAddress - Generate and manage stealth addresses
14
+ *
15
+ * @remarks
16
+ * This hook handles stealth address generation for privacy-preserving transactions.
17
+ * It automatically generates a meta-address on mount and allows regeneration of
18
+ * one-time stealth addresses from that meta-address.
19
+ *
20
+ * Features:
21
+ * - Auto-generates meta-address for the specified chain
22
+ * - Generates one-time stealth addresses
23
+ * - Supports both secp256k1 (EVM) and ed25519 (Solana/NEAR) chains
24
+ * - Copy-to-clipboard functionality
25
+ * - Loading state management
26
+ *
27
+ * @param chain - Target blockchain (determines curve type and address format)
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * import { useStealthAddress } from '@sip-protocol/react'
32
+ *
33
+ * function ReceivePayment() {
34
+ * const {
35
+ * metaAddress,
36
+ * stealthAddress,
37
+ * isGenerating,
38
+ * regenerate,
39
+ * copyToClipboard,
40
+ * } = useStealthAddress('ethereum')
41
+ *
42
+ * return (
43
+ * <div>
44
+ * <p>Share this: {metaAddress}</p>
45
+ * <p>One-time address: {stealthAddress}</p>
46
+ * <button onClick={regenerate}>Generate New</button>
47
+ * <button onClick={copyToClipboard}>Copy</button>
48
+ * </div>
49
+ * )
50
+ * }
51
+ * ```
52
+ */
53
+ export function useStealthAddress(chain: ChainId): {
54
+ metaAddress: string | null
55
+ stealthAddress: string | null
56
+ isGenerating: boolean
57
+ regenerate: () => void
58
+ copyToClipboard: () => Promise<void>
59
+ } {
60
+ const [metaAddress, setMetaAddress] = useState<string | null>(null)
61
+ const [stealthAddress, setStealthAddress] = useState<string | null>(null)
62
+ const [isGenerating, setIsGenerating] = useState<boolean>(false)
63
+
64
+ // Generate meta-address on mount
65
+ useEffect(() => {
66
+ let cancelled = false
67
+
68
+ setIsGenerating(true)
69
+
70
+ // Use setTimeout to make it async and allow state to flush
71
+ const timer = setTimeout(() => {
72
+ if (cancelled) return
73
+
74
+ try {
75
+ // Use ed25519 for Solana/NEAR/Aptos/Sui, secp256k1 for others
76
+ const isEd25519 = isEd25519Chain(chain)
77
+
78
+ const metaAddressData = isEd25519
79
+ ? generateEd25519StealthMetaAddress(chain)
80
+ : generateStealthMetaAddress(chain)
81
+
82
+ const encoded = encodeStealthMetaAddress(metaAddressData.metaAddress)
83
+ if (cancelled) return
84
+ setMetaAddress(encoded)
85
+
86
+ // Generate initial stealth address from meta-address
87
+ const stealthData = isEd25519
88
+ ? generateEd25519StealthAddress(metaAddressData.metaAddress)
89
+ : generateStealthAddress(metaAddressData.metaAddress)
90
+
91
+ if (cancelled) return
92
+ setStealthAddress(stealthData.stealthAddress.address)
93
+ } catch (error) {
94
+ console.error('Failed to generate stealth addresses:', error)
95
+ if (cancelled) return
96
+ setMetaAddress(null)
97
+ setStealthAddress(null)
98
+ } finally {
99
+ if (!cancelled) {
100
+ setIsGenerating(false)
101
+ }
102
+ }
103
+ }, 0)
104
+
105
+ return () => {
106
+ cancelled = true
107
+ clearTimeout(timer)
108
+ }
109
+ }, [chain])
110
+
111
+ // Regenerate stealth address from existing meta-address
112
+ const regenerate = useCallback(() => {
113
+ if (!metaAddress) {
114
+ return
115
+ }
116
+
117
+ setIsGenerating(true)
118
+
119
+ // Use setTimeout to make it async
120
+ setTimeout(() => {
121
+ try {
122
+ // Parse the meta-address back to object
123
+ const parts = metaAddress.split(':')
124
+ if (parts.length < 4) {
125
+ throw new Error('Invalid meta-address format')
126
+ }
127
+
128
+ const [, chainId, spendingKey, viewingKey] = parts
129
+ const metaAddressObj = {
130
+ chain: chainId as ChainId,
131
+ spendingKey: (spendingKey.startsWith('0x') ? spendingKey : `0x${spendingKey}`) as `0x${string}`,
132
+ viewingKey: (viewingKey.startsWith('0x') ? viewingKey : `0x${viewingKey}`) as `0x${string}`,
133
+ }
134
+
135
+ // Generate new stealth address
136
+ const isEd25519 = isEd25519Chain(chain)
137
+ const stealthData = isEd25519
138
+ ? generateEd25519StealthAddress(metaAddressObj)
139
+ : generateStealthAddress(metaAddressObj)
140
+
141
+ setStealthAddress(stealthData.stealthAddress.address)
142
+ } catch (error) {
143
+ console.error('Failed to regenerate stealth address:', error)
144
+ } finally {
145
+ setIsGenerating(false)
146
+ }
147
+ }, 0)
148
+ }, [metaAddress, chain])
149
+
150
+ // Copy stealth address to clipboard
151
+ const copyToClipboard = useCallback(async () => {
152
+ if (!stealthAddress) {
153
+ return
154
+ }
155
+
156
+ try {
157
+ await navigator.clipboard.writeText(stealthAddress)
158
+ } catch (error) {
159
+ console.error('Failed to copy to clipboard:', error)
160
+ // Fallback for older browsers
161
+ const textArea = document.createElement('textarea')
162
+ textArea.value = stealthAddress
163
+ textArea.style.position = 'fixed'
164
+ textArea.style.left = '-999999px'
165
+ document.body.appendChild(textArea)
166
+ textArea.select()
167
+ try {
168
+ document.execCommand('copy')
169
+ } catch (err) {
170
+ console.error('Fallback copy failed:', err)
171
+ } finally {
172
+ document.body.removeChild(textArea)
173
+ }
174
+ }
175
+ }, [stealthAddress])
176
+
177
+ return {
178
+ metaAddress,
179
+ stealthAddress,
180
+ isGenerating,
181
+ regenerate,
182
+ copyToClipboard,
183
+ }
184
+ }
@@ -0,0 +1,190 @@
1
+ import { useState, useCallback } from 'react'
2
+ import type { ViewingKey, EncryptedTransaction } from '@sip-protocol/types'
3
+ import {
4
+ generateViewingKey as sdkGenerateViewingKey,
5
+ decryptWithViewing,
6
+ } from '@sip-protocol/sdk'
7
+
8
+ /**
9
+ * Auditor share entry
10
+ */
11
+ interface AuditorShare {
12
+ auditorId: string
13
+ viewingKeyHash: string
14
+ sharedAt: number
15
+ }
16
+
17
+ /**
18
+ * useViewingKey - Generate and manage viewing keys for compliance
19
+ *
20
+ * @remarks
21
+ * Hook for managing viewing keys that enable selective disclosure of transaction
22
+ * details to auditors or regulators while maintaining on-chain privacy.
23
+ *
24
+ * Features:
25
+ * - Generate cryptographically random viewing keys
26
+ * - Decrypt encrypted transaction data
27
+ * - Share viewing keys with auditors (tracked in state)
28
+ * - Hierarchical key derivation via path parameter
29
+ *
30
+ * @example Basic usage
31
+ * ```tsx
32
+ * import { useViewingKey } from '@sip-protocol/react'
33
+ *
34
+ * function CompliancePanel() {
35
+ * const { viewingKey, generate, decrypt, share } = useViewingKey()
36
+ *
37
+ * const handleGenerateKey = () => {
38
+ * const key = generate('m/0/audit')
39
+ * console.log('Generated viewing key:', key.hash)
40
+ * }
41
+ *
42
+ * const handleDecrypt = async (encrypted: EncryptedTransaction) => {
43
+ * try {
44
+ * const data = await decrypt(encrypted)
45
+ * console.log('Decrypted amount:', data.amount)
46
+ * } catch (e) {
47
+ * console.error('Decryption failed - wrong key')
48
+ * }
49
+ * }
50
+ *
51
+ * return (
52
+ * <div>
53
+ * <button onClick={handleGenerateKey}>Generate Key</button>
54
+ * {viewingKey && <p>Key hash: {viewingKey.hash}</p>}
55
+ * </div>
56
+ * )
57
+ * }
58
+ * ```
59
+ *
60
+ * @example Sharing with auditors
61
+ * ```tsx
62
+ * function AuditManager() {
63
+ * const { viewingKey, generate, share, sharedWith } = useViewingKey()
64
+ *
65
+ * useEffect(() => {
66
+ * generate('m/0/compliance')
67
+ * }, [])
68
+ *
69
+ * const handleShareWithAuditor = async () => {
70
+ * await share('auditor-123')
71
+ * console.log('Shared with:', sharedWith)
72
+ * }
73
+ *
74
+ * return (
75
+ * <div>
76
+ * <button onClick={handleShareWithAuditor}>Share with Auditor</button>
77
+ * <ul>
78
+ * {sharedWith.map(audit => (
79
+ * <li key={audit.auditorId}>{audit.auditorId}</li>
80
+ * ))}
81
+ * </ul>
82
+ * </div>
83
+ * )
84
+ * }
85
+ * ```
86
+ */
87
+ export function useViewingKey() {
88
+ const [viewingKey, setViewingKey] = useState<ViewingKey | null>(null)
89
+ const [sharedWith, setSharedWith] = useState<AuditorShare[]>([])
90
+
91
+ /**
92
+ * Generate a new viewing key
93
+ *
94
+ * @param path - Hierarchical derivation path (BIP32-style, defaults to 'm/0')
95
+ * @returns Generated viewing key
96
+ *
97
+ * @example
98
+ * ```tsx
99
+ * const key = generate('m/0/audit')
100
+ * console.log(key.hash) // "0xabc123..."
101
+ * ```
102
+ */
103
+ const generate = useCallback((path?: string): ViewingKey => {
104
+ const key = sdkGenerateViewingKey(path)
105
+ setViewingKey(key)
106
+ setSharedWith([]) // Reset shares when generating new key
107
+ return key
108
+ }, [])
109
+
110
+ /**
111
+ * Decrypt encrypted transaction data with the current viewing key
112
+ *
113
+ * @param encrypted - Encrypted transaction data
114
+ * @returns Promise resolving to decrypted transaction details
115
+ * @throws {Error} If no viewing key is set or decryption fails
116
+ *
117
+ * @example
118
+ * ```tsx
119
+ * const data = await decrypt(encryptedTransaction)
120
+ * console.log(`Sender: ${data.sender}`)
121
+ * console.log(`Amount: ${data.amount}`)
122
+ * ```
123
+ */
124
+ const decrypt = useCallback(
125
+ async (encrypted: EncryptedTransaction) => {
126
+ if (!viewingKey) {
127
+ throw new Error('No viewing key available. Call generate() first.')
128
+ }
129
+
130
+ return decryptWithViewing(encrypted, viewingKey)
131
+ },
132
+ [viewingKey],
133
+ )
134
+
135
+ /**
136
+ * Share viewing key with an auditor (tracked in state)
137
+ *
138
+ * @param auditorId - Unique identifier for the auditor
139
+ * @returns Promise that resolves when sharing is complete
140
+ * @throws {Error} If no viewing key is set
141
+ *
142
+ * @remarks
143
+ * This function tracks which auditors have been given access to the viewing key.
144
+ * In a production system, you would:
145
+ * - Encrypt the viewing key with the auditor's public key
146
+ * - Store the encrypted key in a database or smart contract
147
+ * - Send the encrypted key to the auditor via secure channel
148
+ *
149
+ * @example
150
+ * ```tsx
151
+ * await share('auditor-alice')
152
+ * await share('auditor-bob')
153
+ * console.log(sharedWith) // [{ auditorId: 'auditor-alice', ... }, ...]
154
+ * ```
155
+ */
156
+ const share = useCallback(
157
+ async (auditorId: string): Promise<void> => {
158
+ if (!viewingKey) {
159
+ throw new Error('No viewing key available. Call generate() first.')
160
+ }
161
+
162
+ const shareEntry: AuditorShare = {
163
+ auditorId,
164
+ viewingKeyHash: viewingKey.hash,
165
+ sharedAt: Date.now(),
166
+ }
167
+
168
+ setSharedWith(prev => [...prev, shareEntry])
169
+
170
+ // In a real implementation:
171
+ // 1. Encrypt viewing key with auditor's public key
172
+ // 2. Store encrypted key on-chain or in secure database
173
+ // 3. Notify auditor via secure channel
174
+ },
175
+ [viewingKey],
176
+ )
177
+
178
+ return {
179
+ /** Current viewing key (null if not generated) */
180
+ viewingKey,
181
+ /** List of auditors who have been given access */
182
+ sharedWith,
183
+ /** Generate a new viewing key */
184
+ generate,
185
+ /** Decrypt encrypted transaction data */
186
+ decrypt,
187
+ /** Share viewing key with an auditor */
188
+ share,
189
+ }
190
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ // Provider
2
+ export { SIPProvider, type SIPProviderProps } from './providers/sip-provider'
3
+
4
+ // Hooks
5
+ export {
6
+ useSIP,
7
+ useStealthAddress,
8
+ usePrivateSwap,
9
+ useViewingKey,
10
+ } from './hooks'
@@ -0,0 +1,50 @@
1
+ import React, { createContext, useContext, useMemo, type ReactNode } from 'react'
2
+ import { SIP, type SIPConfig } from '@sip-protocol/sdk'
3
+
4
+ interface SIPContextValue {
5
+ client: SIP
6
+ config: SIPConfig
7
+ }
8
+
9
+ const SIPContext = createContext<SIPContextValue | undefined>(undefined)
10
+
11
+ export interface SIPProviderProps {
12
+ config: SIPConfig
13
+ children: ReactNode
14
+ }
15
+
16
+ /**
17
+ * SIPProvider wraps your app and provides SIP client instance via context
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * import { SIPProvider } from '@sip-protocol/react'
22
+ *
23
+ * function App() {
24
+ * return (
25
+ * <SIPProvider config={{ nearIntents: { apiUrl: '...' } }}>
26
+ * <YourApp />
27
+ * </SIPProvider>
28
+ * )
29
+ * }
30
+ * ```
31
+ */
32
+ export function SIPProvider({ config, children }: SIPProviderProps) {
33
+ const client = useMemo(() => new SIP(config), [config])
34
+
35
+ const value = useMemo(() => ({ client, config }), [client, config])
36
+
37
+ return <SIPContext.Provider value={value}>{children}</SIPContext.Provider>
38
+ }
39
+
40
+ /**
41
+ * useSIPContext - Internal hook to access SIP context
42
+ * @internal
43
+ */
44
+ export function useSIPContext(): SIPContextValue {
45
+ const context = useContext(SIPContext)
46
+ if (!context) {
47
+ throw new Error('useSIPContext must be used within SIPProvider')
48
+ }
49
+ return context
50
+ }