@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.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/dist/index.d.mts +404 -0
- package/dist/index.d.ts +404 -0
- package/dist/index.js +389 -0
- package/dist/index.mjs +358 -0
- package/package.json +64 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/use-private-swap.ts +268 -0
- package/src/hooks/use-sip.ts +188 -0
- package/src/hooks/use-stealth-address.ts +184 -0
- package/src/hooks/use-viewing-key.ts +190 -0
- package/src/index.ts +10 -0
- package/src/providers/sip-provider.tsx +50 -0
|
@@ -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,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
|
+
}
|