Skip to content

Commit

Permalink
feat: improve transaction builder token selection (#3878)
Browse files Browse the repository at this point in the history
* feat: safe token verification init

* fix: separate list item, translations

* fix: token default logo

* Refactored tokens list and item

* get snapshot token list

* fix confirmDialog confirm token

* fix AvatarToken key

* added filter by verified

* fix texts and translations

* Fix styling and cleanup

* Add balance and refactor

* Fix

* Fixes

* Use stamp

* Add search by contract address

* Fix text

* Remove TODO

* Fix style chevron

* Add address to select input

* Add docs link

* Update src/plugins/safeSnap/components/Form/TokensModal.vue

Co-authored-by: aurelianoB <[email protected]>

* Add more decimals to balance

* Fix

* Fix

* Remove src prop

* Remove explorer link for unverified token and copy address instead

---------

Co-authored-by: Sam <[email protected]>
Co-authored-by: Sam <[email protected]>
Co-authored-by: aurelianoB <[email protected]>
  • Loading branch information
4 people authored Jun 8, 2023
1 parent 6fa74b5 commit eae549d
Show file tree
Hide file tree
Showing 11 changed files with 370 additions and 72 deletions.
10 changes: 4 additions & 6 deletions src/components/AvatarToken.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
<script setup lang="ts">
withDefaults(
defineProps<{
src: string;
address?: string;
address: string;
size?: string;
}>(),
{
size: '22',
address: '0x3901D0fDe232aF1427216b79f5243f8A022d28cf'
size: '22'
}
);
</script>

<template>
<img
:src="src"
:src="`https://cdn.stamp.fyi/token/eth:${address}?s=100`"
class="rounded-full bg-skin-border object-cover"
:style="{
width: `${Number(size)}px`,
Expand All @@ -25,7 +23,7 @@ withDefaults(
@error="
(
$event.target as HTMLImageElement
).src = `https://cdn.stamp.fyi/avatar/eth:${address}?s=100`
).src = `https://cdn.stamp.fyi/token/eth:${address}?s=100`
"
/>
</template>
15 changes: 5 additions & 10 deletions src/components/SetupStrategyBasic.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { call, clone } from '@snapshot-labs/snapshot.js/src/utils';
import { JsonRpcProvider } from '@ethersproject/providers';
import { ERC20ABI } from '@/helpers/abi';
import { isAddress } from '@ethersproject/address';
import { shorten } from '@/helpers/utils';
const BROVIDER_URL = 'https://rpc.snapshot.org';
Expand Down Expand Up @@ -141,27 +142,21 @@ watch(
<BaseBlock v-if="token.name" class="!mt-3 space-x-1 text-left text-sm">
<div class="flex justify-between">
<div class="flex items-center gap-1 truncate">
<AvatarToken
v-if="token.logo"
:src="token.logo"
:address="contract"
class="mr-1"
size="30"
/>
<AvatarToken :address="contract" class="mr-1" size="38" />
<div class="truncate">
<div class="mr-4 truncate whitespace-nowrap text-skin-link">
{{ token.name }}
</div>
<BasePill class="py-1">${{ token.symbol }}</BasePill>
{{ token.symbol }}
</div>
</div>
<div class="flex items-center">
<div class="flex items-end">
<BaseLink
v-if="network == '1'"
class="text-skin-text hover:text-skin-link"
:link="`https://etherscan.io/token/${contract}`"
>
{{ $t('setup.strategy.tokenVoting.seeOnEtherscan') }}
{{ shorten(contract) }}
</BaseLink>
</div>
</div>
Expand Down
8 changes: 1 addition & 7 deletions src/components/TreasuryAssetsListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,7 @@ const { formatCompactNumber, formatNumber } = useIntl();
<li
class="flex items-center gap-2 border-b border-skin-border px-4 py-[12px] last:border-b-0"
>
<AvatarToken
v-if="asset.logo_url"
:src="asset.logo_url"
:address="asset.contract_address"
class="mr-1"
size="38"
/>
<AvatarToken :address="asset.contract_address" class="mr-1" size="38" />

<div class="flex w-full justify-between">
<div class="leading-6">
Expand Down
4 changes: 2 additions & 2 deletions src/components/Ui/CollapsibleContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ defineEmits(['toggle']);
</div>
<button
v-if="showArrow"
class="mr-3 flex cursor-pointer items-center"
class="mr-2 flex cursor-pointer items-center"
@click="$emit('toggle')"
>
<i-ho-chevron-up :class="{ rotate: !open }" />
<i-ho-chevron-up :class="{ rotate: !open }" class="text-xs" />
</button>
<slot name="icons"></slot>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/helpers/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ETH_CONTRACT = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
3 changes: 3 additions & 0 deletions src/helpers/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,9 @@ export interface CollectableAsset extends SafeAsset {
export interface TokenAsset extends SafeAsset {
symbol: string;
decimals: number;
balance: string;
verified?: any;
chainId?: number;
}

export interface CollectableAssetTransaction extends SafeTransaction {
Expand Down
2 changes: 2 additions & 0 deletions src/locales/default.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"searchPlaceholder": "Search",
"searchPlaceholderVotes": "Search: Address, ENS, or Lens",
"searchPlaceholderTokens": "Search: Name, Symbol, or Address",
"searchVotingPower": "Voting power",
"searchChoice": "Choice",
"spaceCount": "{0} space(s)",
Expand Down Expand Up @@ -35,6 +36,7 @@
"strategiesPage": "Strategies",
"space": "Space",
"spaces": "Spaces",
"verified": "Verified",
"verifiedSpace": "Verified space",
"warningSpace": "This space has been flagged as potentially malicious. Proceed with caution.",
"warningShortUrl": "Short URLs are not allowed. Please use the full URL.",
Expand Down
144 changes: 144 additions & 0 deletions src/plugins/safeSnap/components/Form/TokensModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<script setup lang="ts">
import { useConfirmDialog } from '@vueuse/core';
import { TokenAsset } from '@/helpers/interfaces';
import TokensModalItem from './TokensModalItem.vue';
const props = defineProps<{
open: boolean;
tokenAddress: string;
tokens: TokenAsset[];
}>();
const emit = defineEmits(['close', 'tokenAddress']);
const searchInput = ref('');
const showUnverifiedTokens = ref(false);
const confirmDialogOpen = ref(false);
const confirmDialogData = ref(null);
const confirmDialog = useConfirmDialog(confirmDialogOpen);
confirmDialog.onConfirm(token => {
emit('tokenAddress', token.address);
emit('close');
});
const tokensFiltered = computed(() => {
const filterTokens = (token: TokenAsset) => {
const tokenProperties = [token.symbol, token.name, token.address].map(
property => property.toLowerCase()
);
const searchQuery = searchInput.value.toLowerCase();
const searchMatch = tokenProperties.some(property =>
property.includes(searchQuery)
);
const isVerified = token.address === 'main' || token.verified;
return (
(searchMatch || !searchInput.value) &&
(showUnverifiedTokens.value || isVerified)
);
};
return props.tokens.filter(filterTokens);
});
function handleTokenClick(token) {
const isVerified = token.address === 'main' || token.verified;
if (!isVerified) {
confirmDialogData.value = token;
return confirmDialog.reveal();
}
emit('tokenAddress', token.address);
emit('close');
}
</script>

<template>
<BaseModal :open="open" @close="$emit('close')">
<template #header>
<div
class="flex flex-col content-center items-center justify-center gap-x-4"
>
<h3>Assets</h3>
<BaseSearch
v-model="searchInput"
:placeholder="$t('searchPlaceholderTokens')"
modal
focus-on-mount
class="min-h-[60px] w-full flex-auto !px-3 pb-3 sm:!px-4"
>
<template #after>
<BasePopover :focus="false">
<template #button>
<BaseButtonIcon>
<i-ho-funnel class="text-skin-link" />
</BaseButtonIcon>
</template>
<template #content>
<h3 class="-mb-2 mt-3 text-center text-skin-heading">
Filters
</h3>
<div class="m-4 space-y-3">
<div class="space-y-2">
<div class="space-y-2">
<TuneCheckbox
v-model="showUnverifiedTokens"
hint="Show unverified tokens"
name="searchOnlyWithReason"
/>
</div>
</div>
</div>
</template>
</BasePopover>
</template>
</BaseSearch>
</div>
</template>

<template #default="{ maxHeight }">
<div
class="flex w-full flex-col overflow-auto"
:style="{ minHeight: maxHeight }"
>
<TokensModalItem
v-for="token in tokensFiltered"
:key="token.address"
:token="token"
:is-selected="token.address === tokenAddress"
@select="handleTokenClick"
/>

<div
v-if="searchInput.length && tokensFiltered.length === 0"
class="flex flex-row content-start items-start justify-center py-4"
>
<span>{{ $t('noResultsFound') }}</span>
</div>
</div>
</template>
</BaseModal>

<teleport to="#modal">
<ModalConfirmAction
:open="confirmDialogOpen"
show-cancel
@close="confirmDialog.cancel"
@confirm="confirmDialog.confirm(confirmDialogData)"
>
<BaseMessageBlock level="warning-red" class="m-4">
This token isn't known to us. Please make sure it is the correct address
before proceeding.
<BaseLink
link="https://docs.snapshot.org/user-guides/token-verification"
>
{{ $t('learnMore') }}</BaseLink
>
</BaseMessageBlock>
</ModalConfirmAction>
</teleport>
</template>
82 changes: 82 additions & 0 deletions src/plugins/safeSnap/components/Form/TokensModalItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<script setup lang="ts">
import { shorten, explorerUrl } from '@/helpers/utils';
import { TokenAsset } from '@/helpers/interfaces';
import { ETH_CONTRACT } from '@/helpers/constants';
const props = defineProps<{
token: TokenAsset;
isSelected: boolean;
}>();
const emit = defineEmits(['select']);
const { formatNumber, getNumberFormatter } = useIntl();
const { copyToClipboard } = useCopy();
const exploreUrl = computed(() => {
const network = props.token.verified ? String(props.token.chainId) : null;
if (!network) return null;
return explorerUrl(network, props.token.address);
});
</script>

<template>
<button
class="flex h-[64px] w-full cursor-pointer items-center justify-between border-b border-skin-border px-3 py-2 hover:bg-skin-border sm:px-4"
:class="{
'!bg-skin-border': isSelected
}"
@click="emit('select', token)"
>
<div class="flex items-center">
<div class="mr-3 flex">
<AvatarToken
:address="token.address === 'main' ? ETH_CONTRACT : token.address"
size="38"
/>
</div>

<div class="pr-4">
<div class="flex w-full items-center text-skin-link">
<div class="flex items-center gap-1">
{{ token.symbol }}
<i-ho-check-badge
v-if="token.verified || token.address === 'main'"
v-tippy="{ content: $t('verified') }"
class="text-sm text-green"
/>
</div>
</div>
<span class="line-clamp-1 text-left text-skin-text">
{{ token.name }}
</span>
</div>
</div>

<div class="h-full text-right">
<span v-if="token.address !== 'main'" class="text-skin-link">
{{
formatNumber(
Number(token.balance),
getNumberFormatter({ maximumFractionDigits: 6 }).value
)
}}
</span>
<div>
<BaseLink
v-if="token.address !== 'main' && exploreUrl"
:link="exploreUrl"
@click.stop
>
{{ shorten(token.address) }}</BaseLink
>
<span
v-else-if="token.address !== 'main'"
@click.stop="copyToClipboard(token.address)"
>
{{ shorten(token.address) }}
</span>
</div>
</div>
</button>
</template>
Loading

1 comment on commit eae549d

@vercel
Copy link

@vercel vercel bot commented on eae549d Jun 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.