Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding fallback when indexer is down to fetch nfts owners #671

43 changes: 36 additions & 7 deletions src/antelope/chains/EVMChainSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -378,11 +406,12 @@ export default abstract class EVMChainSettings implements ChainSettings {
imageCache: nftResponse.imageCache,
tokenUri: nftResponse.tokenUri,
supply: nftResponse.tokenIdSupply,
owner: isErc1155 ? undefined : nftResponse.owner,
owner: nftResponse.owner ?? account,
}));

this.processNftContractsCalldata(response.contracts);
const shapedNftData = this.shapeNftRawData(shapedIndexerNftData, response.contracts);

return this.processNftRawData(shapedNftData);
}

Expand Down Expand Up @@ -448,7 +477,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;
}

Expand Down
3 changes: 1 addition & 2 deletions src/antelope/stores/nfts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,10 +294,9 @@ export const useNftsStore = defineStore(store_name, {
updateNftOwnerData(label: Label, contractAddress: string, tokenId: string): Promise<void> {
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(
Expand Down
44 changes: 37 additions & 7 deletions src/antelope/types/NFTClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { IPFS_GATEWAY, extractNftMetadata } from 'src/antelope/stores/utils/nft-
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 {
Expand Down Expand Up @@ -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;

Expand All @@ -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
Expand All @@ -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;
}

Expand Down Expand Up @@ -164,7 +178,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,
Expand Down Expand Up @@ -305,7 +319,23 @@ export class Erc1155Nft extends NFT {
return this._owners;
}

async updateOwnerData(indexer: AxiosInstance): Promise<void> {
this._owners = await getErc1155Owners(this.contractAddress, this.id, indexer);

async updateOwnerData(): Promise<void> {
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 };
}
}
}
75 changes: 75 additions & 0 deletions src/antelope/wallets/AntelopeWallets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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';

export class AntelopeWallets {

private trace: (message: string, ...args: unknown[]) => void;
private authenticators: Map<string, EVMAuthenticator> = new Map();
constructor() {
this.trace = createTraceFunction(name);
}

init() {
useFeedbackStore().setDebug(name, isTracingAll());
}

addEVMAuthenticator(authenticator: EVMAuthenticator) {
this.trace('addEVMAuthenticator', authenticator.getName(), authenticator);
this.authenticators.set(authenticator.getName(), authenticator);
}

getAuthenticator(name: string) {
this.trace('getAuthenticator', name);
return this.authenticators.get(name);
}

getChainSettings(label: string) {
return (useChainStore().getChain(label).settings as EVMChainSettings);
}

async getWeb3Provider(): Promise<ethers.providers.Web3Provider> {
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');
}
}
}
78 changes: 1 addition & 77 deletions src/antelope/wallets/index.ts
Original file line number Diff line number Diff line change
@@ -1,83 +1,7 @@
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';

export class AntelopeWallets {

private trace: (message: string, ...args: unknown[]) => void;
private authenticators: Map<string, EVMAuthenticator> = new Map();
constructor() {
this.trace = createTraceFunction(name);
}

init() {
useFeedbackStore().setDebug(name, isTracingAll());
}

addEVMAuthenticator(authenticator: EVMAuthenticator) {
this.trace('addEVMAuthenticator', authenticator.getName(), authenticator);
this.authenticators.set(authenticator.getName(), authenticator);
}

getAuthenticator(name: string) {
this.trace('getAuthenticator', name);
return this.authenticators.get(name);
}

getChainSettings(label: string) {
return (useChainStore().getChain(label).settings as EVMChainSettings);
}

async getWeb3Provider(): Promise<ethers.providers.Web3Provider> {
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';
export * from 'src/antelope/wallets/authenticators/InjectedProviderAuth';
export * from 'src/antelope/wallets/authenticators/MetamaskAuth';
export * from 'src/antelope/wallets/authenticators/OreIdAuth';
export * from 'src/antelope/wallets/authenticators/WalletConnectAuth';
export * from 'src/antelope/wallets/authenticators/SafePalAuth';
export * from 'src/antelope/wallets/AntelopeWallets';
3 changes: 2 additions & 1 deletion src/components/evm/AppNav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ export default defineComponent({
return '0';
},
isProduction() {
return process.env.NODE_ENV === 'production';
// only enable demo route for staging & development
return window.location.origin.includes('telos.net');
},
accountActionText() {
if (this.loggedAccount) {
Expand Down
1 change: 1 addition & 0 deletions src/i18n/en-us/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions src/pages/demo/DemoLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
<li>
<router-link :to="{ name: 'demos.scrollable-cards' }">Scrollable Info Cards</router-link>
</li>
<li>
<router-link :to="{ name: 'demos.indexer' }">Simulate Indexer Down</router-link>
</li>
</ul>

<router-view />
Expand Down
Loading
Loading