@explorins/pers-sdk-react-native 1.5.32 → 1.5.34

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/README.md CHANGED
@@ -176,17 +176,29 @@ function RewardScreen() {
176
176
  ```typescript
177
177
  // Authentication management
178
178
  const {
179
+ isInitialized,
179
180
  isAuthenticated,
180
181
  user,
181
- login,
182
- logout
182
+ login,
183
+ loginWithRawData,
184
+ logout,
185
+ refreshUserData,
186
+ getCurrentUser,
187
+ checkIsAuthenticated,
188
+ refreshTokens,
189
+ clearAuth,
190
+ hasValidAuth
183
191
  } = useAuth();
184
192
 
185
193
  // User profile operations
186
194
  const {
187
- getUserProfile,
188
- updateUserProfile,
189
- getUserPublicData
195
+ getCurrentUser,
196
+ updateCurrentUser,
197
+ getUserById,
198
+ getAllUsersPublic,
199
+ getAllUsers, // Admin
200
+ updateUser, // Admin
201
+ toggleUserStatus // Admin
190
202
  } = useUsers();
191
203
  ```
192
204
 
@@ -198,21 +210,30 @@ const {
198
210
  getTokens,
199
211
  getActiveCreditToken,
200
212
  getRewardTokens,
213
+ getTokenTypes,
214
+ getStatusTokens,
201
215
  getTokenByContract
202
216
  } = useTokens();
203
217
 
204
218
  // Transaction history
205
219
  const {
206
- getUserTransactionHistory,
220
+ createTransaction,
207
221
  getTransactionById,
208
- createTransaction
222
+ getUserTransactionHistory,
223
+ getTenantTransactions, // Admin
224
+ getPaginatedTransactions, // Admin
225
+ exportTransactionsCSV, // Admin
226
+ signingStatus, // UI feedback during blockchain signing
227
+ signingStatusMessage
209
228
  } = useTransactions();
210
229
 
211
230
  // Blockchain transaction signing
212
231
  const {
213
232
  signAndSubmitTransactionWithJWT,
214
233
  isSignerAvailable,
215
- isSignerInitialized
234
+ isSignerInitialized,
235
+ currentStatus,
236
+ statusMessage
216
237
  } = useTransactionSigner();
217
238
  ```
218
239
 
@@ -221,23 +242,42 @@ const {
221
242
  ```typescript
222
243
  // Business operations
223
244
  const {
224
- getBusinessProfile,
225
- updateBusinessProfile,
226
- getBusinessTypes
245
+ getActiveBusinesses,
246
+ getBusinessTypes,
247
+ getBusinesses,
248
+ getBusinessById,
249
+ getBusinessByAccount,
250
+ getBusinessesByType,
251
+ createBusiness, // Admin
252
+ updateBusiness, // Admin
253
+ toggleBusinessStatus // Admin
227
254
  } = useBusiness();
228
255
 
229
256
  // Campaign management
230
257
  const {
231
258
  getActiveCampaigns,
232
- participateInCampaign,
233
- claimCampaignReward
259
+ getCampaignById,
260
+ claimCampaign,
261
+ getUserClaims,
262
+ getCampaignTriggers,
263
+ getAllCampaigns, // Admin
264
+ getCampaignClaims, // Admin
265
+ getCampaignClaimsByUserId, // Admin
266
+ getCampaignClaimsByBusinessId // Admin
234
267
  } = useCampaigns();
235
268
 
236
269
  // Redemption system
237
270
  const {
238
- redeem,
271
+ getActiveRedemptions,
239
272
  getUserRedemptions,
240
- getActiveRedemptions
273
+ redeem,
274
+ getRedemptionTypes,
275
+ signingStatus, // UI feedback during blockchain signing
276
+ signingStatusMessage,
277
+ createRedemption, // Admin
278
+ getAllRedemptions, // Admin
279
+ updateRedemption, // Admin
280
+ toggleRedemptionStatus // Admin
241
281
  } = useRedemptions();
242
282
  ```
243
283
 
@@ -246,31 +286,54 @@ const {
246
286
  ```typescript
247
287
  // Purchase processing
248
288
  const {
249
- createPurchase,
250
- getUserPurchases,
251
- processPayment
289
+ createPaymentIntent,
290
+ getActivePurchaseTokens,
291
+ getAllUserPurchases
252
292
  } = usePurchases();
253
293
 
254
294
  // Multi-tenant support
255
295
  const {
256
- getTenantConfig,
257
- updateTenantConfig,
258
- getTenantAdmins
296
+ getTenantInfo,
297
+ getClientConfig,
298
+ getLoginToken,
299
+ getAdmins
259
300
  } = useTenants();
260
301
 
261
302
  // Analytics & reporting
262
303
  const {
263
- getTransactionAnalytics,
264
- getUserAnalytics,
265
- getBusinessAnalytics
304
+ getTransactionAnalytics
266
305
  } = useAnalytics();
267
306
 
268
- // Web3 & blockchain
307
+ // Web3 & blockchain (wallet addresses from user.wallets)
269
308
  const {
270
- getWalletBalance,
271
- createWallet,
272
- getTransactionHistory
309
+ getTokenBalance,
310
+ getTokenMetadata,
311
+ getTokenCollection,
312
+ resolveIPFSUrl,
313
+ fetchAndProcessMetadata,
314
+ getChainDataById,
315
+ getExplorerUrl
273
316
  } = useWeb3();
317
+
318
+ // User status & achievements
319
+ const {
320
+ getUserStatusTypes,
321
+ getEarnedUserStatus,
322
+ createUserStatusType // Admin
323
+ } = useUserStatus();
324
+
325
+ // File management
326
+ const {
327
+ getSignedPutUrl,
328
+ getSignedGetUrl,
329
+ getSignedUrl,
330
+ optimizeMedia
331
+ } = useFiles();
332
+
333
+ // Donations
334
+ const {
335
+ getDonationTypes
336
+ } = useDonations();
274
337
  ```
275
338
 
276
339
  ## EVM Blockchain Transaction Signing
@@ -455,25 +518,22 @@ function ErrorHandlingExample() {
455
518
  import { useAnalytics } from '@explorins/pers-sdk-react-native';
456
519
 
457
520
  function AnalyticsExample() {
458
- const {
459
- getTransactionAnalytics,
460
- getUserAnalytics,
461
- trackEvent
462
- } = useAnalytics();
521
+ const { getTransactionAnalytics } = useAnalytics();
463
522
 
464
523
  const loadAnalytics = async () => {
465
- const txAnalytics = await getTransactionAnalytics({
466
- dateRange: { start: '2023-01-01', end: '2023-12-31' }
467
- });
468
-
469
- const userAnalytics = await getUserAnalytics();
470
-
471
- // Track custom events
472
- await trackEvent('reward_claimed', {
473
- amount: 100,
474
- type: 'discount_voucher',
475
- timestamp: new Date().toISOString()
476
- });
524
+ try {
525
+ const txAnalytics = await getTransactionAnalytics({
526
+ groupBy: ['day'],
527
+ metrics: ['count', 'sum'],
528
+ startDate: '2023-01-01',
529
+ endDate: '2023-12-31'
530
+ });
531
+
532
+ console.log('Transaction analytics:', txAnalytics.results);
533
+ console.log('Execution time:', txAnalytics.metadata?.executionTime);
534
+ } catch (error) {
535
+ console.error('Failed to load analytics:', error);
536
+ }
477
537
  };
478
538
  }
479
539
  ```
@@ -103,7 +103,7 @@ export const useWeb3 = () => {
103
103
  * chainId: 1
104
104
  * });
105
105
  * console.log('NFT name:', metadata?.name);
106
- * console.log('NFT image:', metadata?.image);
106
+ * console.log('NFT image:', metadata?.imageUrl); // IPFS URLs auto-resolved to HTTPS
107
107
  * ```
108
108
  */
109
109
  const getTokenMetadata = useCallback(async (request) => {
package/dist/index.js CHANGED
@@ -22507,21 +22507,115 @@ const getTokenOfOwnerByIndex = async (tokenContract, ownerAddress, index) => {
22507
22507
  };
22508
22508
 
22509
22509
  /**
22510
- * Clean an ABI to be compatible with Ethers.js
22511
- * Removes 'indexed' property from function inputs (only valid for events)
22510
+ * Clean input/output parameters for functions, constructors, errors
22511
+ * Removes properties that are invalid for non-event parameters
22512
+ */
22513
+ function cleanInputsOrOutputs(params) {
22514
+ if (!Array.isArray(params)) {
22515
+ return [];
22516
+ }
22517
+ return params.map((param) => {
22518
+ if (!param || typeof param !== 'object') {
22519
+ return param;
22520
+ }
22521
+ const cleanParam = {
22522
+ name: param.name ?? '',
22523
+ type: param.type ?? 'uint256', // Default to uint256 if type is missing (shouldn't happen)
22524
+ };
22525
+ // Handle tuple types with components
22526
+ if (param.components && Array.isArray(param.components)) {
22527
+ cleanParam.components = cleanInputsOrOutputs(param.components);
22528
+ }
22529
+ // Preserve internalType only if it doesn't cause issues
22530
+ // (some malformed internalTypes can break ethers)
22531
+ if (param.internalType && typeof param.internalType === 'string') {
22532
+ cleanParam.internalType = param.internalType;
22533
+ }
22534
+ // NOTE: 'indexed' is intentionally NOT included here
22535
+ // It's only valid for event parameters
22536
+ return cleanParam;
22537
+ });
22538
+ }
22539
+ /**
22540
+ * Clean an ABI to be compatible with Ethers.js v6
22541
+ * Handles common Web3.js ABI format incompatibilities:
22542
+ * - Removes 'indexed' from function inputs (only valid for event parameters)
22543
+ * - Removes 'indexed' from function outputs
22544
+ * - Removes 'internalType' if it causes parsing issues
22545
+ * - Ensures 'stateMutability' is valid
22546
+ * - Removes unknown/invalid properties that cause ethers to drop fragments
22512
22547
  */
22513
22548
  function cleanABI(abi) {
22514
22549
  return abi.map(fragment => {
22515
- if (fragment.type === 'function' && fragment.inputs) {
22516
- return {
22517
- ...fragment,
22518
- inputs: fragment.inputs.map((input) => {
22519
- const { indexed, ...cleanInput } = input;
22520
- return cleanInput;
22521
- })
22522
- };
22550
+ // Skip non-object fragments
22551
+ if (!fragment || typeof fragment !== 'object') {
22552
+ return fragment;
22523
22553
  }
22524
- return fragment;
22554
+ const cleanFragment = {
22555
+ type: fragment.type,
22556
+ name: fragment.name,
22557
+ };
22558
+ // Handle functions
22559
+ if (fragment.type === 'function') {
22560
+ // Clean inputs - remove 'indexed' (only valid for events)
22561
+ if (fragment.inputs) {
22562
+ cleanFragment.inputs = cleanInputsOrOutputs(fragment.inputs);
22563
+ }
22564
+ // Clean outputs - remove 'indexed'
22565
+ if (fragment.outputs) {
22566
+ cleanFragment.outputs = cleanInputsOrOutputs(fragment.outputs);
22567
+ }
22568
+ // Preserve valid function properties
22569
+ if (fragment.stateMutability) {
22570
+ cleanFragment.stateMutability = fragment.stateMutability;
22571
+ }
22572
+ // Handle legacy 'constant' and 'payable' for older ABIs
22573
+ if (fragment.constant !== undefined && !fragment.stateMutability) {
22574
+ cleanFragment.stateMutability = fragment.constant ? 'view' : 'nonpayable';
22575
+ }
22576
+ if (fragment.payable && !fragment.stateMutability) {
22577
+ cleanFragment.stateMutability = 'payable';
22578
+ }
22579
+ }
22580
+ // Handle events - 'indexed' IS valid here
22581
+ else if (fragment.type === 'event') {
22582
+ cleanFragment.anonymous = fragment.anonymous ?? false;
22583
+ if (fragment.inputs) {
22584
+ cleanFragment.inputs = fragment.inputs.map((input) => ({
22585
+ name: input.name ?? '',
22586
+ type: input.type,
22587
+ indexed: input.indexed ?? false,
22588
+ // Keep internalType for events if present
22589
+ ...(input.internalType && { internalType: input.internalType })
22590
+ }));
22591
+ }
22592
+ }
22593
+ // Handle constructor
22594
+ else if (fragment.type === 'constructor') {
22595
+ if (fragment.inputs) {
22596
+ cleanFragment.inputs = cleanInputsOrOutputs(fragment.inputs);
22597
+ }
22598
+ if (fragment.stateMutability) {
22599
+ cleanFragment.stateMutability = fragment.stateMutability;
22600
+ }
22601
+ }
22602
+ // Handle fallback/receive
22603
+ else if (fragment.type === 'fallback' || fragment.type === 'receive') {
22604
+ if (fragment.stateMutability) {
22605
+ cleanFragment.stateMutability = fragment.stateMutability;
22606
+ }
22607
+ }
22608
+ // Handle errors
22609
+ else if (fragment.type === 'error') {
22610
+ if (fragment.inputs) {
22611
+ cleanFragment.inputs = cleanInputsOrOutputs(fragment.inputs);
22612
+ }
22613
+ }
22614
+ // Pass through unknown types unchanged
22615
+ else {
22616
+ return fragment;
22617
+ }
22618
+ return cleanFragment;
22525
22619
  });
22526
22620
  }
22527
22621
  /**
@@ -22553,24 +22647,45 @@ function convertAbiToInterface(abi) {
22553
22647
  return abi;
22554
22648
  }
22555
22649
  // String format (JSON)
22650
+ let abiArray;
22556
22651
  if (typeof abi === 'string') {
22557
- const parsedAbi = JSON.parse(abi);
22558
- const cleanedAbi = Array.isArray(parsedAbi) ? cleanABI(parsedAbi) : parsedAbi;
22559
- return new Interface(cleanedAbi);
22652
+ abiArray = JSON.parse(abi);
22653
+ }
22654
+ else if (Array.isArray(abi)) {
22655
+ abiArray = abi;
22560
22656
  }
22561
- // Array or object format
22562
- if (Array.isArray(abi) || typeof abi === 'object') {
22563
- const cleanedAbi = Array.isArray(abi) ? cleanABI(abi) : abi;
22564
- return new Interface(cleanedAbi);
22657
+ else if (typeof abi === 'object') {
22658
+ // Single fragment object
22659
+ abiArray = [abi];
22565
22660
  }
22566
- throw new Error(`Unsupported ABI type: ${typeof abi}`);
22661
+ else {
22662
+ throw new Error(`Unsupported ABI type: ${typeof abi}`);
22663
+ }
22664
+ // Clean the ABI
22665
+ const cleanedAbi = cleanABI(abiArray);
22666
+ // Try to create Interface and validate all fragments were parsed
22667
+ const iface = new Interface(cleanedAbi);
22668
+ // Validation: Check if expected functions are present
22669
+ const expectedFunctions = cleanedAbi
22670
+ .filter((f) => f.type === 'function')
22671
+ .map((f) => f.name);
22672
+ const actualFunctions = iface.fragments
22673
+ .filter(f => f.type === 'function')
22674
+ .map(f => f.name);
22675
+ const droppedFunctions = expectedFunctions.filter((name) => !actualFunctions.includes(name));
22676
+ if (droppedFunctions.length > 0) {
22677
+ console.warn(`[web3-ts] Warning: ${droppedFunctions.length} function(s) were dropped during ABI conversion:`, droppedFunctions.join(', '));
22678
+ }
22679
+ return iface;
22567
22680
  }
22568
22681
  catch (error) {
22569
22682
  if (error.message.includes('JSON')) {
22570
22683
  throw new Error('Invalid JSON format in ABI string');
22571
22684
  }
22572
- if (error.message.includes('invalid fragment') || error.message.includes('parameter cannot be indexed')) {
22573
- throw new Error('Invalid ABI fragment structure - function inputs cannot be indexed');
22685
+ if (error.message.includes('invalid fragment') ||
22686
+ error.message.includes('parameter cannot be indexed')) {
22687
+ console.error('[web3-ts] ABI fragment error:', error.message);
22688
+ throw new Error(`Invalid ABI fragment structure: ${error.message}`);
22574
22689
  }
22575
22690
  throw error;
22576
22691
  }
@@ -23974,7 +24089,7 @@ class CampaignApi {
23974
24089
  * NEW: GET /trigger-sources
23975
24090
  */
23976
24091
  async getCampaignTriggers(options) {
23977
- let url = '/trigger-sources';
24092
+ let url = '/campaign-triggers';
23978
24093
  const params = [];
23979
24094
  if (options) {
23980
24095
  if (options.limit)
@@ -25402,6 +25517,17 @@ class IPFSInfrastructureApi {
25402
25517
  }
25403
25518
  return url;
25404
25519
  }
25520
+ /**
25521
+ * Fetch metadata from tokenURI, resolve all IPFS URLs, and normalize to SDK format.
25522
+ *
25523
+ * NORMALIZATION BOUNDARY: This is the single place where ERC metadata standard
25524
+ * (snake_case: image, animation_url) is converted to SDK format (camelCase: imageUrl, animationUrl).
25525
+ * All downstream consumers receive normalized SDK format with resolved HTTPS URLs.
25526
+ *
25527
+ * @param tokenUri - Token URI (can be ipfs:// or https://)
25528
+ * @param chainId - Chain ID for IPFS gateway selection
25529
+ * @returns Normalized TokenMetadata with resolved HTTPS URLs, or null on error
25530
+ */
25405
25531
  async fetchAndProcessMetadata(tokenUri, chainId) {
25406
25532
  try {
25407
25533
  const resolvedUri = await this.resolveIPFSUrl(tokenUri, chainId);
@@ -25409,15 +25535,25 @@ class IPFSInfrastructureApi {
25409
25535
  if (!response.ok) {
25410
25536
  throw new Error(`HTTP error! status: ${response.status}`);
25411
25537
  }
25412
- const metadata = await response.json();
25413
- // Process and return clean metadata
25538
+ const rawMetadata = await response.json();
25539
+ // Resolve IPFS URLs for media fields
25540
+ const resolvedImageUrl = rawMetadata.image
25541
+ ? await this.resolveIPFSUrl(rawMetadata.image, chainId)
25542
+ : '';
25543
+ const resolvedAnimationUrl = rawMetadata.animation_url
25544
+ ? await this.resolveIPFSUrl(rawMetadata.animation_url, chainId)
25545
+ : undefined;
25546
+ // Extract custom properties (anything not in ERC standard)
25547
+ const customProperties = Object.fromEntries(Object.entries(rawMetadata).filter(([key]) => !['name', 'description', 'image', 'animation_url', 'external_url', 'attributes'].includes(key)));
25548
+ // Return normalized SDK format (typed as TokenMetadata)
25414
25549
  return {
25415
- name: metadata.name || '',
25416
- description: metadata.description || '',
25417
- image: metadata.image ? await this.resolveIPFSUrl(metadata.image, chainId) : '',
25418
- attributes: metadata.attributes || [],
25419
- animation_url: metadata.animation_url ? await this.resolveIPFSUrl(metadata.animation_url, chainId) : undefined,
25420
- external_url: metadata.external_url || undefined
25550
+ name: rawMetadata.name || '',
25551
+ description: rawMetadata.description || '',
25552
+ imageUrl: resolvedImageUrl,
25553
+ animationUrl: resolvedAnimationUrl,
25554
+ externalUrl: rawMetadata.external_url || undefined,
25555
+ attributes: rawMetadata.attributes || [],
25556
+ ...customProperties
25421
25557
  };
25422
25558
  }
25423
25559
  catch (error) {
@@ -25701,8 +25837,8 @@ class MetadataDomainService {
25701
25837
  class ContractDomainService {
25702
25838
  constructor() { }
25703
25839
  analyzeContract(abi) {
25704
- abi = convertAbiToInterface(abi);
25705
- const methods = abi.fragments.filter(item => item.type === 'function').map(item => item.name);
25840
+ const cleanAbi = convertAbiToInterface(abi);
25841
+ const methods = cleanAbi.fragments.filter(item => item.type === 'function').map(item => item.name);
25706
25842
  // ERC-721 detection
25707
25843
  const hasOwnerOf = methods.includes('ownerOf');
25708
25844
  const hasTokenURI = methods.includes('tokenURI');
@@ -25733,28 +25869,6 @@ class ContractDomainService {
25733
25869
  */
25734
25870
  class Web3ApplicationService {
25735
25871
  constructor(web3Api, ipfsApi) {
25736
- // Type-safe metadata conversion methods for ERC-721/ERC-1155 standards
25737
- this.metadataMapper = {
25738
- fromERCStandard: (ercMetadata) => ({
25739
- name: ercMetadata.name || '',
25740
- description: ercMetadata.description || '',
25741
- imageUrl: ercMetadata.image || '',
25742
- externalUrl: ercMetadata.external_url,
25743
- animationUrl: ercMetadata.animation_url,
25744
- animationUrlConverted: undefined, // Will be set by IPFS conversion
25745
- attributes: ercMetadata.attributes || [],
25746
- ...ercMetadata
25747
- }),
25748
- toERCStandard: (metadata) => ({
25749
- name: metadata.name,
25750
- description: metadata.description,
25751
- image: metadata.imageUrl,
25752
- animation_url: metadata.animationUrl,
25753
- external_url: metadata.externalUrl,
25754
- attributes: metadata.attributes,
25755
- ...Object.fromEntries(Object.entries(metadata).filter(([key]) => !['name', 'description', 'imageUrl', 'animationUrl', 'externalUrl', 'attributes', 'animationUrlConverted'].includes(key)))
25756
- })
25757
- };
25758
25872
  // Create domain services with injected infrastructure dependencies
25759
25873
  this.contractDomainService = new ContractDomainService();
25760
25874
  this.metadataDomainService = new MetadataDomainService(ipfsApi);
@@ -25773,7 +25887,15 @@ class Web3ApplicationService {
25773
25887
  });
25774
25888
  }
25775
25889
  /**
25776
- * Get metadata for a specific token from on-chain
25890
+ * Get metadata for a specific token from on-chain.
25891
+ *
25892
+ * Returns metadata with:
25893
+ * - Normalized property names (imageUrl, animationUrl - SDK standard)
25894
+ * - All IPFS URLs already resolved to HTTPS
25895
+ * - Custom properties preserved from original metadata
25896
+ *
25897
+ * Note: Normalization happens at infrastructure layer (IPFS API),
25898
+ * so no additional transformation needed here.
25777
25899
  */
25778
25900
  async getTokenMetadata(request) {
25779
25901
  const domainResult = await this.tokenDomainService.getTokenMetadata({
@@ -25782,6 +25904,7 @@ class Web3ApplicationService {
25782
25904
  tokenId: request.tokenId || '',
25783
25905
  chainId: request.chainId
25784
25906
  });
25907
+ // Metadata is already normalized at infrastructure layer
25785
25908
  return domainResult.metadata;
25786
25909
  }
25787
25910
  /**
@@ -25804,22 +25927,13 @@ class Web3ApplicationService {
25804
25927
  return this.metadataDomainService.resolveIPFSUrl(url, chainId);
25805
25928
  }
25806
25929
  /**
25807
- * Fetch and process metadata from URI with IPFS conversion
25930
+ * Fetch and process metadata from any URI with IPFS conversion.
25931
+ *
25932
+ * Use this for ad-hoc metadata fetching when you have a tokenURI.
25933
+ * Normalization happens at infrastructure layer.
25808
25934
  */
25809
25935
  async fetchAndProcessMetadata(tokenUri, chainId) {
25810
- const domainMetadata = await this.metadataDomainService.fetchAndProcessMetadata(tokenUri, chainId);
25811
- if (!domainMetadata)
25812
- return null;
25813
- // Convert from ERC token standard to our clean interface
25814
- const cleanMetadata = this.metadataMapper.fromERCStandard(domainMetadata);
25815
- // Add IPFS conversion if needed
25816
- if (cleanMetadata.animationUrl?.startsWith('ipfs://')) {
25817
- return {
25818
- ...cleanMetadata,
25819
- animationUrlConverted: await this.resolveIPFSUrl(cleanMetadata.animationUrl, chainId)
25820
- };
25821
- }
25822
- return cleanMetadata;
25936
+ return this.metadataDomainService.fetchAndProcessMetadata(tokenUri, chainId);
25823
25937
  }
25824
25938
  }
25825
25939
 
@@ -34434,7 +34548,7 @@ const useWeb3 = () => {
34434
34548
  * chainId: 1
34435
34549
  * });
34436
34550
  * console.log('NFT name:', metadata?.name);
34437
- * console.log('NFT image:', metadata?.image);
34551
+ * console.log('NFT image:', metadata?.imageUrl); // IPFS URLs auto-resolved to HTTPS
34438
34552
  * ```
34439
34553
  */
34440
34554
  const getTokenMetadata = react.useCallback(async (request) => {