Skip to content

Commit

Permalink
Add Etherscan links to sender and receiver addresses (#381)
Browse files Browse the repository at this point in the history
Add Etherscan links to sender and receiver addr

* Update style

* Check for ens names

* Update Style

Co-authored-by: Matt Solomon <[email protected]>

* Update getEtherscanUrl

* Update blockExplorerUrls

* WIP

* Link ENS name to corresponding addr

* Add etherscan link on Withdrawn button

* Add Read access to fs in foundry.toml

Co-authored-by: Matt Solomon <[email protected]>
  • Loading branch information
garyghayrat and mds1 authored Sep 2, 2022
1 parent 6febf4c commit efa104c
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 29 deletions.
2 changes: 2 additions & 0 deletions contracts-periphery/foundry.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
[profile.default]
verbosity = 3
fs_permissions = [{ access = "read", path = "./"}]

[profile.ci]
fuzz_runs = 10000
fs_permissions = [{ access = "read", path = "./"}]
# See more config options https://github.com/gakonst/foundry/tree/master/config

[rpc_endpoints]
Expand Down
49 changes: 39 additions & 10 deletions frontend/src/components/AccountReceiveTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
</base-tooltip>
</div>
<div @click="copyAddress(props.row.receiver, 'Receiver')" class="cursor-pointer copy-icon-parent">
<span>{{ formatAddress(props.row.receiver) }}</span>
<span>{{ formatNameOrAddress(props.row.receiver) }}</span>
<q-icon color="primary" class="q-ml-sm" name="far fa-copy" />
</div>
</div>
Expand Down Expand Up @@ -233,15 +233,15 @@
<!-- Sender column -->
<div v-else-if="col.name === 'from'" class="d-inline-block">
<div @click="copyAddress(props.row.from, 'Sender')" class="cursor-pointer copy-icon-parent">
<span>{{ col.value }}</span>
<span>{{ formatNameOrAddress(props.row.formattedFrom) }}</span>
<q-icon class="copy-icon" name="far fa-copy" right />
</div>
</div>

<!-- Receiver column -->
<div v-else-if="col.name === 'receiver'" class="d-inline-block">
<div @click="copyAddress(props.row.receiver, 'Receiver')" class="cursor-pointer copy-icon-parent">
<span>{{ formatAddress(col.value) }}</span>
<span>{{ formatNameOrAddress(col.value) }}</span>
<q-icon class="copy-icon" name="far fa-copy" right />
</div>
</div>
Expand Down Expand Up @@ -272,7 +272,17 @@
if (advancedMode) expanded = expanded[0] === props.key ? [] : [props.key];
"
>
{{ $t('AccountReceiveTable.withdrawn') }}<q-icon name="fas fa-check" class="q-ml-sm" />
<div v-if="isNativeToken(props.row.token)" class="cursor-pointer external-link-icon-parent">
<a :href="getSenderOrReceiverEtherscanUrl(props.row.receiver)" class="text-positive" target="_blank">
{{ $t('AccountReceiveTable.withdrawn') }}</a
>
<q-icon name="fas fa-check" class="q-ml-sm" right />
<q-icon class="external-link-icon" name="fas fa-external-link-alt" right />
</div>
<div v-else>
{{ $t('AccountReceiveTable.withdrawn') }}
<q-icon name="fas fa-check" class="q-ml-sm" right />
</div>
</div>
<base-button
v-else
Expand Down Expand Up @@ -336,7 +346,7 @@ import AccountReceiveTableWithdrawConfirmation from 'components/AccountReceiveTa
import BaseTooltip from 'src/components/BaseTooltip.vue';
import WithdrawForm from 'components/WithdrawForm.vue';
import { FeeEstimateResponse } from 'components/models';
import { formatAddress, lookupOrFormatAddresses, toAddress, isAddressSafe } from 'src/utils/address';
import { formatNameOrAddress, lookupOrReturnAddresses, toAddress, isAddressSafe } from 'src/utils/address';
import { MAINNET_PROVIDER } from 'src/utils/constants';
import { getEtherscanUrl } from 'src/utils/utils';
Expand Down Expand Up @@ -385,6 +395,10 @@ function useAdvancedFeatures(spendingKeyPair: KeyPair) {
return { scanDescriptionString, hidePrivateKey, togglePrivateKey, spendingPrivateKey, copyPrivateKey };
}
interface ReceiveTableAnnouncement extends UserAnnouncement {
formattedFrom: string;
}
function useReceivedFundsTable(announcements: UserAnnouncement[], spendingKeyPair: KeyPair) {
const { NATIVE_TOKEN, network, provider, signer, umbra, userAddress, relayer, tokens } = useWalletStore();
const { setIsInWithdrawFlow } = useStatusesStore();
Expand Down Expand Up @@ -424,7 +438,13 @@ function useReceivedFundsTable(announcements: UserAnnouncement[], spendingKeyPai
sortable: true,
format: toString,
},
{ align: 'left', field: 'from', label: vm.$i18n.tc('AccountReceiveTable.sender'), name: 'from', sortable: true },
{
align: 'left',
field: 'from',
label: vm.$i18n.tc('AccountReceiveTable.sender'),
name: 'from',
sortable: true,
},
{
align: 'left',
field: 'receiver',
Expand Down Expand Up @@ -466,16 +486,17 @@ function useReceivedFundsTable(announcements: UserAnnouncement[], spendingKeyPai
};
// Format announcements so from addresses support ENS/CNS, and so we can easily detect withdrawals
const formattedAnnouncements = ref(announcements.reverse()); // We reverse so most recent transaction is first
const formattedAnnouncements = ref(announcements.reverse() as ReceiveTableAnnouncement[]); // We reverse so most recent transaction is first
onMounted(async () => {
isLoading.value = true;
if (!provider.value) throw new Error(vm.$i18n.tc('AccountReceiveTable.wallet-not-connected'));
// Format addresses to use ENS, CNS, or formatted address
const fromAddresses = announcements.map((announcement) => announcement.from);
const formattedAddresses = await lookupOrFormatAddresses(fromAddresses, MAINNET_PROVIDER as Web3Provider);
const formattedAddresses = await lookupOrReturnAddresses(fromAddresses, MAINNET_PROVIDER as Web3Provider);
formattedAnnouncements.value.forEach((announcement, index) => {
announcement.from = formattedAddresses[index];
announcement.formattedFrom = formattedAddresses[index];
announcement.from = fromAddresses[index];
});
// Find announcements that have been withdrawn
Expand Down Expand Up @@ -505,6 +526,13 @@ function useReceivedFundsTable(announcements: UserAnnouncement[], spendingKeyPai
window.open(getEtherscanUrl(row.txHash, chainId));
}
function getSenderOrReceiverEtherscanUrl(address: string) {
if (!provider.value) throw new Error(vm.$i18n.tc('AccountReceiveTable.wallet-not-connected'));
// Assume mainnet if we don't have a provider with a valid chainId
const chainId = provider.value.network.chainId || 1;
return getEtherscanUrl(address, chainId);
}
/**
* @notice Initialize the withdraw process
* @param announcement Announcement to withdraw
Expand Down Expand Up @@ -634,7 +662,7 @@ function useReceivedFundsTable(announcements: UserAnnouncement[], spendingKeyPai
destinationAddress,
executeWithdraw,
expanded,
formatAddress,
formatNameOrAddress,
formatAmount,
formatDate,
formattedAnnouncements,
Expand All @@ -650,6 +678,7 @@ function useReceivedFundsTable(announcements: UserAnnouncement[], spendingKeyPai
isWithdrawInProgress,
mainTableColumns,
openInEtherscan,
getSenderOrReceiverEtherscanUrl,
paginationConfig,
privacyModalAddressWarnings,
showConfirmationModal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<q-card-section>
<div class="text-caption text-grey">{{ $t('AccountReceiveTableWithdrawConfirmation.to') }}</div>
<div>{{ $q.screen.xs ? formatAddress(destinationAddress) : destinationAddress }}</div>
<div>{{ $q.screen.xs ? formatNameOrAddress(destinationAddress) : destinationAddress }}</div>

<div>
<div class="text-caption text-grey q-mt-md">{{ $t('AccountReceiveTableWithdrawConfirmation.amount') }}</div>
Expand Down Expand Up @@ -120,7 +120,7 @@
import { computed, defineComponent, onMounted, PropType, ref } from '@vue/composition-api';
import { utils as umbraUtils, UserAnnouncement } from '@umbra/umbra-js';
import { FeeEstimate } from 'components/models';
import { formatAddress, toAddress } from 'src/utils/address';
import { formatNameOrAddress, toAddress } from 'src/utils/address';
import { BigNumber, formatUnits } from 'src/utils/ethers';
import { getEtherscanUrl, getGasPrice, humanizeTokenAmount, humanizeArithmeticResult } from 'src/utils/utils';
import useWalletStore from 'src/store/wallet';
Expand Down Expand Up @@ -282,7 +282,7 @@ export default defineComponent({
context,
confirmationOptions,
etherscanUrl,
formatAddress,
formatNameOrAddress,
formattedAmount,
formattedAmountReceived,
formattedDefaultTxCost,
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const supportedChains: Array<Chain> = [
logoURI: ETH_NETWORK_LOGO,
},
rpcUrls: ['https://mainnet.optimism.io', `https://optimism-mainnet.infura.io/v3/${String(process.env.INFURA_ID)}`],
blockExplorerUrls: ['https://optimistic.etherscan.io/'],
blockExplorerUrls: ['https://optimistic.etherscan.io'],
iconUrls: ['/networks/optimism.svg'],
logoURI: '/networks/optimism.svg',
},
Expand Down Expand Up @@ -110,7 +110,7 @@ export const supportedChains: Array<Chain> = [
logoURI: ETH_NETWORK_LOGO,
},
rpcUrls: ['https://arb1.arbitrum.io/rpc', `https://arbitrum-mainnet.infura.io/v3/${String(process.env.INFURA_ID)}`],
blockExplorerUrls: ['https://arbiscan.io/'],
blockExplorerUrls: ['https://arbiscan.io'],
iconUrls: ['/networks/arbitrum.svg'],
logoURI: '/networks/arbitrum.svg',
},
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/store/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
SupportedChainId,
TokenInfoExtended,
} from 'components/models';
import { formatAddress, lookupEnsName, lookupCnsName } from 'src/utils/address';
import { formatNameOrAddress, lookupEnsName, lookupCnsName } from 'src/utils/address';
import { ERC20_ABI, MAINNET_PROVIDER, MAINNET_RPC_URL, MULTICALL_ABI, MULTICALL_ADDRESSES } from 'src/utils/constants';
import { BigNumber, Contract, getAddress, Web3Provider, parseUnits } from 'src/utils/ethers';
import { UmbraApi } from 'src/utils/umbra-api';
Expand Down Expand Up @@ -424,7 +424,7 @@ export default function useWalletStore() {
const userDisplayName = computed(() => {
if (userEns.value) return userEns.value;
if (userCns.value) return userCns.value;
return userAddress.value ? formatAddress(userAddress.value) : undefined;
return userAddress.value ? formatNameOrAddress(userAddress.value) : undefined;
});

const keysMatch = computed(() => {
Expand Down
18 changes: 10 additions & 8 deletions frontend/src/utils/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,26 @@
import { CnsQueryResponse, Provider } from 'components/models';
import { utils } from '@umbra/umbra-js';
import { MAINNET_PROVIDER } from 'src/utils/constants';
import { getAddress, Web3Provider } from 'src/utils/ethers';
import { getAddress, Web3Provider, isHexString } from 'src/utils/ethers';
import { getChainById } from 'src/utils/utils';
import { i18n } from '../boot/i18n';
// ================================================== Address Helpers ==================================================

// Returns an address with the following format: 0x1234...abcd
export const formatAddress = (address: string) => `${address.slice(0, 6)}...${address.slice(38)}`;
export const formatNameOrAddress = (nameOrAddress: string) => {
return isHexString(nameOrAddress) ? `${nameOrAddress.slice(0, 6)}...${nameOrAddress.slice(38)}` : nameOrAddress;
};

// Returns an ENS or CNS name if found, otherwise returns the address
export const lookupAddress = async (address: string, provider: Provider) => {
const domainName = await lookupEnsOrCns(address, provider);
return domainName ? domainName : address;
};

// Returns an ENS or CNS name if found, otherwise returns a formatted version of the address
export const lookupOrFormatAddress = async (address: string, provider: Provider) => {
// Returns an ENS or CNS name if found, otherwise returns the address
export const lookupOrReturnAddress = async (address: string, provider: Provider) => {
const domainName = await lookupAddress(address, provider);
return domainName !== address ? domainName : formatAddress(address);
return domainName !== address ? domainName : address;
};

// Returns ENS name that address resolves to, or null if not found
Expand Down Expand Up @@ -80,15 +82,15 @@ export const toAddress = utils.toAddress;
// =============================================== Bulk Address Helpers ================================================
// Duplicates of the above methods for operating on multiple addresses in parallel

export const formatAddresses = (addresses: string[]) => addresses.map(formatAddress);
export const formatAddresses = (addresses: string[]) => addresses.map(formatNameOrAddress);

export const lookupAddresses = async (addresses: string[], provider: Provider) => {
const promises = addresses.map((address) => lookupAddress(address, provider));
return Promise.all(promises);
};

export const lookupOrFormatAddresses = async (addresses: string[], provider: Provider) => {
const promises = addresses.map((address) => lookupOrFormatAddress(address, provider));
export const lookupOrReturnAddresses = async (addresses: string[], provider: Provider) => {
const promises = addresses.map((address) => lookupOrReturnAddress(address, provider));
return Promise.all(promises);
};

Expand Down
11 changes: 7 additions & 4 deletions frontend/src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { supportedChains, TokenInfo } from 'src/components/models';
import { BigNumber, BigNumberish, hexValue, parseUnits, formatUnits } from './ethers';

import { BigNumber, BigNumberish, hexValue, parseUnits, formatUnits, isHexString } from './ethers';
/**
* @notice Generates the Etherscan URL based on the given `txHash` or `address and `chainId`
*/
export const getEtherscanUrl = (txHashOrAddress: string, chainId: number) => {
const group = txHashOrAddress.length === 42 ? 'address' : 'tx';
const group = isHexString(txHashOrAddress) ? (txHashOrAddress.length === 42 ? 'address' : 'tx') : 'ens';
const chain = getChainById(chainId);
const networkPrefix = chain?.blockExplorerUrls?.length ? chain?.blockExplorerUrls[0] : 'https://etherscan.io';
return `${networkPrefix}/${group}/${txHashOrAddress}`;
if (group === 'ens') {
return `${networkPrefix}/enslookup-search?search=${txHashOrAddress}`;
} else {
return `${networkPrefix}/${group}/${txHashOrAddress}`;
}
};

/**
Expand Down

0 comments on commit efa104c

Please sign in to comment.