From 73a97b3f9e83242ad26d5def9bae0a5b7f0f09d9 Mon Sep 17 00:00:00 2001 From: Viterbo Date: Thu, 16 Nov 2023 01:17:46 -0300 Subject: [PATCH 1/9] adding fallback when indexer is down to fetch nfts owners --- src/antelope/chains/EVMChainSettings.ts | 48 +++++++++++++++++++++---- src/antelope/stores/nfts.ts | 3 +- src/antelope/types/NFTClass.ts | 44 +++++++++++++++++++---- src/antelope/wallets/index.ts | 46 ++++++++++++++++++++++++ src/i18n/en-us/index.js | 1 + src/pages/demo/DemoLayout.vue | 3 ++ src/pages/demo/IndexerDemos.vue | 34 ++++++++++++++++++ src/pages/evm/nfts/NftDetailsPage.vue | 21 +++++++---- src/pages/evm/nfts/NftInventoryPage.vue | 6 +++- src/router/routes.js | 5 +++ 10 files changed, 189 insertions(+), 22 deletions(-) create mode 100644 src/pages/demo/IndexerDemos.vue diff --git a/src/antelope/chains/EVMChainSettings.ts b/src/antelope/chains/EVMChainSettings.ts index 2b2869ae1..2671dd331 100644 --- a/src/antelope/chains/EVMChainSettings.ts +++ b/src/antelope/chains/EVMChainSettings.ts @@ -32,9 +32,9 @@ import { IndexerCollectionNftsResponse, Erc721Nft, getErc721Owner, - getErc1155Owners, Erc1155Nft, AntelopeError, + getErc1155OwnersFromIndexer, } from 'src/antelope/types'; import EvmContract from 'src/antelope/stores/utils/contracts/EvmContract'; import { ethers } from 'ethers'; @@ -83,6 +83,16 @@ export default abstract class EVMChainSettings implements ChainSettings { resolve?: (value: EvmContract | false) => void; }> = {}; + // this variable helps to show the indexer health warning only once per session + indexerHealthWarningShown = false; + + // This variable is used to simulate a bad indexer health state + indexerBadHealthSimulated = false; + + simulateIndexerDown(isBad: boolean) { + this.indexerBadHealthSimulated = isBad; + } + constructor(network: string) { this.network = network; @@ -202,11 +212,29 @@ export default abstract class EVMChainSettings implements ChainSettings { return promise; } + /** + * This function checks if the indexer is healthy and warns the user if it is not. + * This warning should appear only once per session. + */ + checkAndWarnIndexerHealth() { + if (!this.indexerHealthWarningShown && !this.isIndexerHealthy()) { + this.indexerHealthWarningShown = true; + const ant = getAntelope(); + ant.config.notifyNeutralMessageHandler( + ant.config.localizationHandler('antelope.chain.indexer_bad_health_warning'), + ); + } + } + isIndexerHealthy(): boolean { - return ( - this._indexerHealthState.state.success && - this._indexerHealthState.state.secondsBehind < getAntelope().config.indexerHealthThresholdSeconds - ); + if (this.indexerBadHealthSimulated) { + return false; + } else { + return ( + this._indexerHealthState.state.success && + this._indexerHealthState.state.secondsBehind < getAntelope().config.indexerHealthThresholdSeconds + ); + } } get indexerHealthState(): IndexerHealthResponse { @@ -374,6 +402,14 @@ export default abstract class EVMChainSettings implements ChainSettings { this.processNftContractsCalldata(response.contracts); const shapedNftData = this.shapeNftRawData(shapedIndexerNftData, response.contracts); + + // if the owner does not comes from the indexer, let's set it + shapedNftData.forEach((nft) => { + if (!nft.data.owner) { + nft.data.owner = account; + } + }); + return this.processNftRawData(shapedNftData); } @@ -439,7 +475,7 @@ export default abstract class EVMChainSettings implements ChainSettings { if (!ownersUpdatedWithinThreeMins) { const indexer = this.getIndexer(); - const owners = await getErc1155Owners(nft.contractAddress, nft.id, indexer); + const owners = await getErc1155OwnersFromIndexer(nft.contractAddress, nft.id, indexer); nft.owners = owners; } diff --git a/src/antelope/stores/nfts.ts b/src/antelope/stores/nfts.ts index 70bf91d2f..8655d26d6 100644 --- a/src/antelope/stores/nfts.ts +++ b/src/antelope/stores/nfts.ts @@ -294,10 +294,9 @@ export const useNftsStore = defineStore(store_name, { updateNftOwnerData(label: Label, contractAddress: string, tokenId: string): Promise { this.trace('updateNftOwnerData', label, contractAddress, tokenId); const network = useChainStore().getChain(label).settings.getNetwork(); - const indexer = (useChainStore().getChain(label).settings as EVMChainSettings).getIndexer(); const nft = this.__contracts[network][contractAddress.toLowerCase()].list.find(nft => nft.id === tokenId); - return nft?.updateOwnerData(indexer) ?? Promise.reject('NFT not found'); + return nft?.updateOwnerData() ?? Promise.reject('NFT not found'); }, async transferNft( diff --git a/src/antelope/types/NFTClass.ts b/src/antelope/types/NFTClass.ts index 2f5a6f2ed..e580000a1 100644 --- a/src/antelope/types/NFTClass.ts +++ b/src/antelope/types/NFTClass.ts @@ -12,9 +12,9 @@ import { extractNftMetadata } from 'src/antelope/stores/utils/nft-utils'; import { useContractStore } from 'src/antelope/stores/contract'; import { useNftsStore } from 'src/antelope/stores/nfts'; import EVMChainSettings from 'src/antelope/chains/EVMChainSettings'; -import { CURRENT_CONTEXT } from 'src/antelope'; +import { CURRENT_CONTEXT, useAccountStore, useChainStore } from 'src/antelope'; import { AxiosInstance } from 'axios'; -import { Contract } from 'ethers'; +import { Contract, ethers } from 'ethers'; import { AntelopeError } from 'src/antelope/types'; export interface NftAttribute { @@ -69,7 +69,7 @@ export async function getErc721Owner(contract: Contract, tokenId: string): Promi return await contract.ownerOf(tokenId); } -export async function getErc1155Owners(contractAddress: string, tokenId: string, indexer: AxiosInstance): Promise<{ [address: string]: number }> { +export async function getErc1155OwnersFromIndexer(contractAddress: string, tokenId: string, indexer: AxiosInstance): Promise<{ [address: string]: number }> { const holdersResponse = (await indexer.get(`/v1/token/${contractAddress}/holders?limit=10000&token_id=${tokenId}`)).data as IndexerTokenHoldersResponse; const holders = holdersResponse.results; @@ -79,6 +79,20 @@ export async function getErc1155Owners(contractAddress: string, tokenId: string, }, {} as { [address: string]: number }); } +export async function getErc1155OwnersFromContract(ownerAddress: string, tokenId: string, contract: Contract): Promise<{ [address: string]: number }> { + // we create a reduced list of owners containing just the balance of the current user + // because we can't get all the owners from the contract (without a loop) + const _owners = await contract.balanceOf(ownerAddress, tokenId).then((balance: ethers.BigNumber) => { + const _balance = balance.toNumber(); + const _owners: { [address: string]: number } = {}; + _owners[ownerAddress] = _balance; + return _owners; + }); + return _owners; +} + + + /** * Construct an NFT from indexer data * @param contract The contract this NFT belongs to @@ -104,7 +118,7 @@ export async function constructNft( const cachedNft = nftStore.__contracts[network]?.[contract.address]?.list.find(nft => nft.id === indexerData.tokenId); if (cachedNft) { - await cachedNft.updateOwnerData(chainSettings.getIndexer()); + await cachedNft.updateOwnerData(); return cachedNft; } @@ -162,7 +176,7 @@ export async function constructNft( } const indexer = chainSettings.getIndexer(); - const owners = await getErc1155Owners(contract.address, indexerData.tokenId, indexer); + const owners = await getErc1155OwnersFromIndexer(contract.address, indexerData.tokenId, indexer); return new Erc1155Nft({ ...commonData, @@ -303,7 +317,23 @@ export class Erc1155Nft extends NFT { return this._owners; } - async updateOwnerData(indexer: AxiosInstance): Promise { - this._owners = await getErc1155Owners(this.contractAddress, this.id, indexer); + + async updateOwnerData(): Promise { + const chainSettings = (useChainStore().currentChain.settings as EVMChainSettings); + if (chainSettings.isIndexerHealthy()) { + const indexer = chainSettings.getIndexer(); + this._owners = await getErc1155OwnersFromIndexer(this.contractAddress, this.id, indexer); + } else { + const account = useAccountStore().getAccount(CURRENT_CONTEXT); + const contract = await useContractStore().getContract(CURRENT_CONTEXT, this.contractAddress); + const contractInstance = await contract?.getContractInstance(); + + if (!contractInstance) { + throw new AntelopeError('antelope.utils.error_contract_instance'); + } + + const updated_owners = await getErc1155OwnersFromContract(account.account, this.id, contractInstance); + this._owners = { ...this._owners, ...updated_owners }; + } } } diff --git a/src/antelope/wallets/index.ts b/src/antelope/wallets/index.ts index 845b0d408..53f7baaea 100644 --- a/src/antelope/wallets/index.ts +++ b/src/antelope/wallets/index.ts @@ -2,6 +2,12 @@ import { isTracingAll } from 'src/antelope/stores/feedback'; import { useFeedbackStore } from 'src/antelope/stores/feedback'; import { createTraceFunction } from 'src/antelope/stores/feedback'; import { EVMAuthenticator } from 'src/antelope/wallets/authenticators/EVMAuthenticator'; +import { useAccountStore } from 'src/antelope/stores/account'; +import { CURRENT_CONTEXT, useChainStore } from 'src/antelope'; +import { RpcEndpoint } from 'universal-authenticator-library'; +import { ethers } from 'ethers'; +import EVMChainSettings from 'src/antelope/chains/EVMChainSettings'; +import { AntelopeError } from 'src/antelope/types'; const name = 'AntelopeWallets'; @@ -27,6 +33,46 @@ export class AntelopeWallets { return this.authenticators.get(name); } + getChainSettings(label: string) { + return (useChainStore().getChain(label).settings as EVMChainSettings); + } + + async getWeb3Provider(): Promise { + this.trace('getWeb3Provider'); + const account = useAccountStore().getAccount(CURRENT_CONTEXT); + try { + // we try first the best solution which is taking the provider from the current authenticator + const authenticator = account.authenticator as EVMAuthenticator; + const provider = authenticator.web3Provider(); + return provider; + } catch(e1) { + this.trace('getWeb3Provider authenticator.web3Provider() Failed!', e1); + } + + // we try to build a web3 provider from a local injected provider it it exists + try { + if (window.ethereum) { + const web3Provider = new ethers.providers.Web3Provider(window.ethereum); + await web3Provider.ready; + return web3Provider; + } + } catch(e2) { + this.trace('getWeb3Provider authenticator.web3Provider() Failed!', e2); + } + + try { + const p:RpcEndpoint = this.getChainSettings(CURRENT_CONTEXT).getRPCEndpoint(); + const url = `${p.protocol}://${p.host}:${p.port}${p.path ?? ''}`; + const jsonRpcProvider = new ethers.providers.JsonRpcProvider(url); + await jsonRpcProvider.ready; + const web3Provider = jsonRpcProvider as ethers.providers.Web3Provider; + return web3Provider; + } catch (e3) { + this.trace('getWeb3Provider authenticator.web3Provider() Failed!', e3); + throw new AntelopeError('antelope.evn.error_no_provider'); + } + } + } export * from 'src/antelope/wallets/authenticators/EVMAuthenticator'; diff --git a/src/i18n/en-us/index.js b/src/i18n/en-us/index.js index e428f3071..c135ffa1a 100644 --- a/src/i18n/en-us/index.js +++ b/src/i18n/en-us/index.js @@ -575,6 +575,7 @@ export default { error_no_default_authenticator: 'No default authenticator found', error_settings_not_found: 'Settings not found', error_staked_ratio: 'Error in getting staked ratio', + indexer_bad_health_warning: 'The indexer is not healthy. You may encounter outdated data.', }, account: { error_login_native: 'An error has occurred trying to login to the native chain', diff --git a/src/pages/demo/DemoLayout.vue b/src/pages/demo/DemoLayout.vue index 91cb7f88a..a538750d7 100644 --- a/src/pages/demo/DemoLayout.vue +++ b/src/pages/demo/DemoLayout.vue @@ -24,6 +24,9 @@
  • Scrollable Info Cards
  • +
  • + Simulate Indexer Down +
  • diff --git a/src/pages/demo/IndexerDemos.vue b/src/pages/demo/IndexerDemos.vue new file mode 100644 index 000000000..e6c1ec595 --- /dev/null +++ b/src/pages/demo/IndexerDemos.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/src/pages/evm/nfts/NftDetailsPage.vue b/src/pages/evm/nfts/NftDetailsPage.vue index 3ae629040..ded99d22c 100644 --- a/src/pages/evm/nfts/NftDetailsPage.vue +++ b/src/pages/evm/nfts/NftDetailsPage.vue @@ -45,7 +45,8 @@ const nftStore = useNftsStore(); const chainStore = useChainStore(); const accountStore = useAccountStore(); -const explorerUrl = (chainStore.currentChain.settings as EVMChainSettings).getExplorerUrl(); +const chainSettings = chainStore.currentChain.settings as EVMChainSettings; +const explorerUrl = chainSettings.getExplorerUrl(); const contractAddress = route.query.contract as string; const nftId = route.query.id as string; @@ -150,11 +151,9 @@ onBeforeMount(async () => { let timer: ReturnType | undefined; const minuteMilliseconds = 60 * 1000; onMounted(() => { - const indexer = (chainStore.currentChain.settings as EVMChainSettings).getIndexer(); - // update owner info once per minute timer = setInterval(async () => { - nft.value?.updateOwnerData(indexer); + nft.value?.updateOwnerData(); }, minuteMilliseconds); }); @@ -178,8 +177,18 @@ watch(loggedAccount, (newAccount: EvmAccountModel) => { } }); + +const ownerFirstCheck = ref(false); +const indexer = chainSettings.getIndexer(); // if details refresh with new owner (on transfer), disable transfer functionality watch(nft, () => { + // Only if the indexer is down or behind, we want to update the owner data only once + // just in case the current user is not the owner anymore + if (nft.value && !chainSettings.isIndexerHealthy() && !ownerFirstCheck.value) { + ownerFirstCheck.value = true; + nft.value.updateOwnerData(); + } + const shouldDisableTransfer = !nft.value || (isErc721.value && nftAsErc721.value.owner !== loggedAccount.value?.address) || (isErc1155.value && !nftAsErc1155.value.owners[loggedAccount.value?.address]); @@ -249,7 +258,7 @@ function removeTab(tab: Tab){