diff --git a/src/app/assets/svg/transfer.svg b/src/app/assets/svg/transfer.svg new file mode 100644 index 0000000..764ab71 --- /dev/null +++ b/src/app/assets/svg/transfer.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/app/components/AliasTransfer/hook/useAliasTransfer.ts b/src/app/components/AliasTransfer/hook/useAliasTransfer.ts new file mode 100644 index 0000000..f16d3fa --- /dev/null +++ b/src/app/components/AliasTransfer/hook/useAliasTransfer.ts @@ -0,0 +1,116 @@ +import { useEffect, useState } from 'react'; +import { useFeeCheck } from '../../../hooks/useFeeCheck'; +import { useInput } from '../../../hooks/useInput'; +import { fetchBackground } from '../../../utils/utils'; +import { GetAliasByAdderssParams, TransferAliasParams } from '../types'; + +const fee = 0.01; + +export const useAliasTransfer = ({ alias }: { alias: string }) => { + const { notEnoughFee } = useFeeCheck(fee); + const [addressStroke, setAddressStroke] = useState(null); + const commentInput = useInput('', { isEmpty: false, customValidation: true }); + const addressInput = useInput('', { isEmpty: false, customError: !!addressStroke }); + const [isChecking, setIsChecking] = useState(false); + const [isSubmitting, setSubmitting] = useState(false); + const [isValidAddress, setValidAddress] = useState(false); + + const [debouncedAddress, setDebouncedAddress] = useState(addressInput.value); + const [transactionSuccess, setTransactionSuccess] = useState(null); + const [errorMsg, setErrorMsg] = useState(''); + + useEffect(() => { + setValidAddress(false); + + const timer = setTimeout(() => { + setDebouncedAddress(addressInput.value); + }, 500); + + return () => clearTimeout(timer); + }, [addressInput.value]); + + useEffect(() => { + if (!debouncedAddress) return; + + const checkAddress = async () => { + setIsChecking(true); + + try { + const data = await fetchBackground({ + method: 'GET_ALIAS_BY_ADDRESS', + address: debouncedAddress, + } as GetAliasByAdderssParams); + + if (data.status === 'NOT_FOUND') { + setValidAddress(true); + setAddressStroke(null); + } else if (data.status === 'OK') { + setValidAddress(false); + setAddressStroke('This wallet already has an alias'); + } else { + setValidAddress(false); + setAddressStroke('No wallet exists for the provided address'); + } + } catch (err) { + console.log(err); + } finally { + setIsChecking(false); + } + }; + + checkAddress(); + }, [debouncedAddress]); + + const onTransfer = async () => { + setSubmitting(true); + + try { + const data = await fetchBackground({ + method: 'UPDATE_ALIAS', + address: addressInput.value, + alias, + comment: commentInput.value, + } as TransferAliasParams); + + if (data?.result?.tx_id) { + setTransactionSuccess(true); + } else { + setTransactionSuccess(false); + + if (data?.error?.code === -7) { + setErrorMsg('Not enough balance'); + } else { + const deamonMsg = data?.error?.message; + + setErrorMsg(deamonMsg || 'Unknown error'); + } + } + } catch (err) { + console.log(err); + } finally { + setSubmitting(false); + } + }; + + const isDisabled = + !isValidAddress || + addressInput.isEmpty || + !addressInput.inputValid || + isChecking || + isSubmitting || + !addressInput.value || + notEnoughFee || + Boolean(addressStroke); + + return { + onTransfer, + transactionSuccess, + errorMsg, + isDisabled, + addressStroke, + commentInput, + addressInput, + notEnoughFee, + fee, + }; +}; diff --git a/src/app/components/AliasTransfer/index.tsx b/src/app/components/AliasTransfer/index.tsx new file mode 100644 index 0000000..7ece83f --- /dev/null +++ b/src/app/components/AliasTransfer/index.tsx @@ -0,0 +1,105 @@ +import React, { useContext } from 'react'; +import { goBack } from 'react-chrome-extension-router'; +import styles from './styles.module.scss'; +import RoutersNav from '../UI/RoutersNav/RoutersNav'; +import MyInput, { inputDataProps } from '../UI/MyInput/MyInput'; +import Button from '../UI/Button/Button'; +import InfoTooltip from '../UI/InfoTooltip'; +import { Store } from '../../store/store-reducer'; +import successImg from '../../assets/images/success-round.png'; +import errorImg from '../../assets/images/failed-round.png'; +import { useAliasTransfer } from './hook/useAliasTransfer'; + +const AliasTransfer = () => { + const { state } = useContext(Store); + const walletAlias = state.wallet?.alias; + + const { + onTransfer, + addressStroke, + commentInput, + addressInput, + fee, + notEnoughFee, + errorMsg, + transactionSuccess, + isDisabled, + } = useAliasTransfer({ + alias: walletAlias, + }); + + if (transactionSuccess !== null) { + return ( +
+
+ transaction + +

+ {transactionSuccess ? `Alias transfered!` : 'Transaction failed!'} +

+ + {!transactionSuccess && ( +

{errorMsg}

+ )} +
+ + +
+ ); + } + + return ( +
+ + +
+ + + + + + +
+
+
+ Transfer fee +
+ +

+ {fee} ZANO +

+
+ + +
+
+
+ ); +}; + +export default AliasTransfer; diff --git a/src/app/components/AliasTransfer/styles.module.scss b/src/app/components/AliasTransfer/styles.module.scss new file mode 100644 index 0000000..feeea7c --- /dev/null +++ b/src/app/components/AliasTransfer/styles.module.scss @@ -0,0 +1,81 @@ +@import 'src/app/styles/variables'; + +.main { + width: 100%; + height: calc($appHeight - 72px); + display: flex; + flex-direction: column; + + &__content { + flex: 1; + display: flex; + flex-direction: column; + gap: 16px; + } + + &__transactionInfo { + margin-block: auto; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + + &_img { + width: 100px; + height: 100px; + } + + &_text { + text-align: center; + font-size: 20px; + line-height: 100%; + color: #1f8feb; + } + + &_errorMsg { + font-size: 16px; + text-align: center; + line-height: 140%; + width: 100%; + word-break: break-all; + } + } + + &__action { + margin-top: auto; + margin-bottom: 24px; + } + + &__bottom { + padding-bottom: 24px; + margin-top: auto; + display: flex; + flex-direction: column; + gap: 8px; + + .fee { + z-index: 10; + display: flex; + justify-content: space-between; + + &__label { + display: flex; + align-items: center; + gap: 8px; + font-size: 16px; + font-weight: 400; + color: #b6b6c4; + } + + &__value { + font-size: 16px; + font-weight: 400; + + &.error { + color: #ff6767; + } + } + } + } +} diff --git a/src/app/components/AliasTransfer/types.ts b/src/app/components/AliasTransfer/types.ts new file mode 100644 index 0000000..68b5101 --- /dev/null +++ b/src/app/components/AliasTransfer/types.ts @@ -0,0 +1,11 @@ +export interface GetAliasByAdderssParams { + method: string; + address: string; +} + +export interface TransferAliasParams { + method: string; + address: string; + alias: string; + comment?: string; +} diff --git a/src/app/components/Wallet/Wallet.module.scss b/src/app/components/Wallet/Wallet.module.scss index 5fe7d84..993c647 100644 --- a/src/app/components/Wallet/Wallet.module.scss +++ b/src/app/components/Wallet/Wallet.module.scss @@ -125,6 +125,11 @@ display: flex; align-items: center; gap: 8px; + + &__actions { + display: flex; + align-items: center; + } } .aliasContent { diff --git a/src/app/components/Wallet/Wallet.tsx b/src/app/components/Wallet/Wallet.tsx index f418302..5d7f04d 100644 --- a/src/app/components/Wallet/Wallet.tsx +++ b/src/app/components/Wallet/Wallet.tsx @@ -1,9 +1,11 @@ import React, { Dispatch, SetStateAction, useContext, useRef, useState } from 'react'; import Decimal from 'decimal.js'; +import { styleText } from 'util'; import copyIcon from '../../assets/svg/copy.svg'; import dotsIcon from '../../assets/svg/dots.svg'; import sendIcon from '../../assets/svg/send.svg'; import editIcon from '../../assets/svg/edit.svg'; +import transferIcon from '../../assets/svg/transfer.svg'; import settingsIcon from '../../assets/svg/settings.svg'; import showIcon from '../../assets/svg/show.svg'; import hideIcon from '../../assets/svg/hide.svg'; @@ -21,6 +23,7 @@ import NavLink from '../UI/NavLink/NavLink'; import { classNames } from '../../utils/classNames'; import { ZANO_ASSET_ID } from '../../../constants'; import AliasManagePage from '../AliasManagePage'; +import AliasTransfer from '../AliasTransfer'; const Wallet = ({ setConnectOpened }: { setConnectOpened: Dispatch> }) => { const { state, dispatch } = useContext(Store); @@ -111,15 +114,28 @@ const Wallet = ({ setConnectOpened }: { setConnectOpened: Dispatch {state.wallet.alias && ( - - edit icon - {/* Tooltip */} - Edit alias - +
+ + edit icon + {/* Tooltip */} + Edit alias + + + + transfer icon + {/* Tooltip */} + Transfer alias + +
)}
diff --git a/src/app/utils/utils.ts b/src/app/utils/utils.ts index 988c55c..2b1eebc 100644 --- a/src/app/utils/utils.ts +++ b/src/app/utils/utils.ts @@ -14,6 +14,7 @@ export async function fetchBackground(data: { success?: boolean; credentials?: { port: string }; alias?: string; + address?: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any }): Promise { return new Promise((resolve, reject) => { diff --git a/src/background/background.ts b/src/background/background.ts index 7e06aab..9694b43 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -19,6 +19,7 @@ import { burnAsset, getAsset, updateAlias, + getAliasByAddress, } from './wallet'; import { truncateToDecimals } from '../app/utils/utils'; @@ -278,6 +279,7 @@ const SELF_ONLY_REQUESTS = [ 'GET_TRANSFER_REQUEST', 'REGISTER_ALIAS', 'UPDATE_ALIAS', + 'GET_ALIAS_BY_ADDRESS', ]; interface Sender { id: string; @@ -590,6 +592,13 @@ async function processRequest(request: RequestType, sender: Sender, sendResponse break; } + case 'GET_ALIAS_BY_ADDRESS': { + getAliasByAddress(String(request.address)) + .then((res) => sendResponse(res)) + .catch(() => sendResponse({ error: 'Internal error' })); + break; + } + case 'IONIC_SWAP_ACCEPT': { try { const swapProposalRsp = await getSwapProposalInfo(request.hex_raw_proposal); diff --git a/src/background/wallet.ts b/src/background/wallet.ts index 36e6243..e1f885f 100644 --- a/src/background/wallet.ts +++ b/src/background/wallet.ts @@ -332,6 +332,13 @@ export const updateAlias = async ({ return data; }; +export const getAliasByAddress = async (address: string) => { + const response = await fetchData('get_alias_by_address', address); + const data = await response.json(); + + return data?.result; +}; + export const transfer = async ( assetId = ZANO_ASSET_ID, destination: string | undefined,