@mantle-rwa/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.
Files changed (66) hide show
  1. package/dist/cjs/components/InvestorDashboard/index.js +156 -0
  2. package/dist/cjs/components/InvestorDashboard/index.js.map +1 -0
  3. package/dist/cjs/components/KYCFlow/index.js +231 -0
  4. package/dist/cjs/components/KYCFlow/index.js.map +1 -0
  5. package/dist/cjs/components/TokenMintForm/index.js +286 -0
  6. package/dist/cjs/components/TokenMintForm/index.js.map +1 -0
  7. package/dist/cjs/components/YieldCalculator/index.js +245 -0
  8. package/dist/cjs/components/YieldCalculator/index.js.map +1 -0
  9. package/dist/cjs/components/index.js +15 -0
  10. package/dist/cjs/components/index.js.map +1 -0
  11. package/dist/cjs/hooks/index.js +9 -0
  12. package/dist/cjs/hooks/index.js.map +1 -0
  13. package/dist/cjs/hooks/useRWA.js +54 -0
  14. package/dist/cjs/hooks/useRWA.js.map +1 -0
  15. package/dist/cjs/index.js +20 -0
  16. package/dist/cjs/index.js.map +1 -0
  17. package/dist/cjs/types/index.js +10 -0
  18. package/dist/cjs/types/index.js.map +1 -0
  19. package/dist/esm/components/InvestorDashboard/index.js +153 -0
  20. package/dist/esm/components/InvestorDashboard/index.js.map +1 -0
  21. package/dist/esm/components/KYCFlow/index.js +228 -0
  22. package/dist/esm/components/KYCFlow/index.js.map +1 -0
  23. package/dist/esm/components/TokenMintForm/index.js +279 -0
  24. package/dist/esm/components/TokenMintForm/index.js.map +1 -0
  25. package/dist/esm/components/YieldCalculator/index.js +236 -0
  26. package/dist/esm/components/YieldCalculator/index.js.map +1 -0
  27. package/dist/esm/components/index.js +8 -0
  28. package/dist/esm/components/index.js.map +1 -0
  29. package/dist/esm/hooks/index.js +5 -0
  30. package/dist/esm/hooks/index.js.map +1 -0
  31. package/dist/esm/hooks/useRWA.js +51 -0
  32. package/dist/esm/hooks/useRWA.js.map +1 -0
  33. package/dist/esm/index.js +12 -0
  34. package/dist/esm/index.js.map +1 -0
  35. package/dist/esm/types/index.js +6 -0
  36. package/dist/esm/types/index.js.map +1 -0
  37. package/dist/styles.css +1 -0
  38. package/dist/types/components/InvestorDashboard/index.d.ts +25 -0
  39. package/dist/types/components/InvestorDashboard/index.d.ts.map +1 -0
  40. package/dist/types/components/KYCFlow/index.d.ts +25 -0
  41. package/dist/types/components/KYCFlow/index.d.ts.map +1 -0
  42. package/dist/types/components/TokenMintForm/index.d.ts +60 -0
  43. package/dist/types/components/TokenMintForm/index.d.ts.map +1 -0
  44. package/dist/types/components/YieldCalculator/index.d.ts +59 -0
  45. package/dist/types/components/YieldCalculator/index.d.ts.map +1 -0
  46. package/dist/types/components/index.d.ts +8 -0
  47. package/dist/types/components/index.d.ts.map +1 -0
  48. package/dist/types/hooks/index.d.ts +5 -0
  49. package/dist/types/hooks/index.d.ts.map +1 -0
  50. package/dist/types/hooks/useRWA.d.ts +22 -0
  51. package/dist/types/hooks/useRWA.d.ts.map +1 -0
  52. package/dist/types/index.d.ts +11 -0
  53. package/dist/types/index.d.ts.map +1 -0
  54. package/dist/types/types/index.d.ts +177 -0
  55. package/dist/types/types/index.d.ts.map +1 -0
  56. package/package.json +66 -0
  57. package/src/components/InvestorDashboard/index.tsx +359 -0
  58. package/src/components/KYCFlow/index.tsx +434 -0
  59. package/src/components/TokenMintForm/index.tsx +590 -0
  60. package/src/components/YieldCalculator/index.tsx +541 -0
  61. package/src/components/index.ts +8 -0
  62. package/src/hooks/index.ts +5 -0
  63. package/src/hooks/useRWA.ts +70 -0
  64. package/src/index.ts +32 -0
  65. package/src/styles/index.css +197 -0
  66. package/src/types/index.ts +193 -0
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@mantle-rwa/react",
3
+ "version": "0.1.0",
4
+ "description": "React components for RWA tokenization on Mantle Network",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/cjs/index.js",
8
+ "module": "./dist/esm/index.js",
9
+ "types": "./dist/types/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/esm/index.js",
13
+ "require": "./dist/cjs/index.js",
14
+ "types": "./dist/types/index.d.ts"
15
+ },
16
+ "./styles.css": "./dist/styles.css"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src"
21
+ ],
22
+ "scripts": {
23
+ "build": "npm run build:esm && npm run build:cjs && npm run build:types && npm run build:css",
24
+ "build:esm": "tsc -p tsconfig.esm.json",
25
+ "build:cjs": "tsc -p tsconfig.cjs.json",
26
+ "build:types": "tsc -p tsconfig.types.json",
27
+ "build:css": "tailwindcss -i ./src/styles/index.css -o ./dist/styles.css --minify",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "lint": "eslint src --ext .ts,.tsx",
31
+ "clean": "rm -rf dist coverage",
32
+ "typecheck": "tsc --noEmit"
33
+ },
34
+ "dependencies": {
35
+ "@mantle-rwa/sdk": "^0.1.0"
36
+ },
37
+ "devDependencies": {
38
+ "@testing-library/jest-dom": "^6.2.0",
39
+ "@testing-library/react": "^14.1.2",
40
+ "@types/react": "^18.2.48",
41
+ "@types/react-dom": "^18.2.18",
42
+ "@typescript-eslint/eslint-plugin": "^6.19.0",
43
+ "@typescript-eslint/parser": "^6.19.0",
44
+ "@vitejs/plugin-react": "^4.2.1",
45
+ "@vitest/coverage-v8": "^1.2.0",
46
+ "autoprefixer": "^10.4.17",
47
+ "eslint": "^8.56.0",
48
+ "eslint-plugin-react": "^7.33.2",
49
+ "eslint-plugin-react-hooks": "^4.6.0",
50
+ "fast-check": "^3.15.0",
51
+ "jsdom": "^24.0.0",
52
+ "postcss": "^8.4.33",
53
+ "tailwindcss": "^3.4.1",
54
+ "typescript": "^5.3.3",
55
+ "vitest": "^1.2.0"
56
+ },
57
+ "peerDependencies": {
58
+ "react": "^18.0.0",
59
+ "react-dom": "^18.0.0",
60
+ "wagmi": "^2.0.0",
61
+ "viem": "^2.0.0"
62
+ },
63
+ "engines": {
64
+ "node": ">=18.0.0"
65
+ }
66
+ }
@@ -0,0 +1,359 @@
1
+ /**
2
+ * InvestorDashboard Component
3
+ * Displays investor portfolio, yield history, and compliance status
4
+ *
5
+ * @see Requirements 11.1, 11.2, 11.3, 11.4, 11.5
6
+ */
7
+
8
+ import React, { useState, useEffect, useCallback } from 'react';
9
+ import type { InvestorDashboardProps, YieldHistoryEntry } from '../../types';
10
+ import { useRWA } from '../../hooks/useRWA';
11
+
12
+ /**
13
+ * Accreditation tier labels
14
+ */
15
+ const TIER_LABELS: Record<number, string> = {
16
+ 0: 'None',
17
+ 1: 'Retail',
18
+ 2: 'Accredited',
19
+ 3: 'Institutional',
20
+ };
21
+
22
+ /**
23
+ * Format a bigint balance to a human-readable string
24
+ */
25
+ function formatBalance(value: bigint, decimals: number = 18): string {
26
+ const divisor = BigInt(10 ** decimals);
27
+ const integerPart = value / divisor;
28
+ const fractionalPart = value % divisor;
29
+ const fractionalStr = fractionalPart.toString().padStart(decimals, '0').slice(0, 2);
30
+ return `${integerPart.toLocaleString()}.${fractionalStr}`;
31
+ }
32
+
33
+ /**
34
+ * Format a date to a localized string
35
+ */
36
+ function formatDate(date: Date): string {
37
+ return date.toLocaleDateString(undefined, {
38
+ year: 'numeric',
39
+ month: 'short',
40
+ day: 'numeric',
41
+ });
42
+ }
43
+
44
+ /**
45
+ * InvestorDashboard component for displaying investor portfolio
46
+ * Shows token balance, yield history, and compliance status
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * <InvestorDashboard
51
+ * tokenAddress="0x..."
52
+ * yieldDistributorAddress="0x..."
53
+ * kycRegistryAddress="0x..."
54
+ * onClaimYield={(id) => console.log('Claiming:', id)}
55
+ * />
56
+ * ```
57
+ */
58
+ export function InvestorDashboard({
59
+ tokenAddress,
60
+ yieldDistributorAddress,
61
+ kycRegistryAddress,
62
+ theme = 'light',
63
+ onClaimYield,
64
+ showPortfolioValue = true,
65
+ priceOracle: _priceOracle, // Reserved for future price oracle integration
66
+ }: InvestorDashboardProps): React.ReactElement {
67
+ const { address, isConnected } = useRWA();
68
+ const [balance, setBalance] = useState<bigint>(BigInt(0));
69
+ const [yieldHistory, setYieldHistory] = useState<YieldHistoryEntry[]>([]);
70
+ const [pendingYield, setPendingYield] = useState<bigint>(BigInt(0));
71
+ const [pendingDistributionId, setPendingDistributionId] = useState<number | null>(null);
72
+ const [isKYCVerified, setIsKYCVerified] = useState(false);
73
+ const [accreditationTier, setAccreditationTier] = useState<number>(0);
74
+ const [kycExpiry, setKycExpiry] = useState<Date | null>(null);
75
+ const [isLoading, setIsLoading] = useState(true);
76
+ const [isClaiming, setIsClaiming] = useState(false);
77
+ const [error, setError] = useState<string | null>(null);
78
+ const [portfolioValue, setPortfolioValue] = useState<bigint>(BigInt(0));
79
+
80
+ const isDark = theme === 'dark';
81
+
82
+ // Fetch dashboard data
83
+ useEffect(() => {
84
+ const fetchData = async () => {
85
+ if (!isConnected || !address) {
86
+ setIsLoading(false);
87
+ return;
88
+ }
89
+
90
+ setError(null);
91
+
92
+ try {
93
+ // In production, these would be actual contract calls via SDK
94
+ // For now, simulating data fetch with realistic mock data
95
+ await new Promise((resolve) => setTimeout(resolve, 500));
96
+
97
+ // Simulate token balance (1000 tokens with 18 decimals)
98
+ const mockBalance = BigInt(1000) * BigInt(10 ** 18);
99
+ setBalance(mockBalance);
100
+
101
+ // Simulate yield history
102
+ const mockYieldHistory: YieldHistoryEntry[] = [
103
+ {
104
+ distributionId: 1,
105
+ amount: BigInt(50) * BigInt(10 ** 6), // 50 USDC
106
+ paymentToken: 'USDC',
107
+ claimedAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
108
+ },
109
+ {
110
+ distributionId: 2,
111
+ amount: BigInt(75) * BigInt(10 ** 6), // 75 USDC
112
+ paymentToken: 'USDC',
113
+ claimedAt: new Date(Date.now() - 60 * 24 * 60 * 60 * 1000),
114
+ },
115
+ ];
116
+ setYieldHistory(mockYieldHistory);
117
+
118
+ // Simulate pending yield (25 USDC)
119
+ setPendingYield(BigInt(25) * BigInt(10 ** 6));
120
+ setPendingDistributionId(3);
121
+
122
+ // Simulate KYC status
123
+ setIsKYCVerified(true);
124
+ setAccreditationTier(2); // Accredited
125
+ setKycExpiry(new Date(Date.now() + 365 * 24 * 60 * 60 * 1000)); // 1 year from now
126
+
127
+ // Simulate portfolio value (assuming $1 per token)
128
+ setPortfolioValue(mockBalance);
129
+ } catch (err) {
130
+ console.error('Failed to fetch dashboard data:', err);
131
+ setError('Failed to load dashboard data. Please try again.');
132
+ } finally {
133
+ setIsLoading(false);
134
+ }
135
+ };
136
+
137
+ fetchData();
138
+ }, [address, isConnected, tokenAddress, yieldDistributorAddress, kycRegistryAddress]);
139
+
140
+ // Handle claim yield
141
+ const handleClaimYield = useCallback(async (distributionId: number) => {
142
+ if (isClaiming) return;
143
+
144
+ setIsClaiming(true);
145
+ setError(null);
146
+
147
+ try {
148
+ // Call the onClaimYield callback
149
+ onClaimYield?.(distributionId);
150
+
151
+ // In production, this would wait for transaction confirmation
152
+ await new Promise((resolve) => setTimeout(resolve, 1000));
153
+
154
+ // Update state after successful claim
155
+ setPendingYield(BigInt(0));
156
+ setPendingDistributionId(null);
157
+ } catch (err) {
158
+ console.error('Failed to claim yield:', err);
159
+ setError('Failed to claim yield. Please try again.');
160
+ } finally {
161
+ setIsClaiming(false);
162
+ }
163
+ }, [isClaiming, onClaimYield]);
164
+
165
+ // Render wallet not connected state
166
+ if (!isConnected) {
167
+ return (
168
+ <div
169
+ className={`rwa-card ${isDark ? 'dark' : ''}`}
170
+ data-testid="investor-dashboard"
171
+ data-theme={theme}
172
+ >
173
+ <p className="text-center text-gray-600 dark:text-gray-400">
174
+ Please connect your wallet to view your dashboard.
175
+ </p>
176
+ </div>
177
+ );
178
+ }
179
+
180
+ // Render loading state
181
+ if (isLoading) {
182
+ return (
183
+ <div
184
+ className={`rwa-card ${isDark ? 'dark' : ''}`}
185
+ data-testid="investor-dashboard"
186
+ data-theme={theme}
187
+ aria-busy="true"
188
+ >
189
+ <div className="flex items-center justify-center py-8">
190
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-mantle-600" />
191
+ <span className="ml-3 text-gray-600 dark:text-gray-400">
192
+ Loading dashboard...
193
+ </span>
194
+ </div>
195
+ </div>
196
+ );
197
+ }
198
+
199
+ return (
200
+ <div
201
+ className={`rwa-card ${isDark ? 'dark' : ''}`}
202
+ data-testid="investor-dashboard"
203
+ data-theme={theme}
204
+ >
205
+ <h2 className="text-lg font-semibold mb-6 text-gray-900 dark:text-white">
206
+ Investor Dashboard
207
+ </h2>
208
+
209
+ {/* Error Alert */}
210
+ {error && (
211
+ <div
212
+ className="mb-4 p-3 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 rounded-md border border-red-200 dark:border-red-800"
213
+ role="alert"
214
+ data-testid="dashboard-error"
215
+ >
216
+ {error}
217
+ </div>
218
+ )}
219
+
220
+ {/* Portfolio Overview - Requirements 11.1, 11.5 */}
221
+ {showPortfolioValue && (
222
+ <section className="mb-6" aria-labelledby="portfolio-heading" data-testid="portfolio-section">
223
+ <h3
224
+ id="portfolio-heading"
225
+ className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2"
226
+ >
227
+ Portfolio Overview
228
+ </h3>
229
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
230
+ <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
231
+ <p className="text-sm text-gray-500 dark:text-gray-400">Token Balance</p>
232
+ <p
233
+ className="text-2xl font-bold text-gray-900 dark:text-white"
234
+ data-testid="token-balance"
235
+ >
236
+ {formatBalance(balance)}
237
+ </p>
238
+ </div>
239
+ <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
240
+ <p className="text-sm text-gray-500 dark:text-gray-400">Portfolio Value</p>
241
+ <p
242
+ className="text-2xl font-bold text-gray-900 dark:text-white"
243
+ data-testid="portfolio-value"
244
+ >
245
+ ${formatBalance(portfolioValue)}
246
+ </p>
247
+ </div>
248
+ <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
249
+ <p className="text-sm text-gray-500 dark:text-gray-400">Pending Yield</p>
250
+ <p
251
+ className="text-2xl font-bold text-mantle-600"
252
+ data-testid="pending-yield"
253
+ >
254
+ ${formatBalance(pendingYield, 6)}
255
+ </p>
256
+ </div>
257
+ </div>
258
+ </section>
259
+ )}
260
+
261
+ {/* Compliance Status - Requirement 11.3 */}
262
+ <section className="mb-6" aria-labelledby="compliance-heading" data-testid="compliance-section">
263
+ <h3
264
+ id="compliance-heading"
265
+ className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2"
266
+ >
267
+ Compliance Status
268
+ </h3>
269
+ <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
270
+ <div className="flex flex-wrap items-center gap-4">
271
+ <div className="flex items-center">
272
+ <span
273
+ className={`w-3 h-3 rounded-full mr-2 ${isKYCVerified ? 'bg-green-500' : 'bg-red-500'}`}
274
+ aria-hidden="true"
275
+ />
276
+ <span className="text-sm text-gray-700 dark:text-gray-300" data-testid="kyc-status">
277
+ KYC: {isKYCVerified ? 'Verified' : 'Not Verified'}
278
+ </span>
279
+ </div>
280
+ <div className="text-sm text-gray-700 dark:text-gray-300">
281
+ Tier:{' '}
282
+ <span className="font-medium" data-testid="accreditation-tier">
283
+ {TIER_LABELS[accreditationTier] || 'Unknown'}
284
+ </span>
285
+ </div>
286
+ {kycExpiry && (
287
+ <div className="text-sm text-gray-500 dark:text-gray-400" data-testid="kyc-expiry">
288
+ Expires: {formatDate(kycExpiry)}
289
+ </div>
290
+ )}
291
+ </div>
292
+ </div>
293
+ </section>
294
+
295
+ {/* Yield History - Requirement 11.2 */}
296
+ <section className="mb-6" aria-labelledby="yield-history-heading" data-testid="yield-history-section">
297
+ <h3
298
+ id="yield-history-heading"
299
+ className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2"
300
+ >
301
+ Yield History
302
+ </h3>
303
+ {yieldHistory.length > 0 ? (
304
+ <ul className="space-y-2" data-testid="yield-history-list">
305
+ {yieldHistory.map((entry) => (
306
+ <li
307
+ key={entry.distributionId}
308
+ className="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"
309
+ data-testid={`yield-entry-${entry.distributionId}`}
310
+ >
311
+ <div>
312
+ <p className="text-sm font-medium text-gray-900 dark:text-white">
313
+ Distribution #{entry.distributionId}
314
+ </p>
315
+ <p className="text-xs text-gray-500 dark:text-gray-400">
316
+ {formatDate(entry.claimedAt)}
317
+ </p>
318
+ </div>
319
+ <p className="font-medium text-gray-900 dark:text-white">
320
+ {formatBalance(entry.amount, 6)} {entry.paymentToken}
321
+ </p>
322
+ </li>
323
+ ))}
324
+ </ul>
325
+ ) : (
326
+ <p
327
+ className="text-sm text-gray-500 dark:text-gray-400 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg"
328
+ data-testid="no-yield-history"
329
+ >
330
+ No yield history yet.
331
+ </p>
332
+ )}
333
+ </section>
334
+
335
+ {/* Claim Button - Requirement 11.4 */}
336
+ {pendingYield > BigInt(0) && pendingDistributionId !== null && (
337
+ <button
338
+ type="button"
339
+ className="rwa-button-primary w-full"
340
+ onClick={() => handleClaimYield(pendingDistributionId)}
341
+ disabled={isClaiming}
342
+ aria-busy={isClaiming}
343
+ data-testid="claim-yield-btn"
344
+ >
345
+ {isClaiming ? (
346
+ <>
347
+ <span className="animate-spin inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full mr-2" />
348
+ Claiming...
349
+ </>
350
+ ) : (
351
+ `Claim Pending Yield ($${formatBalance(pendingYield, 6)})`
352
+ )}
353
+ </button>
354
+ )}
355
+ </div>
356
+ );
357
+ }
358
+
359
+ export default InvestorDashboard;