@b3dotfun/sdk 0.0.58 → 0.0.59-alpha.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/anyspend/react/components/common/CryptoPaymentMethod.js +4 -0
- package/dist/cjs/anyspend/utils/accountStore.d.ts +7 -0
- package/dist/cjs/anyspend/utils/accountStore.js +8 -0
- package/dist/cjs/anyspend/utils/index.d.ts +1 -0
- package/dist/cjs/anyspend/utils/index.js +1 -0
- package/dist/cjs/global-account/react/components/B3DynamicModal.js +17 -0
- package/dist/cjs/global-account/react/hooks/useWagmiConfig.d.ts +441 -1
- package/dist/cjs/global-account/react/hooks/useWagmiConfig.js +2 -0
- package/dist/cjs/shared/react/components/CurrencySelector.js +8 -3
- package/dist/cjs/shared/react/components/FormattedCurrency.d.ts +3 -3
- package/dist/cjs/shared/react/components/FormattedCurrency.js +31 -26
- package/dist/cjs/shared/react/hooks/useCurrencyConversion.d.ts +8 -5
- package/dist/cjs/shared/react/hooks/useCurrencyConversion.js +153 -94
- package/dist/cjs/shared/react/stores/currencyStore.d.ts +83 -8
- package/dist/cjs/shared/react/stores/currencyStore.js +147 -5
- package/dist/esm/anyspend/react/components/common/CryptoPaymentMethod.js +5 -1
- package/dist/esm/anyspend/utils/accountStore.d.ts +7 -0
- package/dist/esm/anyspend/utils/accountStore.js +5 -0
- package/dist/esm/anyspend/utils/index.d.ts +1 -0
- package/dist/esm/anyspend/utils/index.js +1 -0
- package/dist/esm/global-account/react/components/B3DynamicModal.js +17 -0
- package/dist/esm/global-account/react/hooks/useWagmiConfig.d.ts +441 -1
- package/dist/esm/global-account/react/hooks/useWagmiConfig.js +2 -0
- package/dist/esm/shared/react/components/CurrencySelector.js +10 -5
- package/dist/esm/shared/react/components/FormattedCurrency.d.ts +3 -3
- package/dist/esm/shared/react/components/FormattedCurrency.js +31 -26
- package/dist/esm/shared/react/hooks/useCurrencyConversion.d.ts +8 -5
- package/dist/esm/shared/react/hooks/useCurrencyConversion.js +154 -95
- package/dist/esm/shared/react/stores/currencyStore.d.ts +83 -8
- package/dist/esm/shared/react/stores/currencyStore.js +143 -5
- package/dist/types/anyspend/utils/accountStore.d.ts +7 -0
- package/dist/types/anyspend/utils/index.d.ts +1 -0
- package/dist/types/global-account/react/hooks/useWagmiConfig.d.ts +441 -1
- package/dist/types/shared/react/components/FormattedCurrency.d.ts +3 -3
- package/dist/types/shared/react/hooks/useCurrencyConversion.d.ts +8 -5
- package/dist/types/shared/react/stores/currencyStore.d.ts +83 -8
- package/package.json +4 -3
- package/src/anyspend/react/components/common/CryptoPaymentMethod.tsx +6 -2
- package/src/anyspend/utils/accountStore.ts +12 -0
- package/src/anyspend/utils/index.ts +1 -0
- package/src/global-account/react/components/B3DynamicModal.tsx +20 -0
- package/src/global-account/react/hooks/useWagmiConfig.tsx +2 -0
- package/src/shared/react/components/CurrencySelector.tsx +36 -5
- package/src/shared/react/components/FormattedCurrency.tsx +36 -30
- package/src/shared/react/hooks/__tests__/useCurrencyConversion.test.ts +14 -14
- package/src/shared/react/hooks/useCurrencyConversion.ts +163 -96
- package/src/shared/react/stores/currencyStore.ts +216 -10
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useQuery } from "@tanstack/react-query";
|
|
2
2
|
import { formatDisplayNumber } from "@b3dotfun/sdk/shared/utils/number";
|
|
3
|
-
import {
|
|
3
|
+
import { useCurrencyStore, getCurrencySymbol, getCurrencyMetadata } from "../stores/currencyStore";
|
|
4
4
|
|
|
5
5
|
const COINBASE_API_URL = "https://api.coinbase.com/v2/exchange-rates";
|
|
6
6
|
const REFETCH_INTERVAL_MS = 30000;
|
|
@@ -47,9 +47,11 @@ async function fetchAllExchangeRates(baseCurrency: string): Promise<Record<strin
|
|
|
47
47
|
export function useCurrencyConversion() {
|
|
48
48
|
const selectedCurrency = useCurrencyStore(state => state.selectedCurrency);
|
|
49
49
|
const baseCurrency = useCurrencyStore(state => state.baseCurrency);
|
|
50
|
+
const getCustomExchangeRate = useCurrencyStore(state => state.getExchangeRate);
|
|
51
|
+
const customCurrencies = useCurrencyStore(state => state.customCurrencies);
|
|
50
52
|
|
|
51
|
-
// Fetch all exchange rates for the base currency
|
|
52
|
-
const { data:
|
|
53
|
+
// Fetch all exchange rates for the base currency from Coinbase API
|
|
54
|
+
const { data: apiExchangeRates } = useQuery({
|
|
53
55
|
queryKey: ["exchangeRates", baseCurrency],
|
|
54
56
|
queryFn: () => fetchAllExchangeRates(baseCurrency),
|
|
55
57
|
refetchInterval: REFETCH_INTERVAL_MS,
|
|
@@ -58,93 +60,163 @@ export function useCurrencyConversion() {
|
|
|
58
60
|
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, REFETCH_INTERVAL_MS),
|
|
59
61
|
});
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Get exchange rate between two currencies, checking custom rates first, then API rates.
|
|
65
|
+
* Supports chaining through base currency for custom currencies.
|
|
66
|
+
*
|
|
67
|
+
* Examples:
|
|
68
|
+
* - WIN → USD: Checks WIN→USD custom rate, then chains WIN→B3→USD
|
|
69
|
+
* - BTC → EUR: Checks BTC→EUR custom rate, then chains BTC→B3→EUR
|
|
70
|
+
*/
|
|
71
|
+
const getExchangeRate = (from: string, to: string): number | undefined => {
|
|
72
|
+
// If same currency, rate is 1
|
|
73
|
+
if (from === to) return 1;
|
|
74
|
+
|
|
75
|
+
// 1. Check direct custom exchange rate first
|
|
76
|
+
const directCustomRate = getCustomExchangeRate(from, to);
|
|
77
|
+
if (directCustomRate !== undefined) {
|
|
78
|
+
return directCustomRate;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 2. Check direct API rate (from base currency)
|
|
82
|
+
if (from === baseCurrency && apiExchangeRates) {
|
|
83
|
+
return apiExchangeRates[to];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 3. Try to chain through base currency using custom rates
|
|
87
|
+
// e.g., WIN → B3 → USD (where WIN→B3 is custom, B3→USD is API)
|
|
88
|
+
const customFromToBase = getCustomExchangeRate(from, baseCurrency);
|
|
89
|
+
if (customFromToBase !== undefined) {
|
|
90
|
+
// We have a custom rate from 'from' to base
|
|
91
|
+
// Now get rate from base to 'to'
|
|
92
|
+
const baseToTo = apiExchangeRates?.[to] ?? getCustomExchangeRate(baseCurrency, to);
|
|
93
|
+
if (baseToTo !== undefined) {
|
|
94
|
+
return customFromToBase * baseToTo;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 4. Try reverse: chain from base currency through custom rate
|
|
99
|
+
// e.g., USD → B3 → WIN (where B3→WIN is custom)
|
|
100
|
+
const customBaseToTo = getCustomExchangeRate(baseCurrency, to);
|
|
101
|
+
if (customBaseToTo !== undefined && apiExchangeRates) {
|
|
102
|
+
// We have a custom rate from base to 'to'
|
|
103
|
+
// Now get rate from 'from' to base
|
|
104
|
+
const fromToBase = apiExchangeRates[from];
|
|
105
|
+
if (fromToBase !== undefined && fromToBase !== 0) {
|
|
106
|
+
return fromToBase * customBaseToTo;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 5. Fall back to pure API conversion through base
|
|
111
|
+
// e.g., EUR to GBP = (EUR to B3) * (B3 to GBP)
|
|
112
|
+
if (apiExchangeRates) {
|
|
113
|
+
const fromToBase = apiExchangeRates[from];
|
|
114
|
+
const baseToTo = apiExchangeRates[to];
|
|
115
|
+
if (fromToBase && baseToTo && fromToBase !== 0) {
|
|
116
|
+
return baseToTo / fromToBase;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return undefined;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Extract specific rates
|
|
124
|
+
const exchangeRate = getExchangeRate(baseCurrency, selectedCurrency);
|
|
64
125
|
|
|
65
126
|
/**
|
|
66
|
-
* Formats a numeric value as a currency string with
|
|
127
|
+
* Formats a numeric value as a currency string with automatic conversion.
|
|
67
128
|
*
|
|
68
|
-
*
|
|
69
|
-
* -
|
|
70
|
-
* -
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* - Fiat (USD/EUR/GBP/CAD/AUD): 2 decimal places minimum for values < 1000
|
|
74
|
-
* - Handles symbol positioning (prefix for fiat, suffix for crypto)
|
|
129
|
+
* New behavior:
|
|
130
|
+
* - Takes the SOURCE currency (what the value is in) as a required parameter
|
|
131
|
+
* - Automatically converts from source → display currency (selected in picker)
|
|
132
|
+
* - Supports multi-hop conversions (e.g., WIN → B3 → USD)
|
|
133
|
+
* - Applies currency-specific formatting rules to the TARGET currency
|
|
75
134
|
*
|
|
76
|
-
* @param value - The numeric value to format
|
|
135
|
+
* @param value - The numeric value to format
|
|
136
|
+
* @param sourceCurrency - The currency the value is currently in (e.g., "WIN", "B3", "USD")
|
|
77
137
|
* @param options - Optional formatting overrides
|
|
78
|
-
* @param options.decimals - Override number of decimal places
|
|
79
|
-
* @param options.currency - Override currency (bypasses conversion)
|
|
138
|
+
* @param options.decimals - Override number of decimal places for display
|
|
80
139
|
* @returns Formatted currency string with appropriate symbol and decimal places
|
|
81
140
|
*
|
|
82
141
|
* @example
|
|
83
142
|
* ```tsx
|
|
84
|
-
*
|
|
85
|
-
* formatCurrencyValue(
|
|
86
|
-
*
|
|
87
|
-
*
|
|
143
|
+
* // Value is 3031 WIN, user has USD selected
|
|
144
|
+
* formatCurrencyValue(3031, "WIN") // Returns "$30.31" (converts WIN→B3→USD)
|
|
145
|
+
*
|
|
146
|
+
* // Value is 100 B3, user has B3 selected (no conversion)
|
|
147
|
+
* formatCurrencyValue(100, "B3") // Returns "100 B3"
|
|
148
|
+
*
|
|
149
|
+
* // Value is 50 USD, user has EUR selected
|
|
150
|
+
* formatCurrencyValue(50, "USD") // Returns "€45.50" (converts USD→EUR)
|
|
88
151
|
* ```
|
|
89
152
|
*/
|
|
90
|
-
const formatCurrencyValue = (value: number, options?: { decimals?: number
|
|
91
|
-
const overrideCurrency = options?.currency;
|
|
153
|
+
const formatCurrencyValue = (value: number, sourceCurrency: string, options?: { decimals?: number }): string => {
|
|
92
154
|
const overrideDecimals = options?.decimals;
|
|
93
155
|
|
|
94
|
-
//
|
|
95
|
-
if (
|
|
96
|
-
const
|
|
156
|
+
// If source and display currency are the same, no conversion needed
|
|
157
|
+
if (sourceCurrency === selectedCurrency) {
|
|
158
|
+
const customMetadata = getCurrencyMetadata(sourceCurrency);
|
|
159
|
+
const decimalsToUse = overrideDecimals !== undefined ? overrideDecimals : customMetadata?.decimals;
|
|
97
160
|
|
|
98
161
|
const formatted = formatDisplayNumber(value, {
|
|
99
162
|
fractionDigits: decimalsToUse,
|
|
100
|
-
|
|
163
|
+
significantDigits: decimalsToUse === undefined ? 6 : undefined,
|
|
164
|
+
showSubscripts: customMetadata?.showSubscripts ?? false,
|
|
101
165
|
});
|
|
102
|
-
return `${formatted} ${overrideCurrency}`;
|
|
103
|
-
}
|
|
104
166
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
showSubscripts: false,
|
|
110
|
-
});
|
|
111
|
-
return `${formatted} ${baseCurrency}`;
|
|
167
|
+
const symbol = getCurrencySymbol(sourceCurrency);
|
|
168
|
+
const usePrefix = customMetadata?.prefixSymbol ?? ["USD", "EUR", "GBP", "CAD", "AUD"].includes(sourceCurrency);
|
|
169
|
+
|
|
170
|
+
return usePrefix ? `${symbol}${formatted}` : `${formatted} ${symbol}`;
|
|
112
171
|
}
|
|
113
172
|
|
|
114
|
-
//
|
|
115
|
-
|
|
173
|
+
// Get exchange rate from source to display currency
|
|
174
|
+
const conversionRate = getExchangeRate(sourceCurrency, selectedCurrency);
|
|
175
|
+
|
|
176
|
+
// If no conversion rate available, display in source currency
|
|
177
|
+
if (conversionRate === undefined) {
|
|
178
|
+
const customMetadata = getCurrencyMetadata(sourceCurrency);
|
|
116
179
|
const formatted = formatDisplayNumber(value, {
|
|
117
|
-
significantDigits:
|
|
118
|
-
showSubscripts:
|
|
180
|
+
significantDigits: 6,
|
|
181
|
+
showSubscripts: customMetadata?.showSubscripts ?? false,
|
|
119
182
|
});
|
|
120
|
-
|
|
183
|
+
const symbol = getCurrencySymbol(sourceCurrency);
|
|
184
|
+
return `${formatted} ${symbol}`;
|
|
121
185
|
}
|
|
122
186
|
|
|
123
|
-
// Convert value
|
|
124
|
-
const convertedValue = value *
|
|
125
|
-
const symbol = CURRENCY_SYMBOLS[selectedCurrency];
|
|
187
|
+
// Convert value
|
|
188
|
+
const convertedValue = value * conversionRate;
|
|
126
189
|
|
|
127
|
-
//
|
|
128
|
-
const
|
|
190
|
+
// Get symbol and metadata for display currency
|
|
191
|
+
const symbol = getCurrencySymbol(selectedCurrency);
|
|
192
|
+
const customMetadata = getCurrencyMetadata(selectedCurrency);
|
|
193
|
+
const usePrefix = customMetadata?.prefixSymbol ?? ["USD", "EUR", "GBP", "CAD", "AUD"].includes(selectedCurrency);
|
|
129
194
|
|
|
130
195
|
let formatted: string;
|
|
131
196
|
|
|
132
|
-
|
|
133
|
-
|
|
197
|
+
// Apply formatting based on display currency
|
|
198
|
+
if (overrideDecimals !== undefined) {
|
|
199
|
+
formatted = formatDisplayNumber(convertedValue, {
|
|
200
|
+
fractionDigits: overrideDecimals,
|
|
201
|
+
showSubscripts: false,
|
|
202
|
+
});
|
|
203
|
+
} else if (customMetadata) {
|
|
204
|
+
formatted = formatDisplayNumber(convertedValue, {
|
|
205
|
+
fractionDigits: customMetadata.decimals,
|
|
206
|
+
significantDigits: customMetadata.decimals === undefined ? 6 : undefined,
|
|
207
|
+
showSubscripts: customMetadata.showSubscripts ?? false,
|
|
208
|
+
});
|
|
209
|
+
} else if (selectedCurrency === "JPY" || selectedCurrency === "KRW") {
|
|
134
210
|
formatted = formatDisplayNumber(convertedValue, {
|
|
135
211
|
fractionDigits: 0,
|
|
136
212
|
showSubscripts: false,
|
|
137
213
|
});
|
|
138
214
|
} else if (selectedCurrency === "ETH" || selectedCurrency === "SOL") {
|
|
139
|
-
// Crypto currencies use more precision and subscript notation
|
|
140
|
-
// for very small amounts (e.g., 0.0₃45 ETH)
|
|
141
215
|
formatted = formatDisplayNumber(convertedValue, {
|
|
142
216
|
significantDigits: 6,
|
|
143
217
|
showSubscripts: true,
|
|
144
218
|
});
|
|
145
219
|
} else {
|
|
146
|
-
// Standard fiat currencies (USD, EUR, GBP, CAD, AUD)
|
|
147
|
-
// Use 2 decimal places minimum for amounts under 1000
|
|
148
220
|
formatted = formatDisplayNumber(convertedValue, {
|
|
149
221
|
significantDigits: 6,
|
|
150
222
|
fractionDigits: convertedValue < 1000 ? 2 : undefined,
|
|
@@ -152,71 +224,62 @@ export function useCurrencyConversion() {
|
|
|
152
224
|
});
|
|
153
225
|
}
|
|
154
226
|
|
|
155
|
-
|
|
156
|
-
if (prefixCurrencies.includes(selectedCurrency)) {
|
|
157
|
-
return `${symbol}${formatted}`;
|
|
158
|
-
} else {
|
|
159
|
-
// Suffix currencies: JPY, KRW, ETH, SOL, B3
|
|
160
|
-
return `${formatted} ${symbol}`;
|
|
161
|
-
}
|
|
227
|
+
return usePrefix ? `${symbol}${formatted}` : `${formatted} ${symbol}`;
|
|
162
228
|
};
|
|
163
229
|
|
|
164
230
|
/**
|
|
165
231
|
* Formats a tooltip value showing the alternate currency representation.
|
|
166
232
|
*
|
|
167
|
-
*
|
|
168
|
-
* -
|
|
169
|
-
* - When displaying
|
|
170
|
-
* -
|
|
233
|
+
* New behavior:
|
|
234
|
+
* - Takes the SOURCE currency (what the value is in)
|
|
235
|
+
* - When displaying in non-USD: Shows USD equivalent in tooltip
|
|
236
|
+
* - When displaying in USD: Shows source currency in tooltip
|
|
171
237
|
*
|
|
172
238
|
* @param value - The numeric value to format
|
|
173
|
-
* @param
|
|
239
|
+
* @param sourceCurrency - The currency the value is currently in
|
|
174
240
|
* @returns Formatted tooltip string
|
|
175
241
|
*
|
|
176
242
|
* @example
|
|
177
243
|
* ```tsx
|
|
178
|
-
*
|
|
179
|
-
* formatTooltipValue(
|
|
244
|
+
* // Value is 3031 WIN, displaying as EUR
|
|
245
|
+
* formatTooltipValue(3031, "WIN") // Returns "$30.31 USD"
|
|
246
|
+
*
|
|
247
|
+
* // Value is 100 B3, displaying as USD
|
|
248
|
+
* formatTooltipValue(100, "B3") // Returns "100 B3"
|
|
180
249
|
* ```
|
|
181
250
|
*/
|
|
182
|
-
const formatTooltipValue = (value: number,
|
|
183
|
-
const displayCurrency = customCurrency || selectedCurrency;
|
|
251
|
+
const formatTooltipValue = (value: number, sourceCurrency: string): string => {
|
|
184
252
|
const absoluteValue = Math.abs(value);
|
|
185
253
|
|
|
186
|
-
//
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
showSubscripts: true,
|
|
195
|
-
});
|
|
196
|
-
return `$${formatted} USD`;
|
|
197
|
-
} else {
|
|
198
|
-
// Show as-is for other custom currencies
|
|
199
|
-
return `${formatDisplayNumber(absoluteValue, { significantDigits: 6 })} ${customCurrency}`;
|
|
200
|
-
}
|
|
254
|
+
// If displaying in USD, show source currency in tooltip
|
|
255
|
+
if (selectedCurrency === "USD") {
|
|
256
|
+
const formatted = formatDisplayNumber(absoluteValue, {
|
|
257
|
+
significantDigits: 6,
|
|
258
|
+
showSubscripts: getCurrencyMetadata(sourceCurrency)?.showSubscripts ?? false,
|
|
259
|
+
});
|
|
260
|
+
const symbol = getCurrencySymbol(sourceCurrency);
|
|
261
|
+
return `${formatted} ${symbol}`;
|
|
201
262
|
}
|
|
202
263
|
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
264
|
+
// Otherwise, show USD equivalent in tooltip
|
|
265
|
+
const usdRate = getExchangeRate(sourceCurrency, "USD");
|
|
266
|
+
if (usdRate === undefined) {
|
|
267
|
+
// Fallback to source currency if no USD rate
|
|
268
|
+
const formatted = formatDisplayNumber(absoluteValue, {
|
|
207
269
|
significantDigits: 6,
|
|
208
|
-
|
|
209
|
-
showSubscripts: true,
|
|
270
|
+
showSubscripts: getCurrencyMetadata(sourceCurrency)?.showSubscripts ?? false,
|
|
210
271
|
});
|
|
211
|
-
|
|
272
|
+
const symbol = getCurrencySymbol(sourceCurrency);
|
|
273
|
+
return `${formatted} ${symbol}`;
|
|
212
274
|
}
|
|
213
275
|
|
|
214
|
-
|
|
215
|
-
const formatted = formatDisplayNumber(
|
|
216
|
-
significantDigits:
|
|
276
|
+
const usdValue = absoluteValue * usdRate;
|
|
277
|
+
const formatted = formatDisplayNumber(usdValue, {
|
|
278
|
+
significantDigits: 6,
|
|
279
|
+
fractionDigits: usdValue < 1000 ? 2 : undefined,
|
|
217
280
|
showSubscripts: true,
|
|
218
281
|
});
|
|
219
|
-
return
|
|
282
|
+
return `$${formatted} USD`;
|
|
220
283
|
};
|
|
221
284
|
|
|
222
285
|
return {
|
|
@@ -231,8 +294,12 @@ export function useCurrencyConversion() {
|
|
|
231
294
|
/** Format a tooltip value showing alternate currency representation */
|
|
232
295
|
formatTooltipValue,
|
|
233
296
|
/** Symbol for the currently selected currency (e.g., "$", "€", "ETH") */
|
|
234
|
-
selectedCurrencySymbol:
|
|
297
|
+
selectedCurrencySymbol: getCurrencySymbol(selectedCurrency),
|
|
235
298
|
/** Symbol for the base currency */
|
|
236
|
-
baseCurrencySymbol:
|
|
299
|
+
baseCurrencySymbol: getCurrencySymbol(baseCurrency),
|
|
300
|
+
/** Get exchange rate between any two currencies */
|
|
301
|
+
getExchangeRate,
|
|
302
|
+
/** All registered custom currencies */
|
|
303
|
+
customCurrencies,
|
|
237
304
|
};
|
|
238
305
|
}
|
|
@@ -2,11 +2,41 @@ import { create } from "zustand";
|
|
|
2
2
|
import { persist } from "zustand/middleware";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Built-in supported currencies for display and conversion.
|
|
6
6
|
* Includes fiat currencies (USD, EUR, GBP, JPY, CAD, AUD, KRW) and crypto (ETH, SOL, B3).
|
|
7
7
|
*/
|
|
8
8
|
export type SupportedCurrency = "ETH" | "USD" | "EUR" | "GBP" | "JPY" | "CAD" | "AUD" | "B3" | "SOL" | "KRW";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Metadata for a custom currency including display formatting rules.
|
|
12
|
+
*/
|
|
13
|
+
export interface CurrencyMetadata {
|
|
14
|
+
/** The currency code/symbol (e.g., "BTC", "DOGE") */
|
|
15
|
+
code: string;
|
|
16
|
+
/** Display symbol for the currency (e.g., "₿", "Ð") */
|
|
17
|
+
symbol: string;
|
|
18
|
+
/** Human-readable name (e.g., "Bitcoin", "Dogecoin") */
|
|
19
|
+
name: string;
|
|
20
|
+
/** Whether to show symbol before the value (true for $100, false for 100 ETH) */
|
|
21
|
+
prefixSymbol?: boolean;
|
|
22
|
+
/** Number of decimal places to show (undefined uses smart formatting) */
|
|
23
|
+
decimals?: number;
|
|
24
|
+
/** Whether to use subscript notation for small values */
|
|
25
|
+
showSubscripts?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Exchange rate between two currencies.
|
|
30
|
+
*/
|
|
31
|
+
export interface ExchangeRate {
|
|
32
|
+
/** Currency code being converted from */
|
|
33
|
+
from: string;
|
|
34
|
+
/** Currency code being converted to */
|
|
35
|
+
to: string;
|
|
36
|
+
/** Exchange rate multiplier (amount_in_to = amount_in_from * rate) */
|
|
37
|
+
rate: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
10
40
|
/**
|
|
11
41
|
* Currency symbols used for display formatting.
|
|
12
42
|
* Prefix currencies (USD, EUR, GBP, CAD, AUD) show symbol before the amount.
|
|
@@ -45,38 +75,214 @@ export const CURRENCY_NAMES: Record<SupportedCurrency, string> = {
|
|
|
45
75
|
* Currency store state interface.
|
|
46
76
|
* @property selectedCurrency - The currency currently selected for display
|
|
47
77
|
* @property baseCurrency - The base currency for conversion (typically B3)
|
|
78
|
+
* @property customCurrencies - Map of custom currency codes to their metadata
|
|
79
|
+
* @property customExchangeRates - Map of "FROM-TO" pairs to exchange rates
|
|
48
80
|
* @property setSelectedCurrency - Update the selected display currency
|
|
49
81
|
* @property setBaseCurrency - Update the base currency for conversions
|
|
82
|
+
* @property addCurrency - Register a new custom currency with metadata
|
|
83
|
+
* @property removeCurrency - Remove a custom currency
|
|
84
|
+
* @property setExchangeRate - Set a custom exchange rate between two currencies
|
|
85
|
+
* @property getExchangeRate - Get exchange rate between two currencies
|
|
86
|
+
* @property getAllCurrencies - Get all available currencies (built-in + custom)
|
|
50
87
|
*/
|
|
51
88
|
interface CurrencyState {
|
|
52
|
-
selectedCurrency:
|
|
53
|
-
baseCurrency:
|
|
54
|
-
|
|
55
|
-
|
|
89
|
+
selectedCurrency: string;
|
|
90
|
+
baseCurrency: string;
|
|
91
|
+
customCurrencies: Record<string, CurrencyMetadata>;
|
|
92
|
+
customExchangeRates: Record<string, number>;
|
|
93
|
+
setSelectedCurrency: (currency: string) => void;
|
|
94
|
+
setBaseCurrency: (currency: string) => void;
|
|
95
|
+
addCurrency: (metadata: CurrencyMetadata) => void;
|
|
96
|
+
removeCurrency: (code: string) => void;
|
|
97
|
+
setExchangeRate: (from: string, to: string, rate: number) => void;
|
|
98
|
+
getExchangeRate: (from: string, to: string) => number | undefined;
|
|
99
|
+
getAllCurrencies: () => string[];
|
|
56
100
|
}
|
|
57
101
|
|
|
58
102
|
/**
|
|
59
103
|
* Zustand store for managing currency selection and conversion.
|
|
60
104
|
* Persists user's selected currency preference in localStorage.
|
|
105
|
+
* Supports dynamic currency registration and custom exchange rates.
|
|
61
106
|
*
|
|
62
107
|
* @example
|
|
63
108
|
* ```tsx
|
|
64
|
-
* const { selectedCurrency, setSelectedCurrency } = useCurrencyStore();
|
|
65
|
-
*
|
|
66
|
-
*
|
|
109
|
+
* const { selectedCurrency, setSelectedCurrency, addCurrency, setExchangeRate } = useCurrencyStore();
|
|
110
|
+
*
|
|
111
|
+
* // Add a new currency
|
|
112
|
+
* addCurrency({
|
|
113
|
+
* code: "BTC",
|
|
114
|
+
* symbol: "₿",
|
|
115
|
+
* name: "Bitcoin",
|
|
116
|
+
* showSubscripts: true,
|
|
117
|
+
* });
|
|
118
|
+
*
|
|
119
|
+
* // Set exchange rate: 1 BTC = 50000 USD
|
|
120
|
+
* setExchangeRate("BTC", "USD", 50000);
|
|
121
|
+
*
|
|
122
|
+
* // Change display currency
|
|
123
|
+
* setSelectedCurrency('BTC');
|
|
67
124
|
* ```
|
|
68
125
|
*/
|
|
69
126
|
export const useCurrencyStore = create<CurrencyState>()(
|
|
70
127
|
persist(
|
|
71
|
-
set => ({
|
|
128
|
+
(set, get) => ({
|
|
72
129
|
selectedCurrency: "B3",
|
|
73
130
|
baseCurrency: "B3",
|
|
131
|
+
customCurrencies: {},
|
|
132
|
+
customExchangeRates: {},
|
|
133
|
+
|
|
74
134
|
setSelectedCurrency: currency => set({ selectedCurrency: currency }),
|
|
135
|
+
|
|
75
136
|
setBaseCurrency: currency => set({ baseCurrency: currency }),
|
|
137
|
+
|
|
138
|
+
addCurrency: metadata => {
|
|
139
|
+
set(state => ({
|
|
140
|
+
customCurrencies: {
|
|
141
|
+
...state.customCurrencies,
|
|
142
|
+
[metadata.code]: metadata,
|
|
143
|
+
},
|
|
144
|
+
}));
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
removeCurrency: code => {
|
|
148
|
+
set(state => {
|
|
149
|
+
// Remove the currency
|
|
150
|
+
const { [code]: _removed, ...remaining } = state.customCurrencies;
|
|
151
|
+
|
|
152
|
+
// Remove all exchange rates involving this currency
|
|
153
|
+
const filteredRates: Record<string, number> = {};
|
|
154
|
+
for (const [key, rate] of Object.entries(state.customExchangeRates)) {
|
|
155
|
+
// Key format is "FROM-TO", skip if either matches the removed code
|
|
156
|
+
const [from, to] = key.split("-");
|
|
157
|
+
if (from !== code && to !== code) {
|
|
158
|
+
filteredRates[key] = rate;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
customCurrencies: remaining,
|
|
164
|
+
customExchangeRates: filteredRates,
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
setExchangeRate: (from, to, rate) => {
|
|
170
|
+
set(state => {
|
|
171
|
+
const key = `${from}-${to}`;
|
|
172
|
+
const inverseKey = `${to}-${from}`;
|
|
173
|
+
|
|
174
|
+
// Only set inverse rate if rate is not 0 (to avoid Infinity)
|
|
175
|
+
const newRates: Record<string, number> = {
|
|
176
|
+
...state.customExchangeRates,
|
|
177
|
+
[key]: rate,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
if (rate !== 0) {
|
|
181
|
+
newRates[inverseKey] = 1 / rate;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
customExchangeRates: newRates,
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
getExchangeRate: (from, to) => {
|
|
191
|
+
const key = `${from}-${to}`;
|
|
192
|
+
return get().customExchangeRates[key];
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
getAllCurrencies: () => {
|
|
196
|
+
const builtIn = Object.keys(CURRENCY_SYMBOLS);
|
|
197
|
+
const custom = Object.keys(get().customCurrencies);
|
|
198
|
+
return [...builtIn, ...custom];
|
|
199
|
+
},
|
|
76
200
|
}),
|
|
77
201
|
{
|
|
78
202
|
name: "currency-storage",
|
|
79
|
-
version:
|
|
203
|
+
version: 3,
|
|
80
204
|
},
|
|
81
205
|
),
|
|
82
206
|
);
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get the symbol for any currency (built-in or custom).
|
|
210
|
+
*/
|
|
211
|
+
export function getCurrencySymbol(currency: string): string {
|
|
212
|
+
// Check built-in currencies first
|
|
213
|
+
if (currency in CURRENCY_SYMBOLS) {
|
|
214
|
+
return CURRENCY_SYMBOLS[currency as SupportedCurrency];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check custom currencies
|
|
218
|
+
const customCurrencies = useCurrencyStore.getState().customCurrencies;
|
|
219
|
+
const customCurrency = customCurrencies[currency];
|
|
220
|
+
if (customCurrency) {
|
|
221
|
+
return customCurrency.symbol;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Fallback to currency code
|
|
225
|
+
return currency;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get the name for any currency (built-in or custom).
|
|
230
|
+
*/
|
|
231
|
+
export function getCurrencyName(currency: string): string {
|
|
232
|
+
// Check built-in currencies first
|
|
233
|
+
if (currency in CURRENCY_NAMES) {
|
|
234
|
+
return CURRENCY_NAMES[currency as SupportedCurrency];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Check custom currencies
|
|
238
|
+
const customCurrencies = useCurrencyStore.getState().customCurrencies;
|
|
239
|
+
const customCurrency = customCurrencies[currency];
|
|
240
|
+
if (customCurrency) {
|
|
241
|
+
return customCurrency.name;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Fallback to currency code
|
|
245
|
+
return currency;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get metadata for a custom currency.
|
|
250
|
+
*/
|
|
251
|
+
export function getCurrencyMetadata(currency: string): CurrencyMetadata | undefined {
|
|
252
|
+
const customCurrencies = useCurrencyStore.getState().customCurrencies;
|
|
253
|
+
return customCurrencies[currency];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get the number of decimal places for a currency (for converting from smallest unit).
|
|
258
|
+
* Used when parsing amounts from wei/smallest unit format.
|
|
259
|
+
*
|
|
260
|
+
* @param currency - Currency code
|
|
261
|
+
* @returns Number of decimal places (e.g., 18 for ETH/wei, 2 for USD cents, 0 for JPY)
|
|
262
|
+
*/
|
|
263
|
+
export function getCurrencyDecimalPlaces(currency: string): number {
|
|
264
|
+
// Check custom currencies first
|
|
265
|
+
const customCurrencies = useCurrencyStore.getState().customCurrencies;
|
|
266
|
+
const customMetadata = customCurrencies[currency];
|
|
267
|
+
if (customMetadata?.decimals !== undefined) {
|
|
268
|
+
return customMetadata.decimals;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Built-in currencies with 18 decimals (wei-like)
|
|
272
|
+
if (currency === "WIN" || currency === "ETH" || currency === "SOL" || currency === "B3") {
|
|
273
|
+
return 18;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Fiat currencies with cent-like decimals
|
|
277
|
+
if (currency === "USD" || currency === "EUR" || currency === "GBP" || currency === "CAD" || currency === "AUD") {
|
|
278
|
+
return 2;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Currencies without fractional units
|
|
282
|
+
if (currency === "JPY" || currency === "KRW") {
|
|
283
|
+
return 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Default to 18 decimals (wei-like)
|
|
287
|
+
return 18;
|
|
288
|
+
}
|