@mantle-rwa/react 0.1.1 → 0.1.3
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/dist/cjs/components/ConnectWalletPrompt.js +13 -0
- package/dist/cjs/components/ConnectWalletPrompt.js.map +1 -0
- package/dist/cjs/components/ErrorDisplay.js +42 -0
- package/dist/cjs/components/ErrorDisplay.js.map +1 -0
- package/dist/cjs/components/InvestorDashboard.js +156 -0
- package/dist/cjs/components/InvestorDashboard.js.map +1 -0
- package/dist/cjs/components/KYCFlow.js +146 -0
- package/dist/cjs/components/KYCFlow.js.map +1 -0
- package/dist/cjs/components/LoadingSpinner.js +18 -0
- package/dist/cjs/components/LoadingSpinner.js.map +1 -0
- package/dist/cjs/components/TokenMintForm.js +163 -0
- package/dist/cjs/components/TokenMintForm.js.map +1 -0
- package/dist/cjs/components/YieldCalculator.js +97 -0
- package/dist/cjs/components/YieldCalculator.js.map +1 -0
- package/dist/cjs/hooks/useRWA.js +87 -40
- package/dist/cjs/hooks/useRWA.js.map +1 -1
- package/dist/cjs/index.js +11 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types/index.js +2 -2
- package/dist/cjs/types/index.js.map +1 -1
- package/dist/esm/components/ConnectWalletPrompt.js +10 -0
- package/dist/esm/components/ConnectWalletPrompt.js.map +1 -0
- package/dist/esm/components/ErrorDisplay.js +38 -0
- package/dist/esm/components/ErrorDisplay.js.map +1 -0
- package/dist/esm/components/InvestorDashboard.js +153 -0
- package/dist/esm/components/InvestorDashboard.js.map +1 -0
- package/dist/esm/components/KYCFlow.js +143 -0
- package/dist/esm/components/KYCFlow.js.map +1 -0
- package/dist/esm/components/LoadingSpinner.js +15 -0
- package/dist/esm/components/LoadingSpinner.js.map +1 -0
- package/dist/esm/components/TokenMintForm.js +158 -0
- package/dist/esm/components/TokenMintForm.js.map +1 -0
- package/dist/esm/components/YieldCalculator.js +94 -0
- package/dist/esm/components/YieldCalculator.js.map +1 -0
- package/dist/esm/hooks/useRWA.js +86 -39
- package/dist/esm/hooks/useRWA.js.map +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types/index.js +3 -3
- package/dist/esm/types/index.js.map +1 -1
- package/dist/styles.css +3 -1
- package/dist/types/components/ConnectWalletPrompt.d.ts +15 -0
- package/dist/types/components/ConnectWalletPrompt.d.ts.map +1 -0
- package/dist/types/components/ErrorDisplay.d.ts +16 -0
- package/dist/types/components/ErrorDisplay.d.ts.map +1 -0
- package/dist/types/components/InvestorDashboard.d.ts +7 -0
- package/dist/types/components/InvestorDashboard.d.ts.map +1 -0
- package/dist/types/components/KYCFlow.d.ts +7 -0
- package/dist/types/components/KYCFlow.d.ts.map +1 -0
- package/dist/types/components/LoadingSpinner.d.ts +10 -0
- package/dist/types/components/LoadingSpinner.d.ts.map +1 -0
- package/dist/types/components/TokenMintForm.d.ts +15 -0
- package/dist/types/components/TokenMintForm.d.ts.map +1 -0
- package/dist/types/components/YieldCalculator.d.ts +7 -0
- package/dist/types/components/YieldCalculator.d.ts.map +1 -0
- package/dist/types/hooks/useRWA.d.ts +7 -19
- package/dist/types/hooks/useRWA.d.ts.map +1 -1
- package/dist/types/index.d.ts +5 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/types/index.d.ts +113 -131
- package/dist/types/types/index.d.ts.map +1 -1
- package/package.json +5 -3
- package/src/components/ConnectWalletPrompt.tsx +47 -0
- package/src/components/ErrorDisplay.tsx +90 -0
- package/src/components/InvestorDashboard.tsx +315 -0
- package/src/components/KYCFlow.tsx +267 -0
- package/src/components/LoadingSpinner.tsx +33 -0
- package/src/components/TokenMintForm.tsx +291 -0
- package/src/components/YieldCalculator.tsx +250 -0
- package/src/hooks/useRWA.ts +110 -0
- package/src/index.ts +4 -0
- package/src/styles/index.css +68 -14
- package/src/types/index.ts +200 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* KYCFlow - Multi-step KYC verification flow component
|
|
5
|
+
*
|
|
6
|
+
* Guides users through the KYC verification process and displays
|
|
7
|
+
* their current verification status and accreditation tier.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
11
|
+
import { useAccount } from 'wagmi';
|
|
12
|
+
import { useRWA } from '../hooks/useRWA';
|
|
13
|
+
import type { KYCFlowProps, VerificationStatus } from '../types';
|
|
14
|
+
import { AccreditationTier, type InvestorData } from '@mantle-rwa/sdk';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get display name for accreditation tier
|
|
18
|
+
*/
|
|
19
|
+
function getTierName(tier: AccreditationTier): string {
|
|
20
|
+
switch (tier) {
|
|
21
|
+
case AccreditationTier.None:
|
|
22
|
+
return 'None';
|
|
23
|
+
case AccreditationTier.Retail:
|
|
24
|
+
return 'Retail';
|
|
25
|
+
case AccreditationTier.Accredited:
|
|
26
|
+
return 'Accredited';
|
|
27
|
+
case AccreditationTier.Institutional:
|
|
28
|
+
return 'Institutional';
|
|
29
|
+
default:
|
|
30
|
+
return 'Unknown';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Format date for display
|
|
36
|
+
*/
|
|
37
|
+
function formatDate(date: Date): string {
|
|
38
|
+
return date.toLocaleDateString('en-US', {
|
|
39
|
+
year: 'numeric',
|
|
40
|
+
month: 'long',
|
|
41
|
+
day: 'numeric',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* KYCFlow component for guiding users through KYC verification
|
|
47
|
+
*/
|
|
48
|
+
export function KYCFlow({
|
|
49
|
+
registryAddress,
|
|
50
|
+
investorAddress,
|
|
51
|
+
onStatusChange,
|
|
52
|
+
onComplete,
|
|
53
|
+
onError,
|
|
54
|
+
className = '',
|
|
55
|
+
}: KYCFlowProps): JSX.Element {
|
|
56
|
+
const { client, isInitialized, hasSigner } = useRWA();
|
|
57
|
+
const { address: connectedAddress } = useAccount();
|
|
58
|
+
|
|
59
|
+
const targetAddress = investorAddress || connectedAddress;
|
|
60
|
+
|
|
61
|
+
const [status, setStatus] = useState<VerificationStatus>('idle');
|
|
62
|
+
const [investorInfo, setInvestorInfo] = useState<InvestorData | null>(null);
|
|
63
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
64
|
+
const [error, setError] = useState<Error | null>(null);
|
|
65
|
+
|
|
66
|
+
// Update status and notify callback
|
|
67
|
+
const updateStatus = useCallback((newStatus: VerificationStatus) => {
|
|
68
|
+
setStatus(newStatus);
|
|
69
|
+
onStatusChange?.(newStatus);
|
|
70
|
+
}, [onStatusChange]);
|
|
71
|
+
|
|
72
|
+
// Fetch investor KYC data
|
|
73
|
+
const fetchKYCData = useCallback(async () => {
|
|
74
|
+
if (!client || !isInitialized || !registryAddress || !targetAddress) {
|
|
75
|
+
setIsLoading(false);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
setIsLoading(true);
|
|
80
|
+
setError(null);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const registry = client.kyc.connect(registryAddress);
|
|
84
|
+
const info = await registry.getInvestorInfo(targetAddress);
|
|
85
|
+
setInvestorInfo(info);
|
|
86
|
+
|
|
87
|
+
// Determine status based on investor info
|
|
88
|
+
if (info.verified) {
|
|
89
|
+
updateStatus('completed');
|
|
90
|
+
onComplete?.(info.tier);
|
|
91
|
+
} else if (info.tier !== AccreditationTier.None) {
|
|
92
|
+
updateStatus('in_progress');
|
|
93
|
+
} else {
|
|
94
|
+
updateStatus('pending');
|
|
95
|
+
}
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const errorObj = err instanceof Error ? err : new Error('Failed to fetch KYC data');
|
|
98
|
+
setError(errorObj);
|
|
99
|
+
updateStatus('failed');
|
|
100
|
+
onError?.(errorObj);
|
|
101
|
+
} finally {
|
|
102
|
+
setIsLoading(false);
|
|
103
|
+
}
|
|
104
|
+
}, [client, isInitialized, registryAddress, targetAddress, updateStatus, onComplete, onError]);
|
|
105
|
+
|
|
106
|
+
// Fetch data on mount and when dependencies change
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
fetchKYCData();
|
|
109
|
+
}, [fetchKYCData]);
|
|
110
|
+
|
|
111
|
+
// Handle start verification
|
|
112
|
+
const handleStartVerification = useCallback(async () => {
|
|
113
|
+
if (!client || !targetAddress) return;
|
|
114
|
+
|
|
115
|
+
updateStatus('in_progress');
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
// Initiate verification through the KYC provider
|
|
119
|
+
const session = await client.kyc.verifyInvestor(targetAddress);
|
|
120
|
+
|
|
121
|
+
// If there's a redirect URL, open it
|
|
122
|
+
if (session.redirectUrl) {
|
|
123
|
+
window.open(session.redirectUrl, '_blank');
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const errorObj = err instanceof Error ? err : new Error('Failed to start verification');
|
|
127
|
+
setError(errorObj);
|
|
128
|
+
updateStatus('failed');
|
|
129
|
+
onError?.(errorObj);
|
|
130
|
+
}
|
|
131
|
+
}, [client, targetAddress, updateStatus, onError]);
|
|
132
|
+
|
|
133
|
+
// Handle retry
|
|
134
|
+
const handleRetry = useCallback(() => {
|
|
135
|
+
setError(null);
|
|
136
|
+
fetchKYCData();
|
|
137
|
+
}, [fetchKYCData]);
|
|
138
|
+
|
|
139
|
+
// Render loading state
|
|
140
|
+
if (isLoading) {
|
|
141
|
+
return (
|
|
142
|
+
<div className={`rwa-kyc-flow rwa-kyc-flow--loading ${className}`}>
|
|
143
|
+
<div className="flex items-center justify-center p-8">
|
|
144
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
|
145
|
+
<span className="ml-3 text-gray-600 dark:text-gray-300">Loading KYC status...</span>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Render no wallet connected state
|
|
152
|
+
if (!targetAddress) {
|
|
153
|
+
return (
|
|
154
|
+
<div className={`rwa-kyc-flow rwa-kyc-flow--no-wallet ${className}`}>
|
|
155
|
+
<div className="p-6 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
|
156
|
+
<h3 className="text-lg font-semibold text-yellow-800 dark:text-yellow-200">
|
|
157
|
+
Wallet Not Connected
|
|
158
|
+
</h3>
|
|
159
|
+
<p className="mt-2 text-yellow-700 dark:text-yellow-300">
|
|
160
|
+
Please connect your wallet to view your KYC status.
|
|
161
|
+
</p>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Render error state
|
|
168
|
+
if (status === 'failed' && error) {
|
|
169
|
+
return (
|
|
170
|
+
<div className={`rwa-kyc-flow rwa-kyc-flow--error ${className}`}>
|
|
171
|
+
<div className="p-6 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
|
|
172
|
+
<h3 className="text-lg font-semibold text-red-800 dark:text-red-200">
|
|
173
|
+
Verification Failed
|
|
174
|
+
</h3>
|
|
175
|
+
<p className="mt-2 text-red-700 dark:text-red-300">{error.message}</p>
|
|
176
|
+
<button
|
|
177
|
+
onClick={handleRetry}
|
|
178
|
+
className="mt-4 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-md transition-colors"
|
|
179
|
+
>
|
|
180
|
+
Retry
|
|
181
|
+
</button>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Render completed state
|
|
188
|
+
if (status === 'completed' && investorInfo) {
|
|
189
|
+
return (
|
|
190
|
+
<div className={`rwa-kyc-flow rwa-kyc-flow--completed ${className}`}>
|
|
191
|
+
<div className="p-6 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
|
|
192
|
+
<div className="flex items-center">
|
|
193
|
+
<svg className="w-8 h-8 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
194
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
195
|
+
</svg>
|
|
196
|
+
<h3 className="ml-3 text-lg font-semibold text-green-800 dark:text-green-200">
|
|
197
|
+
Verification Complete
|
|
198
|
+
</h3>
|
|
199
|
+
</div>
|
|
200
|
+
<div className="mt-4 space-y-2">
|
|
201
|
+
<p className="text-green-700 dark:text-green-300">
|
|
202
|
+
<span className="font-medium">Accreditation Tier:</span> {getTierName(investorInfo.tier)}
|
|
203
|
+
</p>
|
|
204
|
+
{investorInfo.expiry && (
|
|
205
|
+
<p className="text-green-700 dark:text-green-300">
|
|
206
|
+
<span className="font-medium">Valid Until:</span> {formatDate(investorInfo.expiry)}
|
|
207
|
+
</p>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Render in progress state
|
|
216
|
+
if (status === 'in_progress') {
|
|
217
|
+
return (
|
|
218
|
+
<div className={`rwa-kyc-flow rwa-kyc-flow--in-progress ${className}`}>
|
|
219
|
+
<div className="p-6 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
|
220
|
+
<div className="flex items-center">
|
|
221
|
+
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600" />
|
|
222
|
+
<h3 className="ml-3 text-lg font-semibold text-blue-800 dark:text-blue-200">
|
|
223
|
+
Verification In Progress
|
|
224
|
+
</h3>
|
|
225
|
+
</div>
|
|
226
|
+
<p className="mt-4 text-blue-700 dark:text-blue-300">
|
|
227
|
+
Your identity verification is being processed. This may take a few minutes.
|
|
228
|
+
</p>
|
|
229
|
+
<button
|
|
230
|
+
onClick={fetchKYCData}
|
|
231
|
+
className="mt-4 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors"
|
|
232
|
+
>
|
|
233
|
+
Check Status
|
|
234
|
+
</button>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Render pending state (default)
|
|
241
|
+
return (
|
|
242
|
+
<div className={`rwa-kyc-flow rwa-kyc-flow--pending ${className}`}>
|
|
243
|
+
<div className="p-6 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
|
244
|
+
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200">
|
|
245
|
+
KYC Verification Required
|
|
246
|
+
</h3>
|
|
247
|
+
<p className="mt-2 text-gray-600 dark:text-gray-400">
|
|
248
|
+
Complete identity verification to participate in RWA token offerings.
|
|
249
|
+
</p>
|
|
250
|
+
{!hasSigner ? (
|
|
251
|
+
<p className="mt-4 text-yellow-600 dark:text-yellow-400 text-sm">
|
|
252
|
+
Connect your wallet to start verification.
|
|
253
|
+
</p>
|
|
254
|
+
) : (
|
|
255
|
+
<button
|
|
256
|
+
onClick={handleStartVerification}
|
|
257
|
+
className="mt-4 px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors"
|
|
258
|
+
>
|
|
259
|
+
Start Verification
|
|
260
|
+
</button>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export default KYCFlow;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LoadingSpinner - Shared loading indicator component
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { LoadingSpinnerProps } from '../types';
|
|
8
|
+
|
|
9
|
+
const sizeClasses = {
|
|
10
|
+
sm: 'h-4 w-4',
|
|
11
|
+
md: 'h-6 w-6',
|
|
12
|
+
lg: 'h-8 w-8',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* LoadingSpinner component
|
|
17
|
+
*/
|
|
18
|
+
export function LoadingSpinner({
|
|
19
|
+
size = 'md',
|
|
20
|
+
className = '',
|
|
21
|
+
}: LoadingSpinnerProps): JSX.Element {
|
|
22
|
+
return (
|
|
23
|
+
<div className={`rwa-loading-spinner ${className}`}>
|
|
24
|
+
<div
|
|
25
|
+
className={`animate-spin rounded-full border-b-2 border-blue-600 dark:border-blue-400 ${sizeClasses[size]}`}
|
|
26
|
+
role="status"
|
|
27
|
+
aria-label="Loading"
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default LoadingSpinner;
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TokenMintForm - Form for minting RWA tokens to verified investors
|
|
5
|
+
*
|
|
6
|
+
* Validates recipient address and KYC status before allowing mint.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useState, useCallback } from 'react';
|
|
10
|
+
import { useRWA } from '../hooks/useRWA';
|
|
11
|
+
import type { TokenMintFormProps } from '../types';
|
|
12
|
+
import { AccreditationTier, type TransactionResult } from '@mantle-rwa/sdk';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validate Ethereum address format
|
|
16
|
+
*/
|
|
17
|
+
export function isValidAddress(address: string): boolean {
|
|
18
|
+
return /^0x[a-fA-F0-9]{40}$/.test(address);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validate amount is positive
|
|
23
|
+
*/
|
|
24
|
+
export function isValidAmount(amount: string): boolean {
|
|
25
|
+
const num = parseFloat(amount);
|
|
26
|
+
return !isNaN(num) && num > 0 && isFinite(num);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get display name for accreditation tier
|
|
31
|
+
*/
|
|
32
|
+
function getTierName(tier: AccreditationTier): string {
|
|
33
|
+
switch (tier) {
|
|
34
|
+
case AccreditationTier.None:
|
|
35
|
+
return 'None';
|
|
36
|
+
case AccreditationTier.Retail:
|
|
37
|
+
return 'Retail';
|
|
38
|
+
case AccreditationTier.Accredited:
|
|
39
|
+
return 'Accredited';
|
|
40
|
+
case AccreditationTier.Institutional:
|
|
41
|
+
return 'Institutional';
|
|
42
|
+
default:
|
|
43
|
+
return 'Unknown';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface FormState {
|
|
48
|
+
recipient: string;
|
|
49
|
+
amount: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface ValidationState {
|
|
53
|
+
recipientError: string | null;
|
|
54
|
+
amountError: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface KYCCheckState {
|
|
58
|
+
isChecking: boolean;
|
|
59
|
+
isVerified: boolean | null;
|
|
60
|
+
tier: AccreditationTier | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* TokenMintForm component
|
|
65
|
+
*/
|
|
66
|
+
export function TokenMintForm({
|
|
67
|
+
tokenAddress,
|
|
68
|
+
kycRegistryAddress,
|
|
69
|
+
onSuccess,
|
|
70
|
+
onError,
|
|
71
|
+
className = '',
|
|
72
|
+
}: TokenMintFormProps): JSX.Element {
|
|
73
|
+
const { client, isInitialized, hasSigner } = useRWA();
|
|
74
|
+
|
|
75
|
+
const [form, setForm] = useState<FormState>({ recipient: '', amount: '' });
|
|
76
|
+
const [validation, setValidation] = useState<ValidationState>({ recipientError: null, amountError: null });
|
|
77
|
+
const [kycCheck, setKycCheck] = useState<KYCCheckState>({ isChecking: false, isVerified: null, tier: null });
|
|
78
|
+
const [isPending, setIsPending] = useState(false);
|
|
79
|
+
const [error, setError] = useState<Error | null>(null);
|
|
80
|
+
const [txResult, setTxResult] = useState<TransactionResult | null>(null);
|
|
81
|
+
|
|
82
|
+
// Validate recipient address
|
|
83
|
+
const validateRecipient = useCallback((address: string): string | null => {
|
|
84
|
+
if (!address) return 'Recipient address is required';
|
|
85
|
+
if (!isValidAddress(address)) return 'Invalid Ethereum address format';
|
|
86
|
+
return null;
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
// Validate amount
|
|
90
|
+
const validateAmount = useCallback((amount: string): string | null => {
|
|
91
|
+
if (!amount) return 'Amount is required';
|
|
92
|
+
if (!isValidAmount(amount)) return 'Amount must be a positive number';
|
|
93
|
+
return null;
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
// Handle recipient change
|
|
97
|
+
const handleRecipientChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
98
|
+
const value = e.target.value;
|
|
99
|
+
setForm(prev => ({ ...prev, recipient: value }));
|
|
100
|
+
setValidation(prev => ({ ...prev, recipientError: validateRecipient(value) }));
|
|
101
|
+
setKycCheck({ isChecking: false, isVerified: null, tier: null });
|
|
102
|
+
setTxResult(null);
|
|
103
|
+
}, [validateRecipient]);
|
|
104
|
+
|
|
105
|
+
// Handle amount change
|
|
106
|
+
const handleAmountChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
107
|
+
const value = e.target.value;
|
|
108
|
+
setForm(prev => ({ ...prev, amount: value }));
|
|
109
|
+
setValidation(prev => ({ ...prev, amountError: validateAmount(value) }));
|
|
110
|
+
setTxResult(null);
|
|
111
|
+
}, [validateAmount]);
|
|
112
|
+
|
|
113
|
+
// Check KYC status
|
|
114
|
+
const checkKYC = useCallback(async () => {
|
|
115
|
+
if (!client || !isInitialized || !form.recipient || !isValidAddress(form.recipient)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
setKycCheck({ isChecking: true, isVerified: null, tier: null });
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const registry = client.kyc.connect(kycRegistryAddress);
|
|
123
|
+
const info = await registry.getInvestorInfo(form.recipient);
|
|
124
|
+
setKycCheck({
|
|
125
|
+
isChecking: false,
|
|
126
|
+
isVerified: info.verified,
|
|
127
|
+
tier: info.tier,
|
|
128
|
+
});
|
|
129
|
+
} catch (err) {
|
|
130
|
+
setKycCheck({ isChecking: false, isVerified: false, tier: null });
|
|
131
|
+
}
|
|
132
|
+
}, [client, isInitialized, form.recipient, kycRegistryAddress]);
|
|
133
|
+
|
|
134
|
+
// Handle form submission
|
|
135
|
+
const handleSubmit = useCallback(async (e: React.FormEvent) => {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
|
|
138
|
+
// Validate all fields
|
|
139
|
+
const recipientError = validateRecipient(form.recipient);
|
|
140
|
+
const amountError = validateAmount(form.amount);
|
|
141
|
+
|
|
142
|
+
setValidation({ recipientError, amountError });
|
|
143
|
+
|
|
144
|
+
if (recipientError || amountError) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check KYC if not already checked
|
|
149
|
+
if (kycCheck.isVerified === null) {
|
|
150
|
+
await checkKYC();
|
|
151
|
+
return; // User needs to submit again after KYC check
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Block if not verified
|
|
155
|
+
if (!kycCheck.isVerified) {
|
|
156
|
+
setError(new Error('Recipient is not KYC verified. Cannot mint tokens to unverified addresses.'));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!client || !hasSigner) {
|
|
161
|
+
setError(new Error('Wallet not connected'));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
setIsPending(true);
|
|
166
|
+
setError(null);
|
|
167
|
+
setTxResult(null);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const token = client.token.connect(tokenAddress);
|
|
171
|
+
const result = await token.mint(form.recipient, form.amount);
|
|
172
|
+
setTxResult(result);
|
|
173
|
+
onSuccess?.(result);
|
|
174
|
+
// Clear form on success
|
|
175
|
+
setForm({ recipient: '', amount: '' });
|
|
176
|
+
setKycCheck({ isChecking: false, isVerified: null, tier: null });
|
|
177
|
+
} catch (err) {
|
|
178
|
+
const errorObj = err instanceof Error ? err : new Error('Mint failed');
|
|
179
|
+
setError(errorObj);
|
|
180
|
+
onError?.(errorObj);
|
|
181
|
+
} finally {
|
|
182
|
+
setIsPending(false);
|
|
183
|
+
}
|
|
184
|
+
}, [form, validation, kycCheck, client, hasSigner, tokenAddress, validateRecipient, validateAmount, checkKYC, onSuccess, onError]);
|
|
185
|
+
|
|
186
|
+
const isFormValid = !validation.recipientError && !validation.amountError && form.recipient && form.amount;
|
|
187
|
+
const canSubmit = isFormValid && hasSigner && !isPending && (kycCheck.isVerified === null || kycCheck.isVerified);
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div className={`rwa-token-mint-form ${className}`}>
|
|
191
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
192
|
+
{/* Recipient Address Field */}
|
|
193
|
+
<div>
|
|
194
|
+
<label htmlFor="recipient" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
195
|
+
Recipient Address
|
|
196
|
+
</label>
|
|
197
|
+
<input
|
|
198
|
+
type="text"
|
|
199
|
+
id="recipient"
|
|
200
|
+
value={form.recipient}
|
|
201
|
+
onChange={handleRecipientChange}
|
|
202
|
+
onBlur={checkKYC}
|
|
203
|
+
placeholder="0x..."
|
|
204
|
+
className={`mt-1 block w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-800 dark:text-white ${validation.recipientError
|
|
205
|
+
? 'border-red-500 focus:border-red-500'
|
|
206
|
+
: 'border-gray-300 dark:border-gray-600 focus:border-blue-500'
|
|
207
|
+
}`}
|
|
208
|
+
/>
|
|
209
|
+
{validation.recipientError && (
|
|
210
|
+
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{validation.recipientError}</p>
|
|
211
|
+
)}
|
|
212
|
+
|
|
213
|
+
{/* KYC Status Display */}
|
|
214
|
+
{kycCheck.isChecking && (
|
|
215
|
+
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">Checking KYC status...</p>
|
|
216
|
+
)}
|
|
217
|
+
{kycCheck.isVerified === true && (
|
|
218
|
+
<p className="mt-2 text-sm text-green-600 dark:text-green-400">
|
|
219
|
+
✓ Verified ({getTierName(kycCheck.tier!)})
|
|
220
|
+
</p>
|
|
221
|
+
)}
|
|
222
|
+
{kycCheck.isVerified === false && (
|
|
223
|
+
<p className="mt-2 text-sm text-red-600 dark:text-red-400">
|
|
224
|
+
✗ Not KYC verified - cannot mint to this address
|
|
225
|
+
</p>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{/* Amount Field */}
|
|
230
|
+
<div>
|
|
231
|
+
<label htmlFor="amount" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
232
|
+
Amount
|
|
233
|
+
</label>
|
|
234
|
+
<input
|
|
235
|
+
type="text"
|
|
236
|
+
id="amount"
|
|
237
|
+
value={form.amount}
|
|
238
|
+
onChange={handleAmountChange}
|
|
239
|
+
placeholder="0.00"
|
|
240
|
+
className={`mt-1 block w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-800 dark:text-white ${validation.amountError
|
|
241
|
+
? 'border-red-500 focus:border-red-500'
|
|
242
|
+
: 'border-gray-300 dark:border-gray-600 focus:border-blue-500'
|
|
243
|
+
}`}
|
|
244
|
+
/>
|
|
245
|
+
{validation.amountError && (
|
|
246
|
+
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{validation.amountError}</p>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{/* Wallet Connection Warning */}
|
|
251
|
+
{!hasSigner && (
|
|
252
|
+
<div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-md border border-yellow-200 dark:border-yellow-800">
|
|
253
|
+
<p className="text-sm text-yellow-700 dark:text-yellow-300">
|
|
254
|
+
Connect your wallet to mint tokens.
|
|
255
|
+
</p>
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
|
|
259
|
+
{/* Error Display */}
|
|
260
|
+
{error && (
|
|
261
|
+
<div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-md border border-red-200 dark:border-red-800">
|
|
262
|
+
<p className="text-sm text-red-700 dark:text-red-300">{error.message}</p>
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
{/* Success Display */}
|
|
267
|
+
{txResult && (
|
|
268
|
+
<div className="p-3 bg-green-50 dark:bg-green-900/20 rounded-md border border-green-200 dark:border-green-800">
|
|
269
|
+
<p className="text-sm text-green-700 dark:text-green-300">
|
|
270
|
+
✓ Mint successful! Transaction: {txResult.hash.slice(0, 10)}...
|
|
271
|
+
</p>
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
|
|
275
|
+
{/* Submit Button */}
|
|
276
|
+
<button
|
|
277
|
+
type="submit"
|
|
278
|
+
disabled={!canSubmit}
|
|
279
|
+
className={`w-full py-2 px-4 rounded-md font-medium transition-colors ${canSubmit
|
|
280
|
+
? 'bg-blue-600 hover:bg-blue-700 text-white'
|
|
281
|
+
: 'bg-gray-300 dark:bg-gray-700 text-gray-500 dark:text-gray-400 cursor-not-allowed'
|
|
282
|
+
}`}
|
|
283
|
+
>
|
|
284
|
+
{isPending ? 'Minting...' : kycCheck.isVerified === null && form.recipient ? 'Check KYC & Mint' : 'Mint Tokens'}
|
|
285
|
+
</button>
|
|
286
|
+
</form>
|
|
287
|
+
</div>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export default TokenMintForm;
|