components/transfer/Transfer.vue
<template>
<section class="flex justify-center">
<div
:class="[
'transfer-card',
'w-full',
{
'bg-background-color k-shadow border py-8 px-7': !isMobile,
},
]"
>
<TransactionLoader
v-model="isLoaderModalVisible"
:status="status"
:total-token-amount="
withoutDecimals({ value: totalValues.withoutFee.token })
"
:unit="unit"
:transaction-id="transactionValue"
:total-usd-value="totalValues.withoutFee.usd"
:is-mobile="isMobile"
@close="isLoaderModalVisible = false"
/>
<div class="flex justify-between items-center mb-2">
<p class="font-bold text-3xl">
{{ $t('transfer') }} {{ unit }}
</p>
<NeoDropdown
position="bottom-left"
:mobile-modal="false"
menu-class="is-shadowless no-border-bottom"
>
<template #trigger="{ active }">
<NeoButton
icon="ellipsis-vertical"
no-shadow
class="square-32"
data-testid="transfer-button-options"
:active="active"
/>
</template>
<NeoDropdownItem
v-if="accountId"
v-clipboard:copy="generatePayMeLink"
data-testid="transfer-dropdown-pay-me"
@click="toast($t('toast.urlCopy'))"
>
<NeoIcon
icon="sack-dollar"
class="mr-2"
/>{{
$t('transfers.payMeLink')
}}
</NeoDropdownItem>
<NeoDropdownItem
v-clipboard:copy="recurringPaymentLink"
class="whitespace-nowrap"
data-testid="transfer-dropdown-recurring"
@click="toast($t('toast.urlCopy'))"
>
<NeoIcon
icon="rotate"
class="mr-2"
/>{{
$t('transfers.recurringPaymentLink')
}}
</NeoDropdownItem>
</NeoDropdown>
</div>
<PillTabs
:tabs="tokenTabs"
data-testid="transfer-token-tabs-container"
@select="handleTokenSelect"
/>
<div class="mb-5">
<NeoIcon
class="ml-2"
icon="circle-info"
/>
<span
v-dompurify-html="
$t('transfers.tooltip', [unit, chainNames[urlPrefix]])
"
/>
</div>
<div class="flex justify-between">
<div class="flex flex-col">
<span class="font-bold text-base mb-1">{{
$t('transfers.sender')
}}</span>
<div
v-if="accountId"
class="flex items-center"
>
<Avatar
:value="accountId"
:size="32"
data-testid="transfer-sender-full-address"
/>
<span class="ml-2">
<Identity
:address="accountId"
hide-identity-popover
data-testid="transfer-sender-address"
/>
</span>
<a
v-clipboard:copy="accountId"
class="ml-2"
data-testid="transfer-copy-sender-address"
@click="toast($t('general.copyAddressToClipboard'))"
>
<NeoIcon icon="copy" />
</a>
</div>
<Auth v-else />
</div>
<div class="flex flex-col items-end">
<span class="font-bold text-base mb-1">{{
$t('general.balance')
}}</span>
<div class="flex items-center">
<img
class="mr-2 size-8"
:src="tokenIcon"
alt="token"
>
<Money
:value="balance.token"
inline
/>
</div>
<span class="text-k-grey">≈ ${{ balance.usd }}</span>
</div>
</div>
<hr>
<div
v-if="!isMobile"
class="flex"
>
<div class="font-bold text-base mb-3 flex-1 mr-2 flex-grow-[2]">
{{ $t('transfers.recipient') }}
</div>
<div class="font-bold text-base mb-3 flex-1 flex-grow">
{{ $t('amount') }}
</div>
</div>
<div class="flex-grow flex-col">
<div
v-for="(destinationAddress, index) in targetAddresses"
:key="index"
class="mb-3"
>
<div
v-if="isMobile"
class="font-bold text-base mb-3 flex items-center justify-between"
>
{{ $t('transfers.recipient') }} {{ index + 1 }}
<a
v-if="targetAddresses.length > 1"
@click="deleteAddress(index)"
>
<NeoIcon
class="p-3"
icon="trash"
/>
</a>
</div>
<div
:class="[
'flex',
{
'flex-col': isMobile,
},
]"
>
<AddressInput
v-model="destinationAddress.address"
label=""
class="flex-1 flex-grow-[2]"
:class="[
{
'mr-2': !isMobile,
'mb-2': isMobile,
},
]"
:strict="false"
:is-invalid="isTargetAddressInvalid(destinationAddress)"
placeholder="Enter wallet address"
disable-error
/>
<div
class="flex-1"
:class="{ 'flex flex-grow': !isMobile }"
>
<div
v-if="displayUnit === 'token'"
class="relative"
>
<NeoInput
v-model="destinationAddress.token"
input-class="pr-8"
type="number"
placeholder="0"
step="0.01"
min="0"
icon-right-class="search !hidden"
data-testid="transfer-input-amount-token"
@focus="onAmountFieldFocus(destinationAddress, 'token')"
@update:model-value="
onAmountFieldChange(destinationAddress)
"
/>
<div class="absolute right-2 top-3 text-k-grey">
{{ unit }}
</div>
</div>
<NeoInput
v-else
v-model="destinationAddress.usd"
placeholder="0"
type="number"
step="0.01"
min="0"
icon-right="usd"
icon-right-class="text-k-grey"
data-testid="transfer-input-amount-usd"
@focus="onAmountFieldFocus(destinationAddress, 'usd')"
@update:model-value="onUsdFieldChange(destinationAddress)"
/>
<a
v-if="!isMobile && targetAddresses.length > 1"
class="flex"
data-testid="transfer-remove-recipient"
@click="deleteAddress(index)"
>
<NeoIcon
class="p-3"
icon="trash"
/>
</a>
</div>
</div>
<div class="mt-2">
<AddressChecker
:address="destinationAddress.address"
@check="
(isValid) => handleAddressCheck(destinationAddress, isValid)
"
@change="
(address) => handleAddressChange(destinationAddress, address)
"
/>
</div>
</div>
</div>
<div
class="mb-5 flex justify-center cursor-pointer"
data-testid="transfer-icon-add-recipient"
@click="addAddress"
>
{{ $t('transfers.addAddress') }}
<NeoIcon
class="ml-2"
icon="plus"
/>
</div>
<div class="flex justify-between items-center mb-5">
<div class="flex justify-between items-center">
{{ $t('transfers.sendSameAmount') }}
<!-- tips: don't use `margin` or `padding` directly on the tooltip trigger, it will cause misalignment of the tooltip -->
<span class="mr-2" />
<NeoTooltip :label="$t('transfers.setSameAmount')">
<NeoIcon icon="circle-info" />
</NeoTooltip>
</div>
<NeoSwitch
v-model="sendSameAmount"
data-testid="transfer-switch-same"
/>
</div>
<div class="flex justify-between items-center mb-5">
<span class="font-bold text-base">{{
$t('transfers.displayUnit')
}}</span>
<div class="flex items-center">
<span class="text-base mr-1">{{ $t('transfers.transferable') }}:
</span>
<span
v-if="displayUnit === 'token'"
class="font-bold text-base"
>
<Money
:value="transferableBalance.token"
inline
/>
</span>
<span
v-else
class="font-bold text-base"
>{{ transferableBalance.usd }} USD</span>
</div>
</div>
<div class="flex field has-addons flex-grow justify-center mb-4">
<TabItem
:active="displayUnit === 'token'"
:text="unit"
tag="button"
full-width
no-shadow
data-testid="transfer-tab-token"
@click="displayUnit = 'token'"
/>
<TabItem
:active="displayUnit === 'usd'"
text="USD"
tag="button"
full-width
no-shadow
data-testid="transfer-tab-usd"
@click="displayUnit = 'usd'"
/>
</div>
<div class="flex justify-between items-center mb-2">
<span class="text-xs">{{ $t('transfers.networkFee') }}</span>
<div
class="flex items-center"
data-testid="transfer-network-fee"
>
<span class="text-xs text-k-grey mr-1">({{ displayValues.fee[0] }})</span>
<span class="text-xs">{{ displayValues.fee[1] }}</span>
</div>
</div>
<div class="flex justify-between items-center mb-6">
<span class="font-bold text-base">{{ $t('spotlight.total') }}</span>
<div class="flex items-center">
<span class="text-xs text-k-grey mr-1">({{ displayValues.total.withFee[0] }})</span>
<span
class="font-bold text-base"
data-testid="transfer-total-amount"
>{{ displayValues.total.withFee[1] }}</span>
</div>
</div>
<div class="flex">
<NeoButton
class="flex flex-1 fixed-height is-shadowless"
variant="primary"
:disabled="disabled"
@click="handleOpenConfirmModal"
>
{{ $t('redirect.continue') }}
</NeoButton>
</div>
<TransferConfirmModal
:is-modal-active="isTransferModalVisible"
:display-total-value="displayValues.total.withoutFee"
:token-icon="tokenIcon"
:unit="unit"
:is-mobile="isMobile"
:target-addresses="targetAddresses"
@close="isTransferModalVisible = false"
@confirm="submit"
/>
</div>
</section>
</template>
<script lang="ts" setup>
import { ApiFactory } from '@kodadot1/sub-api'
import { ALTERNATIVE_ENDPOINT_MAP, chainNames } from '@kodadot1/static'
import zipWith from 'lodash/zipWith'
import { isAddress } from '@polkadot/util-crypto'
import type { DispatchError } from '@polkadot/types/interfaces'
import {
NeoButton,
NeoDropdown,
NeoDropdownItem,
NeoIcon,
NeoInput,
NeoSwitch,
NeoTooltip,
} from '@kodadot1/brick'
import {
calculateExactUsdFromToken,
calculateTokenFromUsd,
calculateUsdFromToken,
} from '@/utils/calculation'
import exec, {
estimate,
execResultValue,
txCb,
} from '@/utils/transactionExecutor'
import {
calculateBalance,
calculateBalanceUsdValue,
} from '@/utils/format/balance'
import { getNumberSumOfObjectField } from '@/utils/math'
import { useFiatStore } from '@/stores/fiat'
import Avatar from '@/components/shared/Avatar.vue'
import Identity from '@/components/identity/IdentityIndex.vue'
import { getMovedItemToFront } from '@/utils/objects'
import TransferConfirmModal from '@/components/transfer/TransferConfirmModal.vue'
import type { PillTab } from '@/components/shared/PillTabs.vue'
import PillTabs from '@/components/shared/PillTabs.vue'
import type { TokenDetails } from '@/composables/useToken'
import AddressInput from '@/components/shared/AddressInput.vue'
import TransactionLoader from '@/components/shared/TransactionLoader.vue'
import { KODADOT_DAO } from '@/utils/support'
import { toDefaultAddress } from '@/utils/account'
import AddressChecker from '@/components/shared/AddressChecker.vue'
import TabItem from '@/components/shared/TabItem.vue'
import Auth from '@/components/shared/Auth.vue'
import { useIdentityStore } from '@/stores/identity'
import useExistentialDeposit from '@/composables/useExistentialDeposit'
const Money = defineAsyncComponent(
() => import('@/components/shared/format/Money.vue'),
)
const route = useRoute()
const router = useRouter()
const { $consola, $i18n } = useNuxtApp()
const { unit, decimals, withDecimals, withoutDecimals } = useChain()
const { apiInstance } = useApi()
const { urlPrefix } = usePrefix()
const identityStore = useIdentityStore()
const { isLogIn, accountId } = useAuth()
const { getBalance } = useBalance()
const { fetchFiatPrice, getCurrentTokenValue } = useFiatStore()
const { initTransactionLoader, isLoading, resolveStatus, status }
= useTransactionStatus()
const { toast } = useToast()
const { getTokenIconBySymbol } = useIcon()
const { tokens } = useToken()
const { chainExistentialDeposit } = useExistentialDeposit()
const DOT_BUFFER_FEE = 10000000 // 0.001
const KSM_BUFFER_FEE = 100000000 // 0.0001
type QueryTargetAddress = {
target: string
usdamount?: string
amount?: string
}
export type TargetAddress = {
address: string
usd?: number | string
token?: number | string
isInvalid?: boolean
}
const isTransferModalVisible = ref(false)
const isLoaderModalVisible = ref(false)
const transactionValue = ref('')
const sendSameAmount = ref(false)
const displayUnit = ref<'token' | 'usd'>('usd')
const selectedTabFirst = ref(true)
const tokenTabs = ref<PillTab[]>([])
const targetAddresses = ref<TargetAddress[]>([{ address: '' }])
const txFee = ref<number>(0)
// Computed refs
// balance related
const balance = computed(() => {
const tokenAmount = Number(getBalance(unit.value)) || 0
const usdAmount = calculateBalanceUsdValue(
tokenAmount * Number(currentTokenValue.value),
decimals.value,
2,
)
return {
token: tokenAmount,
usd: usdAmount,
}
})
const generatePayMeLink = computed(() =>
generatePaymentLink([
{
address: accountId.value,
token: targetAddresses.value[0]?.token,
usd: targetAddresses.value[0]?.usd,
},
]),
)
const transferableBalance = computed(() => {
const tokenDeduction = txFee.value + chainExistentialDeposit.value
const tokenAmount = Math.max(balance.value.token - tokenDeduction, 0)
const usdAmount = calculateBalanceUsdValue(
tokenAmount * Number(currentTokenValue.value),
decimals.value,
2,
)
return {
token: tokenAmount,
usd: usdAmount,
}
})
const totalValues: {
withoutFee: {
token: number
usd: number
}
withFee: {
token: number
usd: number
}
} = reactive({
withoutFee: {
token: computed(() => {
const sumTokens = getNumberSumOfObjectField(
targetAddresses.value,
'token',
)
return withDecimals(sumTokens)
}),
usd: computed(() =>
Number(
calculateUsdFromToken(
withoutDecimals({ value: totalValues.withoutFee.token }),
Number(currentTokenValue.value),
).toFixed(4),
),
),
},
withFee: {
token: computed(() => totalValues.withoutFee.token + txFee.value),
usd: computed(() =>
Number(
calculateUsdFromToken(
withoutDecimals({ value: totalValues.withFee.token }),
Number(currentTokenValue.value),
).toFixed(4),
),
),
},
})
const txFeeBuffer = computed(() => {
switch (urlPrefix.value) {
case 'ksm':
return KSM_BUFFER_FEE
case 'dot':
return DOT_BUFFER_FEE
default:
return 0
}
})
// ui related
const isMobile = computed(() => useWindowSize().width.value <= 764)
const tokenIcon = computed(() => getTokenIconBySymbol(unit.value))
const disabled = computed(() => {
const tryingToSendTooMuch
= totalValues.withoutFee.token > transferableBalance.value.token
return !isLogIn.value || tryingToSendTooMuch || !hasValidTarget.value
})
const displayValues = computed(() => ({
fee: getDisplayUnitBasedValues(
calculateExactUsdFromToken(
withoutDecimals({ value: txFee.value }),
Number(currentTokenValue.value),
),
withoutDecimals({ value: txFee.value }),
),
total: {
withoutFee: getDisplayUnitBasedValues(
totalValues.withoutFee.usd,
withoutDecimals({ value: totalValues.withoutFee.token }),
),
withFee: getDisplayUnitBasedValues(
totalValues.withFee.usd,
withoutDecimals({ value: totalValues.withFee.token }),
),
},
}))
// others
const hasValidTarget = computed(() =>
targetAddresses.value.some(
item => isAddress(item.address) && !item.isInvalid && item.token,
),
)
const currentTokenValue = computed(() => getCurrentTokenValue(unit.value))
const recurringPaymentLink = computed(() => {
const addresses = targetAddresses.value.filter(
item => isAddress(item.address) && !item.isInvalid && item.usd,
)
return generatePaymentLink(addresses)
})
// END computed refs
const getDisplayUnitBasedValues = (
usdValue: number,
tokenAmount: number,
): [string, string] => {
return displayUnit.value === 'token'
? [`$${usdValue}`, `${tokenAmount} ${unit.value}`]
: [`${tokenAmount} ${unit.value}`, `$${usdValue}`]
}
const handleTokenSelect = (newToken: string) => {
selectedTabFirst.value = false
const token = tokens.value.find(t => t.symbol === newToken)
if (!token) {
return
}
routerReplace({
params: { prefix: token.defaultChain },
})
}
const generateTokenTabs = (
items: TokenDetails[],
selectedToken: string,
sort = false,
) => {
items = sort ? getMovedItemToFront(items, 'symbol', selectedToken) : items
return items.map(
availableToken =>
({
label: `${availableToken.symbol} $${availableToken.value || '0'}`,
image: availableToken.icon,
value: availableToken.symbol,
active: unit.value === availableToken.symbol,
}) as PillTab,
)
}
const getQueryMultipleKeys = (
queryKey: string,
filter: (value: [string, any]) => boolean,
) =>
Object.entries(route.query)
.filter(([key]) => key.startsWith(queryKey))
.filter(filter)
.map(([, value]) => value as string)
const getQueryTargetAddresses = () =>
getQueryMultipleKeys('target', ([_, address]) => {
if (isAddress(address as string)) {
return true
}
warningMessage(`Unable to use target address ${address}`)
return false
})
const isNumber = value => !isNaN(Number(value))
const getQueryUsdAmounts = () =>
getQueryMultipleKeys('usdamount', ([_, usdamount]) => isNumber(usdamount))
const getQueryAmounts = () =>
getQueryMultipleKeys('amount', ([_, amount]) => isNumber(amount))
const getQueryTargetAddress = ({
target,
usdamount,
amount,
}: QueryTargetAddress): TargetAddress => {
let tokenAmount = Number(amount)
let usdValue = Number(usdamount)
if (amount) {
usdValue = calculateUsdFromToken(
tokenAmount,
Number(currentTokenValue.value),
)
}
else if (usdamount) {
tokenAmount = calculateTokenFromUsd(
Number(getCurrentTokenValue(unit.value)),
usdValue,
)
}
return {
address: target,
usd: usdValue,
token: tokenAmount,
}
}
const checkQueryParams = () => {
const targets = getQueryTargetAddresses()
const usdAmounts = getQueryUsdAmounts()
const amounts = getQueryAmounts()
const queryTargetAddresses = zipWith(
targets,
usdAmounts,
amounts,
(target, usdamount, amount) =>
({
target,
usdamount,
amount,
}) as QueryTargetAddress,
)
if (targets.length) {
targetAddresses.value = queryTargetAddresses.map(getQueryTargetAddress)
sendSameAmount.value = getInitialSendSameAmount(
queryTargetAddresses,
usdAmounts.length !== 0 ? 'usdamount' : 'amount',
)
}
}
const getInitialSendSameAmount = (
queryTargetAddresses: QueryTargetAddress[],
keyToCheck: string,
): boolean => {
const items = queryTargetAddresses.map(x => x[keyToCheck])
return new Set(items).size === 1 && items.length !== 1
}
const onAmountFieldChange = (target: TargetAddress) => {
/* calculating usd value on the basis of price entered */
target.usd = target.token
? calculateUsdFromToken(
Number(getCurrentTokenValue(unit.value)),
Number(target.token),
)
: 0
// update targetAddresses
targetAddresses.value = [...targetAddresses.value]
if (sendSameAmount.value) {
unifyAddressAmount(target)
}
}
const onAmountFieldFocus = (target: TargetAddress, field: 'usd' | 'token') => {
if (Number(target[field]) === 0) {
target[field] = ''
}
}
const onUsdFieldChange = (target: TargetAddress) => {
/* calculating price value on the basis of usd entered */
target.token = target.usd
? calculateTokenFromUsd(
Number(getCurrentTokenValue(unit.value)),
Number(target.usd),
)
: 0
// update targetAddresses
targetAddresses.value = [...targetAddresses.value]
if (sendSameAmount.value) {
unifyAddressAmount(target)
}
}
const handleAddressCheck = (target: TargetAddress, isValid: boolean) => {
target.isInvalid = !isValid
targetAddresses.value = [...targetAddresses.value]
}
const handleAddressChange = (target: TargetAddress, newAddress: string) => {
target.address = newAddress
targetAddresses.value = [...targetAddresses.value]
}
const isTargetAddressInvalid = (target: TargetAddress) => {
return target.isInvalid === undefined ? false : target.isInvalid
}
const unifyAddressAmount = (target: TargetAddress) => {
targetAddresses.value = targetAddresses.value.map(address => ({
...address,
token: target.token,
usd: target.usd,
}))
}
const updateTargetAdressesOnTokenSwitch = () => {
targetAddresses.value.forEach((targetAddress) => {
onUsdFieldChange(targetAddress)
})
}
const handleOpenConfirmModal = () => {
if (!disabled.value) {
targetAddresses.value = targetAddresses.value.filter(
address => address.address && address.token && address.usd,
)
isTransferModalVisible.value = true
}
}
const getTransactionFee = async () => {
const { cb, arg } = await getTransferParams(
targetAddresses.value.map(
() =>
({
address: toDefaultAddress(KODADOT_DAO),
usd: 1,
token: 1,
}) as TargetAddress,
),
decimals.value as number,
)
return estimate(accountId.value, cb as any, arg as any)
}
const calculateTransactionFee = async () => {
txFee.value = 0
const fee = await getTransactionFee()
txFee.value = Number(fee) + txFeeBuffer.value
}
const updateAuthBalance = () => {
accountId.value && identityStore.fetchBalance({ address: accountId.value })
}
const getAmountToTransfer = (amount: number, decimals: number) =>
String(calculateBalance(Number(amount), decimals))
const getTransferParams = async (
addresses: TargetAddress[],
decimals: number,
) => {
const api = await apiInstance.value
const isSingle = targetAddresses.value.length === 1
const firstAddress = addresses[0]
const cb = isSingle
? api.tx.balances.transferAllowDeath
: api.tx.utility.batch
const arg = isSingle
? [
firstAddress.address as string,
getAmountToTransfer(firstAddress.token as number, decimals),
]
: [
addresses.map((target) => {
const amountToTransfer = getAmountToTransfer(
target.token as number,
decimals,
)
return api.tx.balances.transferAllowDeath(
target.address as string,
amountToTransfer,
)
}),
]
return { cb, arg }
}
const submit = async (
event: any,
usedNodeUrls: string[] = [],
): Promise<void> => {
isTransferModalVisible.value = false
initTransactionLoader()
try {
const { cb, arg } = await getTransferParams(
targetAddresses.value,
decimals.value as number,
)
const tx = await exec(
accountId.value,
'',
cb,
arg,
txCb(
() => {
transactionValue.value = execResultValue(tx)
targetAddresses.value = [{ address: '' }]
isLoading.value = false
},
(dispatchError) => {
execResultValue(tx)
onTxError(dispatchError)
isLoading.value = false
},
res => resolveStatus(res.status),
),
)
}
catch (e: any) {
if (e.message === 'Cancelled') {
warningMessage($i18n.t('general.tx.cancelled'), { reportable: false })
isLoading.value = false
isLoaderModalVisible.value = false
return
}
const availableUrls = ALTERNATIVE_ENDPOINT_MAP[urlPrefix.value]
if (usedNodeUrls.length < availableUrls.length) {
const nextTryUrls = availableUrls.filter(
url => !usedNodeUrls.includes(url),
)
// try to connect next possible url
await ApiFactory.useApiInstance(nextTryUrls[0])
submit(event, [nextTryUrls[0], ...usedNodeUrls])
}
if (e instanceof Error) {
$consola.error('[ERR: TRANSFER SUBMIT]', e)
warningMessage(e.toString())
isLoading.value = false
}
}
}
const onTxError = async (dispatchError: DispatchError): Promise<void> => {
await notifyDispatchError(dispatchError)
isLoading.value = false
}
const generatePaymentLink = (addressList: TargetAddress[]): string => {
const url = new URL(`${location.origin}${location.pathname}`)
addressList.forEach((addr, i) => {
const suffix = i === 0 ? '' : i
url.searchParams.append(`target${suffix}`, addr.address)
if (displayUnit.value === 'usd') {
url.searchParams.append(`usdamount${suffix}`, String(addr.usd))
}
else {
url.searchParams.append(`amount${suffix}`, String(addr.token))
}
})
return url.toString()
}
const addAddress = () => {
targetAddresses.value.push({
...(sendSameAmount.value ? targetAddresses.value[0] : {}),
address: '',
})
}
const deleteAddress = (index: number) => {
targetAddresses.value.splice(index, 1)
}
const routerReplace = ({ params = {}, query = {} }) => {
router
.replace({
params: params,
query: {
...route.query,
...query,
},
})
.catch(() => null) // null to further not throw navigation errors
}
// watchers
watch(isLoading, (newValue, oldValue) => {
// trigger modal only when loading change from false => true
// we want to keep modal open when loading changes true => false
if (newValue && !oldValue) {
isLoaderModalVisible.value = isLoading.value
}
})
watch(
tokens,
(items) => {
tokenTabs.value = generateTokenTabs(
items,
unit.value,
selectedTabFirst.value,
)
},
{ immediate: true },
)
watch(sendSameAmount, (value) => {
if (value) {
const tokenAmount = targetAddresses.value[0]?.token
const usdAmount = targetAddresses.value[0]?.usd
targetAddresses.value = targetAddresses.value.map(address => ({
...address,
token: tokenAmount,
usd: usdAmount,
}))
}
})
watch(
unit,
() => {
updateTargetAdressesOnTokenSwitch()
},
{ immediate: true },
)
watch(urlPrefix, updateAuthBalance)
onMounted(() => {
calculateTransactionFee()
updateAuthBalance()
fetchFiatPrice().then(checkQueryParams)
})
watchDebounced(
[urlPrefix, () => targetAddresses.value.length],
() => {
calculateTransactionFee()
},
{ debounce: 500 },
)
watchDebounced(
() => targetAddresses.value[0]?.usd,
(usdamount) => {
routerReplace({ query: { usdamount: (usdamount || 0).toString() } })
},
{ debounce: 300 },
)
</script>
<style lang="scss" scoped>
@import '@/assets/styles/abstracts/variables';
.transfer-card {
max-width: 660px;
@include touch {
width: 100vw;
}
.square-32 {
width: 32px;
height: 32px;
}
.fixed-height {
height: 51px;
}
}
:deep(.o-drop__menu.no-border-bottom) {
@apply border-b-0;
}
</style>