Skip to content

Commit

Permalink
feat(bridge-ui-v2): initial custom tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
KorbinianK committed Aug 3, 2023
1 parent 1eeba9d commit 7b8b706
Show file tree
Hide file tree
Showing 9 changed files with 395 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
import { isAddress } from 'ethereum-address';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n';
import type { Address } from 'viem';
import { Alert } from '$components/Alert';
import { uid } from '$libs/util/uid';
let input: HTMLInputElement;
let inputId = `input-${uid()}`;
let showAlert = true;
let ethereumAddress = '';
export let ethereumAddress: Address | string = '';
let isValidEthereumAddress = false;
let tooShort = true;
Expand All @@ -23,6 +23,7 @@
if (ethereumAddress.length > 41) {
tooShort = false;
validateEthereumAddress();
dispatch('input', ethereumAddress);
} else {
tooShort = true;
}
Expand All @@ -39,9 +40,6 @@
};
export const focus = () => input.focus();
export const value = () => {
return input.value;
};
</script>

<div class="f-col space-y-2">
Expand Down
21 changes: 21 additions & 0 deletions packages/bridge-ui-v2/src/components/Icon/ERC20.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script lang="ts">
export let height = 30;
export let width = 30;
</script>

<svg
xmlns="http://www.w3.org/2000/svg"
class="bg-white !rounded-full"
{height}
{width}
preserveAspectRatio="xMidYMid"
viewBox="-38.39985 -104.22675 332.7987 625.3605"
><path fill="#343434" d="M125.166 285.168l2.795 2.79 127.962-75.638L127.961 0l-2.795 9.5z" /><path
fill="#8C8C8C"
d="M127.962 287.959V0L0 212.32z" /><path
fill="#3C3C3B"
d="M126.386 412.306l1.575 4.6L256 236.587l-128.038 75.6-1.575 1.92z" /><path
fill="#8C8C8C"
d="M0 236.585l127.962 180.32v-104.72z" /><path fill="#141414" d="M127.961 154.159v133.799l127.96-75.637z" /><path
fill="#393939"
d="M127.96 154.159L0 212.32l127.96 75.637z" /></svg>
23 changes: 21 additions & 2 deletions packages/bridge-ui-v2/src/components/Icon/Icon.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,28 @@
| 'up-down-circle'
| 'check-circle'
| 'info-circle'
| 'plus-circle'
| 'circle'
| 'arrow-right'
| 'up-down'
| 'check';
| 'check'
| 'trash';
</script>

<script lang="ts">
export let type: IconType;
export let size = 20;
export let width = size;
export let height = size;
export let minX = 0;
export let minY = 0;
export let vWidth = width;
export let vHeight = height;
export let viewBox = `${minX} ${minY} ${vWidth} ${vHeight}`;
export let fillClass = 'fill-primary-icon';
</script>

<svg {width} {height} class={$$props.class} viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg {width} {height} class={$$props.class} {viewBox} fill="none" xmlns="http://www.w3.org/2000/svg">
{#if type === 'bridge'}
<path
class={fillClass}
Expand Down Expand Up @@ -123,6 +130,12 @@
fill-rule="evenodd"
clip-rule="evenodd"
d="M18 10C18 14.4183 14.4183 18 10 18C5.58172 18 2 14.4183 2 10C2 5.58172 5.58172 2 10 2C14.4183 2 18 5.58172 18 10ZM11 6C11 6.55228 10.5523 7 10 7C9.44771 7 9 6.55228 9 6C9 5.44772 9.44771 5 10 5C10.5523 5 11 5.44772 11 6ZM9 9C8.58579 9 8.25 9.33579 8.25 9.75C8.25 10.1642 8.58579 10.5 9 10.5H9.25338C9.41332 10.5 9.53213 10.6481 9.49743 10.8042L9.03829 12.8704C8.79542 13.9633 9.62706 15 10.7466 15H11C11.4142 15 11.75 14.6642 11.75 14.25C11.75 13.8358 11.4142 13.5 11 13.5H10.7466C10.5867 13.5 10.4679 13.3519 10.5026 13.1958L10.9617 11.1296C11.2046 10.0367 10.3729 9 9.25338 9H9Z" />
{:else if type === 'plus-circle'}
<path
class={fillClass}
fill-rule="evenodd"
clip-rule="evenodd"
d="M15.5 29.5c-7.18 0-13-5.82-13-13s5.82-13 13-13 13 5.82 13 13-5.82 13-13 13zM21.938 15.938c0-0.552-0.448-1-1-1h-4v-4c0-0.552-0.447-1-1-1h-1c-0.553 0-1 0.448-1 1v4h-4c-0.553 0-1 0.448-1 1v1c0 0.553 0.447 1 1 1h4v4c0 0.553 0.447 1 1 1h1c0.553 0 1-0.447 1-1v-4h4c0.552 0 1-0.447 1-1v-1z" />
{:else if type === 'circle'}
<circle class={fillClass} cx="10" cy="10" r="6" />
{:else if type === 'arrow-right'}
Expand All @@ -145,5 +158,11 @@
clip-rule="evenodd"
d="M16.7045 4.15347C17.034 4.4045 17.0976 4.87509 16.8466 5.20457L8.84657 15.7046C8.71541 15.8767 8.51627 15.9838 8.30033 15.9983C8.08439 16.0129 7.87271 15.9334 7.71967 15.7804L3.21967 11.2804C2.92678 10.9875 2.92678 10.5126 3.21967 10.2197C3.51256 9.92682 3.98744 9.92682 4.28033 10.2197L8.17351 14.1129L15.6534 4.29551C15.9045 3.96603 16.3751 3.90243 16.7045 4.15347Z" />
</svg>
{:else if type === 'trash'}
<path
class={fillClass}
fill-rule="evenodd"
clip-rule="evenodd"
d="M20,6H16V5a3,3,0,0,0-3-3H11A3,3,0,0,0,8,5V6H4A1,1,0,0,0,4,8H5V19a3,3,0,0,0,3,3h8a3,3,0,0,0,3-3V8h1a1,1,0,0,0,0-2ZM10,5a1,1,0,0,1,1-1h2a1,1,0,0,1,1,1V6H10Zm7,14a1,1,0,0,1-1,1H8a1,1,0,0,1-1-1V8H17Z" />
{/if}
</svg>
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
<script lang="ts">
import AddressInput from '$components/Bridge/AddressInput.svelte';
import type { Address } from 'viem';
import { erc20ABI, readContract, getNetwork } from '@wagmi/core';
import { formatUnits } from 'viem';
import { t } from 'svelte-i18n';
import { Icon } from '$components/Icon';
import Erc20 from '$components/Icon/ERC20.svelte';
import { fetchToken } from '@wagmi/core';
import { Spinner } from '$components/Spinner';
import { TokenType, type Token, type TokenEnv } from '$libs/token';
import { getLogger } from '$libs/util/logger';
import { uid } from '$libs/util/uid';
import { account } from '$stores/account';
import { tokenService } from '$libs/storage/services';
import { Alert } from '$components/Alert';
const log = getLogger('component:AddCustomERC20');
let dialogId = `dialog-${uid()}`;
export let modalOpen: boolean = false;
export let loading: boolean = false;
export let loadingTokenDetails: boolean = false;
let tokenDetails: (TokenEnv & { balance: bigint; decimals: number }) | null;
let tokenError: string = '';
let tokenAddress: Address | string = '0x6A08CDA7dde383BBc8267f079d20E1ad3C270fff';
let customTokens: Token[] = [];
let customToken: Token | null = null;
let disabled = true;
const addCustomErc20Token = () => {
if (customToken) {
tokenService.storeToken(customToken, $account?.address as Address);
customTokens = tokenService.getTokens($account?.address as Address);
}
tokenAddress = '';
};
const closeModal = () => {
modalOpen = false;
resetForm();
};
const resetForm = () => {
customToken = null;
tokenDetails = null;
tokenError = '';
isValidEthereumAddress = false;
};
const openModal = () => {
modalOpen = true;
customTokens = tokenService.getTokens($account?.address as Address);
log('customTokens', customTokens);
};
const remove = async (token: Token) => {
log('remove token', token);
const address = $account.address;
tokenService.removeToken(token, address as Address);
customTokens = tokenService.getTokens(address as Address);
};
let isValidEthereumAddress = false;
const onAddressValidation = async (event: { detail: { isValidEthereumAddress: boolean } }) => {
log('triggered onAddressValidation');
isValidEthereumAddress = event.detail.isValidEthereumAddress;
if (isValidEthereumAddress) {
await onAddressChange(tokenAddress);
} else {
resetForm();
}
};
const onAddressChange = async (tokenAddress: string) => {
log('Fetching token details for address "%s"…', tokenAddress);
try {
const tokenInfo = await fetchToken({ address: tokenAddress as Address });
const balance = await readContract({
address: tokenAddress as Address,
abi: erc20ABI,
functionName: 'balanceOf',
args: [$account?.address as Address],
});
log({ balance });
tokenDetails = { ...tokenInfo, balance };
const { chain } = getNetwork();
if ($account.address && chain) {
customToken = {
name: tokenDetails.name,
addresses: {
[chain?.id]: tokenDetails.address,
},
decimals: tokenDetails.decimals,
symbol: tokenDetails.symbol,
logoComponent: null,
type: TokenType.ERC20,
} as Token;
log({ customToken });
}
} catch (error) {
tokenError = 'Error fetching token details';
console.error('Failed to fetch token: ', error);
}
};
$: {
if (modalOpen) openModal();
resetForm();
if (isValidEthereumAddress) {
onAddressChange(tokenAddress);
}
}
$: customTokens = tokenService.getTokens($account?.address as Address);
$: disabled = tokenError !== '';
const closeModalIfClickedOutside = (e: MouseEvent) => {
if (e.target === e.currentTarget) {
closeModal();
}
};
const closeModalIfKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closeModal();
}
};
</script>

<svelte:window on:keydown={closeModalIfKeyDown} />

<dialog id={dialogId} class="modal modal-bottom md:modal-middle" class:modal-open={modalOpen}>
<div
class="modal-box relative px-6 py-[35px] md:py-[20px] bg-primary-base-background text-primary-base-content text-center">
<button class="absolute right-6 top-[35px] md:top-[20px]" on:click={closeModal}>
<Icon type="x-close" fillClass="fill-secondary-icon" size={24} />
</button>
<div class="mt-4 mb-2">
<AddressInput bind:ethereumAddress={tokenAddress} on:addressvalidation={onAddressValidation} />
{#if tokenDetails}
<div class="w-full flex items-center justify-between">
<span>Name: {tokenDetails.symbol}</span>
<span>Balance: {formatUnits(tokenDetails.balance, tokenDetails.decimals)}</span>
</div>
{:else if loadingTokenDetails}
<Spinner />
{:else if tokenError !== '' && tokenAddress !== ''}
<Alert type="error" forceColumnFlow>
<p class="font-bold">{$t('bridge.errors.custom_token.not_found')}</p>
<p>{$t('bridge.errors.custom_token.description')}</p>
</Alert>
{:else}
<div class="min-h-[25px]" />
{/if}
</div>
{#if loading}
<Spinner />
{:else}
<button class="btn btn-primary" disabled={Boolean(disabled)} on:click={addCustomErc20Token}> Add </button>
{/if}

{#if customTokens.length > 0}
<div class="flex h-full w-full flex-col justify-between mt-6">
<h3>Your imported tokens:</h3>
{#each customTokens as ct (ct.symbol)}
<div class="flex items-center justify-between">
<div class="flex items-center">
<Erc20 />
<span class="bg-transparent">{ct.symbol}</span>
</div>
<button class="btn btn-sm btn-ghost flex justify-center items-center" on:click={() => remove(ct)}>
<Icon type="trash" fillClass="fill-secondary-icon" size={24} />
</button>
</div>
{/each}
</div>
{/if}
<!-- We catch key events aboe -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div role="button" tabindex="0" class="overlay-backdrop" on:click={closeModalIfClickedOutside} />
</div>
</dialog>
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
<script lang="ts">
import { ClickMask } from '$components/ClickMask';
import { Icon } from '$components/Icon';
import { tokenService } from '$libs/storage/services';
import type { Token } from '$libs/token';
import { classNames } from '$libs/util/classNames';
import { noop } from '$libs/util/noop';
import { account } from '$stores/account';
import AddCustomErc20 from './AddCustomERC20.svelte';
import type { Address } from 'viem';
import { symbolToIconMap } from './symbolToIconMap';
import Erc20 from '$components/Icon/ERC20.svelte';
import { onMount, onDestroy } from 'svelte';
export let id: string;
export let menuOpen = false;
export let tokens: Token[] = [];
export let customTokens: Token[] = [];
export let value: Maybe<Token> = null;
let modalOpen = false;
export let selectToken: (token: Token) => void = noop;
export let closeMenu: () => void = noop;
Expand All @@ -25,6 +35,25 @@
}
};
}
function showAddERC20() {
modalOpen = true;
}
const handleStorageChange = (newTokens: Token[]) => {
customTokens = newTokens;
};
onMount(() => {
tokenService.subscribeToChanges(handleStorageChange);
if ($account?.address) {
customTokens = tokenService.getTokens($account?.address as Address);
}
});
onDestroy(() => {
tokenService.unsubscribeFromChanges(handleStorageChange);
});
</script>

<!-- Desktop (or larger) view -->
Expand All @@ -44,6 +73,39 @@
</div>
</li>
{/each}
{#each customTokens as token (token.symbol)}
<li
role="option"
tabindex="0"
aria-selected={token === value}
on:click={() => selectToken(token)}
on:keydown={getTokenKeydownHandler(token)}>
<div class="p-4">
<i role="img" aria-label={token.name}>
<Erc20 />
</i>
<span class="body-bold">{token.symbol}</span>
</div>
</li>
{/each}
<div class="h-sep" />
<li>
<button on:click={showAddERC20} class="flex hover:bg-dark-5 flex justify-center items-center p-4 rounded-sm">
<Icon type="plus-circle" fillClass="fill-primary-icon" size={20} vWidth={30} vHeight={30} />
<span
class="
text-sm
font-medium
bg-transparent
flex-1
w-[100px]
px-0
pl-2">
Add Custom
</span>
</button>
</li>
</ul>

<ClickMask fn={closeMenu} active={menuOpen} />
<AddCustomErc20 bind:modalOpen />
Loading

0 comments on commit 7b8b706

Please sign in to comment.