diff --git a/src/antelope/chains/evm/telos-evm-testnet/index.ts b/src/antelope/chains/evm/telos-evm-testnet/index.ts index 2d606ac4..2d890eae 100644 --- a/src/antelope/chains/evm/telos-evm-testnet/index.ts +++ b/src/antelope/chains/evm/telos-evm-testnet/index.ts @@ -46,9 +46,9 @@ const W_TOKEN = new TokenClass({ const RPC_ENDPOINT = { protocol: 'https', - host: 'testnet.telos.net', + host: 'rpc.testnet.telos.net', port: 443, - path: '/evm', + path: '/', }; const ESCROW_CONTRACT_ADDRESS = '0x7E9cF9fBc881652B05BB8F26298fFAB538163b6f'; const API_ENDPOINT = 'https://api-dev.telos.net/v1'; diff --git a/src/antelope/chains/evm/telos-evm/index.ts b/src/antelope/chains/evm/telos-evm/index.ts index 3a409935..f9a97981 100644 --- a/src/antelope/chains/evm/telos-evm/index.ts +++ b/src/antelope/chains/evm/telos-evm/index.ts @@ -46,9 +46,9 @@ const W_TOKEN = new TokenClass({ const RPC_ENDPOINT = { protocol: 'https', - host: 'mainnet.telos.net', + host: 'rpc.telos.net', port: 443, - path: '/evm', + path: '/', }; const ESCROW_CONTRACT_ADDRESS = '0x95F5713A1422Aa3FBD3DCB8D553945C128ee3855'; const API_ENDPOINT = 'https://api.telos.net/v1'; diff --git a/src/antelope/stores/allowances.ts b/src/antelope/stores/allowances.ts index 7a3fd730..ba2d774c 100644 --- a/src/antelope/stores/allowances.ts +++ b/src/antelope/stores/allowances.ts @@ -22,6 +22,7 @@ import { isErc20AllowanceRow, isErc721SingleAllowanceRow, isNftCollectionAllowanceRow, + EvmContractFactoryData, } from 'src/antelope/types'; import { createTraceFunction } from 'src/antelope/config'; import EVMChainSettings from 'src/antelope/chains/EVMChainSettings'; @@ -226,94 +227,107 @@ export const useAllowancesStore = defineStore(store_name, { // actions async fetchAllowancesForAccount(account: string): Promise { this.trace('fetchAllowancesForAccount', account); - useFeedbackStore().setLoading('fetchAllowancesForAccount'); + try { + useFeedbackStore().setLoading('fetchAllowancesForAccount'); - const chainSettings = useChainStore().currentChain.settings as EVMChainSettings; + const chainSettings = useChainStore().currentChain.settings as EVMChainSettings; - if (chainSettings.isNative()) { - this.trace('fetchAllowancesForAccount', 'Native chain does not have allowances'); - return; - } + if (chainSettings.isNative()) { + this.trace('fetchAllowancesForAccount', 'Native chain does not have allowances'); + return; + } - const erc20AllowancesPromise = chainSettings.fetchErc20Allowances(account, { limit: ALLOWANCES_LIMIT }); - const erc721AllowancesPromise = chainSettings.fetchErc721Allowances(account, { limit: ALLOWANCES_LIMIT }); - const erc1155AllowancesPromise = chainSettings.fetchErc1155Allowances(account, { limit: ALLOWANCES_LIMIT }); + const erc20AllowancesPromise = chainSettings.fetchErc20Allowances(account, { limit: ALLOWANCES_LIMIT }); + const erc721AllowancesPromise = chainSettings.fetchErc721Allowances(account, { limit: ALLOWANCES_LIMIT }); + const erc1155AllowancesPromise = chainSettings.fetchErc1155Allowances(account, { limit: ALLOWANCES_LIMIT }); - let allowancesResults: IndexerAllowanceResponse[]; + let allowancesResults: IndexerAllowanceResponse[]; - try { - allowancesResults = await Promise.all([erc20AllowancesPromise, erc721AllowancesPromise, erc1155AllowancesPromise]); - } catch (e) { - console.error('Error fetching allowances', e); - useFeedbackStore().unsetLoading('fetchAllowancesForAccount'); - throw new AntelopeError('antelope.allowances.error_fetching_allowances'); - } + try { + allowancesResults = await Promise.all([erc20AllowancesPromise, erc721AllowancesPromise, erc1155AllowancesPromise]); + } catch (e) { + console.error('Error fetching allowances', e); + useFeedbackStore().unsetLoading('fetchAllowancesForAccount'); + throw new AntelopeError('antelope.allowances.error_fetching_allowances'); + } - const erc20AllowancesData = (allowancesResults[0] as IndexerAllowanceResponseErc20)?.results ?? []; - const erc721AllowancesData = (allowancesResults[1] as IndexerAllowanceResponseErc721)?.results ?? []; - const erc1155AllowancesData = (allowancesResults[2] as IndexerAllowanceResponseErc1155)?.results ?? []; + const erc20AllowancesData = (allowancesResults[0] as IndexerAllowanceResponseErc20)?.results ?? []; + const erc721AllowancesData = (allowancesResults[1] as IndexerAllowanceResponseErc721)?.results ?? []; + const erc1155AllowancesData = (allowancesResults[2] as IndexerAllowanceResponseErc1155)?.results ?? []; - const shapedErc20AllowanceRowPromises = Promise.allSettled(erc20AllowancesData.map(allowanceData => this.shapeErc20AllowanceRow(allowanceData))); - const shapedErc721AllowanceRowPromises = Promise.allSettled(erc721AllowancesData.map(allowanceData => this.shapeErc721AllowanceRow(allowanceData))); - const shapedErc1155AllowanceRowPromises = Promise.allSettled(erc1155AllowancesData.map(allowanceData => this.shapeErc1155AllowanceRow(allowanceData))); + // Load these in the cache so they're available later and we don't abuse the indexer API + allowancesResults.map((result) => { + for (const [address, contract] of Object.entries(result.contracts)) { + useContractStore().createAndStoreContract(CURRENT_CONTEXT, address, contract as EvmContractFactoryData); + } + }); - const [settledErc20Results, settledErc721Results, settledErc1155Results] = await Promise.allSettled([ - shapedErc20AllowanceRowPromises, - shapedErc721AllowanceRowPromises, - shapedErc1155AllowanceRowPromises, - ]); + const shapedErc20AllowanceRowPromises = Promise.allSettled(erc20AllowancesData.map(allowanceData => this.shapeErc20AllowanceRow(allowanceData))); + const shapedErc721AllowanceRowPromises = Promise.allSettled(erc721AllowancesData.map(allowanceData => this.shapeErc721AllowanceRow(allowanceData))); + const shapedErc1155AllowanceRowPromises = Promise.allSettled(erc1155AllowancesData.map(allowanceData => this.shapeErc1155AllowanceRow(allowanceData))); - if (settledErc20Results.status === 'fulfilled') { - const shapedErc20Rows: ShapedAllowanceRowERC20[] = []; + const [settledErc20Results, settledErc721Results, settledErc1155Results] = await Promise.allSettled([ + shapedErc20AllowanceRowPromises, + shapedErc721AllowanceRowPromises, + shapedErc1155AllowanceRowPromises, + ]); - settledErc20Results.value.forEach((result) => { - if (result.status === 'fulfilled') { - result.value && shapedErc20Rows.push(result.value); - } else { - console.error('Error shaping ERC20 allowance row', result.reason); - } - }); + if (settledErc20Results.status === 'fulfilled') { + const shapedErc20Rows: ShapedAllowanceRowERC20[] = []; - this.setErc20Allowances(CURRENT_CONTEXT, shapedErc20Rows); - } else { - console.error('Error shaping ERC20 allowance rows', settledErc20Results.reason); - } + settledErc20Results.value.forEach((result) => { + if (result.status === 'fulfilled') { + result.value && shapedErc20Rows.push(result.value); + } else { + console.error('Error shaping ERC20 allowance row', result.reason); + } + }); - if (settledErc721Results.status === 'fulfilled') { - const shapedErc721Rows: (ShapedAllowanceRowSingleERC721 | ShapedAllowanceRowNftCollection)[] = []; + this.setErc20Allowances(CURRENT_CONTEXT, shapedErc20Rows); + } else { + console.error('Error shaping ERC20 allowance rows', settledErc20Results.reason); + } - settledErc721Results.value.forEach((result) => { - if (result.status === 'fulfilled') { - result.value && shapedErc721Rows.push(result.value); - } else { - console.error('Error shaping ERC721 allowance row', result.reason); - } - }); + if (settledErc721Results.status === 'fulfilled') { + const shapedErc721Rows: (ShapedAllowanceRowSingleERC721 | ShapedAllowanceRowNftCollection)[] = []; - this.setErc721Allowances(CURRENT_CONTEXT, shapedErc721Rows); - } else { - console.error('Error shaping ERC721 allowance rows', settledErc721Results.reason); - } + settledErc721Results.value.forEach((result) => { + if (result.status === 'fulfilled') { + result.value && shapedErc721Rows.push(result.value); + } else { + console.error('Error shaping ERC721 allowance row', result.reason); + } + }); - if (settledErc1155Results.status === 'fulfilled') { - const shapedErc1155Rows: ShapedAllowanceRowNftCollection[] = []; + this.setErc721Allowances(CURRENT_CONTEXT, shapedErc721Rows); + } else { + console.error('Error shaping ERC721 allowance rows', settledErc721Results.reason); + } - settledErc1155Results.value.forEach((result) => { - if (result.status === 'fulfilled') { - result.value && shapedErc1155Rows.push(result.value); - } else { - console.error('Error shaping ERC1155 allowance row', result.reason); - } - }); + if (settledErc1155Results.status === 'fulfilled') { + const shapedErc1155Rows: ShapedAllowanceRowNftCollection[] = []; - this.setErc1155Allowances(CURRENT_CONTEXT, shapedErc1155Rows); - } else { - console.error('Error shaping ERC1155 allowance rows', settledErc1155Results.reason); - } + settledErc1155Results.value.forEach((result) => { + if (result.status === 'fulfilled') { + result.value && shapedErc1155Rows.push(result.value); + } else { + console.error('Error shaping ERC1155 allowance row', result.reason); + } + }); + + this.setErc1155Allowances(CURRENT_CONTEXT, shapedErc1155Rows); + } else { + console.error('Error shaping ERC1155 allowance rows', settledErc1155Results.reason); + } - useFeedbackStore().unsetLoading('fetchAllowancesForAccount'); + useFeedbackStore().unsetLoading('fetchAllowancesForAccount'); - return Promise.resolve(); + return Promise.resolve(); + } catch (e) { + useFeedbackStore().unsetLoading('fetchAllowancesForAccount'); + console.error('Error fetching allowances', e); + throw new AntelopeError('antelope.allowances.error_fetching_allowances'); + } }, async updateErc20Allowance( owner: string, @@ -544,6 +558,16 @@ export const useAllowancesStore = defineStore(store_name, { this.__erc_721_allowances[label] = []; this.__erc_1155_allowances[label] = []; }, + async fetchBalanceString(data: IndexerErc20AllowanceResult): Promise { + const indexer = (useChainStore().loggedChain.settings as EVMChainSettings).getIndexer(); + const results = (await indexer.get(`/v1/token/${data.contract}/holders?account=${data.owner}`)).data.results; + if (results.length === 0) { + return '0'; + } else { + const balanceString = results[0].balance; + return balanceString; + } + }, async shapeErc20AllowanceRow(data: IndexerErc20AllowanceResult): Promise { try { const spenderContract = await useContractStore().getContract(CURRENT_CONTEXT, data.spender); @@ -559,9 +583,7 @@ export const useAllowancesStore = defineStore(store_name, { )?.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; - + const balanceString = await this.fetchBalanceString(data); balance = BigNumber.from(balanceString); } diff --git a/src/antelope/stores/contract.ts b/src/antelope/stores/contract.ts index 18aaceca..715ecd85 100644 --- a/src/antelope/stores/contract.ts +++ b/src/antelope/stores/contract.ts @@ -585,6 +585,24 @@ export const useContractStore = defineStore(store_name, { this.__contracts[network].cached[index] = null; }, + async fetchIsContract(addressLower: string): Promise { + // We use a try/catch in case the request returns a 404 or similar + try { + const indexer = (useChainStore().loggedChain.settings as EVMChainSettings).getIndexer(); + const response = await indexer.get('/v1/contract/' + addressLower); + + // If we have a valid data.results array and it has at least one element, return true + if (response.data?.results?.length > 0) { + return true; + } else { + return false; + } + } catch (error) { + // If an error is thrown (e.g. 404), we assume it's not a contract + return false; + } + }, + async addressIsContract(label: string, address: string) { const network = useChainStore().getChain(label).settings.getNetwork(); const addressLower = address.toLowerCase(); @@ -609,13 +627,10 @@ export const useContractStore = defineStore(store_name, { } 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; - + const isContract = await this.fetchIsContract(addressLower); if (!isContract && !this.__accounts[network].addresses.includes(addressLower)) { this.__accounts[network].addresses.push(addressLower); } - resolve(isContract); }); diff --git a/src/boot/antelope.ts b/src/boot/antelope.ts index 705179a1..50df87d0 100644 --- a/src/boot/antelope.ts +++ b/src/boot/antelope.ts @@ -78,7 +78,6 @@ export default boot(({ app }) => { next: async () => { // first recreate the authenticators based on the new network const zeroAuthenticators = app.config.globalProperties.recreateAuthenticator(); - console.log('zeroAuthenticators', zeroAuthenticators); // set the new authenticators list ant.config.setAuthenticatorsGetter(() => zeroAuthenticators); for (const authenticator of zeroAuthenticators) { diff --git a/src/boot/ual.js b/src/boot/ual.js index 35722885..4f444c44 100644 --- a/src/boot/ual.js +++ b/src/boot/ual.js @@ -137,11 +137,9 @@ export default boot(async ({ app, store }) => { app.config.globalProperties.recreateAuthenticator = function() { - console.log('UAL.recreateAuthenticator()'); if (useChainStore().currentChain.settings.isNative()) { const settings = useChainStore().currentNativeChain.settings; - console.log('UAL.recreateAuthenticator()', { settings }); const ual_chain = { chainId: settings.getChainId(), @@ -164,7 +162,6 @@ export default boot(async ({ app, store }) => { return authenticators; } else { - console.log('UAL.recreateAuthenticator() - not native chain'); return []; } }; diff --git a/src/i18n/en-us/index.js b/src/i18n/en-us/index.js index d6ca242e..6e161e68 100644 --- a/src/i18n/en-us/index.js +++ b/src/i18n/en-us/index.js @@ -301,6 +301,7 @@ export default { aside_content_fragment_4_bold: 'approvals ', aside_content_fragment_5: 'enable app functionality and enhance your user experience. However, it\'s prudent to review and manage these permissions to deter unauthorized transactions, adding a layer of security to your wallet.', revoke_selected: 'Revoke selected', + refresh: 'Refresh', search_label: 'Filter by token, allowance, spender, or contract address', includes_cancelled_allowances: 'Includes cancelled allowances', excludes_cancelled_allowances: 'Does not include cancelled allowances', diff --git a/src/pages/evm/allowances/AllowancesPage.vue b/src/pages/evm/allowances/AllowancesPage.vue index 22f3b87a..c9c3ed92 100644 --- a/src/pages/evm/allowances/AllowancesPage.vue +++ b/src/pages/evm/allowances/AllowancesPage.vue @@ -149,23 +149,21 @@ const shapedAllowanceRows = computed(() => { const enableRevokeButton = computed(() => selectedRows.value.length > 0); +const updateAllowances = () => { + loading.value = true; + return useAllowancesStore().fetchAllowancesForAccount(userAddress.value).then(() => { + loading.value = false; + }); +}; + // watchers watch(userAddress, (address) => { if (address) { - loading.value = true; - useAllowancesStore().fetchAllowancesForAccount(address).then(() => { - loading.value = false; - }); + updateAllowances(); } }, { immediate: true }); -// methods -onMounted(() => { - timeout.value = setTimeout(() => { - useAllowancesStore().fetchAllowancesForAccount(userAddress.value); - }, 13000); -}); onBeforeUnmount(() => { if (timeout.value) { @@ -230,7 +228,7 @@ function handleRevokeSelectedClicked() { cancelBatchRevokeButtonLoading.value = true; setTimeout(() => { - useAllowancesStore().fetchAllowancesForAccount(userAddress.value).finally(() => { + updateAllowances().finally(() => { cancelBatchRevokeButtonLoading.value = false; showRevokeInProgressModal.value = false; @@ -264,9 +262,11 @@ function handleRevokeSelectedClicked() {
diff --git a/src/pages/evm/allowances/AllowancesPageControls.vue b/src/pages/evm/allowances/AllowancesPageControls.vue index d12694bc..012c5ca4 100644 --- a/src/pages/evm/allowances/AllowancesPageControls.vue +++ b/src/pages/evm/allowances/AllowancesPageControls.vue @@ -5,11 +5,17 @@ import { useI18n } from 'vue-i18n'; const { t: $t } = useI18n(); -const props = defineProps<{ +defineProps<{ enableRevokeButton: boolean; + loading: boolean; }>(); -const emit = defineEmits(['revoke-selected', 'search-updated', 'include-cancelled-updated']); +const emit = defineEmits([ + 'refresh', + 'revoke-selected', + 'search-updated', + 'include-cancelled-updated', +]); // data const searchText = ref(''); @@ -24,6 +30,11 @@ const includeCancelledLabel = computed( function handleRevokeSelected() { emit('revoke-selected'); } + +function handleRefresh() { + emit('refresh'); +} + - - {{ $t('evm_allowances.revoke_selected') }} - + + + {{ $t('evm_allowances.revoke_selected') }} + + + + + {{ $t('evm_allowances.refresh') }} + + +
+ @@ -80,7 +110,7 @@ function handleRevokeSelected() { order: 2; } - #{$this}__revoke-button { + #{$this}__buttons { order: 1; } } @@ -92,9 +122,10 @@ function handleRevokeSelected() { max-width: 580px; } - &__revoke-button { - width: max-content; + &__buttons { flex-shrink: 0; + display: flex; + gap: 16px; } }