@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
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YieldCalculator Component
|
|
3
|
+
* Preview and calculate yield distributions before execution
|
|
4
|
+
*
|
|
5
|
+
* @see Requirements 13.1, 13.2, 13.3, 13.4
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState, useCallback, useMemo } from 'react';
|
|
9
|
+
import type { YieldCalculatorProps, DistributionPreviewEntry, DistributionConfig, PaymentToken } from '../../types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Calculate proportional yield distribution for holders
|
|
13
|
+
* This is the core calculation function for Property 26
|
|
14
|
+
*
|
|
15
|
+
* @param totalAmount - Total amount to distribute (in smallest unit)
|
|
16
|
+
* @param holders - Array of holder addresses and balances
|
|
17
|
+
* @returns Array of distribution preview entries
|
|
18
|
+
*/
|
|
19
|
+
export function calculateDistribution(
|
|
20
|
+
totalAmount: bigint,
|
|
21
|
+
holders: Array<{ address: string; balance: bigint }>
|
|
22
|
+
): DistributionPreviewEntry[] {
|
|
23
|
+
if (holders.length === 0 || totalAmount <= BigInt(0)) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const totalSupply = holders.reduce((sum, h) => sum + h.balance, BigInt(0));
|
|
28
|
+
|
|
29
|
+
if (totalSupply === BigInt(0)) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Property 26: Yield Calculator Distribution Accuracy
|
|
34
|
+
// Sum of per-holder amounts must equal total amount (within rounding)
|
|
35
|
+
return holders.map((holder) => {
|
|
36
|
+
// Calculate percentage with 2 decimal precision (multiply by 10000, then divide by 100)
|
|
37
|
+
const percentage = Number(holder.balance * BigInt(10000) / totalSupply) / 100;
|
|
38
|
+
|
|
39
|
+
// Calculate yield amount proportionally
|
|
40
|
+
const yieldAmount = (totalAmount * holder.balance) / totalSupply;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
address: holder.address,
|
|
44
|
+
balance: holder.balance,
|
|
45
|
+
yieldAmount,
|
|
46
|
+
percentage,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Calculate the sum of all yield amounts in a distribution
|
|
53
|
+
* Used to verify Property 26 (distribution accuracy)
|
|
54
|
+
*/
|
|
55
|
+
export function sumDistributionAmounts(entries: DistributionPreviewEntry[]): bigint {
|
|
56
|
+
return entries.reduce((sum, entry) => sum + entry.yieldAmount, BigInt(0));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Format a bigint amount for display with decimals
|
|
61
|
+
*/
|
|
62
|
+
export function formatAmount(value: bigint, decimals: number): string {
|
|
63
|
+
if (decimals <= 0) {
|
|
64
|
+
return value.toLocaleString();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const divisor = BigInt(10 ** decimals);
|
|
68
|
+
const integerPart = value / divisor;
|
|
69
|
+
const fractionalPart = value % divisor;
|
|
70
|
+
const fractionalStr = fractionalPart.toString().padStart(decimals, '0').slice(0, 2);
|
|
71
|
+
return `${integerPart.toLocaleString()}.${fractionalStr}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse a decimal string amount to bigint with given decimals
|
|
76
|
+
*/
|
|
77
|
+
export function parseAmount(value: string, decimals: number): bigint {
|
|
78
|
+
const num = parseFloat(value);
|
|
79
|
+
if (isNaN(num) || num < 0) {
|
|
80
|
+
return BigInt(0);
|
|
81
|
+
}
|
|
82
|
+
return BigInt(Math.floor(num * (10 ** decimals)));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Export distribution preview to CSV format
|
|
87
|
+
*/
|
|
88
|
+
export function exportToCSV(
|
|
89
|
+
entries: DistributionPreviewEntry[],
|
|
90
|
+
tokenSymbol: string,
|
|
91
|
+
decimals: number
|
|
92
|
+
): string {
|
|
93
|
+
const headers = 'Address,Balance,Yield Amount,Percentage\n';
|
|
94
|
+
const rows = entries.map((entry) =>
|
|
95
|
+
`${entry.address},${entry.balance.toString()},${formatAmount(entry.yieldAmount, decimals)} ${tokenSymbol},${entry.percentage.toFixed(2)}%`
|
|
96
|
+
).join('\n');
|
|
97
|
+
return headers + rows;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Export distribution preview to JSON format
|
|
102
|
+
*/
|
|
103
|
+
export function exportToJSON(entries: DistributionPreviewEntry[]): string {
|
|
104
|
+
return JSON.stringify(entries.map((entry) => ({
|
|
105
|
+
address: entry.address,
|
|
106
|
+
balance: entry.balance.toString(),
|
|
107
|
+
yieldAmount: entry.yieldAmount.toString(),
|
|
108
|
+
percentage: entry.percentage,
|
|
109
|
+
})), null, 2);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Generate colors for pie chart segments
|
|
114
|
+
*/
|
|
115
|
+
function generateChartColors(count: number): string[] {
|
|
116
|
+
const baseColors = [
|
|
117
|
+
'#3B82F6', // blue
|
|
118
|
+
'#10B981', // green
|
|
119
|
+
'#F59E0B', // amber
|
|
120
|
+
'#EF4444', // red
|
|
121
|
+
'#8B5CF6', // purple
|
|
122
|
+
'#EC4899', // pink
|
|
123
|
+
'#06B6D4', // cyan
|
|
124
|
+
'#F97316', // orange
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
const colors: string[] = [];
|
|
128
|
+
for (let i = 0; i < count; i++) {
|
|
129
|
+
colors.push(baseColors[i % baseColors.length]);
|
|
130
|
+
}
|
|
131
|
+
return colors;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* YieldCalculator component for previewing yield distributions
|
|
136
|
+
* Shows per-holder breakdown with optional chart visualization
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```tsx
|
|
140
|
+
* <YieldCalculator
|
|
141
|
+
* tokenAddress="0x..."
|
|
142
|
+
* yieldDistributorAddress="0x..."
|
|
143
|
+
* supportedPaymentTokens={[{ address: '0x...', symbol: 'USDC', decimals: 6 }]}
|
|
144
|
+
* onDistribute={(config) => console.log('Distributing:', config)}
|
|
145
|
+
* />
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export function YieldCalculator({
|
|
149
|
+
tokenAddress,
|
|
150
|
+
yieldDistributorAddress: _yieldDistributorAddress, // Reserved for future contract interaction
|
|
151
|
+
supportedPaymentTokens,
|
|
152
|
+
onDistribute,
|
|
153
|
+
showChart = true,
|
|
154
|
+
allowExport = true,
|
|
155
|
+
theme = 'light',
|
|
156
|
+
}: YieldCalculatorProps): React.ReactElement {
|
|
157
|
+
const [totalAmount, setTotalAmount] = useState('');
|
|
158
|
+
const [selectedToken, setSelectedToken] = useState(supportedPaymentTokens[0]?.address ?? '');
|
|
159
|
+
const [snapshotDate, setSnapshotDate] = useState<string>(new Date().toISOString().split('T')[0]);
|
|
160
|
+
const [claimWindowDays, setClaimWindowDays] = useState('30');
|
|
161
|
+
const [preview, setPreview] = useState<DistributionPreviewEntry[]>([]);
|
|
162
|
+
const [isCalculating, setIsCalculating] = useState(false);
|
|
163
|
+
const [chartType, setChartType] = useState<'bar' | 'pie'>('bar');
|
|
164
|
+
|
|
165
|
+
const isDark = theme === 'dark';
|
|
166
|
+
|
|
167
|
+
const selectedTokenInfo = useMemo<PaymentToken | undefined>(() =>
|
|
168
|
+
supportedPaymentTokens.find((t) => t.address === selectedToken),
|
|
169
|
+
[supportedPaymentTokens, selectedToken]
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const calculatePreview = useCallback(async () => {
|
|
173
|
+
if (!totalAmount || parseFloat(totalAmount) <= 0) return;
|
|
174
|
+
|
|
175
|
+
setIsCalculating(true);
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
// Placeholder: In full implementation, this would fetch actual holder data
|
|
179
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
180
|
+
|
|
181
|
+
// Mock holder data for preview
|
|
182
|
+
const mockHolders = [
|
|
183
|
+
{ address: '0x1234567890123456789012345678901234567890', balance: BigInt(500) * BigInt(10 ** 18) },
|
|
184
|
+
{ address: '0x2345678901234567890123456789012345678901', balance: BigInt(300) * BigInt(10 ** 18) },
|
|
185
|
+
{ address: '0x3456789012345678901234567890123456789012', balance: BigInt(200) * BigInt(10 ** 18) },
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
const totalAmountBigInt = parseAmount(totalAmount, selectedTokenInfo?.decimals ?? 6);
|
|
189
|
+
const previewEntries = calculateDistribution(totalAmountBigInt, mockHolders);
|
|
190
|
+
|
|
191
|
+
setPreview(previewEntries);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error('Failed to calculate preview:', error);
|
|
194
|
+
} finally {
|
|
195
|
+
setIsCalculating(false);
|
|
196
|
+
}
|
|
197
|
+
}, [totalAmount, selectedTokenInfo]);
|
|
198
|
+
|
|
199
|
+
const handleDistribute = useCallback(() => {
|
|
200
|
+
if (!selectedTokenInfo || !totalAmount) return;
|
|
201
|
+
|
|
202
|
+
const config: DistributionConfig = {
|
|
203
|
+
tokenAddress,
|
|
204
|
+
paymentToken: selectedToken,
|
|
205
|
+
totalAmount,
|
|
206
|
+
snapshotDate: new Date(snapshotDate),
|
|
207
|
+
claimWindowDays: parseInt(claimWindowDays, 10),
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
onDistribute?.(config);
|
|
211
|
+
}, [tokenAddress, selectedToken, totalAmount, snapshotDate, claimWindowDays, onDistribute, selectedTokenInfo]);
|
|
212
|
+
|
|
213
|
+
const handleExport = useCallback((format: 'csv' | 'json') => {
|
|
214
|
+
if (preview.length === 0) return;
|
|
215
|
+
|
|
216
|
+
let content: string;
|
|
217
|
+
let filename: string;
|
|
218
|
+
let mimeType: string;
|
|
219
|
+
|
|
220
|
+
if (format === 'csv') {
|
|
221
|
+
content = exportToCSV(preview, selectedTokenInfo?.symbol ?? '', selectedTokenInfo?.decimals ?? 6);
|
|
222
|
+
filename = `yield-distribution-${Date.now()}.csv`;
|
|
223
|
+
mimeType = 'text/csv';
|
|
224
|
+
} else {
|
|
225
|
+
content = exportToJSON(preview);
|
|
226
|
+
filename = `yield-distribution-${Date.now()}.json`;
|
|
227
|
+
mimeType = 'application/json';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const blob = new Blob([content], { type: mimeType });
|
|
231
|
+
const url = URL.createObjectURL(blob);
|
|
232
|
+
const a = document.createElement('a');
|
|
233
|
+
a.href = url;
|
|
234
|
+
a.download = filename;
|
|
235
|
+
a.click();
|
|
236
|
+
URL.revokeObjectURL(url);
|
|
237
|
+
}, [preview, selectedTokenInfo]);
|
|
238
|
+
|
|
239
|
+
// Calculate total distributed for verification
|
|
240
|
+
const totalDistributed = useMemo(() => sumDistributionAmounts(preview), [preview]);
|
|
241
|
+
|
|
242
|
+
// Generate chart colors
|
|
243
|
+
const chartColors = useMemo(() => generateChartColors(preview.length), [preview.length]);
|
|
244
|
+
|
|
245
|
+
// Calculate pie chart segments
|
|
246
|
+
const pieSegments = useMemo(() => {
|
|
247
|
+
if (preview.length === 0) return [];
|
|
248
|
+
|
|
249
|
+
let cumulativePercentage = 0;
|
|
250
|
+
return preview.map((entry, index) => {
|
|
251
|
+
const startAngle = cumulativePercentage * 3.6; // Convert percentage to degrees
|
|
252
|
+
cumulativePercentage += entry.percentage;
|
|
253
|
+
const endAngle = cumulativePercentage * 3.6;
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
...entry,
|
|
257
|
+
startAngle,
|
|
258
|
+
endAngle,
|
|
259
|
+
color: chartColors[index],
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
}, [preview, chartColors]);
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<div
|
|
266
|
+
className={`rwa-card ${isDark ? 'dark' : ''}`}
|
|
267
|
+
data-testid="yield-calculator"
|
|
268
|
+
data-theme={theme}
|
|
269
|
+
>
|
|
270
|
+
<h2 className="text-lg font-semibold mb-4">Yield Calculator</h2>
|
|
271
|
+
|
|
272
|
+
{/* Configuration Form */}
|
|
273
|
+
<div className="space-y-4 mb-6">
|
|
274
|
+
<div className="grid grid-cols-2 gap-4">
|
|
275
|
+
<div>
|
|
276
|
+
<label htmlFor="total-amount" className="rwa-label">
|
|
277
|
+
Total Distribution Amount
|
|
278
|
+
</label>
|
|
279
|
+
<input
|
|
280
|
+
id="total-amount"
|
|
281
|
+
type="number"
|
|
282
|
+
className="rwa-input mt-1"
|
|
283
|
+
placeholder="0.00"
|
|
284
|
+
value={totalAmount}
|
|
285
|
+
onChange={(e) => setTotalAmount(e.target.value)}
|
|
286
|
+
min="0"
|
|
287
|
+
step="any"
|
|
288
|
+
data-testid="total-amount-input"
|
|
289
|
+
/>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<div>
|
|
293
|
+
<label htmlFor="payment-token" className="rwa-label">
|
|
294
|
+
Payment Token
|
|
295
|
+
</label>
|
|
296
|
+
<select
|
|
297
|
+
id="payment-token"
|
|
298
|
+
className="rwa-input mt-1"
|
|
299
|
+
value={selectedToken}
|
|
300
|
+
onChange={(e) => setSelectedToken(e.target.value)}
|
|
301
|
+
data-testid="payment-token-select"
|
|
302
|
+
>
|
|
303
|
+
{supportedPaymentTokens.map((token) => (
|
|
304
|
+
<option key={token.address} value={token.address}>
|
|
305
|
+
{token.symbol}
|
|
306
|
+
</option>
|
|
307
|
+
))}
|
|
308
|
+
</select>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<div className="grid grid-cols-2 gap-4">
|
|
313
|
+
<div>
|
|
314
|
+
<label htmlFor="snapshot-date" className="rwa-label">
|
|
315
|
+
Snapshot Date
|
|
316
|
+
</label>
|
|
317
|
+
<input
|
|
318
|
+
id="snapshot-date"
|
|
319
|
+
type="date"
|
|
320
|
+
className="rwa-input mt-1"
|
|
321
|
+
value={snapshotDate}
|
|
322
|
+
onChange={(e) => setSnapshotDate(e.target.value)}
|
|
323
|
+
data-testid="snapshot-date-input"
|
|
324
|
+
/>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<div>
|
|
328
|
+
<label htmlFor="claim-window" className="rwa-label">
|
|
329
|
+
Claim Window (days)
|
|
330
|
+
</label>
|
|
331
|
+
<input
|
|
332
|
+
id="claim-window"
|
|
333
|
+
type="number"
|
|
334
|
+
className="rwa-input mt-1"
|
|
335
|
+
value={claimWindowDays}
|
|
336
|
+
onChange={(e) => setClaimWindowDays(e.target.value)}
|
|
337
|
+
min="1"
|
|
338
|
+
max="365"
|
|
339
|
+
data-testid="claim-window-input"
|
|
340
|
+
/>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<button
|
|
345
|
+
type="button"
|
|
346
|
+
className="rwa-button-secondary w-full"
|
|
347
|
+
onClick={calculatePreview}
|
|
348
|
+
disabled={isCalculating || !totalAmount}
|
|
349
|
+
aria-busy={isCalculating}
|
|
350
|
+
data-testid="calculate-preview-btn"
|
|
351
|
+
>
|
|
352
|
+
{isCalculating ? 'Calculating...' : 'Calculate Preview'}
|
|
353
|
+
</button>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
{/* Preview Results */}
|
|
357
|
+
{preview.length > 0 && (
|
|
358
|
+
<div className="space-y-4" data-testid="distribution-preview">
|
|
359
|
+
<div className="flex justify-between items-center">
|
|
360
|
+
<h3 className="text-sm font-medium">Distribution Preview</h3>
|
|
361
|
+
<div className="flex items-center space-x-4">
|
|
362
|
+
{/* Chart Type Toggle */}
|
|
363
|
+
{showChart && (
|
|
364
|
+
<div className="flex space-x-1" role="group" aria-label="Chart type">
|
|
365
|
+
<button
|
|
366
|
+
type="button"
|
|
367
|
+
className={`px-2 py-1 text-xs rounded ${chartType === 'bar' ? 'bg-mantle-600 text-white' : 'bg-gray-200 dark:bg-gray-700'}`}
|
|
368
|
+
onClick={() => setChartType('bar')}
|
|
369
|
+
data-testid="chart-type-bar"
|
|
370
|
+
>
|
|
371
|
+
Bar
|
|
372
|
+
</button>
|
|
373
|
+
<button
|
|
374
|
+
type="button"
|
|
375
|
+
className={`px-2 py-1 text-xs rounded ${chartType === 'pie' ? 'bg-mantle-600 text-white' : 'bg-gray-200 dark:bg-gray-700'}`}
|
|
376
|
+
onClick={() => setChartType('pie')}
|
|
377
|
+
data-testid="chart-type-pie"
|
|
378
|
+
>
|
|
379
|
+
Pie
|
|
380
|
+
</button>
|
|
381
|
+
</div>
|
|
382
|
+
)}
|
|
383
|
+
{allowExport && (
|
|
384
|
+
<div className="flex space-x-2">
|
|
385
|
+
<button
|
|
386
|
+
type="button"
|
|
387
|
+
className="text-xs text-mantle-600 hover:text-mantle-700"
|
|
388
|
+
onClick={() => handleExport('csv')}
|
|
389
|
+
data-testid="export-csv-btn"
|
|
390
|
+
>
|
|
391
|
+
Export CSV
|
|
392
|
+
</button>
|
|
393
|
+
<button
|
|
394
|
+
type="button"
|
|
395
|
+
className="text-xs text-mantle-600 hover:text-mantle-700"
|
|
396
|
+
onClick={() => handleExport('json')}
|
|
397
|
+
data-testid="export-json-btn"
|
|
398
|
+
>
|
|
399
|
+
Export JSON
|
|
400
|
+
</button>
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
{/* Chart Visualization */}
|
|
407
|
+
{showChart && (
|
|
408
|
+
<div data-testid="distribution-chart">
|
|
409
|
+
{chartType === 'bar' ? (
|
|
410
|
+
/* Bar Chart */
|
|
411
|
+
<div className="space-y-2" role="img" aria-label="Distribution bar chart" data-testid="bar-chart">
|
|
412
|
+
{preview.map((entry, index) => (
|
|
413
|
+
<div key={entry.address} className="flex items-center space-x-2">
|
|
414
|
+
<span className="text-xs w-24 truncate" title={entry.address}>
|
|
415
|
+
{entry.address.slice(0, 6)}...{entry.address.slice(-4)}
|
|
416
|
+
</span>
|
|
417
|
+
<div className="flex-1 h-4 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
|
418
|
+
<div
|
|
419
|
+
className="h-full rounded-full transition-all duration-300"
|
|
420
|
+
style={{
|
|
421
|
+
width: `${entry.percentage}%`,
|
|
422
|
+
backgroundColor: chartColors[index],
|
|
423
|
+
}}
|
|
424
|
+
data-testid={`bar-segment-${index}`}
|
|
425
|
+
/>
|
|
426
|
+
</div>
|
|
427
|
+
<span className="text-xs w-16 text-right">
|
|
428
|
+
{entry.percentage.toFixed(1)}%
|
|
429
|
+
</span>
|
|
430
|
+
</div>
|
|
431
|
+
))}
|
|
432
|
+
</div>
|
|
433
|
+
) : (
|
|
434
|
+
/* Pie Chart (SVG-based) */
|
|
435
|
+
<div className="flex items-center justify-center" data-testid="pie-chart">
|
|
436
|
+
<svg viewBox="0 0 100 100" className="w-48 h-48" role="img" aria-label="Distribution pie chart">
|
|
437
|
+
{pieSegments.map((segment, index) => {
|
|
438
|
+
// Convert angles to radians and calculate arc path
|
|
439
|
+
const startRad = (segment.startAngle - 90) * Math.PI / 180;
|
|
440
|
+
const endRad = (segment.endAngle - 90) * Math.PI / 180;
|
|
441
|
+
|
|
442
|
+
const x1 = 50 + 40 * Math.cos(startRad);
|
|
443
|
+
const y1 = 50 + 40 * Math.sin(startRad);
|
|
444
|
+
const x2 = 50 + 40 * Math.cos(endRad);
|
|
445
|
+
const y2 = 50 + 40 * Math.sin(endRad);
|
|
446
|
+
|
|
447
|
+
const largeArc = segment.percentage > 50 ? 1 : 0;
|
|
448
|
+
|
|
449
|
+
const pathD = `M 50 50 L ${x1} ${y1} A 40 40 0 ${largeArc} 1 ${x2} ${y2} Z`;
|
|
450
|
+
|
|
451
|
+
return (
|
|
452
|
+
<path
|
|
453
|
+
key={segment.address}
|
|
454
|
+
d={pathD}
|
|
455
|
+
fill={segment.color}
|
|
456
|
+
stroke="white"
|
|
457
|
+
strokeWidth="0.5"
|
|
458
|
+
data-testid={`pie-segment-${index}`}
|
|
459
|
+
>
|
|
460
|
+
<title>{`${segment.address.slice(0, 6)}...${segment.address.slice(-4)}: ${segment.percentage.toFixed(1)}%`}</title>
|
|
461
|
+
</path>
|
|
462
|
+
);
|
|
463
|
+
})}
|
|
464
|
+
</svg>
|
|
465
|
+
{/* Legend */}
|
|
466
|
+
<div className="ml-4 space-y-1">
|
|
467
|
+
{preview.map((entry, index) => (
|
|
468
|
+
<div key={entry.address} className="flex items-center text-xs">
|
|
469
|
+
<div
|
|
470
|
+
className="w-3 h-3 rounded-sm mr-2"
|
|
471
|
+
style={{ backgroundColor: chartColors[index] }}
|
|
472
|
+
/>
|
|
473
|
+
<span className="truncate max-w-[100px]" title={entry.address}>
|
|
474
|
+
{entry.address.slice(0, 6)}...{entry.address.slice(-4)}
|
|
475
|
+
</span>
|
|
476
|
+
<span className="ml-2 text-gray-500">
|
|
477
|
+
{entry.percentage.toFixed(1)}%
|
|
478
|
+
</span>
|
|
479
|
+
</div>
|
|
480
|
+
))}
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
)}
|
|
484
|
+
</div>
|
|
485
|
+
)}
|
|
486
|
+
|
|
487
|
+
{/* Detailed Table */}
|
|
488
|
+
<div className="overflow-x-auto" data-testid="distribution-table">
|
|
489
|
+
<table className="w-full text-sm">
|
|
490
|
+
<thead>
|
|
491
|
+
<tr className="border-b dark:border-gray-700">
|
|
492
|
+
<th className="text-left py-2">Address</th>
|
|
493
|
+
<th className="text-right py-2">Yield Amount</th>
|
|
494
|
+
<th className="text-right py-2">%</th>
|
|
495
|
+
</tr>
|
|
496
|
+
</thead>
|
|
497
|
+
<tbody>
|
|
498
|
+
{preview.map((entry, index) => (
|
|
499
|
+
<tr key={entry.address} className="border-b dark:border-gray-700" data-testid={`table-row-${index}`}>
|
|
500
|
+
<td className="py-2">
|
|
501
|
+
<span title={entry.address}>
|
|
502
|
+
{entry.address.slice(0, 10)}...{entry.address.slice(-8)}
|
|
503
|
+
</span>
|
|
504
|
+
</td>
|
|
505
|
+
<td className="text-right py-2" data-testid={`yield-amount-${index}`}>
|
|
506
|
+
{formatAmount(entry.yieldAmount, selectedTokenInfo?.decimals ?? 6)} {selectedTokenInfo?.symbol}
|
|
507
|
+
</td>
|
|
508
|
+
<td className="text-right py-2" data-testid={`percentage-${index}`}>
|
|
509
|
+
{entry.percentage.toFixed(2)}%
|
|
510
|
+
</td>
|
|
511
|
+
</tr>
|
|
512
|
+
))}
|
|
513
|
+
</tbody>
|
|
514
|
+
<tfoot>
|
|
515
|
+
<tr className="font-medium">
|
|
516
|
+
<td className="py-2">Total</td>
|
|
517
|
+
<td className="text-right py-2" data-testid="total-distributed">
|
|
518
|
+
{formatAmount(totalDistributed, selectedTokenInfo?.decimals ?? 6)} {selectedTokenInfo?.symbol}
|
|
519
|
+
</td>
|
|
520
|
+
<td className="text-right py-2" data-testid="total-percentage">100%</td>
|
|
521
|
+
</tr>
|
|
522
|
+
</tfoot>
|
|
523
|
+
</table>
|
|
524
|
+
</div>
|
|
525
|
+
|
|
526
|
+
{/* Distribute Button */}
|
|
527
|
+
<button
|
|
528
|
+
type="button"
|
|
529
|
+
className="rwa-button-primary w-full"
|
|
530
|
+
onClick={handleDistribute}
|
|
531
|
+
data-testid="execute-distribution-btn"
|
|
532
|
+
>
|
|
533
|
+
Execute Distribution
|
|
534
|
+
</button>
|
|
535
|
+
</div>
|
|
536
|
+
)}
|
|
537
|
+
</div>
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export default YieldCalculator;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useRWA hook for accessing RWA SDK functionality in React components
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useState, useEffect } from 'react';
|
|
6
|
+
import type { UseRWAReturn } from '../types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hook for accessing RWA SDK functionality
|
|
10
|
+
* Provides connection status and SDK client access
|
|
11
|
+
*/
|
|
12
|
+
export function useRWA(): UseRWAReturn {
|
|
13
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
14
|
+
const [chainId, setChainId] = useState<number | undefined>(undefined);
|
|
15
|
+
const [address, setAddress] = useState<string | undefined>(undefined);
|
|
16
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
17
|
+
const [error, setError] = useState<Error | null>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
// Initialize SDK client
|
|
21
|
+
const initializeSDK = async () => {
|
|
22
|
+
try {
|
|
23
|
+
// Check if window.ethereum is available (browser wallet)
|
|
24
|
+
if (typeof window !== 'undefined' && window.ethereum) {
|
|
25
|
+
const accounts = await window.ethereum.request?.({
|
|
26
|
+
method: 'eth_accounts',
|
|
27
|
+
}) as string[] | undefined;
|
|
28
|
+
|
|
29
|
+
if (accounts && accounts.length > 0) {
|
|
30
|
+
setAddress(accounts[0]);
|
|
31
|
+
setIsConnected(true);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const chainIdHex = await window.ethereum.request?.({
|
|
35
|
+
method: 'eth_chainId',
|
|
36
|
+
}) as string | undefined;
|
|
37
|
+
|
|
38
|
+
if (chainIdHex) {
|
|
39
|
+
setChainId(parseInt(chainIdHex, 16));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setIsInitialized(true);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
setError(err instanceof Error ? err : new Error('Failed to initialize RWA SDK'));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
initializeSDK();
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
isInitialized,
|
|
54
|
+
chainId,
|
|
55
|
+
address,
|
|
56
|
+
isConnected,
|
|
57
|
+
error,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Extend Window interface for ethereum provider
|
|
62
|
+
declare global {
|
|
63
|
+
interface Window {
|
|
64
|
+
ethereum?: {
|
|
65
|
+
request?: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
|
|
66
|
+
on?: (event: string, callback: (...args: unknown[]) => void) => void;
|
|
67
|
+
removeListener?: (event: string, callback: (...args: unknown[]) => void) => void;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mantle-rwa/react
|
|
3
|
+
* React components for Real-World Asset tokenization on Mantle Network
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Components
|
|
7
|
+
export { KYCFlow } from './components/KYCFlow';
|
|
8
|
+
export { InvestorDashboard } from './components/InvestorDashboard';
|
|
9
|
+
export { TokenMintForm } from './components/TokenMintForm';
|
|
10
|
+
export { YieldCalculator } from './components/YieldCalculator';
|
|
11
|
+
|
|
12
|
+
// Hooks
|
|
13
|
+
export { useRWA } from './hooks/useRWA';
|
|
14
|
+
|
|
15
|
+
// Types
|
|
16
|
+
export type {
|
|
17
|
+
Theme,
|
|
18
|
+
KYCProviderType,
|
|
19
|
+
KYCRequiredField,
|
|
20
|
+
KYCResult,
|
|
21
|
+
KYCFlowProps,
|
|
22
|
+
KYCFlowStyles,
|
|
23
|
+
InvestorDashboardProps,
|
|
24
|
+
YieldHistoryEntry,
|
|
25
|
+
TokenMintFormProps,
|
|
26
|
+
BatchMintEntry,
|
|
27
|
+
YieldCalculatorProps,
|
|
28
|
+
PaymentToken,
|
|
29
|
+
DistributionConfig,
|
|
30
|
+
DistributionPreviewEntry,
|
|
31
|
+
UseRWAReturn,
|
|
32
|
+
} from './types';
|