diff --git a/package-lock.json b/package-lock.json
index 0088ed6ad..886cbf705 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -99,6 +99,36 @@
"webpack-dev-server": "^4.11.0"
}
},
+ "../../xverse-core/xverse-core": {
+ "name": "@secretkeylabs/xverse-core",
+ "version": "0.0.1",
+ "extraneous": true,
+ "hasInstallScript": true,
+ "license": "ISC",
+ "dependencies": {
+ "@stacks/encryption": "4.3.5",
+ "@stacks/keychain": "4.3.5",
+ "@stacks/network": "4.3.5",
+ "@stacks/transactions": "4.3.5",
+ "@stacks/wallet-sdk": "^5.0.2",
+ "axios": "0.27.2",
+ "bignumber.js": "9.1.0",
+ "bip32": "^2.0.6",
+ "bip39": "3.0.3",
+ "bitcoinjs-lib": "5.2.0",
+ "buffer": "6.0.3",
+ "process": "^0.11.10",
+ "util": "^0.12.4"
+ },
+ "devDependencies": {
+ "jest": "^29.0.3",
+ "rimraf": "^3.0.2",
+ "ts-loader": "^9.4.1",
+ "typescript": "^4.8.3",
+ "webpack": "^5.74.0",
+ "webpack-cli": "^4.10.0"
+ }
+ },
"node_modules/@adobe/css-tools": {
"version": "4.0.1",
"license": "MIT"
diff --git a/src/app/screens/confirmStxTransaxtion/confirmStxTransactionComponent/index.tsx b/src/app/components/confirmStxTransactionComponent/index.tsx
similarity index 88%
rename from src/app/screens/confirmStxTransaxtion/confirmStxTransactionComponent/index.tsx
rename to src/app/components/confirmStxTransactionComponent/index.tsx
index 4e2a748c9..4966b2c46 100644
--- a/src/app/screens/confirmStxTransaxtion/confirmStxTransactionComponent/index.tsx
+++ b/src/app/components/confirmStxTransactionComponent/index.tsx
@@ -35,9 +35,8 @@ const Container = styled.div`
const ButtonContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'row',
- marginLeft: props.theme.spacing(8),
- marginRight: props.theme.spacing(8),
- marginBottom: props.theme.spacing(8),
+ marginBottom: props.theme.spacing(20),
+ marginTop: props.theme.spacing(24),
}));
const TransparentButtonContainer = styled.div((props) => ({
@@ -49,12 +48,10 @@ const TransparentButtonContainer = styled.div((props) => ({
const Button = styled.button((props) => ({
display: 'flex',
flexDirection: 'row',
- justifyContent: 'flex-end',
- alignItems: 'center',
borderRadius: props.theme.radius(1),
backgroundColor: 'transparent',
width: '100%',
- marginTop: props.theme.spacing(5),
+ marginTop: props.theme.spacing(10),
}));
const ButtonText = styled.div((props) => ({
@@ -76,6 +73,7 @@ interface Props {
onConfirmClick: (transactions: StacksTransaction[]) => void;
children: ReactNode;
isSponsored?: boolean;
+ isNft?: boolean;
}
function ConfirmStxTransationComponent({
@@ -85,6 +83,7 @@ function ConfirmStxTransationComponent({
children,
onConfirmClick,
onCancelClick,
+ isNft,
}: Props) {
const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' });
const {
@@ -98,7 +97,7 @@ function ConfirmStxTransationComponent({
const [buttonLoading, setButtonLoading] = useState(loading);
const handleBackButtonClick = () => {
- navigate('/send-stx');
+ navigate(-1);
};
useEffect(() => {
@@ -167,7 +166,7 @@ function ConfirmStxTransationComponent({
<>
-
+ {!isNft && }
{children}
-
-
-
+
+
+
+
+
-
+
+
-
-
>
);
}
diff --git a/src/app/components/recipinetAddressView/index.tsx b/src/app/components/recipinetAddressView/index.tsx
index b45192061..6ad18d2d6 100644
--- a/src/app/components/recipinetAddressView/index.tsx
+++ b/src/app/components/recipinetAddressView/index.tsx
@@ -2,6 +2,8 @@ import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import ArrowSquareOut from '@assets/img/arrow_square_out.svg';
import { getExplorerUrl } from '@utils/helper';
+import { useBnsName } from '@hooks/useBnsName';
+import useWalletSelector from '@hooks/useWalletSelector';
const InfoContainer = styled.div((props) => ({
display: 'flex',
@@ -47,8 +49,9 @@ interface Props {
recipient: string;
}
function RecipientAddressView({ recipient }: Props) {
+ const { network } = useWalletSelector();
const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' });
-
+ const bnsName = useBnsName(recipient, network);
const handleOnPress = () => {
window.open(getExplorerUrl(recipient));
};
@@ -56,6 +59,7 @@ function RecipientAddressView({ recipient }: Props) {
return (
{t('RECEPIENT_ADDRESS')}
+ {bnsName}
{recipient}
diff --git a/src/app/components/sendForm/index.tsx b/src/app/components/sendForm/index.tsx
index d94ce3090..166a4dd8b 100644
--- a/src/app/components/sendForm/index.tsx
+++ b/src/app/components/sendForm/index.tsx
@@ -2,7 +2,9 @@ import { CurrencyTypes } from '@utils/constants';
import { FungibleToken } from '@secretkeylabs/xverse-core/types';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
-import { SetStateAction, useState } from 'react';
+import {
+ ReactNode, SetStateAction, useEffect, useState,
+} from 'react';
import BigNumber from 'bignumber.js';
import IconBitcoin from '@assets/img/send/ic_sats_ticker.svg';
import IconStacks from '@assets/img/dashboard/stack_icon.svg';
@@ -14,6 +16,7 @@ import ActionButton from '@components/button';
import {
btcToSats, getBtcFiatEquivalent, getStxFiatEquivalent, stxToMicrostacks,
} from '@secretkeylabs/xverse-core/currency';
+import { useBNSResolver, useDebounce } from '@hooks/useBnsName';
const ScrollContainer = styled.div`
display: flex;
@@ -23,6 +26,8 @@ const ScrollContainer = styled.div`
&::-webkit-scrollbar {
display: none;
}
+ margin-left: 5%;
+ margin-right: 5%;
`;
const OuterContainer = styled.div({
display: 'flex',
@@ -39,8 +44,6 @@ const RowContainer = styled.div({
const InfoContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'row',
- marginLeft: props.theme.spacing(8),
- marginRight: props.theme.spacing(8),
padding: props.theme.spacing(8),
border: `1px solid ${props.theme.colors.background.elevation3}`,
borderRadius: 8,
@@ -50,14 +53,10 @@ const Container = styled.div((props) => ({
display: 'flex',
flexDirection: 'column',
marginTop: props.theme.spacing(11),
- marginLeft: props.theme.spacing(8),
- marginRight: props.theme.spacing(8),
}));
const ErrorContainer = styled.div((props) => ({
marginTop: props.theme.spacing(8),
- marginLeft: props.theme.spacing(10),
- marginRight: props.theme.spacing(10),
}));
const ErrorText = styled.h1((props) => ({
@@ -94,6 +93,11 @@ const SubText = styled.h1((props) => ({
color: props.theme.colors.white['400'],
}));
+const AssociatedText = styled.h1((props) => ({
+ ...props.theme.body_xs,
+ wordWrap: 'break-word',
+}));
+
const BalanceText = styled.h1((props) => ({
...props.theme.body_medium_m,
color: props.theme.colors.white['400'],
@@ -131,8 +135,6 @@ const TickerImage = styled.img((props) => ({
}));
const SendButtonContainer = styled.div((props) => ({
- paddingLeft: props.theme.spacing(8),
- paddingRight: props.theme.spacing(8),
paddingBottom: props.theme.spacing(8),
paddingTop: props.theme.spacing(4),
}));
@@ -146,6 +148,7 @@ interface Props {
hideMemo?: boolean;
buttonText?: string;
processing?: boolean;
+ children?: ReactNode;
}
function SendForm({
@@ -158,16 +161,35 @@ function SendForm({
hideMemo = false,
buttonText,
processing,
+ children,
}: Props) {
const { t } = useTranslation('translation', { keyPrefix: 'SEND' });
const [amount, setAmount] = useState('');
const [memo, setMemo] = useState('');
const [fiatAmount, setFiatAmount] = useState('0');
+ const [showError, setShowError] = useState(error);
const [recipientAddress, setRecipientAddress] = useState('');
-
- const { stxBtcRate, btcFiatRate, fiatCurrency } = useSelector(
+ const {
+ stxBtcRate, btcFiatRate, fiatCurrency, stxAddress,
+ } = useSelector(
(state: StoreState) => state.walletState,
);
+ const debouncedSearchTerm = useDebounce(recipientAddress, 300);
+ const associatedAddress = useBNSResolver(
+ debouncedSearchTerm,
+ stxAddress,
+ currencyType,
+ );
+
+ useEffect(() => {
+ if (error) {
+ if (associatedAddress !== '' && error.includes(t('ERRORS.ADDRESS_INVALID'))) {
+ setShowError('');
+ } else {
+ setShowError(error);
+ }
+ }
+ }, [error, associatedAddress]);
function getFiatEquivalent(value: number) {
if ((currencyType === 'FT' && !fungibleToken?.tokenFiatRate) || currencyType === 'NFT') {
@@ -262,6 +284,10 @@ function SendForm({
);
}
+ const onAddressInputChange = (e: { target: { value: SetStateAction } }) => {
+ setRecipientAddress(e.target.value);
+ };
+
function renderEnterRecepientSection() {
return (
@@ -270,22 +296,29 @@ function SendForm({
} }) => setRecipientAddress(e.target.value)}
+ onChange={onAddressInputChange}
/>
+ {associatedAddress && currencyType !== 'BTC' && (
+ <>
+ {t('ASSOCIATED_ADDRESS')}
+ {associatedAddress}
+ >
+ )}
);
}
const handleOnPress = () => {
- onPressSend(recipientAddress, amount, memo);
+ onPressSend(associatedAddress !== '' ? associatedAddress : debouncedSearchTerm, amount, memo);
};
return (
{!disableAmountInput && renderEnterAmountSection()}
+ {children}
{renderEnterRecepientSection()}
{currencyType !== 'BTC' && currencyType !== 'NFT' && !hideMemo && (
<>
@@ -311,7 +344,7 @@ function SendForm({
)}
- {error}
+ {showError}
diff --git a/src/app/components/shareNft/index.tsx b/src/app/components/shareNft/index.tsx
index 8adad553c..d9389a985 100644
--- a/src/app/components/shareNft/index.tsx
+++ b/src/app/components/shareNft/index.tsx
@@ -13,9 +13,6 @@ import ShareLinkRow from './shareLinkRow';
const Container = styled.button((props) => ({
display: 'flex',
flexDirection: 'column',
- position: 'absolute',
- top: 0,
- right: 0,
justifyContent: 'center',
paddingLeft: props.theme.spacing(6),
paddingRight: props.theme.spacing(8),
diff --git a/src/app/components/topRow/index.tsx b/src/app/components/topRow/index.tsx
index de6c08909..7f6ba2c09 100644
--- a/src/app/components/topRow/index.tsx
+++ b/src/app/components/topRow/index.tsx
@@ -27,8 +27,8 @@ const RowContainer = styled.div((props) => ({
flexDirection: 'row',
alignItems: 'center',
paddingTop: props.theme.spacing(11),
- paddingLeft: props.theme.spacing(11),
- paddingRight: props.theme.spacing(11),
+ paddingLeft: '5%',
+ paddingRight: '5%',
}));
interface Props {
diff --git a/src/app/components/transferFeeView/index.tsx b/src/app/components/transferFeeView/index.tsx
index ce8fac7c0..1a96130bd 100644
--- a/src/app/components/transferFeeView/index.tsx
+++ b/src/app/components/transferFeeView/index.tsx
@@ -10,7 +10,7 @@ import styled from 'styled-components';
const RowContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'row',
- marginTop: props.theme.spacing(6),
+ marginTop: props.theme.spacing(8),
}));
const FeeText = styled.h1((props) => ({
diff --git a/src/app/hooks/useBnsName.ts b/src/app/hooks/useBnsName.ts
new file mode 100644
index 000000000..53f80ce59
--- /dev/null
+++ b/src/app/hooks/useBnsName.ts
@@ -0,0 +1,62 @@
+import { useEffect, useState } from 'react';
+import { SettingsNetwork, validateStxAddress } from '@secretkeylabs/xverse-core';
+import {
+ fetchAddressOfBnsName, getBnsName,
+} from '@secretkeylabs/xverse-core/api';
+import useWalletSelector from './useWalletSelector';
+
+export const useBnsName = (walletAddress: string, network: SettingsNetwork) => {
+ const [bnsName, setBnsName] = useState('');
+
+ useEffect(() => {
+ (async () => {
+ const name = await getBnsName(walletAddress, network);
+ setBnsName(name ?? '');
+ })();
+ }, [walletAddress]);
+
+ return bnsName;
+};
+
+export const useBNSResolver = (
+ recipientAddress: string,
+ walletAddress: string,
+ currencyType: string,
+) => {
+ const { network } = useWalletSelector();
+ const [associatedAddress, setAssociatedAddress] = useState('');
+
+ useEffect(() => {
+ (async () => {
+ if (currencyType !== 'BTC') {
+ if (!validateStxAddress({ stxAddress: recipientAddress, network })) {
+ const address = await fetchAddressOfBnsName(
+ recipientAddress.toLocaleLowerCase(),
+ walletAddress.toLocaleLowerCase(),
+ network,
+ );
+ setAssociatedAddress(address ?? '');
+ } else {
+ setAssociatedAddress('');
+ }
+ } else {
+ setAssociatedAddress(recipientAddress);
+ }
+ })();
+ }, [recipientAddress]);
+
+ return associatedAddress;
+};
+
+export function useDebounce(value: string, delay: number) {
+ const [debouncedValue, setDebouncedValue] = useState(value);
+ useEffect(() => {
+ const handler = setTimeout(() => {
+ setDebouncedValue(value);
+ }, delay);
+ return () => {
+ clearTimeout(handler);
+ };
+ }, [value, delay]);
+ return debouncedValue;
+}
diff --git a/src/app/hooks/useNftDataSelector.ts b/src/app/hooks/useNftDataSelector.ts
new file mode 100644
index 000000000..a9a4309ed
--- /dev/null
+++ b/src/app/hooks/useNftDataSelector.ts
@@ -0,0 +1,14 @@
+import { StoreState } from '@stores/index';
+import { useSelector } from 'react-redux';
+
+const useNftDataSelector = () => {
+ const nftDataState = useSelector((state: StoreState) => ({
+ ...state.nftDataState,
+ }));
+
+ return {
+ ...nftDataState,
+ };
+};
+
+export default useNftDataSelector;
diff --git a/src/app/hooks/useNftReducer.ts b/src/app/hooks/useNftReducer.ts
new file mode 100644
index 000000000..dee6e9f3c
--- /dev/null
+++ b/src/app/hooks/useNftReducer.ts
@@ -0,0 +1,21 @@
+import { NftDetailResponse } from '@secretkeylabs/xverse-core/types';
+import { setNftDataAction } from '@stores/nftData/actions/actionCreator';
+import { useDispatch } from 'react-redux';
+import useNftDataSelector from './useNftDataSelector';
+
+const useNftDataReducer = () => {
+ const { nftData } = useNftDataSelector();
+ const dispatch = useDispatch();
+ const storeNftData = (data:NftDetailResponse) => {
+ if (data && !nftData.includes(data.data)) {
+ const modifiedNftList = [...nftData];
+ modifiedNftList.push(data.data);
+ dispatch(setNftDataAction(modifiedNftList));
+ }
+ };
+ return {
+ storeNftData,
+ };
+};
+
+export default useNftDataReducer;
diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx
index 958fe40d3..db1ad2bc5 100644
--- a/src/app/routes/index.tsx
+++ b/src/app/routes/index.tsx
@@ -22,11 +22,14 @@ import ForgotPassword from '@screens/forgotPassword';
import BackupWalletSteps from '@screens/backupWalletSteps';
import Stacking from '@screens/stacking';
import NftDashboard from '@screens/nftDashboard';
+import NftDetailScreen from '@screens/nftDetail';
import Setting from '@screens/settings';
import FiatCurrencyScreen from '@screens/settings/fiatCurrency';
import ChangePasswordScreen from '@screens/settings/changePassword';
import ChangeNetworkScreen from '@screens/settings/changeNetwork';
import BackupWalletScreen from '@screens/settings/backupWallet';
+import SendNft from '@screens/sendNft';
+import ConfirmNftTransaction from '@screens/confirmNftTransaction';
const router = createHashRouter([
{
@@ -73,6 +76,10 @@ const router = createHashRouter([
path: 'send-btc',
element: ,
},
+ {
+ path: 'nft-dashboard/nft-detail/:id/send-nft',
+ element: ,
+ },
{
path: 'confirm-stx-tx',
element: ,
@@ -121,6 +128,14 @@ const router = createHashRouter([
path: 'nft-dashboard',
element: ,
},
+ {
+ path: 'nft-dashboard/nft-detail/:id',
+ element: ,
+ },
+ {
+ path: 'confirm-nft-tx/:id',
+ element: ,
+ },
{
path: 'settings',
element: ,
diff --git a/src/app/screens/confirmNftTransaction/index.tsx b/src/app/screens/confirmNftTransaction/index.tsx
new file mode 100644
index 000000000..1a437556e
--- /dev/null
+++ b/src/app/screens/confirmNftTransaction/index.tsx
@@ -0,0 +1,154 @@
+import { useTranslation } from 'react-i18next';
+import styled from 'styled-components';
+import { useMutation } from '@tanstack/react-query';
+import { useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { useLocation, useNavigate, useParams } from 'react-router-dom';
+import { StacksTransaction } from '@secretkeylabs/xverse-core/types';
+import { broadcastSignedTransaction } from '@secretkeylabs/xverse-core/transactions';
+import Seperator from '@components/seperator';
+import { StoreState } from '@stores/index';
+import BottomBar from '@components/tabBar';
+import { fetchStxWalletDataRequestAction } from '@stores/wallet/actions/actionCreators';
+import RecipientAddressView from '@components/recipinetAddressView';
+import ConfirmStxTransationComponent from '@components/confirmStxTransactionComponent';
+import useNftDataSelector from '@hooks/useNftDataSelector';
+import NftImage from '@screens/nftDashboard/nftImage';
+
+const InfoContainer = styled.div((props) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ marginTop: props.theme.spacing(12),
+}));
+
+const TitleText = styled.h1((props) => ({
+ ...props.theme.headline_category_s,
+ color: props.theme.colors.white['400'],
+ textTransform: 'uppercase',
+}));
+
+const ValueText = styled.h1((props) => ({
+ ...props.theme.body_m,
+ marginTop: props.theme.spacing(2),
+ wordBreak: 'break-all',
+}));
+
+const Container = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+});
+
+const NFtContainer = styled.div((props) => ({
+ maxWidth: 450,
+ width: '60%',
+ display: 'flex',
+ aspectRatio: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderRadius: 8,
+ padding: props.theme.spacing(5),
+ marginBottom: props.theme.spacing(6),
+}));
+
+const NftTitleText = styled.h1((props) => ({
+ ...props.theme.headline_s,
+ color: props.theme.colors.white['0'],
+ textAlign: 'center',
+}));
+
+function ConfirmNftTransaction() {
+ const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' });
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+ const location = useLocation();
+ const { id } = useParams();
+ const { nftData } = useNftDataSelector();
+ const nftIdDetails = id!.split('::');
+ const nft = nftData.find((nftItem) => nftItem?.asset_id === nftIdDetails[1]);
+ const { unsignedTx, recipientAddress } = location.state;
+ const {
+ stxBtcRate, network, stxAddress, fiatCurrency,
+ } = useSelector(
+ (state: StoreState) => state.walletState,
+ );
+
+ const {
+ isLoading,
+ error: txError,
+ data: stxTxBroadcastData,
+ mutate,
+ } = useMutation<
+ string,
+ Error,
+ { signedTx: StacksTransaction }>(async ({ signedTx }) => broadcastSignedTransaction(signedTx, network.type));
+
+ useEffect(() => {
+ if (stxTxBroadcastData) {
+ navigate('/tx-status', {
+ state: {
+ txid: stxTxBroadcastData,
+ currency: 'STX',
+ error: '',
+ },
+ });
+ setTimeout(() => {
+ dispatch(fetchStxWalletDataRequestAction(stxAddress, network, fiatCurrency, stxBtcRate));
+ }, 1000);
+ }
+ }, [stxTxBroadcastData]);
+
+ useEffect(() => {
+ if (txError) {
+ navigate('/tx-status', {
+ state: {
+ txid: '',
+ currency: 'STX',
+ error: txError.toString(),
+ },
+ });
+ }
+ }, [txError]);
+
+ const networkInfoSection = (
+
+ {t('NETWORK')}
+ {network.type}
+
+ );
+
+ const handleOnConfirmClick = (txs: StacksTransaction[]) => {
+ mutate({ signedTx: txs[0] });
+ };
+
+ const handleOnCancelClick = () => {
+ navigate(-1);
+ };
+
+ return (
+ <>
+
+
+
+
+
+ {nft?.token_metadata.name}
+
+
+ {networkInfoSection}
+
+
+
+ >
+ );
+}
+export default ConfirmNftTransaction;
diff --git a/src/app/screens/confirmStxTransaxtion/index.tsx b/src/app/screens/confirmStxTransaxtion/index.tsx
index a6113b0b4..f12af79bd 100644
--- a/src/app/screens/confirmStxTransaxtion/index.tsx
+++ b/src/app/screens/confirmStxTransaxtion/index.tsx
@@ -13,7 +13,7 @@ import { StoreState } from '@stores/index';
import BottomBar from '@components/tabBar';
import { fetchStxWalletDataRequestAction } from '@stores/wallet/actions/actionCreators';
import RecipientAddressView from '@components/recipinetAddressView';
-import ConfirmStxTransationComponent from './confirmStxTransactionComponent';
+import ConfirmStxTransationComponent from '../../components/confirmStxTransactionComponent';
const InfoContainer = styled.div((props) => ({
display: 'flex',
@@ -51,7 +51,6 @@ function ConfirmStxTransaction() {
} = useSelector(
(state: StoreState) => state.walletState,
);
-
const {
isLoading,
error: txError,
@@ -60,7 +59,7 @@ function ConfirmStxTransaction() {
} = useMutation<
string,
Error,
- { signedTx: StacksTransaction }>(async ({ signedTx }) => broadcastSignedTransaction(signedTx, network));
+ { signedTx: StacksTransaction }>(async ({ signedTx }) => broadcastSignedTransaction(signedTx, network.type));
useEffect(() => {
if (stxTxBroadcastData) {
diff --git a/src/app/screens/confrimBtcTransaction/confirmBtcTransactionComponent/index.tsx b/src/app/screens/confrimBtcTransaction/confirmBtcTransactionComponent/index.tsx
index 4b2ec0365..8bbe8f44d 100644
--- a/src/app/screens/confrimBtcTransaction/confirmBtcTransactionComponent/index.tsx
+++ b/src/app/screens/confrimBtcTransaction/confirmBtcTransactionComponent/index.tsx
@@ -113,7 +113,7 @@ function ConfirmBtcTransactionComponent({
index: selectedAccount?.id ?? 0,
fee: new BigNumber(txFee),
seedPhrase,
- network,
+ network: network.type,
}));
useEffect(() => {
diff --git a/src/app/screens/confrimBtcTransaction/index.tsx b/src/app/screens/confrimBtcTransaction/index.tsx
index 55edf26a1..361faa4fa 100644
--- a/src/app/screens/confrimBtcTransaction/index.tsx
+++ b/src/app/screens/confrimBtcTransaction/index.tsx
@@ -43,14 +43,13 @@ function ConfirmBtcTransaction() {
const [recipientAddress, setRecipientAddress] = useState('');
const location = useLocation();
const { fee, amount, signedTxHex } = location.state;
-
const {
isLoading,
error: txError,
data: btcTxBroadcastData,
mutate,
} = useMutation(
- async ({ signedTx }) => broadcastRawBtcTransaction(signedTx, network),
+ async ({ signedTx }) => broadcastRawBtcTransaction(signedTx, network.type),
);
useEffect(() => {
diff --git a/src/app/screens/nftDashboard/index.tsx b/src/app/screens/nftDashboard/index.tsx
index 1e510a7fa..aebceb523 100644
--- a/src/app/screens/nftDashboard/index.tsx
+++ b/src/app/screens/nftDashboard/index.tsx
@@ -19,16 +19,16 @@ import ShareDialog from '@components/shareNft';
import Nft from './nft';
const Container = styled.div`
-display: flex;
-flex-direction: column;
-flex: 1;
-margin-left: 5%;
-margin-right: 5%;
-margin-bottom: 5%;
-overflow-y: auto;
-&::-webkit-scrollbar {
- display: none;
-}
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ margin-left: 5%;
+ margin-right: 5%;
+ margin-bottom: 5%;
+ overflow-y: auto;
+ &::-webkit-scrollbar {
+ display: none;
+ }
`;
const GridContainer = styled.div((props) => ({
@@ -38,6 +38,12 @@ const GridContainer = styled.div((props) => ({
gridTemplateColumns: 'repeat(auto-fit,minmax(150px,1fr))',
}));
+const ShareDialogeContainer = styled.div({
+ position: 'absolute',
+ top: 0,
+ right: 0,
+});
+
const WebGalleryButtonContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'row',
@@ -76,7 +82,7 @@ const ShareButtonContainer = styled.div((props) => ({
width: '100%',
}));
-const Button = styled.button((props) => ({
+const WebGalleryButton = styled.button((props) => ({
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-start',
@@ -87,7 +93,7 @@ const Button = styled.button((props) => ({
marginTop: props.theme.spacing(5),
}));
-const ButtonText = styled.div((props) => ({
+const WebGalleryButtonText = styled.div((props) => ({
...props.theme.body_xs,
fontWeight: 700,
color: props.theme.colors.white['0'],
@@ -125,8 +131,8 @@ const NoCollectiblesText = styled.h1((props) => ({
}));
const BarLoaderContainer = styled.div((props) => ({
- display: 'flex',
marginTop: props.theme.spacing(5),
+ maxWidth: 300,
}));
function NftDashboard() {
@@ -146,12 +152,6 @@ function NftDashboard() {
navigate('/account-list');
};
- const loader = (
-
-
-
- );
-
const openInGalleryView = async () => {
await chrome.tabs.create({
url: chrome.runtime.getURL('options.html#/nft-dashboard'),
@@ -193,15 +193,19 @@ function NftDashboard() {
{t('COLLECTIBLES')}
- {isLoading ? loader
+ {isLoading ? (
+
+
+
+ )
: {`${data?.total} ${t('ITEMS')}`}}
-
+
@@ -214,8 +218,9 @@ function NftDashboard() {
transparent
/>
-
- {showShareNftOptions && }
+
+ {showShareNftOptions && }
+
{isLoading ? (
diff --git a/src/app/screens/nftDashboard/nft.tsx b/src/app/screens/nftDashboard/nft.tsx
index 1952fc4ff..e46357577 100644
--- a/src/app/screens/nftDashboard/nft.tsx
+++ b/src/app/screens/nftDashboard/nft.tsx
@@ -5,6 +5,7 @@ import { NonFungibleToken, getBnsNftName } from '@secretkeylabs/xverse-core/type
import { BNS_CONTRACT } from '@utils/constants';
import NftUser from '@assets/img/nftDashboard/nft_user.svg';
import { useNavigate } from 'react-router-dom';
+import useNftDataReducer from '@hooks/useNftReducer';
import NftImage from './nftImage';
interface Props {
@@ -42,6 +43,8 @@ const GridItemContainer = styled.button((props) => ({
function Nft({ asset }: Props) {
const navigate = useNavigate();
+ const { storeNftData } = useNftDataReducer();
+ const url = `${asset.asset_identifier}::${asset.value.repr}`;
const { data } = useQuery(
['nft-meta-data', asset.asset_identifier],
async () => {
@@ -64,7 +67,10 @@ function Nft({ asset }: Props) {
}
const handleOnClick = () => {
-
+ storeNftData(data);
+ if (asset.asset_identifier !== BNS_CONTRACT) {
+ navigate(`nft-detail/${url}`);
+ }
};
return (
@@ -80,7 +86,6 @@ function Nft({ asset }: Props) {
)}
{getName()}
-
);
}
export default Nft;
diff --git a/src/app/screens/nftDashboard/nftImage.tsx b/src/app/screens/nftDashboard/nftImage.tsx
index b7dd4216d..eff08eb08 100644
--- a/src/app/screens/nftDashboard/nftImage.tsx
+++ b/src/app/screens/nftDashboard/nftImage.tsx
@@ -1,10 +1,10 @@
import { Suspense } from 'react';
import styled from 'styled-components';
import { Ring } from 'react-spinners-css';
+import Img from 'react-image';
import { TokenMetaData } from '@secretkeylabs/xverse-core/types/api/stacks/assets';
import { getFetchableUrl } from '@utils/helper';
import NftPlaceholderImage from '@assets/img/nftDashboard/ic_nft_diamond.svg';
-import Img from 'react-image';
const ImageContainer = styled.div((props) => ({
padding: props.theme.spacing(10),
@@ -39,7 +39,7 @@ function NftImage({ metadata }: Props) {
-)}
+ )}
unloader={showNftImagePlaceholder}
/>
@@ -48,9 +48,7 @@ function NftImage({ metadata }: Props) {
if (metadata?.asset_protocol) {
return (
-
-
-
+
);
}
diff --git a/src/app/screens/nftDetail/descriptionTile.tsx b/src/app/screens/nftDetail/descriptionTile.tsx
new file mode 100644
index 000000000..6b8f98fea
--- /dev/null
+++ b/src/app/screens/nftDetail/descriptionTile.tsx
@@ -0,0 +1,36 @@
+import styled from 'styled-components';
+
+const DescriptionHeadingText = styled.h1((props) => ({
+ ...props.theme.headline_category_s,
+ color: props.theme.colors.white['400'],
+ marginBottom: props.theme.spacing(2),
+ letterSpacing: '0.02em',
+ textTransform: 'uppercase',
+}));
+
+const DescriptionValueText = styled.h1((props) => ({
+ ...props.theme.body_m,
+ color: props.theme.colors.white['0'],
+ marginBottom: props.theme.spacing(21),
+}));
+
+const ColumnContainer = styled.h1({
+ display: 'flex',
+ flexDirection: 'column',
+});
+
+interface Props {
+ title: string,
+ value: string
+}
+
+function DescriptionTile({ title, value }: Props) {
+ return (
+
+ {title}
+ {value}
+
+ );
+}
+
+export default DescriptionTile;
diff --git a/src/app/screens/nftDetail/index.tsx b/src/app/screens/nftDetail/index.tsx
new file mode 100644
index 000000000..4396cb192
--- /dev/null
+++ b/src/app/screens/nftDetail/index.tsx
@@ -0,0 +1,398 @@
+import styled from 'styled-components';
+import { useTranslation } from 'react-i18next';
+import { useNavigate, useParams } from 'react-router-dom';
+import NftImage from '@screens/nftDashboard/nftImage';
+import ArrowSquareOut from '@assets/img/arrow_square_out.svg';
+import TopRow from '@components/topRow';
+import BottomTabBar from '@components/tabBar';
+import SquaresFour from '@assets/img/nftDashboard/squares_four.svg';
+import ArrowUpRight from '@assets/img/dashboard/arrow_up_right.svg';
+import ShareNetwork from '@assets/img/nftDashboard/share_network.svg';
+import ActionButton from '@components/button';
+import useWalletSelector from '@hooks/useWalletSelector';
+import { useEffect, useState } from 'react';
+import ShareDialog from '@components/shareNft';
+import { GAMMA_URL } from '@utils/constants';
+import { getExplorerUrl } from '@utils/helper';
+import useNftDataSelector from '@hooks/useNftDataSelector';
+import useNftDataReducer from '@hooks/useNftReducer';
+import { useMutation } from '@tanstack/react-query';
+import { getNftDetail } from '@secretkeylabs/xverse-core/api';
+import { NftData } from '@secretkeylabs/xverse-core/types/api/stacks/assets';
+import { NftDetailResponse } from '@secretkeylabs/xverse-core/types';
+import { Ring } from 'react-spinners-css';
+import NftAttribute from './nftAttribute';
+import DescriptionTile from './descriptionTile';
+
+const Container = styled.div`
+display: flex;
+flex-direction: column;
+flex: 1;
+overflow-y: auto;
+margin-left: 5%;
+margin-right: 5%;
+&::-webkit-scrollbar {
+ display: none;
+}`;
+
+const ButtonContainer = styled.div((props) => ({
+ display: 'flex',
+ position: 'relative',
+ flexDirection: 'row',
+ maxWidth: 400,
+ marginTop: props.theme.spacing(6),
+ marginBottom: props.theme.spacing(12),
+}));
+
+const ShareDialogeContainer = styled.div({
+ position: 'absolute',
+ bottom: 0,
+ right: 0,
+});
+
+const ExtensionContainer = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ flex: 1,
+});
+
+const NFtContainer = styled.div((props) => ({
+ maxWidth: 450,
+ width: '60%',
+ display: 'flex',
+ aspectRatio: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderRadius: 8,
+ padding: props.theme.spacing(5),
+ marginTop: props.theme.spacing(15),
+ marginBottom: props.theme.spacing(12),
+}));
+
+const NftTitleText = styled.h1((props) => ({
+ ...props.theme.headline_s,
+ color: props.theme.colors.white['0'],
+ textAlign: 'center',
+}));
+
+const NftGalleryTitleText = styled.h1((props) => ({
+ ...props.theme.headline_l,
+ color: props.theme.colors.white['0'],
+ marginBottom: props.theme.spacing(12),
+}));
+
+const DescriptionText = styled.h1((props) => ({
+ ...props.theme.headline_m,
+ color: props.theme.colors.white['0'],
+ marginBottom: props.theme.spacing(12),
+}));
+
+const NftOwnedByText = styled.h1((props) => ({
+ ...props.theme.body_m,
+ color: props.theme.colors.white['400'],
+ textAlign: 'center',
+}));
+
+const OwnerAddressText = styled.h1((props) => ({
+ ...props.theme.body_medium_m,
+ textAlign: 'center',
+ marginLeft: props.theme.spacing(3),
+}));
+
+const BottomBarContainer = styled.div({
+ marginTop: 'auto',
+});
+
+const RowContainer = styled.h1((props) => ({
+ display: 'flex',
+ marginTop: props.theme.spacing(3),
+ flexDirection: 'row',
+}));
+
+const GridContainer = styled.div((props) => ({
+ display: 'grid',
+ marginTop: props.theme.spacing(6),
+ columnGap: props.theme.spacing(8),
+ rowGap: props.theme.spacing(6),
+ gridTemplateColumns: 'repeat(auto-fit,minmax(150px,1fr))',
+ paddingBottom: props.theme.spacing(20),
+ marginBottom: props.theme.spacing(12),
+ borderBottom: `1px solid ${props.theme.colors.white['400']}`,
+}));
+
+const ShareButtonContainer = styled.div((props) => ({
+ marginLeft: props.theme.spacing(2),
+ width: '100%',
+}));
+
+const DescriptionContainer = styled.h1((props) => ({
+ display: 'flex',
+ marginLeft: props.theme.spacing(20),
+ marginTop: props.theme.spacing(15),
+ flexDirection: 'column',
+}));
+
+const AttributeText = styled.h1((props) => ({
+ ...props.theme.headline_category_s,
+ color: props.theme.colors.white['400'],
+ marginBottom: props.theme.spacing(2),
+ letterSpacing: '0.02em',
+ textTransform: 'uppercase',
+}));
+
+const WebGalleryButton = styled.button((props) => ({
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'flex-start',
+ alignItems: 'center',
+ borderRadius: props.theme.radius(1),
+ backgroundColor: 'transparent',
+ width: '100%',
+ marginTop: props.theme.spacing(5),
+}));
+
+const WebGalleryButtonText = styled.div((props) => ({
+ ...props.theme.body_xs,
+ fontWeight: 700,
+ color: props.theme.colors.white['0'],
+ textAlign: 'center',
+}));
+
+const ButtonImage = styled.img((props) => ({
+ marginRight: props.theme.spacing(3),
+ alignSelf: 'center',
+ transform: 'all',
+}));
+
+const WebGalleryButtonContainer = styled.div((props) => ({
+ display: 'flex',
+ flexDirection: 'row',
+ marginTop: props.theme.spacing(4),
+}));
+
+const Button = styled.button((props) => ({
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'flex-start',
+ alignItems: 'center',
+ background: 'transparent',
+ marginBottom: props.theme.spacing(12),
+}));
+
+const ButtonText = styled.h1((props) => ({
+ ...props.theme.body_m,
+ color: props.theme.colors.white['400'],
+}));
+
+const ButtonHiglightedText = styled.h1((props) => ({
+ ...props.theme.body_m,
+ color: props.theme.colors.white['0'],
+ marginLeft: props.theme.spacing(2),
+ marginRight: props.theme.spacing(2),
+}));
+
+const LoaderContainer = styled.div({
+ display: 'flex',
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+});
+
+function NftDetailScreen() {
+ const { t } = useTranslation('translation', { keyPrefix: 'NFT_DETAIL_SCREEN' });
+ const navigate = useNavigate();
+ const { stxAddress } = useWalletSelector();
+ const { id } = useParams();
+ const nftIdDetails = id!.split('::');
+ const { nftData } = useNftDataSelector();
+ const { storeNftData } = useNftDataReducer();
+ const [nft, setNft] = useState(undefined);
+
+ const {
+ isLoading,
+ data: nftDetailsData,
+ mutate,
+ } = useMutation<
+ NftDetailResponse | undefined,
+ Error,
+ { principal: string }>(async ({ principal }) => {
+ const contractInfo: string[] = principal.split('.');
+ return getNftDetail(
+ nftIdDetails[2].replace('u', ''),
+ contractInfo[0],
+ contractInfo[1],
+ );
+ });
+
+ const [showShareNftOptions, setShowNftOptions] = useState(false);
+ const isGalleryOpen: boolean = document.documentElement.clientWidth > 360;
+
+ useEffect(() => {
+ const data = nftData.find((nftItem) => nftItem?.asset_id === nftIdDetails[1]);
+ if (!data) {
+ mutate({ principal: nftIdDetails[0] });
+ } else {
+ setNft(data);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (nftDetailsData) {
+ storeNftData(nftDetailsData);
+ setNft(nftDetailsData?.data);
+ }
+ }, [nftDetailsData]);
+
+ const handleBackButtonClick = () => {
+ navigate('/nft-dashboard');
+ };
+
+ const onSharePress = () => {
+ setShowNftOptions(true);
+ };
+
+ const onCrossPress = () => {
+ setShowNftOptions(false);
+ };
+
+ const onGammaPress = () => {
+ window.open(`${GAMMA_URL}collections/${nft?.token_metadata?.contract_id}`);
+ };
+
+ const onExplorerPress = () => {
+ const address = nft?.token_metadata?.contract_id?.split('.')!;
+ window.open(getExplorerUrl(address[0]));
+ };
+
+ const openInGalleryView = async () => {
+ await chrome.tabs.create({
+ url: chrome.runtime.getURL(`options.html#/nft-dashboard/nft-detail/${id}`),
+ });
+ };
+
+ const handleOnSendClick = () => {
+ navigate('send-nft', {
+ state: {
+ nft,
+ },
+ });
+ };
+
+ const nftImage = (
+
+
+
+ );
+
+ const ownedByView = (
+
+ {t('OWNED_BY')}
+
+ {`${stxAddress.substring(0, 4)}...${stxAddress.substring(
+ stxAddress.length - 4,
+ stxAddress.length,
+ )}`}
+
+
+ );
+
+ const buttons = (
+
+
+
+
+
+
+ {showShareNftOptions && }
+
+
+ );
+
+ const extensionView = (
+ <>
+
+ {nftImage}
+ {nft?.token_metadata.name}
+ {ownedByView}
+
+
+
+ <>
+
+ {t('WEB_GALLERY')}
+ >
+
+
+
+ {buttons}
+ >
+ );
+
+ const galleryView = (
+ isLoading || !nft ? (
+
+
+
+ ) : (
+
+ {nft?.token_metadata.name}
+ {buttons}
+
+ {nftImage}
+
+
+ {t('DESCRIPTION')}
+
+
+ {nft?.rarity_score && }
+
+ {nft?.nft_token_attributes.length !== 0 && (
+ <>
+ {t('ATTRIBUTES')}
+
+ {nft?.nft_token_attributes.map((attribute) => ())}
+
+ >
+ )}
+
+
+
+
+
+ )
+
+ );
+
+ return (
+ <>
+
+
+ {isGalleryOpen ? galleryView : extensionView}
+
+
+
+
+
+ >
+
+ );
+}
+
+export default NftDetailScreen;
diff --git a/src/app/screens/nftDetail/nftAttribute.tsx b/src/app/screens/nftDetail/nftAttribute.tsx
new file mode 100644
index 000000000..3e25a76ef
--- /dev/null
+++ b/src/app/screens/nftDetail/nftAttribute.tsx
@@ -0,0 +1,38 @@
+import styled from 'styled-components';
+
+const Container = styled.h1((props) => ({
+ display: 'flex',
+ borderRadius: 20,
+ border: `1px solid ${props.theme.colors.background.elevation3}`,
+ flexDirection: 'row',
+ marginEnd: 10,
+ padding: props.theme.spacing(5),
+ alignItems: 'center',
+}));
+
+const TypeText = styled.h1((props) => ({
+ ...props.theme.body_m,
+ color: props.theme.colors.white['400'],
+}));
+
+const ValueText = styled.h1((props) => ({
+ ...props.theme.body_medium_m,
+ color: props.theme.colors.white['0'],
+ marginLeft: props.theme.spacing(3),
+}));
+
+interface Props {
+ type: string;
+ value: string;
+}
+
+function NftAttribute({ type, value }: Props) {
+ return (
+
+ {type}
+ {value}
+
+ );
+}
+
+export default NftAttribute;
diff --git a/src/app/screens/sendNft/index.tsx b/src/app/screens/sendNft/index.tsx
new file mode 100644
index 000000000..e479e6317
--- /dev/null
+++ b/src/app/screens/sendNft/index.tsx
@@ -0,0 +1,171 @@
+import { useTranslation } from 'react-i18next';
+import { useEffect, useState } from 'react';
+import styled from 'styled-components';
+import { useNavigate, useParams } from 'react-router-dom';
+import {
+ StacksTransaction, cvToHex, uintCV, UnsignedStacksTransation,
+} from '@secretkeylabs/xverse-core/types';
+import { useMutation } from '@tanstack/react-query';
+import { generateUnsignedTransaction } from '@secretkeylabs/xverse-core/transactions';
+import { validateStxAddress } from '@secretkeylabs/xverse-core';
+import SendForm from '@components/sendForm';
+import useStxPendingTxData from '@hooks/useStxPendingTxData';
+import useWalletSelector from '@hooks/useWalletSelector';
+import TopRow from '@components/topRow';
+import BottomBar from '@components/tabBar';
+import { checkNftExists } from '@utils/helper';
+import NftImage from '@screens/nftDashboard/nftImage';
+import useNftDataSelector from '@hooks/useNftDataSelector';
+
+const Container = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ flex: 1,
+});
+
+const NFtContainer = styled.div((props) => ({
+ maxWidth: 450,
+ width: '60%',
+ display: 'flex',
+ aspectRatio: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderRadius: 8,
+ padding: props.theme.spacing(5),
+ marginTop: props.theme.spacing(15),
+ marginBottom: props.theme.spacing(6),
+}));
+
+const NftTitleText = styled.h1((props) => ({
+ ...props.theme.headline_s,
+ color: props.theme.colors.white['0'],
+ textAlign: 'center',
+}));
+
+function SendNft() {
+ const { t } = useTranslation('translation', { keyPrefix: 'SEND' });
+ const navigate = useNavigate();
+ const { id } = useParams();
+ const { nftData } = useNftDataSelector();
+ const nftIdDetails = id!.split('::');
+ const nft = nftData.find((nftItem) => nftItem?.asset_id === nftIdDetails[1]);
+ const { data: stxPendingTxData } = useStxPendingTxData();
+ const {
+ stxAddress,
+ stxPublicKey,
+ network,
+ feeMultipliers,
+ } = useWalletSelector();
+ const [error, setError] = useState('');
+ const [recipientAddress, setRecipientAddress] = useState('');
+ const { isLoading, data, mutate } = useMutation<
+ StacksTransaction,
+ Error,
+ { tokenId: string; associatedAddress: string }
+ >(async ({ tokenId, associatedAddress }) => {
+ const principal = nft?.fully_qualified_token_id?.split('::')!;
+ const name = principal[1].split(':')[0];
+ const contractInfo: string[] = principal[0].split('.');
+ const unsginedTx: UnsignedStacksTransation = {
+ amount: tokenId,
+ senderAddress: stxAddress,
+ recipientAddress: associatedAddress,
+ contractAddress: contractInfo[0],
+ contractName: contractInfo[1],
+ assetName: name,
+ publicKey: stxPublicKey,
+ network,
+ pendingTxs: stxPendingTxData?.pendingTransactions ?? [],
+ memo: '',
+ isNFT: true,
+
+ };
+ const unsignedTx: StacksTransaction = await generateUnsignedTransaction(
+ unsginedTx,
+ );
+ if (feeMultipliers?.stxSendTxMultiplier) {
+ unsignedTx.setFee(
+ unsignedTx.auth.spendingCondition.fee
+ * BigInt(feeMultipliers.stxSendTxMultiplier),
+ );
+ }
+ return unsignedTx;
+ });
+
+ useEffect(() => {
+ if (data) {
+ navigate(`/confirm-nft-tx/${id}`, {
+ state: {
+ unsignedTx: data,
+ recipientAddress,
+ nft,
+ },
+ });
+ }
+ }, [data]);
+
+ const handleBackButtonClick = () => {
+ navigate(-1);
+ };
+
+ function validateFields(associatedAddress: string): boolean {
+ if (!associatedAddress) {
+ setError(t('ERRORS.ADDRESS_REQUIRED'));
+ return false;
+ }
+
+ if (!validateStxAddress({ stxAddress: associatedAddress, network: network.type })) {
+ setError(t('ERRORS.ADDRESS_INVALID'));
+ return false;
+ }
+
+ if (associatedAddress === stxAddress) {
+ setError(t('ERRORS.SEND_TO_SELF'));
+ return false;
+ }
+
+ return true;
+ }
+
+ const onPressSendNFT = async (associatedAddress: string) => {
+ if (stxPendingTxData) {
+ if (checkNftExists(stxPendingTxData?.pendingTransactions, nft!)) {
+ setError(t('ERRORS.NFT_SEND_DETAIL'));
+ return;
+ }
+ }
+ setRecipientAddress(associatedAddress);
+ if (validateFields(associatedAddress.trim()) && nft) {
+ setError('');
+ const tokenId = nft?.value?.hex
+ ?? cvToHex(uintCV(nft.token_id.toString()));
+ mutate({ tokenId, associatedAddress });
+ }
+ };
+ return (
+ <>
+
+
+
+
+
+
+ {nft?.token_metadata.name}
+
+
+
+ >
+ );
+}
+
+export default SendNft;
diff --git a/src/app/stores/index.ts b/src/app/stores/index.ts
index 8b2fc958a..2ac39040b 100644
--- a/src/app/stores/index.ts
+++ b/src/app/stores/index.ts
@@ -4,16 +4,19 @@ import { persistReducer, persistStore } from 'redux-persist';
import createSagaMiddleware from 'redux-saga';
import walletReducer from './wallet/walletReducer';
import rootSaga from './root/saga';
+import NftDataStateReducer from './nftData/reducer';
export const storage = new ChromeStorage(chrome.storage.local, chrome.runtime);
const rootPersistConfig = {
key: 'root',
storage,
+ blacklist: ['nftDataState'],
};
const appReducer = combineReducers({
walletState: walletReducer,
+ nftDataState: NftDataStateReducer,
});
const rootReducer = (state: any, action: any) => appReducer(state, action);
diff --git a/src/app/stores/nftData/actions/actionCreator.ts b/src/app/stores/nftData/actions/actionCreator.ts
new file mode 100644
index 000000000..983f2ca6e
--- /dev/null
+++ b/src/app/stores/nftData/actions/actionCreator.ts
@@ -0,0 +1,10 @@
+/* eslint-disable import/prefer-default-export */
+import { NftData } from '@secretkeylabs/xverse-core/types/api/stacks/assets';
+import * as actions from './types';
+
+export function setNftDataAction(nftData: NftData[]): actions.SetNftData {
+ return {
+ type: actions.SetNftDataKey,
+ nftData,
+ };
+}
diff --git a/src/app/stores/nftData/actions/types.ts b/src/app/stores/nftData/actions/types.ts
new file mode 100644
index 000000000..0820c66f6
--- /dev/null
+++ b/src/app/stores/nftData/actions/types.ts
@@ -0,0 +1,14 @@
+import { NftData } from '@secretkeylabs/xverse-core/types/api/stacks/assets';
+
+export interface NftDataState {
+ nftData: NftData[];
+}
+
+export const SetNftDataKey = 'SetNftData';
+
+export interface SetNftData {
+ type: typeof SetNftDataKey;
+ nftData: NftData[];
+}
+
+export type NftDataAction = SetNftData;
diff --git a/src/app/stores/nftData/reducer.ts b/src/app/stores/nftData/reducer.ts
new file mode 100644
index 000000000..2639076ec
--- /dev/null
+++ b/src/app/stores/nftData/reducer.ts
@@ -0,0 +1,23 @@
+import { NftDataAction, NftDataState, SetNftDataKey } from './actions/types';
+
+const initialNftDataState :NftDataState = {
+ nftData: [],
+};
+
+const NftDataStateReducer = (
+ // eslint-disable-next-line @typescript-eslint/default-param-last
+ state: NftDataState = initialNftDataState,
+ action: NftDataAction,
+): NftDataState => {
+ switch (action.type) {
+ case SetNftDataKey:
+ return {
+ ...state,
+ nftData: action.nftData,
+ };
+ default:
+ return state;
+ }
+};
+
+export default NftDataStateReducer;
diff --git a/src/app/utils/helper.ts b/src/app/utils/helper.ts
index 2f01544d6..8a8d75560 100644
--- a/src/app/utils/helper.ts
+++ b/src/app/utils/helper.ts
@@ -1,3 +1,5 @@
+import { StxMempoolTransactionData } from '@secretkeylabs/xverse-core/types';
+import { NftData } from '@secretkeylabs/xverse-core/types/api/stacks/assets';
import { Account } from '@stores/wallet/actions/types';
import { getStacksInfo } from '@secretkeylabs/xverse-core/api';
import BigNumber from 'bignumber.js';
@@ -65,6 +67,25 @@ export function getFetchableUrl(uri: string, protocol: string): string | undefin
}
return undefined;
}
+/**
+ * check if nft transaction exists in pending transactions
+ * @param pendingTransactions
+ * @param nft
+ * @returns true if nft exists, false otherwise
+ */
+export function checkNftExists(
+ pendingTransactions: StxMempoolTransactionData[],
+ nft: NftData,
+): boolean {
+ const principal: string[] = nft?.fully_qualified_token_id?.split('::');
+ const transaction = pendingTransactions.find(
+ (tx) => tx.contractCall?.contract_id === principal[0]
+ && tx.contractCall.function_args[0].repr.substring(1)
+ === nft.token_id.toString(),
+ );
+ if (transaction) return true;
+ return false;
+}
export async function isValidURL(str: string): Promise {
if (validUrl.isUri(str)) {
diff --git a/src/locales/en.json b/src/locales/en.json
index 1f945fe6c..c150d340b 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -49,6 +49,7 @@
"RECEPIENT_PLACEHOLDER": "Address or .btc domain",
"MEMO": "Add a memo (optional)",
"MEMO_PLACEHOLDER": "Memo",
+ "ASSOCIATED_ADDRESS": "Associated Address",
"MEMO_INFO": "Adding a memo can have an impact on the transaction fee",
"NEXT": "Next",
"ERRORS": {
@@ -61,8 +62,10 @@
"MINIMUM_AMOUNT": "Minimum amount is 0.000001 STX",
"INSUFFICIENT_BALANCE": "Insufficient balance",
"MEMO_LENGTH": "Memo exceeds allowed length",
- "INSUFFICIENT_BALANCE_FEES": "Insufficient balance when including transaction fees"
- }
+ "INSUFFICIENT_BALANCE_FEES": "Insufficient balance when including transaction fees",
+ "NFT_SEND_DETAIL": "This Nft is already sent and is in pending state"
+ },
+ "SEND_NFT":"Send NFT"
},
"CONFIRM_TRANSACTION": {
"SEND":"Send",
@@ -196,6 +199,25 @@
"COPY": "Copy link",
"NFT_DETAIL":"Item detail"
},
+ "NFT_DETAIL_SCREEN": {
+ "NFT_DETAIL":"Item detail",
+ "WEB_GALLERY": "Open web gallery",
+ "SEND": "Send",
+ "SHARE": "Share",
+ "OWNED_BY": "Owned By",
+ "DESCRIPTION": "Description",
+ "NAME": "Name",
+ "CREATOR": "Creator",
+ "LAST_SOLD": "Last sold",
+ "STATUS": "Status",
+ "RARITY": "Overall rarity",
+ "CONTRACT_ID": "Contract ID",
+ "ATTRIBUTES": "Attributes",
+ "VIEW_CONTRACT": "View the contract on",
+ "STACKS_EXPLORER":"Stacks Explorer",
+ "DETAILS": "See detail on",
+ "GAMMA": "Gamma.io"
+ },
"RESET_WALLET_SCREEN": {
"ENTER_PASSWORD": "Enter your password to reset your wallet",
"PASSWORD": "Password",