@mantle-rwa/react 0.1.0 → 0.1.2

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 (80) hide show
  1. package/LICENSE +0 -0
  2. package/dist/cjs/components/ConnectWalletPrompt.js +13 -0
  3. package/dist/cjs/components/ConnectWalletPrompt.js.map +1 -0
  4. package/dist/cjs/components/ErrorDisplay.js +42 -0
  5. package/dist/cjs/components/ErrorDisplay.js.map +1 -0
  6. package/dist/cjs/components/InvestorDashboard.js +156 -0
  7. package/dist/cjs/components/InvestorDashboard.js.map +1 -0
  8. package/dist/cjs/components/KYCFlow.js +146 -0
  9. package/dist/cjs/components/KYCFlow.js.map +1 -0
  10. package/dist/cjs/components/LoadingSpinner.js +18 -0
  11. package/dist/cjs/components/LoadingSpinner.js.map +1 -0
  12. package/dist/cjs/components/TokenMintForm.js +163 -0
  13. package/dist/cjs/components/TokenMintForm.js.map +1 -0
  14. package/dist/cjs/components/YieldCalculator.js +97 -0
  15. package/dist/cjs/components/YieldCalculator.js.map +1 -0
  16. package/dist/cjs/hooks/useRWA.js +87 -40
  17. package/dist/cjs/hooks/useRWA.js.map +1 -1
  18. package/dist/cjs/index.js +11 -1
  19. package/dist/cjs/index.js.map +1 -1
  20. package/dist/cjs/types/index.js +2 -2
  21. package/dist/cjs/types/index.js.map +1 -1
  22. package/dist/esm/components/ConnectWalletPrompt.js +10 -0
  23. package/dist/esm/components/ConnectWalletPrompt.js.map +1 -0
  24. package/dist/esm/components/ErrorDisplay.js +38 -0
  25. package/dist/esm/components/ErrorDisplay.js.map +1 -0
  26. package/dist/esm/components/InvestorDashboard.js +153 -0
  27. package/dist/esm/components/InvestorDashboard.js.map +1 -0
  28. package/dist/esm/components/KYCFlow.js +143 -0
  29. package/dist/esm/components/KYCFlow.js.map +1 -0
  30. package/dist/esm/components/LoadingSpinner.js +15 -0
  31. package/dist/esm/components/LoadingSpinner.js.map +1 -0
  32. package/dist/esm/components/TokenMintForm.js +158 -0
  33. package/dist/esm/components/TokenMintForm.js.map +1 -0
  34. package/dist/esm/components/YieldCalculator.js +94 -0
  35. package/dist/esm/components/YieldCalculator.js.map +1 -0
  36. package/dist/esm/hooks/useRWA.js +86 -39
  37. package/dist/esm/hooks/useRWA.js.map +1 -1
  38. package/dist/esm/index.js +4 -0
  39. package/dist/esm/index.js.map +1 -1
  40. package/dist/esm/types/index.js +3 -3
  41. package/dist/esm/types/index.js.map +1 -1
  42. package/dist/styles.css +3 -1
  43. package/dist/types/components/ConnectWalletPrompt.d.ts +15 -0
  44. package/dist/types/components/ConnectWalletPrompt.d.ts.map +1 -0
  45. package/dist/types/components/ErrorDisplay.d.ts +16 -0
  46. package/dist/types/components/ErrorDisplay.d.ts.map +1 -0
  47. package/dist/types/components/InvestorDashboard.d.ts +7 -0
  48. package/dist/types/components/InvestorDashboard.d.ts.map +1 -0
  49. package/dist/types/components/KYCFlow.d.ts +7 -0
  50. package/dist/types/components/KYCFlow.d.ts.map +1 -0
  51. package/dist/types/components/LoadingSpinner.d.ts +10 -0
  52. package/dist/types/components/LoadingSpinner.d.ts.map +1 -0
  53. package/dist/types/components/TokenMintForm.d.ts +15 -0
  54. package/dist/types/components/TokenMintForm.d.ts.map +1 -0
  55. package/dist/types/components/YieldCalculator.d.ts +7 -0
  56. package/dist/types/components/YieldCalculator.d.ts.map +1 -0
  57. package/dist/types/hooks/useRWA.d.ts +7 -19
  58. package/dist/types/hooks/useRWA.d.ts.map +1 -1
  59. package/dist/types/index.d.ts +5 -1
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/dist/types/types/index.d.ts +113 -131
  62. package/dist/types/types/index.d.ts.map +1 -1
  63. package/package.json +65 -63
  64. package/src/components/ConnectWalletPrompt.tsx +47 -0
  65. package/src/components/ErrorDisplay.tsx +90 -0
  66. package/src/components/InvestorDashboard.tsx +315 -0
  67. package/src/components/KYCFlow.tsx +267 -0
  68. package/src/components/LoadingSpinner.tsx +33 -0
  69. package/src/components/TokenMintForm.tsx +291 -0
  70. package/src/components/YieldCalculator.tsx +250 -0
  71. package/src/hooks/useRWA.ts +90 -50
  72. package/src/index.ts +5 -17
  73. package/src/styles/index.css +60 -175
  74. package/src/types/index.ts +142 -135
  75. package/src/components/InvestorDashboard/index.tsx +0 -359
  76. package/src/components/KYCFlow/index.tsx +0 -434
  77. package/src/components/TokenMintForm/index.tsx +0 -590
  78. package/src/components/YieldCalculator/index.tsx +0 -541
  79. package/src/components/index.ts +0 -8
  80. package/src/hooks/index.ts +0 -5
@@ -0,0 +1,250 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * YieldCalculator - Calculator for previewing yield distributions
5
+ *
6
+ * Shows how yields will be allocated to token holders before creating a distribution.
7
+ */
8
+
9
+ import { useState, useCallback, useEffect, useRef } from 'react';
10
+ import { formatUnits } from 'viem';
11
+ import { useRWA } from '../hooks/useRWA';
12
+ import type { YieldCalculatorProps } from '../types';
13
+ import type { DistributionPreview, HolderDistribution } from '@mantle-rwa/sdk';
14
+
15
+ /**
16
+ * Format address for display
17
+ */
18
+ function formatAddress(address: string): string {
19
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
20
+ }
21
+
22
+ /**
23
+ * Format token amount for display
24
+ */
25
+ function formatAmount(amount: bigint, decimals: number = 18): string {
26
+ const formatted = formatUnits(amount, decimals);
27
+ const num = parseFloat(formatted);
28
+ return num.toLocaleString(undefined, { maximumFractionDigits: 4 });
29
+ }
30
+
31
+ /**
32
+ * YieldCalculator component
33
+ */
34
+ export function YieldCalculator({
35
+ tokenAddress,
36
+ yieldDistributorAddress: _yieldDistributorAddress,
37
+ holderAddresses,
38
+ onCalculate,
39
+ onError,
40
+ className = '',
41
+ }: YieldCalculatorProps): JSX.Element {
42
+ const { client, isInitialized } = useRWA();
43
+
44
+ const [amount, setAmount] = useState<string>('');
45
+ const [preview, setPreview] = useState<DistributionPreview | null>(null);
46
+ const [isLoading, setIsLoading] = useState(false);
47
+ const [error, setError] = useState<Error | null>(null);
48
+
49
+ // Debounce timer ref
50
+ const debounceRef = useRef<NodeJS.Timeout | null>(null);
51
+
52
+ // Calculate preview
53
+ const calculatePreview = useCallback(async (distributionAmount: string) => {
54
+ if (!client || !isInitialized || !distributionAmount || parseFloat(distributionAmount) <= 0) {
55
+ setPreview(null);
56
+ return;
57
+ }
58
+
59
+ setIsLoading(true);
60
+ setError(null);
61
+
62
+ try {
63
+ const result = await client.yield.previewDistribution(
64
+ tokenAddress,
65
+ distributionAmount,
66
+ undefined, // snapshotId
67
+ holderAddresses
68
+ );
69
+ setPreview(result);
70
+ onCalculate?.(result);
71
+ } catch (err) {
72
+ const errorObj = err instanceof Error ? err : new Error('Failed to calculate preview');
73
+ setError(errorObj);
74
+ onError?.(errorObj);
75
+ setPreview(null);
76
+ } finally {
77
+ setIsLoading(false);
78
+ }
79
+ }, [client, isInitialized, tokenAddress, holderAddresses, onCalculate, onError]);
80
+
81
+ // Handle amount change with debounce
82
+ const handleAmountChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
83
+ const value = e.target.value;
84
+ setAmount(value);
85
+
86
+ // Clear previous debounce
87
+ if (debounceRef.current) {
88
+ clearTimeout(debounceRef.current);
89
+ }
90
+
91
+ // Debounce the calculation
92
+ debounceRef.current = setTimeout(() => {
93
+ calculatePreview(value);
94
+ }, 500);
95
+ }, [calculatePreview]);
96
+
97
+ // Cleanup debounce on unmount
98
+ useEffect(() => {
99
+ return () => {
100
+ if (debounceRef.current) {
101
+ clearTimeout(debounceRef.current);
102
+ }
103
+ };
104
+ }, []);
105
+
106
+ // Recalculate when holder addresses change
107
+ useEffect(() => {
108
+ if (amount && parseFloat(amount) > 0) {
109
+ calculatePreview(amount);
110
+ }
111
+ }, [holderAddresses]); // eslint-disable-line react-hooks/exhaustive-deps
112
+
113
+ // Handle manual calculate button
114
+ const handleCalculate = useCallback(() => {
115
+ calculatePreview(amount);
116
+ }, [amount, calculatePreview]);
117
+
118
+ return (
119
+ <div className={`rwa-yield-calculator ${className}`}>
120
+ <div className="space-y-6">
121
+ {/* Amount Input */}
122
+ <div>
123
+ <label htmlFor="distribution-amount" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
124
+ Total Distribution Amount
125
+ </label>
126
+ <div className="mt-1 flex rounded-md shadow-sm">
127
+ <input
128
+ type="text"
129
+ id="distribution-amount"
130
+ value={amount}
131
+ onChange={handleAmountChange}
132
+ placeholder="Enter amount to distribute"
133
+ className="flex-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-l-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:text-white"
134
+ />
135
+ <button
136
+ type="button"
137
+ onClick={handleCalculate}
138
+ disabled={isLoading || !amount}
139
+ className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-r-md transition-colors"
140
+ >
141
+ {isLoading ? 'Calculating...' : 'Calculate'}
142
+ </button>
143
+ </div>
144
+ </div>
145
+
146
+ {/* Loading State */}
147
+ {isLoading && (
148
+ <div className="flex items-center justify-center p-4">
149
+ <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600" />
150
+ <span className="ml-2 text-gray-600 dark:text-gray-300">Calculating distribution...</span>
151
+ </div>
152
+ )}
153
+
154
+ {/* Error State */}
155
+ {error && (
156
+ <div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
157
+ <p className="text-red-700 dark:text-red-300">{error.message}</p>
158
+ <button
159
+ onClick={handleCalculate}
160
+ className="mt-2 text-sm text-red-600 dark:text-red-400 underline hover:no-underline"
161
+ >
162
+ Retry
163
+ </button>
164
+ </div>
165
+ )}
166
+
167
+ {/* Preview Results */}
168
+ {preview && !isLoading && (
169
+ <div className="space-y-4">
170
+ {/* Summary */}
171
+ <div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
172
+ <h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">Distribution Summary</h4>
173
+ <div className="mt-2 grid grid-cols-2 gap-4">
174
+ <div>
175
+ <p className="text-xs text-gray-500 dark:text-gray-400">Total Holders</p>
176
+ <p className="text-lg font-semibold text-gray-900 dark:text-white">{preview.totalHolders}</p>
177
+ </div>
178
+ <div>
179
+ <p className="text-xs text-gray-500 dark:text-gray-400">Total Supply</p>
180
+ <p className="text-lg font-semibold text-gray-900 dark:text-white">
181
+ {formatAmount(preview.totalSupplyAtSnapshot)}
182
+ </p>
183
+ </div>
184
+ </div>
185
+ </div>
186
+
187
+ {/* Holder Distributions Table */}
188
+ {preview.distributions.length > 0 && (
189
+ <div className="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
190
+ <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
191
+ <thead className="bg-gray-50 dark:bg-gray-800">
192
+ <tr>
193
+ <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
194
+ Address
195
+ </th>
196
+ <th className="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
197
+ Balance
198
+ </th>
199
+ <th className="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
200
+ Yield Amount
201
+ </th>
202
+ <th className="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
203
+ Share
204
+ </th>
205
+ </tr>
206
+ </thead>
207
+ <tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
208
+ {preview.distributions.map((dist: HolderDistribution) => (
209
+ <tr key={dist.address}>
210
+ <td className="px-4 py-3 whitespace-nowrap text-sm font-mono text-gray-900 dark:text-white">
211
+ {formatAddress(dist.address)}
212
+ </td>
213
+ <td className="px-4 py-3 whitespace-nowrap text-sm text-right text-gray-600 dark:text-gray-300">
214
+ {formatAmount(dist.balance)}
215
+ </td>
216
+ <td className="px-4 py-3 whitespace-nowrap text-sm text-right font-medium text-green-600 dark:text-green-400">
217
+ {formatAmount(dist.yieldAmount)}
218
+ </td>
219
+ <td className="px-4 py-3 whitespace-nowrap text-sm text-right text-gray-600 dark:text-gray-300">
220
+ {dist.percentage.toFixed(2)}%
221
+ </td>
222
+ </tr>
223
+ ))}
224
+ </tbody>
225
+ </table>
226
+ </div>
227
+ )}
228
+
229
+ {preview.distributions.length === 0 && (
230
+ <div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
231
+ <p className="text-yellow-700 dark:text-yellow-300">
232
+ No holder addresses provided or no holders have balances.
233
+ </p>
234
+ </div>
235
+ )}
236
+ </div>
237
+ )}
238
+
239
+ {/* Empty State */}
240
+ {!preview && !isLoading && !error && (
241
+ <div className="p-6 text-center text-gray-500 dark:text-gray-400">
242
+ <p>Enter a distribution amount to preview how yields will be allocated.</p>
243
+ </div>
244
+ )}
245
+ </div>
246
+ </div>
247
+ );
248
+ }
249
+
250
+ export default YieldCalculator;
@@ -1,70 +1,110 @@
1
+ 'use client';
2
+
1
3
  /**
2
- * useRWA hook for accessing RWA SDK functionality in React components
4
+ * useRWA - Core hook for accessing the RWA SDK client
5
+ *
6
+ * Provides access to the @mantle-rwa/sdk RWAClient instance with
7
+ * automatic wagmi wallet integration.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { useRWA } from '@mantle-rwa/react';
12
+ *
13
+ * function MyComponent() {
14
+ * const { client, isInitialized, hasSigner } = useRWA();
15
+ *
16
+ * if (!isInitialized) return <div>Loading...</div>;
17
+ *
18
+ * // Use client.token, client.kyc, client.yield, client.compliance
19
+ * }
20
+ * ```
3
21
  */
4
22
 
5
- import { useState, useEffect } from 'react';
6
- import type { UseRWAReturn } from '../types';
23
+ import { useState, useEffect, useCallback, useMemo } from 'react';
24
+ import { useAccount, useWalletClient } from 'wagmi';
25
+ import { BrowserProvider } from 'ethers';
26
+ import { RWAClient } from '@mantle-rwa/sdk';
27
+ import type { UseRWAConfig, UseRWAReturn, ContractAddresses } from '../types';
28
+
29
+ // Default contract addresses (can be overridden via config)
30
+ const DEFAULT_CONTRACTS: ContractAddresses = {
31
+ token: undefined,
32
+ kycRegistry: undefined,
33
+ yieldDistributor: undefined,
34
+ assetVault: undefined,
35
+ factory: undefined,
36
+ };
7
37
 
8
38
  /**
9
- * Hook for accessing RWA SDK functionality
10
- * Provides connection status and SDK client access
39
+ * Hook for initializing and managing the RWA SDK client
40
+ *
41
+ * @param config - Optional configuration for network and contract addresses
42
+ * @returns UseRWAReturn object with client, state, and utilities
11
43
  */
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);
44
+ export function useRWA(config?: UseRWAConfig): UseRWAReturn {
45
+ const { isConnected } = useAccount();
46
+ const { data: walletClient } = useWalletClient();
47
+
48
+ const [client, setClient] = useState<RWAClient | null>(null);
49
+ const [isLoading, setIsLoading] = useState(true);
17
50
  const [error, setError] = useState<Error | null>(null);
18
51
 
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;
52
+ const network = config?.network ?? 'mantle-sepolia';
28
53
 
29
- if (accounts && accounts.length > 0) {
30
- setAddress(accounts[0]);
31
- setIsConnected(true);
32
- }
54
+ // Initialize or reinitialize the client
55
+ const initializeClient = useCallback(async () => {
56
+ setIsLoading(true);
57
+ setError(null);
33
58
 
34
- const chainIdHex = await window.ethereum.request?.({
35
- method: 'eth_chainId',
36
- }) as string | undefined;
59
+ try {
60
+ let rwaClient: RWAClient;
37
61
 
38
- if (chainIdHex) {
39
- setChainId(parseInt(chainIdHex, 16));
40
- }
41
- }
62
+ if (walletClient && isConnected) {
63
+ // Create client with wallet signer for write operations
64
+ const provider = new BrowserProvider(walletClient.transport);
65
+ const signer = await provider.getSigner();
42
66
 
43
- setIsInitialized(true);
44
- } catch (err) {
45
- setError(err instanceof Error ? err : new Error('Failed to initialize RWA SDK'));
67
+ rwaClient = new RWAClient({
68
+ network,
69
+ signer,
70
+ });
71
+ } else {
72
+ // Create read-only client (no signer)
73
+ rwaClient = new RWAClient({
74
+ network,
75
+ });
46
76
  }
47
- };
48
77
 
49
- initializeSDK();
50
- }, []);
78
+ setClient(rwaClient);
79
+ } catch (err) {
80
+ const errorMessage = err instanceof Error ? err.message : 'Failed to initialize RWA client';
81
+ setError(new Error(errorMessage));
82
+ setClient(null);
83
+ } finally {
84
+ setIsLoading(false);
85
+ }
86
+ }, [walletClient, isConnected, network]);
87
+
88
+ // Initialize client on mount and when wallet changes
89
+ useEffect(() => {
90
+ initializeClient();
91
+ }, [initializeClient]);
92
+
93
+ // Memoized contract addresses
94
+ const contracts = useMemo<ContractAddresses>(() => ({
95
+ ...DEFAULT_CONTRACTS,
96
+ ...config?.contracts,
97
+ }), [config?.contracts]);
51
98
 
52
99
  return {
53
- isInitialized,
54
- chainId,
55
- address,
56
- isConnected,
100
+ client,
101
+ isInitialized: client !== null && !isLoading,
102
+ isLoading,
57
103
  error,
104
+ hasSigner: client?.hasSigner ?? false,
105
+ contracts,
106
+ reinitialize: initializeClient,
58
107
  };
59
108
  }
60
109
 
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
- }
110
+ export default useRWA;
package/src/index.ts CHANGED
@@ -7,26 +7,14 @@
7
7
  export { KYCFlow } from './components/KYCFlow';
8
8
  export { InvestorDashboard } from './components/InvestorDashboard';
9
9
  export { TokenMintForm } from './components/TokenMintForm';
10
+ export { isValidAddress, isValidAmount } from './components/TokenMintForm';
10
11
  export { YieldCalculator } from './components/YieldCalculator';
12
+ export { ErrorDisplay, formatErrorMessage } from './components/ErrorDisplay';
13
+ export { LoadingSpinner } from './components/LoadingSpinner';
14
+ export { ConnectWalletPrompt } from './components/ConnectWalletPrompt';
11
15
 
12
16
  // Hooks
13
17
  export { useRWA } from './hooks/useRWA';
14
18
 
15
19
  // 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';
20
+ export type * from './types';