diff --git a/.vscode/settings.json b/.vscode/settings.json index 5eb9b7735..ae74f58c1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,4 +4,4 @@ "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"], "vetur.experimental.templateInterpolationService": true -} +} \ No newline at end of file diff --git a/src/antelope/chains/EVMChainSettings.ts b/src/antelope/chains/EVMChainSettings.ts index 514534afd..faaf46fd9 100644 --- a/src/antelope/chains/EVMChainSettings.ts +++ b/src/antelope/chains/EVMChainSettings.ts @@ -47,6 +47,7 @@ import { dateIsWithinXMinutes } from 'src/antelope/stores/utils/date-utils'; import { CURRENT_CONTEXT, getAntelope, useContractStore, useNftsStore } from 'src/antelope'; import { WEI_PRECISION, PRICE_UPDATE_INTERVAL_IN_MIN } from 'src/antelope/stores/utils'; import { BehaviorSubject, filter } from 'rxjs'; +import { createTraceFunction } from 'src/antelope/config'; export default abstract class EVMChainSettings implements ChainSettings { @@ -97,6 +98,9 @@ export default abstract class EVMChainSettings implements ChainSettings { // This observable is used to check if the indexer health state was already checked indexerChecked$ = new BehaviorSubject(false); + // This function is used to trace the execution of the code + trace = createTraceFunction('EVMChainSettings'); + simulateIndexerDown(isBad: boolean) { this.indexerBadHealthSimulated = isBad; } @@ -152,6 +156,7 @@ export default abstract class EVMChainSettings implements ChainSettings { } async init(): Promise { + this.trace('init'); // this is called only when this chain is needed to avoid initialization of all chains if (this.ready) { return this.initPromise; @@ -302,6 +307,7 @@ export default abstract class EVMChainSettings implements ChainSettings { } async getBalances(account: string): Promise { + this.trace('getBalances', account); if (!this.hasIndexerSupport()) { console.error('Indexer API not supported for this chain:', this.getNetwork()); return []; @@ -366,6 +372,7 @@ export default abstract class EVMChainSettings implements ChainSettings { // get the NFTs belonging to a particular contract (collection) async getNftsForCollection(collection: string, params: IndexerCollectionNftsFilter): Promise { + this.trace('getNftsForCollection', collection, params); if (!this.hasIndexerSupport()) { console.error('Error fetching NFTs, Indexer API not supported for this chain:', this.getNetwork()); return []; @@ -384,6 +391,7 @@ export default abstract class EVMChainSettings implements ChainSettings { supply: nftResponse.supply, owner: nftResponse.owner, })); + this.trace('getNftsForCollection', { shapedIndexerNftData, response }); // we fix the supportedInterfaces property if it is undefined in the response but present in the request Object.values(response.contracts).forEach((contract) => { @@ -392,7 +400,10 @@ export default abstract class EVMChainSettings implements ChainSettings { this.processNftContractsCalldata(response.contracts); const shapedNftData = this.shapeNftRawData(shapedIndexerNftData, response.contracts); - return this.processNftRawData(shapedNftData); + this.trace('getNftsForCollection', { shapedNftData }); + const finalNftData = this.processNftRawData(shapedNftData); + this.trace('getNftsForCollection', { finalNftData }); + return finalNftData; } // get the NFTs belonging to a particular account @@ -471,6 +482,7 @@ export default abstract class EVMChainSettings implements ChainSettings { // process the shaped raw data into NFTs async processNftRawData(shapedRawNfts: NftRawData[]): Promise { + this.trace('processNftRawData', shapedRawNfts); const contractStore = useContractStore(); const nftsStore = useNftsStore(); @@ -502,6 +514,7 @@ export default abstract class EVMChainSettings implements ChainSettings { return nft; }); + this.trace('processNftRawData', 'erc1155Nfts', erc1155Nfts); const erc721RawData = shapedRawNfts.filter(({ contract }) => contract.supportedInterfaces.includes('erc721')); const erc721Nfts = erc721RawData.map(async ({ data, contract }) => { @@ -519,6 +532,7 @@ export default abstract class EVMChainSettings implements ChainSettings { return nft; }); + this.trace('processNftRawData', 'erc721Nfts', erc721Nfts); const settledPromises = await Promise.allSettled([...erc1155Nfts, ...erc721Nfts]); @@ -529,7 +543,10 @@ export default abstract class EVMChainSettings implements ChainSettings { console.error('Error constructing NFT', reason); }); - return fulfilledPromises.map(result => result.value as Collectible); + const nfts = fulfilledPromises.map(result => result.value as Collectible); + this.trace('processNftRawData', 'nfts', nfts); + + return nfts; } constructTokenId(token: TokenSourceInfo): string { diff --git a/src/antelope/stores/account.ts b/src/antelope/stores/account.ts index d34c9bf10..5a52645fa 100644 --- a/src/antelope/stores/account.ts +++ b/src/antelope/stores/account.ts @@ -48,6 +48,7 @@ export interface LoginNativeActionData { export interface LoginEVMActionData { authenticator: EVMAuthenticator network: string, + autoLogAccount?: string, } export interface SendActionData { @@ -153,14 +154,14 @@ export const useAccountStore = defineStore(store_name, { return success; }, - async loginEVM({ authenticator, network }: LoginEVMActionData): Promise { + async loginEVM({ authenticator, network, autoLogAccount }: LoginEVMActionData): Promise { this.trace('loginEVM', network); const label = authenticator.label; getAntelope().events.onClear.next({ label }); let success = false; try { - const rawAddress = await authenticator.login(network); + const rawAddress = autoLogAccount ? await authenticator.autoLogin(network, autoLogAccount) : await authenticator.login(network); this.trace('loginEVM', 'authenticator finished with address', rawAddress); if (rawAddress) { @@ -242,6 +243,7 @@ export const useAccountStore = defineStore(store_name, { useFeedbackStore().setLoading('account.autoLogin'); const network = localStorage.getItem('network'); const account = localStorage.getItem('account'); + const rawAddress = localStorage.getItem('rawAddress'); const isNative = localStorage.getItem('isNative') === 'true'; const autoLogin = localStorage.getItem('autoLogin'); this.trace('autoLogin', account, isNative, autoLogin); @@ -265,9 +267,11 @@ export const useAccountStore = defineStore(store_name, { console.error(getAntelope().wallets); throw new Error('antelope.account.error_auto_login'); } + const autoLogAccount = rawAddress ?? account; return this.loginEVM({ authenticator, network, + autoLogAccount, }); } } @@ -293,6 +297,31 @@ export const useAccountStore = defineStore(store_name, { } }, + async assertNetworkConnection(label: string): Promise { + if (!await useAccountStore().isConnectedToCorrectNetwork(label)) { + return new Promise(async (resolve) => { + const ant = getAntelope(); + const authenticator = useAccountStore().loggedAccount.authenticator as EVMAuthenticator; + try { + await authenticator.ensureCorrectChain(); + if (!await useAccountStore().isConnectedToCorrectNetwork(label)) { + resolve(false); + } else { + resolve(true); + } + } catch (error) { + const message = (error as Error).message; + if (message === 'antelope.evm.error_switch_chain_rejected') { + ant.config.notifyNeutralMessageHandler(message); + } + resolve(false); + } + }); + } else { + return true; + } + }, + async sendAction({ account, data, name, actor, permission }: SendActionData): Promise { this.trace('sendAction', account, data, name, actor, permission); try { diff --git a/src/antelope/stores/allowances.ts b/src/antelope/stores/allowances.ts index 0f53bce9c..ab18018bf 100644 --- a/src/antelope/stores/allowances.ts +++ b/src/antelope/stores/allowances.ts @@ -1,5 +1,4 @@ import { defineStore } from 'pinia'; -import { filter } from 'rxjs'; import { formatUnits } from 'ethers/lib/utils'; import { BigNumber } from 'ethers'; @@ -36,6 +35,7 @@ import { CURRENT_CONTEXT, getAntelope, useAccountStore, + useBalancesStore, useChainStore, useContractStore, useFeedbackStore, @@ -217,15 +217,6 @@ export const useAllowancesStore = defineStore(store_name, { init: () => { const allowancesStore = useAllowancesStore(); const ant = getAntelope(); - ant.events.onAccountChanged.pipe( - filter(({ label, account }) => !!label && !!account), - ).subscribe({ - next: ({ label, account }) => { - if (label === CURRENT_CONTEXT && account?.account) { - allowancesStore.fetchAllowancesForAccount(account?.account); - } - }, - }); ant.events.onClear.subscribe(({ label }) => { allowancesStore.clearAllowances(label); @@ -554,9 +545,20 @@ export const useAllowancesStore = defineStore(store_name, { const tokenInfo = useTokensStore().__tokens[CURRENT_CONTEXT].find(token => token.address.toLowerCase() === data.contract.toLowerCase()); const tokenContract = await useContractStore().getContract(CURRENT_CONTEXT, data.contract); - const tokenContractInstance = await tokenContract?.getContractInstance(); - const maxSupply = await tokenContractInstance?.totalSupply() as BigNumber | undefined; - const balance = await tokenContractInstance?.balanceOf(data.owner) as BigNumber | undefined; + + const maxSupply = tokenContract?.maxSupply; + + const balancesStore = useBalancesStore(); + let balance = balancesStore.__balances[CURRENT_CONTEXT]?.find( + balance => balance.token.address.toLowerCase() === data.contract.toLowerCase(), + )?.amount; + + if (!balance) { + const indexer = (useChainStore().loggedChain.settings as EVMChainSettings).getIndexer(); + const balanceString = (await indexer.get(`/v1/token/${data.contract}/holders?account=${data.owner}`)).data.results[0].balance; + + balance = BigNumber.from(balanceString); + } if (!balance || !tokenInfo || !maxSupply) { return null; @@ -612,7 +614,10 @@ export const useAllowancesStore = defineStore(store_name, { } const collectionInfo = await useContractStore().getContract(CURRENT_CONTEXT, data.contract); - const balance = await (await collectionInfo?.getContractInstance())?.balanceOf(data.owner); + const indexer = (useChainStore().loggedChain.settings as EVMChainSettings).getIndexer(); + const balanceString = (await indexer.get(`/v1/token/${data.contract}/holders?account=${data.owner}`)).data.results[0].balance; + + const balance = BigNumber.from(balanceString); return collectionInfo ? { ...commonAttributes, @@ -641,14 +646,9 @@ export const useAllowancesStore = defineStore(store_name, { return null; } - const balancePromises = collectionNftIds.map(async (tokenId) => { - const contractInstance = await collectionInfo?.getContractInstance(); - return contractInstance?.balanceOf(data.owner, tokenId) as BigNumber; - }); - - - const balancesOfAllIdsInCollection = await Promise.all(balancePromises); - const balance = balancesOfAllIdsInCollection.reduce((acc, balance) => acc.add(balance ?? 0), BigNumber.from(0)); + const indexer = (useChainStore().loggedChain.settings as EVMChainSettings).getIndexer(); + const holderInfoForOwner = (await indexer.get(`/v1/token/${data.contract}/holders?account=${data.owner}&limit=${ALLOWANCES_LIMIT}`)).data.results as { balance: string }[]; + const totalNftsOwned = holderInfoForOwner.reduce((acc, holderInfo) => acc.add(holderInfo.balance), BigNumber.from(0)); return collectionInfo ? { lastUpdated: data.updated, @@ -657,7 +657,7 @@ export const useAllowancesStore = defineStore(store_name, { allowed: data.approved, collectionAddress: collectionInfo.address, collectionName: collectionInfo.name, - balance, + balance: totalNftsOwned, } : null; } catch(e) { console.error('Error shaping ERC1155 allowance row', e); diff --git a/src/antelope/stores/chain.ts b/src/antelope/stores/chain.ts index 177d18121..ba831bcf9 100644 --- a/src/antelope/stores/chain.ts +++ b/src/antelope/stores/chain.ts @@ -187,7 +187,7 @@ export const useChainStore = defineStore(store_name, { const stkToken = chain_settings.getStakedSystemToken(); const abi = [stlosAbiPreviewDeposit[0], stlosAbiPreviewRedeem[0]]; - const provider = await getAntelope().wallets.getWeb3Provider(); + const provider = await getAntelope().wallets.getWeb3Provider(label); const contractInstance = new ethers.Contract(stkToken.address, abi, provider); // Now we preview a deposit of 1 SYS to get the ratio const oneSys = ethers.utils.parseUnits('1.0', sysToken.decimals); diff --git a/src/antelope/stores/contract.ts b/src/antelope/stores/contract.ts index f587a2c72..18aacecac 100644 --- a/src/antelope/stores/contract.ts +++ b/src/antelope/stores/contract.ts @@ -45,9 +45,9 @@ import { const LOCAL_SORAGE_CONTRACTS_KEY = 'antelope.contracts'; -const createManager = (signer?: ethers.Signer):EvmContractManagerI => ({ +const createManager = (label: string, signer?: ethers.Signer):EvmContractManagerI => ({ getSigner: async () => signer ?? null, - getWeb3Provider: () => getAntelope().wallets.getWeb3Provider(), + getWeb3Provider: () => getAntelope().wallets.getWeb3Provider(label), getFunctionIface: (hash:string) => toRaw(useEVMStore().getFunctionIface(hash)), getEventIface: (hash:string) => toRaw(useEVMStore().getEventIface(hash)), }); @@ -61,9 +61,13 @@ export interface ContractStoreState { processing: Record> }, } - // addresses which have been checked and are known not to be contract addresses __accounts: { - [network: string]: string[], + [network: string]: { + // addresses which are being checked to see if they are account (non-contract) addresses + processing: Record>, + // addresses which have been checked and are known not to be contract addresses + addresses: string[], + }, }, } @@ -183,7 +187,7 @@ export const useContractStore = defineStore(store_name, { return this.__contracts[network].cached[addressLower]; } - const isContract = await this.addressIsContract(network, address); + const isContract = await this.addressIsContract(label, address); if (!isContract) { // address is an account, not a contract @@ -538,7 +542,7 @@ export const useContractStore = defineStore(store_name, { throw new AntelopeError('antelope.contracts.error_label_required'); } - const isContract = await this.addressIsContract(network, address); + const isContract = await this.addressIsContract(label, address); if (!isContract) { // address is an account, not a contract @@ -557,7 +561,7 @@ export const useContractStore = defineStore(store_name, { || (metadata.abi ?? []).length > 0 && (metadata.abi ?? []).length > (this.__contracts[network].cached[index]?.abi?.length ?? 0) ) { // This manager provides the signer and the web3 provider - metadata.manager = createManager(signer); + metadata.manager = createManager(label, signer); // we create the contract using the factory const contract = this.__factory.buildContract(metadata); @@ -581,26 +585,44 @@ export const useContractStore = defineStore(store_name, { this.__contracts[network].cached[index] = null; }, - async addressIsContract(network: string, address: string) { + async addressIsContract(label: string, address: string) { + const network = useChainStore().getChain(label).settings.getNetwork(); const addressLower = address.toLowerCase(); + + if (this.__contracts[network]?.cached[addressLower] || this.__contracts[network]?.metadata[addressLower]) { + return true; + } + if (!this.__accounts[network]) { - this.__accounts[network] = []; + this.__accounts[network] = { + processing: {}, + addresses: [], + }; } - if (this.__accounts[network].includes(addressLower)) { + if (this.__accounts[network].addresses.includes(addressLower)) { return false; } - const provider = await getAntelope().wallets.getWeb3Provider(); - const code = await provider.getCode(address); + if (!!this.__accounts[network].processing[addressLower]) { + return this.__accounts[network].processing[addressLower]; + } - const isContract = code !== '0x'; + this.__accounts[network].processing[addressLower] = new Promise(async (resolve) => { + const indexer = (useChainStore().loggedChain.settings as EVMChainSettings).getIndexer(); + const isContract = (await indexer.get(`/v1/contract/${addressLower}`)).data.results.length > 0; - if (!isContract && !this.__accounts[network].includes(addressLower)) { - this.__accounts[network].push(addressLower); - } + if (!isContract && !this.__accounts[network].addresses.includes(addressLower)) { + this.__accounts[network].addresses.push(addressLower); + } - return isContract; + resolve(isContract); + }); + + return this.__accounts[network].processing[addressLower].then((isContract) => { + delete this.__accounts[network].processing[addressLower]; + return isContract; + }); }, }, }); diff --git a/src/antelope/stores/history.ts b/src/antelope/stores/history.ts index 8abb115f5..382765b6e 100644 --- a/src/antelope/stores/history.ts +++ b/src/antelope/stores/history.ts @@ -53,6 +53,7 @@ export const transfers_filter_limit = 10000; export interface HistoryState { __evm_filter: IndexerTransactionsFilter; + __evm_last_filter_used: IndexerTransactionsFilter | null; __evm_transactions: { [label: Label]: { transactions: EvmTransaction[], @@ -146,30 +147,36 @@ export const useHistoryStore = defineStore(store_name, { await this.fetchEvmNftTransfersForAccount(label, this.__evm_filter.address); } - const transactionsResponse = await chainSettings.getEVMTransactions(toRaw(this.__evm_filter)); - const contracts = transactionsResponse.contracts; - const transactions = transactionsResponse.results; + const lastFilterUsedStr = JSON.stringify(this.__evm_last_filter_used); + const currentFilterStr = JSON.stringify(this.__evm_filter); + if (lastFilterUsedStr !== currentFilterStr) { + this.__evm_last_filter_used = { ... toRaw(this.__evm_filter) }; - this.setEvmTransactionsRowCount(label, transactionsResponse.total_count); + const transactionsResponse = await chainSettings.getEVMTransactions(toRaw(this.__evm_filter)); + const contracts = transactionsResponse.contracts; + const transactions = transactionsResponse.results; - const contractAddresses = Object.keys(contracts); - const parsedContracts: Record = {}; - contractAddresses.forEach((address: string) => { - const extraInfo = JSON.parse(contracts[address]?.calldata ?? '{}'); - parsedContracts[address] = { - ...contracts[address], - ...extraInfo, - }; - }); + this.setEvmTransactionsRowCount(label, transactionsResponse.total_count); - // cache contracts - contractAddresses.forEach((address) => { - contractStore.createAndStoreContract(label, address, parsedContracts[address]); - }); + const contractAddresses = Object.keys(contracts); + const parsedContracts: Record = {}; + contractAddresses.forEach((address: string) => { + const extraInfo = JSON.parse(contracts[address]?.calldata ?? '{}'); + parsedContracts[address] = { + ...contracts[address], + ...extraInfo, + }; + }); + + // cache contracts + contractAddresses.forEach((address) => { + contractStore.createAndStoreContract(label, address, parsedContracts[address]); + }); - this.setEVMTransactions(label, transactions); + this.setEVMTransactions(label, transactions); - await this.shapeTransactions(label, transactions); + await this.shapeTransactions(label, transactions); + } } catch (error) { console.error(error); throw new AntelopeError('antelope.history.error_fetching_transactions'); @@ -181,6 +188,7 @@ export const useHistoryStore = defineStore(store_name, { } else { feedbackStore.unsetLoading('history.fetchEVMTransactionsForAccount'); } + } }, @@ -488,6 +496,7 @@ const historyInitialState: HistoryState = { __evm_filter: { address: '', }, + __evm_last_filter_used: null, __shaped_evm_transaction_rows: { }, __evm_transactions_pagination_data: {}, diff --git a/src/antelope/stores/nfts.ts b/src/antelope/stores/nfts.ts index 91b3e45a7..0be253ee4 100644 --- a/src/antelope/stores/nfts.ts +++ b/src/antelope/stores/nfts.ts @@ -230,6 +230,7 @@ export const useNftsStore = defineStore(store_name, { tokenId, type, }; + this.trace('fetchNftDetails', 'filter:', new_filter); // If we already have a contract for that network and contract, we search for the NFT in that list first this.__contracts[network] = this.__contracts[network] || {}; @@ -246,6 +247,7 @@ export const useNftsStore = defineStore(store_name, { nft => nft.contractAddress.toLowerCase() === contract.toLowerCase() && nft.id === tokenId, ); if (nft) { + this.trace('fetchNftDetails', 'found in cache:', nft); return nft; } } else { @@ -260,6 +262,7 @@ export const useNftsStore = defineStore(store_name, { // we don't have the NFT on any cache, we fetch it from the indexer useFeedbackStore().setLoading('updateNFTsForAccount'); if (chain.settings.isNative() || (chain.settings as EVMChainSettings).hasIndexerSupport()) { + this.trace('fetchNftDetails', 'fetching from indexer'); promise = chain.settings.getNftsForCollection(contract, new_filter).then((nfts) => { const contractLower = contract.toLowerCase(); @@ -273,6 +276,7 @@ export const useNftsStore = defineStore(store_name, { if (!chain.settings.isNative()) { // this means we have the indexer down // we have the contract and the address so we try to fetch the NFT from the contract + this.trace('fetchNftDetails', 'indexer down, fetching from contract'); useEVMStore().getNFT( contract, tokenId, diff --git a/src/antelope/stores/rex.ts b/src/antelope/stores/rex.ts index e79ce986a..c6b6cba5f 100644 --- a/src/antelope/stores/rex.ts +++ b/src/antelope/stores/rex.ts @@ -74,7 +74,7 @@ export const useRexStore = defineStore(store_name, { filter(({ label, account }) => !!label && !!account), ).subscribe({ next: async ({ label, account }) => { - if (label === 'current') { + if (label === CURRENT_CONTEXT) { await useRexStore().updateRexDataForAccount(label, toRaw(account)); } }, diff --git a/src/antelope/stores/utils/contracts/EvmContract.ts b/src/antelope/stores/utils/contracts/EvmContract.ts index 1a1571d79..d3dc8a76c 100644 --- a/src/antelope/stores/utils/contracts/EvmContract.ts +++ b/src/antelope/stores/utils/contracts/EvmContract.ts @@ -1,4 +1,4 @@ -import { ContractInterface, ethers } from 'ethers'; +import { BigNumber, ContractInterface, ethers } from 'ethers'; import { markRaw } from 'vue'; import { AntelopeError, EvmContractCalldata, @@ -13,6 +13,7 @@ import { TRANSFER_SIGNATURES, } from 'src/antelope/types'; import { Interface } from 'ethers/lib/utils'; +import { parseUnits } from 'ethers/lib/utils'; export default class EvmContract { @@ -26,6 +27,7 @@ export default class EvmContract { private readonly _manager?: EvmContractManagerI; private readonly _token?: TokenSourceInfo | null; + private _contractInstance?: ethers.Contract; private _verified?: boolean; constructor({ @@ -117,6 +119,14 @@ export default class EvmContract { return this._token; } + get maxSupply() { + if (!this.isToken() || !this._properties?.supply || !this._properties?.decimals) { + return BigNumber.from(0); + } + + return parseUnits(this._properties.supply, this._properties.decimals); + } + isNonFungible() { return (this._supportedInterfaces.includes('erc721')); } @@ -137,6 +147,11 @@ export default class EvmContract { if (!this.abi){ throw new AntelopeError('antelope.utils.error_contract_instance'); } + + if (this._contractInstance) { + return this._contractInstance; + } + const signer = await this._manager?.getSigner(); let provider; @@ -145,6 +160,7 @@ export default class EvmContract { } const contract = new ethers.Contract(this.address, this.abi, signer ?? provider ?? undefined); + this._contractInstance = contract; return contract; } diff --git a/src/antelope/stores/utils/nft-utils.ts b/src/antelope/stores/utils/nft-utils.ts index e22235ce5..be4591fc8 100644 --- a/src/antelope/stores/utils/nft-utils.ts +++ b/src/antelope/stores/utils/nft-utils.ts @@ -47,15 +47,18 @@ export async function extractNftMetadata( // We need to look at the metadata // we iterate over the metadata properties for (const property in metadata) { - const value = metadata[property]; - if (!value) { + const _value = metadata[property]; + + if (typeof _value !== 'string') { continue; } + + const value = _value.replace('ipfs://', IPFS_GATEWAY) as string; + // if the value is a string and contains a valid url of a known media format, use it. // image formats: .gif, .avif, .apng, .jpeg, .jpg, .jfif, .pjpeg, .pjp, .png, .svg, .webp if ( !image && // if we already have a preview, we don't need to keep looking - typeof value === 'string' && urlIsPicture(value) ) { image = value; @@ -63,7 +66,6 @@ export async function extractNftMetadata( // audio formats: .mp3, .wav, .aac, .webm if ( !mediaSource && // if we already have a source, we don't need to keep looking - typeof value === 'string' && await urlIsAudio(value) ) { mediaType = NFTSourceTypes.AUDIO; @@ -72,7 +74,6 @@ export async function extractNftMetadata( // video formats: .mp4, .webm, .ogg if ( !mediaSource && // if we already have a source, we don't need to keep looking - typeof value === 'string' && await urlIsVideo(value) ) { mediaType = NFTSourceTypes.VIDEO; @@ -81,7 +82,7 @@ export async function extractNftMetadata( const regex = /^data:(image|audio|video)\/\w+;base64,[\w+/=]+$/; - const match = typeof value === 'string' && value.match(regex); + const match = value.match(regex); if (match) { const contentType = match[1]; diff --git a/src/antelope/types/NFTClass.ts b/src/antelope/types/NFTClass.ts index 48f2235d5..61610e3fc 100644 --- a/src/antelope/types/NFTClass.ts +++ b/src/antelope/types/NFTClass.ts @@ -16,6 +16,10 @@ import { CURRENT_CONTEXT, useAccountStore, useChainStore } from 'src/antelope'; import { AxiosInstance } from 'axios'; import { Contract, ethers } from 'ethers'; import { AntelopeError } from 'src/antelope/types'; +import { createTraceFunction } from 'src/antelope/config'; + +// This function is used to trace the execution of the code +const trace = createTraceFunction('NFTClass'); export interface NftAttribute { label: string; @@ -106,6 +110,7 @@ export async function constructNft( contractStore: ReturnType, nftStore: ReturnType, ): Promise { + trace('constructNft', { contract, indexerData, chainSettings, contractStore, nftStore }); const network = chainSettings.getNetwork(); const isErc721 = contract.supportedInterfaces.includes('erc721'); @@ -118,6 +123,7 @@ export async function constructNft( const cachedNft = nftStore.__contracts[network]?.[contract.address]?.list.find(nft => nft.id === indexerData.tokenId); if (cachedNft) { + trace('constructNft', 'contract found in cache'); await cachedNft.updateOwnerData(); return cachedNft; } @@ -146,6 +152,7 @@ export async function constructNft( indexerData.metadata.image = ((indexerData.metadata.image as string) ?? '').replace('ipfs://', IPFS_GATEWAY); const { image, mediaType, mediaSource } = await extractNftMetadata(indexerData.imageCache ?? '', indexerData.tokenUri ?? '', indexerData.metadata ?? {}); + trace('constructNft', 'extracted metadata:', { image, mediaType, mediaSource }); const commonData: NftPrecursorData = { name: (indexerData.metadata?.name ?? '') as string, id: indexerData.tokenId, @@ -166,6 +173,7 @@ export async function constructNft( const contractInstance = await (await contractStore.getContract(CURRENT_CONTEXT, contract.address, 'erc721'))?.getContractInstance(); if (!contractInstance) { + console.error('Error getting contract instance'); throw new AntelopeError('antelope.utils.error_contract_instance'); } @@ -286,6 +294,7 @@ export class Erc721Nft extends NFT { } async updateOwnerData(): Promise { + trace('Erc721Nft.updateOwnerData', { contract: this.contract, id: this.id }); const contract = await useContractStore().getContract(CURRENT_CONTEXT, this.contractAddress); const contractInstance = await contract?.getContractInstance(); diff --git a/src/antelope/wallets/AntelopeWallets.ts b/src/antelope/wallets/AntelopeWallets.ts index 26fca5477..83b4c8a7b 100644 --- a/src/antelope/wallets/AntelopeWallets.ts +++ b/src/antelope/wallets/AntelopeWallets.ts @@ -1,5 +1,4 @@ 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'; @@ -13,6 +12,9 @@ export class AntelopeWallets { private trace: AntelopeDebugTraceType; private authenticators: Map = new Map(); + private web3Provider: ethers.providers.Web3Provider | null = null; + private web3ProviderInitializationPromise: Promise | null = null; + constructor() { this.trace = createTraceFunction(name); } @@ -35,39 +37,36 @@ export class AntelopeWallets { return (useChainStore().getChain(label).settings as EVMChainSettings); } - async getWeb3Provider(): Promise { + async getWeb3Provider(label = CURRENT_CONTEXT): 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); + // If a provider instance already exists, return it immediately. + if (this.web3Provider) { + return this.web3Provider; } - 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'); + // If an initialization is already underway, wait for it to complete. + if (this.web3ProviderInitializationPromise) { + return this.web3ProviderInitializationPromise; } + + // Start the initialization. + this.web3ProviderInitializationPromise = (async () => { + try { + const p: RpcEndpoint = this.getChainSettings(label).getRPCEndpoint(); + const url = `${p.protocol}://${p.host}:${p.port}${p.path ?? ''}`; + const jsonRpcProvider = new ethers.providers.JsonRpcProvider(url); + await jsonRpcProvider.ready; + this.web3Provider = jsonRpcProvider as ethers.providers.Web3Provider; + return this.web3Provider; + } catch (e) { + this.trace('getWeb3Provider authenticator.web3Provider() Failed!', e); + this.web3ProviderInitializationPromise = null; // Reset to allow retries. + throw new AntelopeError('antelope.evn.error_no_provider'); + } + })(); + + return this.web3ProviderInitializationPromise; } + } diff --git a/src/antelope/wallets/authenticators/EVMAuthenticator.ts b/src/antelope/wallets/authenticators/EVMAuthenticator.ts index a208ae070..f3949fc1e 100644 --- a/src/antelope/wallets/authenticators/EVMAuthenticator.ts +++ b/src/antelope/wallets/authenticators/EVMAuthenticator.ts @@ -67,6 +67,28 @@ export abstract class EVMAuthenticator { } } + /** + * This method MUST be implemented on the derived class to perform any auto login action if needed. + * @param network network to connect to + * @param account account address being auto logged in + * @returns the account address of the user + */ + async autoLogin(network: string, account: string): Promise { + this.trace('login', network, account); + const chain = useChainStore(); + try { + chain.setChain(CURRENT_CONTEXT, network); + return account as addressString; + } catch (error) { + if ((error as unknown as ExceptionError).code === 4001) { + throw new AntelopeError('antelope.evm.error_connect_rejected'); + } else { + console.error('Error:', error); + throw new AntelopeError('antelope.evm.error_login'); + } + } + } + async ensureCorrectChain(): Promise { this.trace('ensureCorrectChain'); if (usePlatformStore().isMobile) { @@ -110,7 +132,7 @@ export abstract class EVMAuthenticator { async getSystemTokenBalance(address: addressString | string): Promise { this.trace('getSystemTokenBalance', address); try { - const provider = await this.web3Provider(); + const provider = await getAntelope().wallets.getWeb3Provider(this.label); if (provider) { return provider.getBalance(address); } else { @@ -132,7 +154,7 @@ export abstract class EVMAuthenticator { async getERC20TokenBalance(address: addressString | string, token: addressString): Promise { this.trace('getERC20TokenBalance', [address, token]); try { - const provider = await this.web3Provider(); + const provider = await getAntelope().wallets.getWeb3Provider(this.label); if (provider) { const erc20Contract = new ethers.Contract(token, erc20Abi, provider); const balance = await erc20Contract.balanceOf(address); diff --git a/src/antelope/wallets/authenticators/InjectedProviderAuth.ts b/src/antelope/wallets/authenticators/InjectedProviderAuth.ts index a893f27ae..4792fc40a 100644 --- a/src/antelope/wallets/authenticators/InjectedProviderAuth.ts +++ b/src/antelope/wallets/authenticators/InjectedProviderAuth.ts @@ -2,7 +2,7 @@ import { BigNumber, ethers } from 'ethers'; import { BehaviorSubject, filter, map } from 'rxjs'; -import { getAntelope, useAccountStore, useChainStore, useEVMStore, useFeedbackStore } from 'src/antelope'; +import { useAccountStore, useChainStore, useEVMStore, useFeedbackStore } from 'src/antelope'; import { AntelopeError, EthereumProvider, @@ -27,7 +27,6 @@ export abstract class InjectedProviderAuth extends EVMAuthenticator { async initInjectedProvider(authenticator: InjectedProviderAuth): Promise { this.trace('initInjectedProvider', authenticator.getName(), [authenticator.getProvider()]); const provider: EthereumProvider | null = authenticator.getProvider(); - const ant = getAntelope(); if (provider && !provider.__initialized) { this.trace('initInjectedProvider', authenticator.getName(), 'initializing provider'); @@ -42,36 +41,6 @@ export abstract class InjectedProviderAuth extends EVMAuthenticator { } } - // this handler activates only when the user comes back from switching to the wrong network on the wallet - // It checks if the user is on the correct network and if not, it shows a notification with a button to switch - const checkNetworkHandler = async () => { - window.removeEventListener('focus', checkNetworkHandler); - if (useAccountStore().loggedAccount) { - const authenticator = useAccountStore().loggedAccount.authenticator as EVMAuthenticator; - if (await authenticator.isConnectedToCorrectChain()) { - this.trace('checkNetworkHandler', 'correct network'); - } else { - const networkName = useChainStore().loggedChain.settings.getDisplay(); - const errorMessage = ant.config.localizationHandler('evm_wallet.incorrect_network', { networkName }); - ant.config.notifyFailureWithAction(errorMessage, { - label: ant.config.localizationHandler('evm_wallet.switch'), - handler: () => { - authenticator.ensureCorrectChain(); - }, - }); - } - } - }; - - provider.on('chainChanged', (value) => { - const newNetwork = value as string; - this.trace('provider.chainChanged', newNetwork); - window.removeEventListener('focus', checkNetworkHandler); - if (useAccountStore().loggedAccount) { - window.addEventListener('focus', checkNetworkHandler); - } - }); - provider.on('accountsChanged', async (value) => { const accounts = value as string[]; const network = useChainStore().currentChain.settings.getNetwork(); diff --git a/src/boot/antelope.ts b/src/boot/antelope.ts index aa968eff0..69cb8fbdc 100644 --- a/src/boot/antelope.ts +++ b/src/boot/antelope.ts @@ -110,4 +110,8 @@ export default boot(({ app }) => { ant.stores.chain.setChain(CURRENT_CONTEXT, network); } + // We can simulate the indexer being down for testing purposes by uncommenting the following line + // (ant.stores.chain.currentChain.settings as EVMChainSettings).simulateIndexerDown(true); + + }); diff --git a/src/boot/errorHandling.js b/src/boot/errorHandling.js index c2c714c92..9e52cd11f 100644 --- a/src/boot/errorHandling.js +++ b/src/boot/errorHandling.js @@ -58,6 +58,7 @@ class NotificationAction { this.label = payload.label; this.class = payload.class; this.handler = payload.handler; + this.onDismiss = payload.onDismiss; this.iconRight = payload.iconRight; this.color = payload.color; } @@ -142,11 +143,14 @@ const notifyMessage = function(type, icon, title, message, payload, remember = ' class: 'c-notify__action-btn c-notify__action-btn--hide', }; + let onDismiss = null; + // adding buttons if (typeof payload === 'string' && type === 'success') { actions.push(link_btn); } else if (typeof payload === 'object' && payload instanceof NotificationAction) { actions.push(action_btn); + onDismiss = payload.onDismiss; } else if (typeof payload === 'object' && type === 'error') { actions.push(details_btn); } else { @@ -206,6 +210,7 @@ const notifyMessage = function(type, icon, title, message, payload, remember = ' html: true, classes: 'c-notify', actions, + onDismiss, }); }; diff --git a/src/pages/evm/allowances/EditAllowanceModal.vue b/src/pages/evm/allowances/EditAllowanceModal.vue index 61f0cb094..97b8f7522 100644 --- a/src/pages/evm/allowances/EditAllowanceModal.vue +++ b/src/pages/evm/allowances/EditAllowanceModal.vue @@ -18,6 +18,7 @@ import { isErc721SingleAllowanceRow, } from 'src/antelope/types/Allowances'; import { + CURRENT_CONTEXT, getAntelope, useAccountStore, useAllowancesStore, @@ -168,6 +169,10 @@ async function handleSubmit() { let tx: TransactionResponse; let neutralMessageText: string; + if (!await useAccountStore().assertNetworkConnection(CURRENT_CONTEXT)) { + return; + } + if (rowIsErc20Row.value) { tx = await useAllowancesStore().updateErc20Allowance( userAddress.value, diff --git a/src/pages/evm/nfts/NftTransferForm.vue b/src/pages/evm/nfts/NftTransferForm.vue index 6f4546f80..8d179a63d 100644 --- a/src/pages/evm/nfts/NftTransferForm.vue +++ b/src/pages/evm/nfts/NftTransferForm.vue @@ -73,11 +73,15 @@ onMounted(() => { }); async function startTransfer() { + const label = CURRENT_CONTEXT; transferLoading.value = true; const nameString = `${props.nft.contractPrettyName || props.nft.contractAddress} #${props.nft.id}`; try { + if (!await useAccountStore().assertNetworkConnection(label)) { + return; + } const trx = await nftStore.transferNft( - CURRENT_CONTEXT, + label, contractAddress, nftId, nftType.value, diff --git a/src/pages/evm/staking/StakingPageHeader.vue b/src/pages/evm/staking/StakingPageHeader.vue index 3433d1c10..936a23bdd 100644 --- a/src/pages/evm/staking/StakingPageHeader.vue +++ b/src/pages/evm/staking/StakingPageHeader.vue @@ -13,7 +13,6 @@ import { useRoute } from 'vue-router'; const route = useRoute(); -const label = 'current'; const { t: $t } = useI18n(); const userStore = useUserStore(); const balancesStore = useBalancesStore(); @@ -29,7 +28,7 @@ const stakedToken = chainSettings.getStakedSystemToken(); // First cell: Staked -const unstakedRatio = computed(() => chainStore.getUnstakedRatio(label)); +const unstakedRatio = computed(() => chainStore.getUnstakedRatio(CURRENT_CONTEXT)); const isStakedLoading = computed(() => stakedTokenBalanceBn.value === undefined || unstakedRatio.value.isZero()); const stakedExpressedInSystemBalanceBn = computed(() => { if (stakedTokenBalanceBn.value && !unstakedRatio.value.isZero()) { @@ -48,7 +47,7 @@ const stakedFiatValueBn = computed(() => { } }); const stakedTokenBalanceBn = computed(() => { - const balances = balancesStore.getBalances(label); + const balances = balancesStore.getBalances(CURRENT_CONTEXT); const stakedTokenBalance = balances.find(balance => balance.token.symbol === stakedToken.symbol)?.amount; if (stakedTokenBalance) { return stakedTokenBalance; @@ -57,7 +56,7 @@ const stakedTokenBalanceBn = computed(() => { } }); const systemTokenPrice = computed(() => { - const balances = balancesStore.getBalances(label); + const balances = balancesStore.getBalances(CURRENT_CONTEXT); const systemTokenPriceBalance = balances.find(balance => balance.token.symbol === systemToken.symbol)?.token.price.value; if (systemTokenPriceBalance) { return systemTokenPriceBalance; @@ -68,7 +67,7 @@ const systemTokenPrice = computed(() => { // Second cell: Unstaking const unstakingBalanceBn = computed(() => { - const rexData = useRexStore().getRexData(label); + const rexData = useRexStore().getRexData(CURRENT_CONTEXT); if (rexData) { const totalBalance = rexData.balance; const withdrawableBalance = rexData.withdrawable; @@ -92,7 +91,7 @@ const unstakingFiatValueBn = computed(() => { const isUnstakingLoading = computed(() => unstakingBalanceBn.value === undefined); // Third cell: Withdrawable -const withdrawableBalanceBn = computed(() => useRexStore().getRexData(label)?.withdrawable); +const withdrawableBalanceBn = computed(() => useRexStore().getRexData(CURRENT_CONTEXT)?.withdrawable); const withdrawableFiatValueBn = computed(() => { if (withdrawableBalanceBn.value && systemTokenPrice.value && !systemTokenPrice.value.isZero()) { const ratioNumber = ethers.utils.formatUnits(systemTokenPrice.value, systemToken.price.decimals); @@ -118,7 +117,7 @@ const unlockPeriod = computed(() => rexStore.getUnstakingPeriodString(CURRENT_CO const unlockPeriodLoading = computed(() => unlockPeriod.value === '--'); const tvlAmountBn = computed(() => { - const totalStaking = useRexStore().getRexData(label)?.totalStaking; + const totalStaking = useRexStore().getRexData(CURRENT_CONTEXT)?.totalStaking; if (totalStaking) { return totalStaking; } else { @@ -189,21 +188,21 @@ const intervalTimer = setInterval(() => { if (isStakedLoading.value) { // if staked balance is still undefined we force the balances update if (stakedTokenBalanceBn.value === undefined) { - balancesStore.updateBalances(label); + balancesStore.updateBalances(CURRENT_CONTEXT); } // if unstaked ratio is still zero we force the chain update if (unstakedRatio.value.isZero()) { - chainStore.updateStakedRatio(label); + chainStore.updateStakedRatio(CURRENT_CONTEXT); } } // is unstaking still loading? if (isUnstakingLoading.value || isWithdrawableLoading.value || unlockPeriodLoading.value) { // we need to update rex data - useRexStore().updateRexData(label); + useRexStore().updateRexData(CURRENT_CONTEXT); } if (apyisLoading.value) { // force apy update - chainStore.updateApy(label); + chainStore.updateApy(CURRENT_CONTEXT); } if ( diff --git a/src/pages/evm/staking/StakingTab.vue b/src/pages/evm/staking/StakingTab.vue index 0f7f8001b..ccc933090 100644 --- a/src/pages/evm/staking/StakingTab.vue +++ b/src/pages/evm/staking/StakingTab.vue @@ -19,7 +19,6 @@ import ConversionRateBadge from 'src/components/ConversionRateBadge.vue'; import CurrencyInput from 'src/components/evm/inputs/CurrencyInput.vue'; import { WEI_PRECISION, formatWei } from 'src/antelope/stores/utils'; -const label = 'current'; const { t: $t } = useI18n(); const uiDecimals = 2; @@ -43,7 +42,7 @@ const oneEth = ethers.BigNumber.from('1'.concat('0'.repeat(systemTokenDecimals)) const inputModelValue = ref(ethers.constants.Zero); const estimatedGas = ref(ethers.constants.Zero); // computed -const stakedRatio = computed(() => chainStore.getStakedRatio(label)); +const stakedRatio = computed(() => chainStore.getStakedRatio(CURRENT_CONTEXT)); const outputModelValue = computed(() => { if (stakedRatio.value.isZero()) { return ethers.constants.Zero; @@ -118,14 +117,7 @@ onBeforeMount(() => { async function handleCtaClick() { const label = CURRENT_CONTEXT; - if (!await accountStore.isConnectedToCorrectNetwork(label)) { - const networkName = useChainStore().loggedChain.settings.getDisplay(); - const errorMessage = ant.config.localizationHandler('evm_wallet.incorrect_network', { networkName }); - - ant.config.notifyFailureWithAction(errorMessage, { - label: ant.config.localizationHandler('evm_wallet.switch'), - }); - + if (!await useAccountStore().assertNetworkConnection(label)) { return; } diff --git a/src/pages/evm/staking/UnstakingTab.vue b/src/pages/evm/staking/UnstakingTab.vue index 6e65dcc67..1ffe4a172 100644 --- a/src/pages/evm/staking/UnstakingTab.vue +++ b/src/pages/evm/staking/UnstakingTab.vue @@ -19,8 +19,6 @@ import ConversionRateBadge from 'src/components/ConversionRateBadge.vue'; import CurrencyInput from 'src/components/evm/inputs/CurrencyInput.vue'; import { formatWei } from 'src/antelope/stores/utils'; -const label = 'current'; - const { t: $t } = useI18n(); const uiDecimals = 2; const ant = getAntelope(); @@ -44,7 +42,7 @@ const oneEth = ethers.BigNumber.from('1'.concat('0'.repeat(systemTokenDecimals)) const inputModelValue = ref(ethers.constants.Zero); // computed -const unstakedRatio = computed(() => chainStore.getUnstakedRatio(label)); +const unstakedRatio = computed(() => chainStore.getUnstakedRatio(CURRENT_CONTEXT)); const outputModelValue = computed(() => { if (unstakedRatio.value.isZero()) { return ethers.constants.Zero; @@ -98,14 +96,7 @@ const ctaIsLoading = computed(() => ant.stores.feedback.isLoading('unstakeEVMSys async function handleCtaClick() { const label = CURRENT_CONTEXT; - if (!await accountStore.isConnectedToCorrectNetwork(label)) { - const networkName = useChainStore().loggedChain.settings.getDisplay(); - const errorMessage = ant.config.localizationHandler('evm_wallet.incorrect_network', { networkName }); - - ant.config.notifyFailureWithAction(errorMessage, { - label: ant.config.localizationHandler('evm_wallet.switch'), - }); - + if (!await useAccountStore().assertNetworkConnection(label)) { return; } diff --git a/src/pages/evm/staking/WithdrawTab.vue b/src/pages/evm/staking/WithdrawTab.vue index b94bc33ce..23656a97d 100644 --- a/src/pages/evm/staking/WithdrawTab.vue +++ b/src/pages/evm/staking/WithdrawTab.vue @@ -46,14 +46,7 @@ const handleWithdrawClick = async () => { return; } const label = CURRENT_CONTEXT; - if (!accountStore.isConnectedToCorrectNetwork(label)) { - const networkName = chainStore.loggedChain.settings.getDisplay(); - const errorMessage = ant.config.localizationHandler('evm_wallet.incorrect_network', { networkName }); - - ant.config.notifyFailureWithAction(errorMessage, { - label: ant.config.localizationHandler('evm_wallet.switch'), - }); - + if (!await useAccountStore().assertNetworkConnection(label)) { return; } diff --git a/src/pages/evm/wallet/SendPage.vue b/src/pages/evm/wallet/SendPage.vue index 40d1a656d..062150348 100644 --- a/src/pages/evm/wallet/SendPage.vue +++ b/src/pages/evm/wallet/SendPage.vue @@ -221,18 +221,8 @@ export default defineComponent({ // before sending the transaction, we check if the user is connected to the correct network const label = CURRENT_CONTEXT; - if (!await useAccountStore().isConnectedToCorrectNetwork(label)) { - const authenticator = useAccountStore().loggedAccount.authenticator as EVMAuthenticator; - const networkName = useChainStore().loggedChain.settings.getDisplay(); - const errorMessage = ant.config.localizationHandler('evm_wallet.incorrect_network', { networkName }); - ant.config.notifyFailureWithAction(errorMessage, { - label: ant.config.localizationHandler('evm_wallet.switch'), - handler: async () => { - // we force the useer to manually re enter the amount which triggers updateTokenTransferConfig - this.amount = ethers.constants.Zero; - await authenticator.ensureCorrectChain(); - }, - }); + if (!await useAccountStore().assertNetworkConnection(label)) { + this.amount = ethers.constants.Zero; return; } diff --git a/src/pages/evm/wallet/WalletTransactionsTab.vue b/src/pages/evm/wallet/WalletTransactionsTab.vue index 00469173c..2056b8e32 100644 --- a/src/pages/evm/wallet/WalletTransactionsTab.vue +++ b/src/pages/evm/wallet/WalletTransactionsTab.vue @@ -1,5 +1,5 @@