@b3dotfun/sdk 0.0.58-alpha.1 → 0.0.58-alpha.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 (23) hide show
  1. package/dist/cjs/shared/react/components/CurrencySelector.js +8 -3
  2. package/dist/cjs/shared/react/components/FormattedCurrency.d.ts +3 -3
  3. package/dist/cjs/shared/react/components/FormattedCurrency.js +31 -26
  4. package/dist/cjs/shared/react/hooks/useCurrencyConversion.d.ts +8 -5
  5. package/dist/cjs/shared/react/hooks/useCurrencyConversion.js +153 -94
  6. package/dist/cjs/shared/react/stores/currencyStore.d.ts +83 -8
  7. package/dist/cjs/shared/react/stores/currencyStore.js +147 -5
  8. package/dist/esm/shared/react/components/CurrencySelector.js +10 -5
  9. package/dist/esm/shared/react/components/FormattedCurrency.d.ts +3 -3
  10. package/dist/esm/shared/react/components/FormattedCurrency.js +31 -26
  11. package/dist/esm/shared/react/hooks/useCurrencyConversion.d.ts +8 -5
  12. package/dist/esm/shared/react/hooks/useCurrencyConversion.js +154 -95
  13. package/dist/esm/shared/react/stores/currencyStore.d.ts +83 -8
  14. package/dist/esm/shared/react/stores/currencyStore.js +143 -5
  15. package/dist/types/shared/react/components/FormattedCurrency.d.ts +3 -3
  16. package/dist/types/shared/react/hooks/useCurrencyConversion.d.ts +8 -5
  17. package/dist/types/shared/react/stores/currencyStore.d.ts +83 -8
  18. package/package.json +1 -1
  19. package/src/shared/react/components/CurrencySelector.tsx +36 -5
  20. package/src/shared/react/components/FormattedCurrency.tsx +36 -30
  21. package/src/shared/react/hooks/__tests__/useCurrencyConversion.test.ts +14 -14
  22. package/src/shared/react/hooks/useCurrencyConversion.ts +163 -96
  23. 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 { CURRENCY_SYMBOLS, useCurrencyStore } from "../stores/currencyStore";
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: exchangeRates } = useQuery({
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
- // Extract specific rates from the full rates object
62
- const exchangeRate = exchangeRates?.[selectedCurrency];
63
- const usdRate = exchangeRates?.["USD"];
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 proper conversion and formatting.
127
+ * Formats a numeric value as a currency string with automatic conversion.
67
128
  *
68
- * Behavior:
69
- * - When exchange rate is unavailable, displays value in base currency
70
- * - Applies currency-specific formatting rules:
71
- * - JPY/KRW: No decimal places
72
- * - ETH/SOL: 6 significant digits with subscript notation for small values
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 (in base currency)
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
- * formatCurrencyValue(100) // Returns "$100.00" if USD is selected
85
- * formatCurrencyValue(0.0001) // Returns "0.0₄1 ETH" if ETH is selected
86
- * formatCurrencyValue(1500) // Returns "¥1,500" if JPY is selected
87
- * formatCurrencyValue(100, { decimals: 4, currency: "ETH" }) // Returns "100.0000 ETH"
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; currency?: string }): string => {
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
- // Custom currency provided - bypass conversion and use simple formatting
95
- if (overrideCurrency) {
96
- const decimalsToUse = overrideDecimals !== undefined ? overrideDecimals : overrideCurrency === "B3" ? 0 : 2;
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
- showSubscripts: false,
163
+ significantDigits: decimalsToUse === undefined ? 6 : undefined,
164
+ showSubscripts: customMetadata?.showSubscripts ?? false,
101
165
  });
102
- return `${formatted} ${overrideCurrency}`;
103
- }
104
166
 
105
- // Custom decimals for base currency without conversion
106
- if (overrideDecimals !== undefined && selectedCurrency === baseCurrency) {
107
- const formatted = formatDisplayNumber(value, {
108
- fractionDigits: overrideDecimals,
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
- // If showing base currency, no conversion needed
115
- if (selectedCurrency === baseCurrency || !exchangeRate) {
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: baseCurrency === "B3" ? 6 : 8,
118
- showSubscripts: true,
180
+ significantDigits: 6,
181
+ showSubscripts: customMetadata?.showSubscripts ?? false,
119
182
  });
120
- return `${formatted} ${baseCurrency}`;
183
+ const symbol = getCurrencySymbol(sourceCurrency);
184
+ return `${formatted} ${symbol}`;
121
185
  }
122
186
 
123
- // Convert value using current exchange rate
124
- const convertedValue = value * exchangeRate;
125
- const symbol = CURRENCY_SYMBOLS[selectedCurrency];
187
+ // Convert value
188
+ const convertedValue = value * conversionRate;
126
189
 
127
- // Currencies that display symbol before the number (e.g., $100.00)
128
- const prefixCurrencies = ["USD", "EUR", "GBP", "CAD", "AUD"];
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
- if (selectedCurrency === "JPY" || selectedCurrency === "KRW") {
133
- // Japanese Yen and Korean Won don't use decimal places
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
- // Apply currency symbol with correct positioning
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
- * Behavior:
168
- * - When displaying base currency: Shows USD equivalent
169
- * - When displaying other currency: Shows base currency equivalent
170
- * - For custom currencies: Shows appropriate conversion or original value
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 customCurrency - Optional custom currency override
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
- * formatTooltipValue(100) // Returns "$150.00 USD" if displaying B3 with rate 1.5
179
- * formatTooltipValue(100, "ETH") // Returns "100.0000 ETH" if custom currency
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, customCurrency?: string): string => {
183
- const displayCurrency = customCurrency || selectedCurrency;
251
+ const formatTooltipValue = (value: number, sourceCurrency: string): string => {
184
252
  const absoluteValue = Math.abs(value);
185
253
 
186
- // Custom currency provided
187
- if (customCurrency) {
188
- if (customCurrency === baseCurrency) {
189
- // Show USD equivalent for base currency using USD rate
190
- const usdValue = usdRate ? absoluteValue * usdRate : absoluteValue;
191
- const formatted = formatDisplayNumber(usdValue, {
192
- significantDigits: 6,
193
- fractionDigits: usdValue < 1000 ? 2 : undefined,
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
- // Showing base currency - display USD equivalent
204
- if (displayCurrency === baseCurrency) {
205
- const usdValue = usdRate ? absoluteValue * usdRate : absoluteValue;
206
- const formatted = formatDisplayNumber(usdValue, {
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
- fractionDigits: usdValue < 1000 ? 2 : undefined,
209
- showSubscripts: true,
270
+ showSubscripts: getCurrencyMetadata(sourceCurrency)?.showSubscripts ?? false,
210
271
  });
211
- return `$${formatted} USD`;
272
+ const symbol = getCurrencySymbol(sourceCurrency);
273
+ return `${formatted} ${symbol}`;
212
274
  }
213
275
 
214
- // Showing other currency - display base currency equivalent
215
- const formatted = formatDisplayNumber(absoluteValue, {
216
- significantDigits: baseCurrency === "B3" ? 6 : 8,
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 `${formatted} ${baseCurrency}`;
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: CURRENCY_SYMBOLS[selectedCurrency],
297
+ selectedCurrencySymbol: getCurrencySymbol(selectedCurrency),
235
298
  /** Symbol for the base currency */
236
- baseCurrencySymbol: CURRENCY_SYMBOLS[baseCurrency],
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
- * Supported currencies for display and conversion.
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: SupportedCurrency;
53
- baseCurrency: SupportedCurrency;
54
- setSelectedCurrency: (currency: SupportedCurrency) => void;
55
- setBaseCurrency: (currency: SupportedCurrency) => void;
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
- * // Change display currency to USD
66
- * setSelectedCurrency('USD');
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: 2,
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
+ }