import { ethers } from 'ethers'; import { normalizeChainId } from './utils/chainUtils'; import { Collection } from './Collection'; import { uniq } from 'lodash'; import { AlchemyTokenResponse, AlchemyTokensResponse, ITokenDefinition, MoralisTokensResponse, OmniteTokenResponse, TokenData, TokensResponse, } from './types'; import config from './config/config'; import { request } from './external/apiClient'; import { ipfsFetchTokenImageUrl } from './external/pinataApiClient'; import { getIpfsCid, ipfsConvertToGateway } from './utils/ipfs'; import { getCollectionIds, getCollectionRecords, } from './external/rpcApiClient'; const getUserTokens = async ( address: string, chainId?: string ): Promise => { const chains = chainId ? config.availableWallets.filter((wallet) => wallet.chainId === chainId) : config.availableWallets; const nfts = await Promise.allSettled( chains.map(async (wallet) => { if (wallet.nftApi.type === 'Alchemy') { const response = await request( 'GET', `${wallet.nftApi.apiKey}/getNFTs/?owner=${address}`, undefined, wallet.nftApi.apiBase ); const responseAsAlchemyTokenResponse = response as AlchemyTokensResponse; const asTokensData: Array = responseAsAlchemyTokenResponse.ownedNfts.map( (token: AlchemyTokenResponse): TokenData => { const asTokenData: TokenData = { tokenAddress: token.contract.address, tokenId: parseInt(token.id.tokenId).toString(), amount: 0, name: token.title, tokenUri: token.tokenUri.raw, chainId: wallet.chainId, tokenName: token.title.split(' #')[0], tokenSymbol: '', metadata: '', }; return asTokenData; } ); return asTokensData; } if (wallet.nftApi.type === 'Moralis') { const response = await request( 'GET', `/api/v2/${address}/nft/?chain=${wallet.chainId}` ); const body = (await response) as MoralisTokensResponse; return body?.result?.map( (result): TokenData => ({ tokenAddress: result.token_address, tokenId: result.token_id, amount: Number(result.amount), name: result.name, tokenUri: result.token_uri, chainId: wallet.chainId, tokenSymbol: result.symbol, tokenName: result.name, metadata: result.metadata, }) ); } if (wallet.nftApi.type === 'Omnite') { const response = await request( 'GET', `/${wallet.nftApi.apiPrefix}/wallet/${address}`, undefined, config.omniteApiBaseUrl ); const body = (await response) as OmniteTokenResponse; return Object.values(body) .flat() ?.filter((v) => !!v.metadata) .map( (result): TokenData => ({ tokenAddress: result.collectionAddress, tokenId: result.tokenId, amount: 1, name: result.metadata?.name || 'n/a', tokenUri: result.tokenUri || '', chainId: wallet.chainId, tokenSymbol: 'aaa', tokenName: 'aaaa', metadata: result.metadata?.rawMetadata!, }) ); } }) ); return { tokens: nfts .map((v) => { if (v.status === 'rejected') { console.warn(v.reason); return null; } return v.value; }) .filter((v: T): v is NonNullable => !!v) .flat(), chainId, }; }; export class Wallet { static ensureValidChain = async ( signer: ethers.providers.JsonRpcSigner | undefined, chainId: string ) => { if (!signer) { throw new Error('No signer defined'); } const currentChainId = normalizeChainId(await signer.getChainId()); if (chainId !== currentChainId) { // await oswitchNetwork(signer.provider)(chainId); throw new Error('Invalid chain'); } return chainId; }; static fetchTokens = async (account: string) => { if (!account) { throw new Error( 'Cannot refresh data for empty account. Fetch first data set with account set.' ); } const ownedTokens = await getUserTokens(account); const nftBalances = await Promise.all( ownedTokens.tokens .filter((v) => !!v) ?.map(async (v) => { let imageUrl: string | undefined = ''; if (!v.tokenUri) { v.tokenUri = await Collection.getTokenUri( v.chainId, v.tokenAddress, v.tokenId ); } if (v.metadata) { try { const object: { image: string | null } = JSON.parse( v.metadata ); imageUrl = ipfsConvertToGateway(object.image); } catch (error) { console.warn(error); } } if (!imageUrl) { try { imageUrl = await ipfsFetchTokenImageUrl(v.tokenUri); } catch (err) { console.warn(err); } } let name = v.name; if (!name) { try { name = await Collection.getName( v.chainId, v.tokenAddress ); } catch (err) { console.warn(err); } } return { ...v, name: name || 'n/a', imageUrl, rawIpfsUrl: getIpfsCid(v.tokenUri), }; }) ).then(async (parsedBalances) => { const collectionAddresses = parsedBalances.reduce((prev, curr) => { if (!Array.isArray(prev[curr.chainId])) { prev[curr.chainId] = []; } if (prev[curr.chainId].includes(curr.tokenAddress)) { return prev; } prev[curr.chainId].push(curr.tokenAddress); return prev; }, {} as { [chainId: string]: Array }); const collectionsIds = await getCollectionIds(collectionAddresses); return parsedBalances.map((v) => { const collectionId: string | null = collectionsIds[v.chainId][v.tokenAddress]; return { ...v, collectionId: collectionId, // if empty then its not created by our contract }; }) as Array; }); const collectionIds = uniq( nftBalances .map((v) => v.collectionId) .filter((v): v is string => !!v) ); const { collections } = await getCollectionRecords( account, collectionIds ); return { collections, nftBalances, account }; }; }