@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.
- package/dist/cjs/components/InvestorDashboard/index.js +156 -0
- package/dist/cjs/components/InvestorDashboard/index.js.map +1 -0
- package/dist/cjs/components/KYCFlow/index.js +231 -0
- package/dist/cjs/components/KYCFlow/index.js.map +1 -0
- package/dist/cjs/components/TokenMintForm/index.js +286 -0
- package/dist/cjs/components/TokenMintForm/index.js.map +1 -0
- package/dist/cjs/components/YieldCalculator/index.js +245 -0
- package/dist/cjs/components/YieldCalculator/index.js.map +1 -0
- package/dist/cjs/components/index.js +15 -0
- package/dist/cjs/components/index.js.map +1 -0
- package/dist/cjs/hooks/index.js +9 -0
- package/dist/cjs/hooks/index.js.map +1 -0
- package/dist/cjs/hooks/useRWA.js +54 -0
- package/dist/cjs/hooks/useRWA.js.map +1 -0
- package/dist/cjs/index.js +20 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/types/index.js +10 -0
- package/dist/cjs/types/index.js.map +1 -0
- package/dist/esm/components/InvestorDashboard/index.js +153 -0
- package/dist/esm/components/InvestorDashboard/index.js.map +1 -0
- package/dist/esm/components/KYCFlow/index.js +228 -0
- package/dist/esm/components/KYCFlow/index.js.map +1 -0
- package/dist/esm/components/TokenMintForm/index.js +279 -0
- package/dist/esm/components/TokenMintForm/index.js.map +1 -0
- package/dist/esm/components/YieldCalculator/index.js +236 -0
- package/dist/esm/components/YieldCalculator/index.js.map +1 -0
- package/dist/esm/components/index.js +8 -0
- package/dist/esm/components/index.js.map +1 -0
- package/dist/esm/hooks/index.js +5 -0
- package/dist/esm/hooks/index.js.map +1 -0
- package/dist/esm/hooks/useRWA.js +51 -0
- package/dist/esm/hooks/useRWA.js.map +1 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/types/index.js +6 -0
- package/dist/esm/types/index.js.map +1 -0
- package/dist/styles.css +1 -0
- package/dist/types/components/InvestorDashboard/index.d.ts +25 -0
- package/dist/types/components/InvestorDashboard/index.d.ts.map +1 -0
- package/dist/types/components/KYCFlow/index.d.ts +25 -0
- package/dist/types/components/KYCFlow/index.d.ts.map +1 -0
- package/dist/types/components/TokenMintForm/index.d.ts +60 -0
- package/dist/types/components/TokenMintForm/index.d.ts.map +1 -0
- package/dist/types/components/YieldCalculator/index.d.ts +59 -0
- package/dist/types/components/YieldCalculator/index.d.ts.map +1 -0
- package/dist/types/components/index.d.ts +8 -0
- package/dist/types/components/index.d.ts.map +1 -0
- package/dist/types/hooks/index.d.ts +5 -0
- package/dist/types/hooks/index.d.ts.map +1 -0
- package/dist/types/hooks/useRWA.d.ts +22 -0
- package/dist/types/hooks/useRWA.d.ts.map +1 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +177 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/package.json +66 -0
- package/src/components/InvestorDashboard/index.tsx +359 -0
- package/src/components/KYCFlow/index.tsx +434 -0
- package/src/components/TokenMintForm/index.tsx +590 -0
- package/src/components/YieldCalculator/index.tsx +541 -0
- package/src/components/index.ts +8 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useRWA.ts +70 -0
- package/src/index.ts +32 -0
- package/src/styles/index.css +197 -0
- 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;
|