Skip to content

Commit

Permalink
initial moralis integration
Browse files Browse the repository at this point in the history
  • Loading branch information
KorbinianK committed Apr 29, 2024
1 parent 5d82f05 commit c6beca0
Show file tree
Hide file tree
Showing 10 changed files with 625 additions and 94 deletions.
2 changes: 2 additions & 0 deletions packages/bridge-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
},
"type": "module",
"dependencies": {
"@moralisweb3/common-evm-utils": "^2.26.1",
"@wagmi/connectors": "^4.1.18",
"@wagmi/core": "^2.6.9",
"@walletconnect/ethereum-provider": "^2.12.2",
Expand All @@ -70,6 +71,7 @@
"buffer": "^6.0.3",
"debug": "^4.3.4",
"events": "^3.3.0",
"moralis": "^2.26.1",
"object-hash": "^3.0.0",
"svelte-i18n": "^3.7.4",
"viem": "^2.7.11"
Expand Down
185 changes: 104 additions & 81 deletions packages/bridge-ui/src/libs/bridge/fetchNFTs.ts
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 };
// }
28 changes: 28 additions & 0 deletions packages/bridge-ui/src/routes/api/nft/+server.ts
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',
},
});
}
};
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[]>;
}
17 changes: 17 additions & 0 deletions packages/bridge-ui/src/thirdparty/domain/models/NFT.ts
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 packages/bridge-ui/src/thirdparty/domain/services/NFTService.ts
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);
}
}
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();
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 packages/bridge-ui/src/thirdparty/infrastructure/types/moralis.ts
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;
}
Loading

0 comments on commit c6beca0

Please sign in to comment.