-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5d82f05
commit c6beca0
Showing
10 changed files
with
625 additions
and
94 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,95 +1,118 @@ | ||
import type { Address } from 'viem'; | ||
|
||
import { eventIndexerApiServices } from '$libs/eventIndexer/initEventIndexer'; | ||
import { type NFT, TokenType } from '$libs/token'; | ||
import { checkOwnershipOfNFTs } from '$libs/token/checkOwnership'; | ||
import { fetchNFTImageUrl } from '$libs/token/fetchNFTImageUrl'; | ||
import { getTokenWithInfoFromAddress } from '$libs/token/getTokenWithInfoFromAddress'; | ||
import { getLogger } from '$libs/util/logger'; | ||
|
||
const log = getLogger('bridge:fetchNFTs'); | ||
|
||
function deduplicateNFTs(nftArrays: NFT[][]): NFT[] { | ||
const nftMap: Map<string, NFT> = new Map(); | ||
nftArrays.flat().forEach((nft) => { | ||
Object.entries(nft.addresses).forEach(([chainID, address]) => { | ||
const uniqueKey = `${address}-${chainID}-${nft.tokenId}`; | ||
|
||
if (!nftMap.has(uniqueKey)) { | ||
nftMap.set(uniqueKey, nft); | ||
} | ||
}); | ||
}); | ||
return Array.from(nftMap.values()); | ||
} | ||
import type { NFT } from '$libs/token'; | ||
// import { checkOwnershipOfNFTs } from '$libs/token/checkOwnership'; | ||
// import { fetchNFTImageUrl } from '$libs/token/fetchNFTImageUrl'; | ||
// import { getTokenWithInfoFromAddress } from '$libs/token/getTokenWithInfoFromAddress'; | ||
// import { getLogger } from '$libs/util/logger'; | ||
|
||
// const log = getLogger('bridge:fetchNFTs'); | ||
|
||
// function deduplicateNFTs(nftArrays: NFT[][]): NFT[] { | ||
// const nftMap: Map<string, NFT> = new Map(); | ||
// nftArrays.flat().forEach((nft) => { | ||
// Object.entries(nft.addresses).forEach(([chainID, address]) => { | ||
// const uniqueKey = `${address}-${chainID}-${nft.tokenId}`; | ||
|
||
// if (!nftMap.has(uniqueKey)) { | ||
// nftMap.set(uniqueKey, nft); | ||
// } | ||
// }); | ||
// }); | ||
// return Array.from(nftMap.values()); | ||
// } | ||
|
||
export async function fetchNFTs( | ||
userAddress: Address, | ||
srcChainId: number, | ||
): Promise<{ nfts: NFT[]; error: Error | null }> { | ||
let error: Error | null = null; | ||
// const error: Error | null = null; | ||
|
||
// Fetch from all indexers | ||
const indexerPromises: Promise<NFT[]>[] = eventIndexerApiServices.map(async (eventIndexerApiService) => { | ||
const { items: result } = await eventIndexerApiService.getAllNftsByAddressFromAPI(userAddress, BigInt(srcChainId), { | ||
page: 0, | ||
size: 100, | ||
}); | ||
|
||
const nftsPromises: Promise<NFT>[] = result.map(async (nft) => { | ||
const type: TokenType = TokenType[nft.contractType as keyof typeof TokenType]; | ||
return getTokenWithInfoFromAddress({ | ||
contractAddress: nft.contractAddress, | ||
srcChainId, | ||
owner: userAddress, | ||
tokenId: Number(nft.tokenID), | ||
type, | ||
}) as Promise<NFT>; | ||
try { | ||
const moralisResponse = await fetch('/api/nft', { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify({ address: userAddress, chainId: srcChainId }), | ||
}); | ||
|
||
const nftsSettled = await Promise.allSettled(nftsPromises); | ||
const nfts = nftsSettled | ||
.filter((result) => result.status === 'fulfilled') | ||
.map((result) => (result as PromiseFulfilledResult<NFT>).value); | ||
if (moralisResponse.ok) { | ||
const responseData = await moralisResponse.json(); | ||
|
||
return nfts; | ||
}); | ||
const { nfts } = responseData; | ||
|
||
let nftArrays: NFT[][] = []; | ||
try { | ||
nftArrays = await Promise.all(indexerPromises); | ||
} catch (e) { | ||
log('error fetching nfts from indexer services', e); | ||
error = e as Error; | ||
} | ||
|
||
// Deduplicate based on address and chainID | ||
const deduplicatedNfts = deduplicateNFTs(nftArrays); | ||
|
||
// Fetch image for each NFT | ||
const promises = Promise.all( | ||
deduplicatedNfts.map(async (nft) => { | ||
const nftWithImage = await fetchNFTImageUrl(nft); | ||
return nftWithImage; | ||
}), | ||
); | ||
const nftsWithImage = await promises; | ||
|
||
// Double check the ownership | ||
const ownsAllNfts = await checkOwnershipOfNFTs(nftsWithImage, userAddress, srcChainId); | ||
log(`user ${userAddress} owns all NFTs:`, ownsAllNfts); | ||
// filter out the NFTs that the user doesn't own | ||
const filteredNfts = nftsWithImage.filter((nft) => { | ||
const isOwned = ownsAllNfts.successfulOwnershipChecks.find((result) => result.tokenId === nft.tokenId); | ||
return isOwned; | ||
}); | ||
|
||
if (filteredNfts.length !== nftsWithImage.length) { | ||
//TODO: handle this case differently? maybe show a warning to the user? | ||
log(`found ${nftsWithImage.length - filteredNfts.length} tokens that the user doesn't own`); | ||
return { nfts, error: null }; | ||
} else { | ||
console.error('HTTP error:', moralisResponse.statusText); | ||
return { nfts: [], error: new Error(moralisResponse.statusText) }; | ||
} | ||
} catch (error) { | ||
console.error('Fetch error:', error); | ||
return { nfts: [], error: new Error('') }; | ||
} | ||
|
||
log(`found ${filteredNfts.length} unique NFTs from all indexers`, filteredNfts); | ||
|
||
return { nfts: filteredNfts, error }; | ||
} | ||
// // Fetch from all indexers | ||
// const indexerPromises: Promise<NFT[]>[] = eventIndexerApiServices.map(async (eventIndexerApiService) => { | ||
// const { items: result } = await eventIndexerApiService.getAllNftsByAddressFromAPI(userAddress, BigInt(srcChainId), { | ||
// page: 0, | ||
// size: 100, | ||
// }); | ||
|
||
// const nftsPromises: Promise<NFT>[] = result.map(async (nft) => { | ||
// const type: TokenType = TokenType[nft.contractType as keyof typeof TokenType]; | ||
// return getTokenWithInfoFromAddress({ | ||
// contractAddress: nft.contractAddress, | ||
// srcChainId, | ||
// owner: userAddress, | ||
// tokenId: Number(nft.tokenID), | ||
// type, | ||
// }) as Promise<NFT>; | ||
// }); | ||
|
||
// const nftsSettled = await Promise.allSettled(nftsPromises); | ||
// const nfts = nftsSettled | ||
// .filter((result) => result.status === 'fulfilled') | ||
// .map((result) => (result as PromiseFulfilledResult<NFT>).value); | ||
|
||
// return nfts; | ||
// }); | ||
|
||
// let nftArrays: NFT[][] = []; | ||
// try { | ||
// nftArrays = await Promise.all(indexerPromises); | ||
// } catch (e) { | ||
// log('error fetching nfts from indexer services', e); | ||
// error = e as Error; | ||
// } | ||
|
||
// // Deduplicate based on address and chainID | ||
// const deduplicatedNfts = deduplicateNFTs(nftArrays); | ||
|
||
// // Fetch image for each NFT | ||
// const promises = Promise.all( | ||
// deduplicatedNfts.map(async (nft) => { | ||
// const nftWithImage = await fetchNFTImageUrl(nft); | ||
// return nftWithImage; | ||
// }), | ||
// ); | ||
// const nftsWithImage = await promises; | ||
|
||
// // Double check the ownership | ||
// const ownsAllNfts = await checkOwnershipOfNFTs(nftsWithImage, userAddress, srcChainId); | ||
// log(`user ${userAddress} owns all NFTs:`, ownsAllNfts); | ||
// // filter out the NFTs that the user doesn't own | ||
// const filteredNfts = nftsWithImage.filter((nft) => { | ||
// const isOwned = ownsAllNfts.successfulOwnershipChecks.find((result) => result.tokenId === nft.tokenId); | ||
// return isOwned; | ||
// }); | ||
|
||
// if (filteredNfts.length !== nftsWithImage.length) { | ||
// //TODO: handle this case differently? maybe show a warning to the user? | ||
// log(`found ${nftsWithImage.length - filteredNfts.length} tokens that the user doesn't own`); | ||
// } | ||
|
||
// log(`found ${filteredNfts.length} unique NFTs from all indexers`, filteredNfts); | ||
|
||
// return { nfts: filteredNfts, error }; | ||
// } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import type { RequestHandler } from '@sveltejs/kit'; | ||
import { NFTService } from 'src/thirdparty/domain/services/NFTService'; | ||
import moralisRepository from 'src/thirdparty/infrastructure/api/MoralisNFTRepository.server'; | ||
|
||
const nftService = new NFTService(moralisRepository); | ||
|
||
export const POST: RequestHandler = async ({ request }) => { | ||
try { | ||
const { address, chainId } = await request.json(); | ||
|
||
const nfts = await nftService.fetchNFTsByAddress(address, chainId); | ||
|
||
return new Response(JSON.stringify({ nfts }), { | ||
status: 200, | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
}); | ||
} catch (error) { | ||
console.error('Failed to fetch NFTs:', error); | ||
return new Response(JSON.stringify({ error: 'Failed to retrieve NFT data' }), { | ||
status: 500, | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
}); | ||
} | ||
}; |
5 changes: 5 additions & 0 deletions
5
packages/bridge-ui/src/thirdparty/domain/interfaces/INFTRepository.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import type { NFT } from '../models/NFT'; | ||
|
||
export interface INFTRepository { | ||
findByAddress(address: string, chainId: number): Promise<NFT[]>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import type { Address } from 'viem'; | ||
|
||
import type { NFTMetadata, TokenType } from '$libs/token'; | ||
|
||
export interface NFT { | ||
type: TokenType; | ||
name: string; | ||
symbol: string; | ||
addresses: Record<string, Address>; | ||
owner: Address; | ||
imported?: boolean; | ||
mintable?: boolean; | ||
balance?: bigint; | ||
tokenId: number | string; | ||
uri?: string; | ||
metadata?: NFTMetadata; | ||
} |
10 changes: 10 additions & 0 deletions
10
packages/bridge-ui/src/thirdparty/domain/services/NFTService.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import type { INFTRepository } from '../interfaces/INFTRepository'; | ||
import type { NFT } from '../models/NFT'; | ||
|
||
export class NFTService { | ||
constructor(private repository: INFTRepository) {} | ||
|
||
async fetchNFTsByAddress(address: string, chainId: number): Promise<NFT[]> { | ||
return await this.repository.findByAddress(address, chainId); | ||
} | ||
} |
46 changes: 46 additions & 0 deletions
46
packages/bridge-ui/src/thirdparty/infrastructure/api/MoralisNFTRepository.server.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
// src/lib/infrastructure/api/NFTRepository.server.ts | ||
import Moralis from 'moralis'; | ||
import type { Address } from 'viem'; | ||
|
||
import { MORALIS_API_KEY } from '$env/static/private'; | ||
|
||
import type { INFTRepository } from '../../domain/interfaces/INFTRepository'; | ||
import type { NFT } from '../../domain/models/NFT'; | ||
import { mapToNFTFromMoralis } from '../mappers/nft/MoralisNFTMapper'; | ||
import type { NFTApiData } from '../types/moralis'; | ||
|
||
class MoralisNFTRepository implements INFTRepository { | ||
private static instance: MoralisNFTRepository; | ||
|
||
private constructor() { | ||
Moralis.start({ | ||
apiKey: MORALIS_API_KEY, | ||
}).catch(console.error); | ||
} | ||
|
||
public static getInstance(): MoralisNFTRepository { | ||
if (!MoralisNFTRepository.instance) { | ||
MoralisNFTRepository.instance = new MoralisNFTRepository(); | ||
} | ||
return MoralisNFTRepository.instance; | ||
} | ||
|
||
async findByAddress(address: Address, chainId: number): Promise<NFT[]> { | ||
try { | ||
const response = await Moralis.EvmApi.nft.getWalletNFTs({ | ||
chain: chainId, | ||
format: 'decimal', | ||
excludeSpam: true, | ||
mediaItems: false, | ||
address: address, | ||
}); | ||
|
||
return response.result.map((nft) => mapToNFTFromMoralis(nft as unknown as NFTApiData, chainId)); | ||
} catch (e) { | ||
console.error('Failed to fetch NFTs from Moralis:', e); | ||
return []; | ||
} | ||
} | ||
} | ||
|
||
export default MoralisNFTRepository.getInstance(); |
20 changes: 20 additions & 0 deletions
20
packages/bridge-ui/src/thirdparty/infrastructure/mappers/nft/MoralisNFTMapper.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import type { NFT } from 'src/thirdparty/domain/models/NFT'; | ||
import type { Address } from 'viem'; | ||
|
||
import type { TokenType } from '$libs/token'; | ||
|
||
import type { NFTApiData } from '../../types/moralis'; | ||
|
||
export function mapToNFTFromMoralis(apiData: NFTApiData, chainId: number): NFT { | ||
return { | ||
tokenId: apiData.tokenId, | ||
uri: apiData.tokenUri, | ||
owner: apiData.ownerOf as Address, | ||
name: apiData.name, | ||
symbol: apiData.symbol, | ||
type: apiData.contractType as TokenType, | ||
addresses: { | ||
[chainId]: apiData.tokenAddress as Address, | ||
}, | ||
}; | ||
} |
21 changes: 21 additions & 0 deletions
21
packages/bridge-ui/src/thirdparty/infrastructure/types/moralis.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import type { LimitNumberError } from 'ajv/dist/vocabularies/validation/limitNumber'; | ||
import type { Address } from 'viem'; | ||
|
||
export interface NFTApiData { | ||
tokenId: string | number; | ||
contractType: string; | ||
chain: LimitNumberError; | ||
tokenUri: string; | ||
tokenAddress: Address; | ||
tokenHash: string; | ||
metadata: string; | ||
name: string; | ||
symbol: string; | ||
ownerOf: Address; | ||
blockNumberMinted: bigint; | ||
blockNumber: bigint; | ||
lastMetadataSync: Date; | ||
lastTokenUriSync: Date; | ||
amount: number | string; | ||
possibleSpam: boolean; | ||
} |
Oops, something went wrong.