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

Add Etherscan links to sender and receiver addresses #381

Merged
merged 10 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
mds1 marked this conversation as resolved.
Show resolved Hide resolved
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));
mds1 marked this conversation as resolved.
Show resolved Hide resolved
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