1+ import { Contract } from "starknet" ;
2+ import { ERC721_ABI } from "../abi/ERC721_ABI" ;
3+ import { starknetService } from "./starknet.service" ;
4+ import { ipfsService } from "./ipfs.service" ;
5+ import type { AssetIP , NFTAsset } from "../types/asset" ;
6+
7+ const IPFS_RETRY_LIMIT = 2 ;
8+
9+ export type PublicTimelineAsset = {
10+ asset : NFTAsset ;
11+ metadata ?: AssetIP | null ;
12+ } ;
13+
14+ // Helper function to convert u256 to a string representation of the number
15+ function u256ToString ( u256 : any ) : string {
16+ if ( ! u256 ) return "0" ;
17+ // Try low/high first
18+ if ( typeof u256 . low !== "undefined" && typeof u256 . high !== "undefined" ) {
19+ const low = BigInt ( u256 . low ) ;
20+ const high = BigInt ( u256 . high ) ;
21+ return ( ( high << BigInt ( 128 ) ) + low ) . toString ( ) ;
22+ }
23+ // Try array/object with 0/1 keys
24+ if ( typeof u256 [ 0 ] !== "undefined" && typeof u256 [ 1 ] !== "undefined" ) {
25+ const low = BigInt ( u256 [ 0 ] ) ;
26+ const high = BigInt ( u256 [ 1 ] ) ;
27+ return ( ( high << BigInt ( 128 ) ) + low ) . toString ( ) ;
28+ }
29+ // Try direct string/number
30+ if ( typeof u256 === "string" || typeof u256 === "number" || typeof u256 === "bigint" ) {
31+ return u256 . toString ( ) ;
32+ }
33+ console . error ( "Unknown u256 format" , u256 ) ;
34+ return "0" ;
35+ }
36+
37+ // Helper function to decode ByteArray to a string
38+ function decodeByteArray ( byteArray : { data : string [ ] ; pending_word : string ; pending_word_len : number } ) : string {
39+ const data = byteArray . data . map ( felt => {
40+ const hex = felt . substring ( 2 ) ;
41+ return Buffer . from ( hex , 'hex' ) . toString ( 'utf8' ) ;
42+ } ) ;
43+ const pending = byteArray . pending_word ;
44+ const pendingLen = byteArray . pending_word_len ;
45+ const pendingHex = pending . substring ( 2 ) ;
46+ const pendingStr = Buffer . from ( pendingHex , 'hex' ) . toString ( 'utf8' ) . slice ( 0 , pendingLen ) ;
47+ return data . join ( '' ) + pendingStr ;
48+ }
49+
50+ async function fetchIpfsWithRetry ( hashOrUri : string , retries = IPFS_RETRY_LIMIT ) : Promise < AssetIP | null > {
51+ let lastError ;
52+ for ( let i = 0 ; i < retries ; i ++ ) {
53+ try {
54+ let hash = hashOrUri ;
55+ if ( hash . startsWith ( 'ipfs://' ) ) hash = hash . replace ( 'ipfs://' , '' ) ;
56+ const meta = await ipfsService . getFromIPFS ( hash ) ;
57+ if ( meta && typeof meta === 'object' && 'timestamp' in meta && 'type' in meta ) return meta ;
58+ return null ;
59+ } catch ( err ) {
60+ lastError = err ;
61+ await new Promise ( res => setTimeout ( res , 500 ) ) ;
62+ }
63+ }
64+ console . error ( "IPFS fetch failed for" , hashOrUri , lastError ) ;
65+ return null ;
66+ }
67+
68+ export async function getPublicTimelineAssets ( page = 0 , pageSize = 12 , assetType ?: string ) : Promise < PublicTimelineAsset [ ] > {
69+ const contractAddress = process . env . NEXT_PUBLIC_CONTRACT_ADDRESS_MIP ;
70+ if ( ! contractAddress ) {
71+ console . warn ( "NEXT_PUBLIC_CONTRACT_ADDRESS_MIP is not set." ) ;
72+ return [ ] ;
73+ }
74+
75+ const contract = new Contract ( ERC721_ABI , contractAddress , starknetService [ 'provider' ] ) ;
76+
77+ // Get total supply, correctly parsing u256
78+ let totalSupply = 0 ;
79+ try {
80+ const result = await contract . call ( "total_supply" , [ ] ) as any ;
81+ console . log ( "total_supply raw result" , result ) ;
82+ totalSupply = Number ( result ) ;
83+ } catch ( e ) {
84+ console . error ( "Failed to fetch total_supply from contract" , e ) ;
85+ return [ ] ;
86+ }
87+ // console.log("totalSupply", totalSupply);
88+
89+ // 2. Get token IDs for this page
90+ const start = page * pageSize ;
91+ const end = Math . min ( start + pageSize , totalSupply ) ;
92+ const tokenIds : string [ ] = [ ] ;
93+ for ( let i = start ; i < end ; i ++ ) {
94+ try {
95+ // Call token_by_index and parse the u256 result
96+ const result = await contract . call ( "token_by_index" , [ i ] ) as any ;
97+ const tokenId = result . toString ( ) ;
98+ tokenIds . push ( tokenId ) ;
99+ } catch ( e ) {
100+ console . warn ( `Failed to fetch token_by_index(${ i } )` , e ) ;
101+ }
102+ }
103+
104+ // 3. Fetch NFTAsset for each tokenId
105+ const nfts : NFTAsset [ ] = await Promise . all (
106+ tokenIds . map ( async ( tokenId ) => {
107+ const [ ownerResult , tokenUriResult ] = await Promise . all ( [
108+ contract . call ( "owner_of" , [ tokenId ] ) as any ,
109+ contract . call ( "token_uri" , [ tokenId ] ) as any
110+ ] ) ;
111+ const owner = ownerResult . toString ( ) ;
112+ const tokenURI = tokenUriResult . toString ( ) ;
113+
114+ return {
115+ contractAddress,
116+ tokenId : tokenId . toString ( ) ,
117+ owner,
118+ tokenURI,
119+ type : "ERC721" ,
120+ } as NFTAsset ;
121+ } )
122+ ) ;
123+ // console.log("NFTs", nfts);
124+
125+ // 4. Fetch IPFS metadata for each asset (tokenURI)
126+ const assetsWithMeta : PublicTimelineAsset [ ] = await Promise . all (
127+ nfts . map ( async ( asset ) => {
128+ let meta : AssetIP | null = null ;
129+ if ( asset . tokenURI && asset . tokenURI . startsWith ( 'ipfs://' ) ) {
130+ meta = await fetchIpfsWithRetry ( asset . tokenURI ) ;
131+ }
132+ return {
133+ asset,
134+ metadata : meta ,
135+ } ;
136+ } )
137+ ) ;
138+ // console.log("assetsWithMeta", assetsWithMeta);
139+
140+ // 5. Sort and Filter
141+ let sorted = assetsWithMeta . sort ( ( a , b ) => {
142+ const ta = a . metadata ?. timestamp ? Date . parse ( a . metadata . timestamp ) : 0 ;
143+ const tb = b . metadata ?. timestamp ? Date . parse ( b . metadata . timestamp ) : 0 ;
144+ if ( tb !== ta ) return tb - ta ;
145+ return Number ( b . asset . tokenId ) - Number ( a . asset . tokenId ) ;
146+ } ) ;
147+
148+ if ( assetType ) {
149+ if ( assetType === "other" ) {
150+ sorted = sorted . filter ( a => {
151+ const metaType = a . metadata ?. type ?. toLowerCase ( ) . trim ( ) ;
152+ return ! metaType || ! [ "art" , "music" , "docs" ] . includes ( metaType ) ;
153+ } ) ;
154+ } else {
155+ sorted = sorted . filter ( a => {
156+ const metaType = a . metadata ?. type ?. toLowerCase ( ) . trim ( ) ;
157+ return metaType === assetType . toLowerCase ( ) . trim ( ) ;
158+ } ) ;
159+ }
160+ }
161+
162+ return sorted ;
163+ }
0 commit comments