Skip to content

Commit

Permalink
Merge branch 'main' into rate_limiter
Browse files Browse the repository at this point in the history
  • Loading branch information
dantaik authored May 4, 2024
2 parents 9621529 + f769e77 commit 1ff1573
Show file tree
Hide file tree
Showing 20 changed files with 361 additions and 14,972 deletions.
4 changes: 4 additions & 0 deletions packages/bridge-ui/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ export PUBLIC_SLOW_L1_BRIDGING_WARNING=false

# Fees
export PUBLIC_FEE_MULTIPLIER=

# APIs
export MORALIS_PROJECT_ID=""
export MORALIS_API_KEY=""
2 changes: 2 additions & 0 deletions packages/bridge-ui/.eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ package-lock.json
yarn.lock

src/generated/*

.vercel/**
1 change: 1 addition & 0 deletions packages/bridge-ui/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ CHANGELOG.md
pnpm-lock.yaml
package-lock.json
yarn.lock
.vercel/**
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.3.1",
"@wagmi/core": "^2.8.1",
"@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": "^4.0.0",
"viem": "^2.9.29"
Expand Down
7 changes: 7 additions & 0 deletions packages/bridge-ui/src/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,10 @@ export const ipfsConfig = {
gatewayTimeout: 200,
overallTimeout: 5000,
};

export const moralisApiConfig = {
limit: 10,
format: 'decimal',
excludeSpam: true,
mediaItems: false,
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,26 @@
import { selectedImportMethod } from './state';
let foundNFTs: NFT[] = [];
// States
let scanning = false;
let canProceed = false;
export let validating = false;
const scanForNFTs = async () => {
const nextPage = async () => {
await scanForNFTs(false);
};
const scanForNFTs = async (refresh: boolean) => {
scanning = true;
$selectedNFTs = [];
const accountAddress = $account?.address;
const srcChainId = $srcChain?.id;
const destChainId = $destChain?.id;
if (!accountAddress || !srcChainId || !destChainId) return;
const nftsFromAPIs = await fetchNFTs(accountAddress, srcChainId);
const nftsFromAPIs = await fetchNFTs({ address: accountAddress, chainId: srcChainId, refresh });
foundNFTs = nftsFromAPIs.nfts;
scanning = false;
Expand All @@ -47,9 +55,6 @@
reset();
};
// States
let scanning = false;
$: canImport = ($account?.isConnected && $srcChain?.id && $destChain && !scanning) || false;
$: {
Expand All @@ -74,9 +79,9 @@
{#if $selectedImportMethod === ImportMethod.MANUAL}
<ManualImport bind:validating />
{:else if $selectedImportMethod === ImportMethod.SCAN}
<ScannedImport {scanForNFTs} bind:foundNFTs bind:canProceed />
<ScannedImport refresh={() => scanForNFTs(true)} {nextPage} bind:foundNFTs bind:canProceed />
{:else}
<ImportActions bind:scanning {canImport} {scanForNFTs} />
<ImportActions bind:scanning {canImport} scanForNFTs={() => scanForNFTs(false)} />
{/if}

<OnAccount change={onAccountChange} />
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,45 @@
import { enteredAmount, selectedNFTs, tokenBalance } from '$components/Bridge/state';
import { ImportMethod } from '$components/Bridge/types';
import { ActionButton, Button } from '$components/Button';
import { IconFlipper } from '$components/Icon';
import { Icon, IconFlipper } from '$components/Icon';
import RotatingIcon from '$components/Icon/RotatingIcon.svelte';
import { NFTDisplay } from '$components/NFTs';
import { NFTView } from '$components/NFTs/types';
import type { NFT } from '$libs/token';
import { selectedImportMethod } from './state';
export let scanForNFTs: () => Promise<void>;
export let refresh: () => Promise<void>;
export let nextPage: () => Promise<void>;
export let foundNFTs: NFT[] = [];
export let canProceed = false;
let nftView: NFTView = NFTView.LIST;
let scanning = false;
let hasMoreNFTs = true;
let tokenAmountInput: TokenAmountInput;
function onScanClick() {
let previousNFTs: NFT[] = [];
const handleNextPage = () => {
previousNFTs = foundNFTs;
scanning = true;
scanForNFTs().finally(() => {
nextPage().finally(() => {
scanning = false;
});
if (previousNFTs.length === foundNFTs.length) {
hasMoreNFTs = false;
}
};
function onRefreshClick() {
scanning = true;
hasMoreNFTs = true;
refresh().finally(() => {
scanning = false;
});
}
Expand Down Expand Up @@ -81,7 +98,7 @@
type="neutral"
shape="circle"
class="bg-neutral rounded-full w-[28px] h-[28px] border-none"
on:click={onScanClick}>
on:click={onRefreshClick}>
<RotatingIcon loading={scanning} type="refresh" size={13} />
</Button>

Expand All @@ -97,6 +114,21 @@
</div>
<div>
<NFTDisplay loading={scanning} nfts={foundNFTs} {nftView} />
<div class="flex pt-[18px]">
<button
class="btn btn-sm rounded-full items-center {hasMoreNFTs
? 'border-primary-brand'
: 'border-none'} dark:text-white hover:bg-primary-interactive-hover btn-secondary bg-transparent light:text-black"
disabled={!hasMoreNFTs}
on:click={handleNextPage}>
{#if hasMoreNFTs}
<span class="text-primary-color">{$t('paginator.more')}</span>
{:else}
<Icon type="check-circle" class="text-primary-brand" />
<span class="text-primary-color">{$t('paginator.everything_loaded')}</span>
{/if}
</button>
</div>
</div>
</section>
{#if nftHasAmount}
Expand Down
4 changes: 3 additions & 1 deletion packages/bridge-ui/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,9 @@
},
"paginator": {
"of": "of",
"page": "Page"
"page": "Page",
"more": "Fetch more...",
"everything_loaded": "Everything loaded"
},
"paused_modal": {
"description": "The bridge is currently not available. Follow our official communication channels for more information. ",
Expand Down
131 changes: 84 additions & 47 deletions packages/bridge-ui/src/libs/bridge/fetchNFTs.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,86 @@
import type { Address } from 'viem';

import { isL2Chain } from '$libs/chain';
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';
import type { FetchNftArgs } from '$nftAPI/infrastructure/types/common';

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);
}
});
export const fetchNFTs = async ({
address: userAddress,
chainId: srcChainId,
refresh,
}: FetchNftArgs): Promise<{ nfts: NFT[]; error: Error | null }> => {
let nfts: NFT[] = [];
try {
if (isL2Chain(srcChainId)) {
// Todo: replace with a third party service once available
// right now we have to use our own indexer for L2
nfts = await fetchL2NFTs({ userAddress, srcChainId, refresh });
} else {
nfts = await fetchL1NFTs({ userAddress, srcChainId, refresh });
}

const promises = Promise.all(
nfts.map(async (nft: NFT) => {
const nftWithImage = await fetchNFTImageUrl(nft);
return nftWithImage;
}),
);

const nftsWithImage = await promises;
nfts = nftsWithImage;
return { nfts, error: null };
} catch (error) {
console.error('Fetch error:', error);
return { nfts: [], error: new Error('') };
}
};

const fetchL1NFTs = async ({
userAddress,
srcChainId,
refresh,
}: {
userAddress: Address;
srcChainId: number;
refresh: boolean;
}) => {
log('fetching L1 NFTs', { userAddress, srcChainId, refresh });
const moralisResponse = await fetch('/api/nft', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ address: userAddress, chainId: srcChainId, refresh }),
});
return Array.from(nftMap.values());
}

export async function fetchNFTs(
userAddress: Address,
srcChainId: number,
): Promise<{ nfts: NFT[]; error: Error | null }> {
let error: Error | null = null;
if (moralisResponse.ok) {
const responseData = await moralisResponse.json();
log('cursor', responseData);
const { nfts } = responseData;

// Fetch from all indexers
return nfts;
} else {
console.error('HTTP error:', moralisResponse.statusText);
return { nfts: [], error: new Error(moralisResponse.statusText) };
}
};

const fetchL2NFTs = async ({
userAddress,
srcChainId,
refresh,
}: {
userAddress: Address;
srcChainId: number;
refresh: boolean;
}) => {
log('fetching L2 NFTs', { userAddress, srcChainId, refresh });
const indexerPromises: Promise<NFT[]>[] = eventIndexerApiServices.map(async (eventIndexerApiService) => {
const { items: result } = await eventIndexerApiService.getAllNftsByAddressFromAPI(userAddress, BigInt(srcChainId), {
page: 0,
Expand Down Expand Up @@ -60,36 +111,22 @@ export async function fetchNFTs(
nftArrays = await Promise.all(indexerPromises);
} catch (e) {
log('error fetching nfts from indexer services', e);
error = e as Error;
throw e;
}
return deduplicateNFTs(nftArrays);
};

// 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;
// Deduplicate based on address and chainID
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}`;

// 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 (!nftMap.has(uniqueKey)) {
nftMap.set(uniqueKey, nft);
}
});
});

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 };
return Array.from(nftMap.values());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { NFT } from '$nftAPI/domain/models/NFT';
import type { FetchNftArgs } from '$nftAPI/infrastructure/types/common';

export interface INFTRepository {
findByAddress({ address, chainId, refresh }: FetchNftArgs): Promise<NFT[]>;
}
17 changes: 17 additions & 0 deletions packages/bridge-ui/src/libs/nft/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: string | number;
tokenId: number | string;
uri?: string;
metadata?: NFTMetadata;
}
20 changes: 20 additions & 0 deletions packages/bridge-ui/src/libs/nft/domain/services/NFTService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Address } from 'viem';

import type { INFTRepository } from '../interfaces/INFTRepository';
import type { NFT } from '../models/NFT';

export class NFTService {
constructor(private repository: INFTRepository) {}

async fetchNFTsByAddress({
address,
chainId,
refresh,
}: {
address: Address;
chainId: number;
refresh: boolean;
}): Promise<NFT[]> {
return await this.repository.findByAddress({ address, chainId, refresh });
}
}
Loading

0 comments on commit 1ff1573

Please sign in to comment.