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

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.
@@ -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) => {