From 649337f6059f09d15dd54899bf9f230d82a7fca8 Mon Sep 17 00:00:00 2001 From: jordankzf Date: Tue, 30 Jul 2024 12:57:50 +0800 Subject: [PATCH 001/227] Show Swap button in SIP-10 Coin detail --- src/app/screens/coinDashboard/coinHeader.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/screens/coinDashboard/coinHeader.tsx b/src/app/screens/coinDashboard/coinHeader.tsx index 98e0bf6ea..75177e0b2 100644 --- a/src/app/screens/coinDashboard/coinHeader.tsx +++ b/src/app/screens/coinDashboard/coinHeader.tsx @@ -168,7 +168,9 @@ export default function CoinHeader({ currency, fungibleToken }: Props) { const isCrossChainSwapsEnabled = useHasFeature(FeatureId.CROSS_CHAIN_SWAPS); const showRunesSwap = - (currency === 'FT' && fungibleToken?.protocol === 'runes') || currency === 'BTC'; + (currency === 'FT' && fungibleToken?.protocol === 'runes') || + fungibleToken?.protocol === 'stacks' || + currency === 'BTC'; // ledger is disabled for now const showSwaps = isCrossChainSwapsEnabled && showRunesSwap && !isLedgerAccount(selectedAccount); From b9a531eddfeabf8f75e581d5e967d0636ab6e64c Mon Sep 17 00:00:00 2001 From: jordankzf Date: Tue, 30 Jul 2024 13:04:35 +0800 Subject: [PATCH 002/227] Notes --- src/app/screens/swap/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index b12ecdf54..c9e37b780 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -125,6 +125,7 @@ export default function SwapScreen() { const { fiatCurrency } = useWalletSelector(); + // Hook for SIP-10 const { unfilteredData } = useRuneFungibleTokensQuery(); const { data: btcBalance } = useBtcWalletData(); const { btcFiatRate } = useCoinRates(); @@ -135,6 +136,7 @@ export default function SwapScreen() { const defaultFrom = params.get('from'); const { quotes, loading: quotesLoading, error: quotesError, fetchQuotes } = useGetQuotes(); + // Combined list const runesCoinsList = unfilteredData ?? []; useEffect(() => { From 34b518fe4babbc89b9be8b7092dddb85e335b093 Mon Sep 17 00:00:00 2001 From: jordankzf Date: Tue, 30 Jul 2024 14:19:21 +0800 Subject: [PATCH 003/227] Show Swap button in STX Coin detail --- src/app/screens/coinDashboard/coinHeader.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/screens/coinDashboard/coinHeader.tsx b/src/app/screens/coinDashboard/coinHeader.tsx index 75177e0b2..fe21c13f7 100644 --- a/src/app/screens/coinDashboard/coinHeader.tsx +++ b/src/app/screens/coinDashboard/coinHeader.tsx @@ -170,7 +170,8 @@ export default function CoinHeader({ currency, fungibleToken }: Props) { const showRunesSwap = (currency === 'FT' && fungibleToken?.protocol === 'runes') || fungibleToken?.protocol === 'stacks' || - currency === 'BTC'; + currency === 'BTC' || + currency === 'STX'; // ledger is disabled for now const showSwaps = isCrossChainSwapsEnabled && showRunesSwap && !isLedgerAccount(selectedAccount); From f3bd69a472685d88249f01b71d476cb6410a0e0c Mon Sep 17 00:00:00 2001 From: jordankzf Date: Tue, 30 Jul 2024 17:56:35 +0800 Subject: [PATCH 004/227] wip --- .../components/tokenFromBottomSheet/index.tsx | 2 +- .../tokenFromBottomSheet/useFromTokens.ts | 8 +- src/app/screens/swap/index.tsx | 118 ++++++++++++------ src/app/screens/swap/useMasterCoinsList.tsx | 18 +++ 4 files changed, 107 insertions(+), 39 deletions(-) create mode 100644 src/app/screens/swap/useMasterCoinsList.tsx diff --git a/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx b/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx index 1a989bb10..70589f00d 100644 --- a/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx +++ b/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx @@ -30,7 +30,7 @@ interface Props { visible: boolean; title: string; to?: TokenBasic; - onSelectCoin: (token: FungibleToken | 'BTC') => void; + onSelectCoin: (token: FungibleToken) => void; onClose: () => void; } diff --git a/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts b/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts index 06d585620..2a69305b1 100644 --- a/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts +++ b/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts @@ -14,10 +14,16 @@ import { sortFtByFiatBalance } from '@utils/tokens'; const useFromTokens = (to?: TokenBasic) => { const { network } = useWalletSelector(); - const { unfilteredData: runesCoinsList } = useRuneFungibleTokensQuery(); + const { acceptableCoinList: sip10FtList } = useStxCurrencyConversion(); + const { unfilteredData: runesFtList } = useRuneFungibleTokensQuery(); const { stxBtcRate, btcFiatRate } = useCoinRates(); const { btcAddress } = useSelectedAccount(); + const coinsMasterList = useMemo( + () => [...sip10FtList, ...(runesFtList || []), btcFt, stxFt] ?? [], + [sip10FtList, runesFtList], + ); + const filteredRunesTokensObject = (runesCoinsList ?? []).reduce((acc, ft) => { acc[ft.principal] = ft; return acc; diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index c9e37b780..4af452627 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -10,6 +10,7 @@ import { AnalyticsEvents, btcToSats, getBtcFiatEquivalent, + type BaseToken, type ExecuteOrderRequest, type FungibleToken, type GetUtxosRequest, @@ -26,7 +27,7 @@ import { satsToBtcString } from '@utils/helper'; import { trackMixPanel } from '@utils/mixpanel'; import { getFtBalance } from '@utils/tokens'; import BigNumber from 'bignumber.js'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; @@ -38,6 +39,7 @@ import TokenFromBottomSheet from './components/tokenFromBottomSheet'; import TokenToBottomSheet from './components/tokenToBottomSheet'; import QuoteSummary from './quoteSummary'; import QuotesModal from './quotesModal'; +import { useStxCurrencyConversion } from './useStxCurrencyConversion'; import { mapFTNativeSwapTokenToTokenBasic, mapFTProtocolToSwapProtocol, @@ -93,14 +95,51 @@ const Icon = styled.img` rotate: 90deg; `; -const mapFtToSwapToken = (ft: FungibleToken | 'BTC'): Token => ({ - ticker: ft === 'BTC' ? 'BTC' : ft.principal ?? '', - name: ft === 'BTC' ? 'Bitcoin' : ft.name ?? ft.assetName ?? '', - protocol: ft === 'BTC' ? 'btc' : mapFTProtocolToSwapProtocol(ft.protocol ?? 'runes'), - divisibility: ft === 'BTC' ? 8 : ft?.decimals ?? 0, - logo: ft === 'BTC' ? undefined : ft.image ?? ft.runeInscriptionId ?? '', - symbol: ft === 'BTC' ? undefined : ft.runeSymbol ?? '', -}); +export const btcFt: FungibleToken = { + name: 'Bitcoin', + balance: '', + total_sent: '', + total_received: '', + principal: 'BTC', + assetName: 'Bitcoin', +}; + +export const stxFt: FungibleToken = { + name: 'Stacks', + balance: '', + total_sent: '', + total_received: '', + principal: 'STX', + assetName: 'Stacks', +}; + +const mapFtToSwapToken = (st: FungibleToken): Token => { + if (st.principal === 'BTC') { + return { + ticker: 'BTC', + name: 'Bitcoin', + protocol: 'btc', + divisibility: 8, + }; + } + if (st.principal === 'STX') { + return { + ticker: 'STX', + name: 'Stacks', + protocol: 'stx', + divisibility: 6, + }; + } + + return { + ticker: st.principal ?? '', + name: st.name ?? st.assetName ?? '', + protocol: mapFTProtocolToSwapProtocol(st.protocol ?? 'runes'), + divisibility: st.decimals ?? 0, + logo: st.image ?? st.runeInscriptionId ?? '', + symbol: st.runeSymbol ?? '', + }; +}; export default function SwapScreen() { const [amount, setAmount] = useState(''); @@ -112,7 +151,7 @@ export default function SwapScreen() { const [tokenSelectionBottomSheet, setTokenSelectionBottomSheet] = useState<'from' | 'to' | null>( null, ); - const [fromToken, setFromToken] = useState(); + const [fromToken, setFromToken] = useState(); const [toToken, setToToken] = useState(); const [utxosRequest, setUtxosRequest] = useState(null); const [inputError, setInputError] = useState(''); @@ -126,7 +165,8 @@ export default function SwapScreen() { const { fiatCurrency } = useWalletSelector(); // Hook for SIP-10 - const { unfilteredData } = useRuneFungibleTokensQuery(); + const { acceptableCoinList: sip10FtList } = useStxCurrencyConversion(); + const { unfilteredData: runesFtList } = useRuneFungibleTokensQuery(); const { data: btcBalance } = useBtcWalletData(); const { btcFiatRate } = useCoinRates(); const navigate = useNavigate(); @@ -137,17 +177,17 @@ export default function SwapScreen() { const { quotes, loading: quotesLoading, error: quotesError, fetchQuotes } = useGetQuotes(); // Combined list - const runesCoinsList = unfilteredData ?? []; + const coinsMasterList = useMemo( + () => [...sip10FtList, ...(runesFtList || []), btcFt, stxFt] ?? [], + [sip10FtList, runesFtList], + ); useEffect(() => { if (defaultFrom) { - const token = - defaultFrom === 'BTC' - ? 'BTC' - : runesCoinsList.find((coin) => coin.principal === defaultFrom); + const token = coinsMasterList.find((coin) => coin.principal === defaultFrom); setFromToken(token); } - }, [defaultFrom, runesCoinsList.length]); + }, [defaultFrom, coinsMasterList.length]); const handleGoBack = () => { navigate(-1); @@ -165,7 +205,8 @@ export default function SwapScreen() { setGetQuotesModalVisible(true); }, [quotes, quotesLoading, quotesError]); - const amountForQuote = fromToken === 'BTC' ? btcToSats(new BigNumber(amount)).toString() : amount; + const amountForQuote = + fromToken?.principal === 'BTC' ? btcToSats(new BigNumber(amount)).toString() : amount; const getQuotes = async () => { if (!fromToken || !toToken) { @@ -173,7 +214,7 @@ export default function SwapScreen() { } trackMixPanel(AnalyticsEvents.FetchSwapQuote, { - from: fromToken === 'BTC' ? 'BTC' : fromToken.name, + from: fromToken.principal === 'BTC' ? 'BTC' : fromToken.name, to: toToken.protocol === 'btc' ? 'BTC' : toToken.name ?? toToken.ticker, }); @@ -196,7 +237,7 @@ export default function SwapScreen() { // add more protocols here when needed switch (ftProtocol) { case 'runes': - return runesCoinsList.find((coin) => coin.principal === ticker); + return coinsMasterList.find((coin) => coin.principal === ticker); default: return undefined; } @@ -213,9 +254,7 @@ export default function SwapScreen() { setAmount(''); setHasQuoteError(false); const newFrom = - toToken.protocol === 'btc' - ? 'BTC' - : getUserFTFromTokenTicker(toToken.protocol, toToken.ticker) ?? mapSwapTokenToFT(toToken); + getUserFTFromTokenTicker(toToken.protocol, toToken.ticker) ?? mapSwapTokenToFT(toToken); const newTo = mapFtToSwapToken(fromToken); setFromToken(newFrom); setToToken(newTo); @@ -226,7 +265,7 @@ export default function SwapScreen() { setToToken(token); }; - const onChangeFromToken = (token: FungibleToken | 'BTC') => { + const onChangeFromToken = (token: FungibleToken) => { setInputError(''); setAmount(''); setHasQuoteError(false); @@ -241,7 +280,7 @@ export default function SwapScreen() { return; } - if (fromToken === 'BTC') { + if (fromToken.principal === 'BTC') { const amountInSats = btcToSats(new BigNumber(value)); setInputError( BigNumber(amountInSats).gt(BigNumber(btcBalance ?? 0)) @@ -263,7 +302,7 @@ export default function SwapScreen() { return undefined; } - if (fromToken === 'BTC') { + if (fromToken.principal === 'BTC') { return satsToBtcString(BigNumber(btcBalance ?? 0)); } @@ -276,7 +315,7 @@ export default function SwapScreen() { const getFromAmountFiatValue = () => { const balance = new BigNumber(amount || '0'); - if (fromToken === 'BTC') { + if (fromToken?.principal === 'BTC') { const amountInSats = btcToSats(new BigNumber(balance)); return getBtcFiatEquivalent(amountInSats, new BigNumber(btcFiatRate)).toFixed(2); } @@ -295,7 +334,7 @@ export default function SwapScreen() { } // we can't use max for btc - if (fromToken === 'BTC') { + if (fromToken.principal === 'BTC' || fromToken.principal === 'STX') { return; } @@ -312,9 +351,11 @@ export default function SwapScreen() { const isGetQuotesDisabled = ifFormInValid || quotesLoading || Boolean(quotesError); const isMaxDisabled = - !fromToken || fromToken === 'BTC' || BigNumber(amount).eq(getFtBalance(fromToken)); + !fromToken || fromToken.principal === 'BTC' || BigNumber(amount).eq(getFtBalance(fromToken)); const isRunesToBtcRoute = - fromToken !== 'BTC' && fromToken?.protocol === 'runes' && toToken?.protocol === 'btc'; + fromToken?.principal !== 'BTC' && + fromToken?.protocol === 'runes' && + toToken?.protocol === 'btc'; useEffect(() => { if (errorMessage) { @@ -341,7 +382,7 @@ export default function SwapScreen() { trackMixPanel(AnalyticsEvents.SelectSwapQuote, { provider: provider.provider.name, - from: fromToken === 'BTC' ? 'BTC' : fromToken.name, + from: fromToken.principal === 'BTC' ? 'BTC' : fromToken.name, to: toToken.protocol === 'btc' ? 'BTC' : toToken.name ?? toToken.ticker, }); @@ -357,7 +398,8 @@ export default function SwapScreen() { providerCode: provider.provider.code, from: provider.from, to: provider.to, - amount: fromToken === 'BTC' ? btcToSats(new BigNumber(amount)).toString() : amount, + amount: + fromToken.principal === 'BTC' ? btcToSats(new BigNumber(amount)).toString() : amount, }; setUtxosRequest(request); } @@ -371,7 +413,7 @@ export default function SwapScreen() { } trackMixPanel(AnalyticsEvents.SignSwap, { provider: provider.name, - from: fromToken === 'BTC' ? 'BTC' : fromToken.name, + from: fromToken.principal === 'BTC' ? 'BTC' : fromToken.name, to: toToken.protocol === 'btc' ? 'BTC' : toToken.name ?? toToken.ticker, }); }; @@ -485,16 +527,18 @@ export default function SwapScreen() { fiatValue: getFromAmountFiatValue(), fiatCurrency, protocol: - fromToken === 'BTC' + fromToken?.principal === 'BTC' ? 'btc' : fromToken?.protocol ? mapFTProtocolToSwapProtocol(fromToken.protocol) : undefined, - decimals: fromToken === 'BTC' ? 8 : fromToken?.decimals, - unit: fromToken === 'BTC' ? 'BTC' : fromToken?.runeSymbol ?? '', + decimals: fromToken?.principal === 'BTC' ? 8 : fromToken?.decimals, + unit: fromToken?.principal === 'BTC' ? 'BTC' : fromToken?.runeSymbol ?? '', }} max={ - fromToken === 'BTC' ? undefined : { isDisabled: isMaxDisabled, onClick: onClickMax } + fromToken?.principal === 'BTC' + ? undefined + : { isDisabled: isMaxDisabled, onClick: onClickMax } } balance={getFromBalance()} /> diff --git a/src/app/screens/swap/useMasterCoinsList.tsx b/src/app/screens/swap/useMasterCoinsList.tsx new file mode 100644 index 000000000..e2e1dfde2 --- /dev/null +++ b/src/app/screens/swap/useMasterCoinsList.tsx @@ -0,0 +1,18 @@ +import { useRuneFungibleTokensQuery } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; +import { useMemo } from 'react'; +import { btcFt, stxFt } from './index'; +import { useStxCurrencyConversion } from './useStxCurrencyConversion'; + +const useMasterCoinsList = () => { + const { acceptableCoinList: sip10FtList } = useStxCurrencyConversion(); + const { unfilteredData: runesFtList } = useRuneFungibleTokensQuery(); + + const coinsMasterList = useMemo( + () => [...sip10FtList, ...(runesFtList || []), btcFt, stxFt] ?? [], + [sip10FtList, runesFtList], + ); + + return coinsMasterList; +}; + +export default useMasterCoinsList; From de8748a161c25feae0c19ff66d97ff4279468fb6 Mon Sep 17 00:00:00 2001 From: jordankzf Date: Thu, 1 Aug 2024 21:32:02 +0800 Subject: [PATCH 005/227] wip 2 --- .../tokenFromBottomSheet/useFromTokens.ts | 6 ++---- src/app/screens/swap/index.tsx | 18 ----------------- src/app/screens/swap/useMasterCoinsList.tsx | 20 ++++++++++++++++++- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts b/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts index 2a69305b1..eb96c3b2c 100644 --- a/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts +++ b/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts @@ -2,6 +2,7 @@ import { useRuneFungibleTokensQuery } from '@hooks/queries/runes/useRuneFungible import useCoinRates from '@hooks/queries/useCoinRates'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; +import useMasterCoinsList from '@screens/swap/useMasterCoinsList'; import { mapFTProtocolToSwapProtocol } from '@screens/swap/utils'; import { getXverseApiClient, @@ -19,10 +20,7 @@ const useFromTokens = (to?: TokenBasic) => { const { stxBtcRate, btcFiatRate } = useCoinRates(); const { btcAddress } = useSelectedAccount(); - const coinsMasterList = useMemo( - () => [...sip10FtList, ...(runesFtList || []), btcFt, stxFt] ?? [], - [sip10FtList, runesFtList], - ); + const coinsMasterList = useMasterCoinsList(); const filteredRunesTokensObject = (runesCoinsList ?? []).reduce((acc, ft) => { acc[ft.principal] = ft; diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index 4af452627..1d4a0a63f 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -95,24 +95,6 @@ const Icon = styled.img` rotate: 90deg; `; -export const btcFt: FungibleToken = { - name: 'Bitcoin', - balance: '', - total_sent: '', - total_received: '', - principal: 'BTC', - assetName: 'Bitcoin', -}; - -export const stxFt: FungibleToken = { - name: 'Stacks', - balance: '', - total_sent: '', - total_received: '', - principal: 'STX', - assetName: 'Stacks', -}; - const mapFtToSwapToken = (st: FungibleToken): Token => { if (st.principal === 'BTC') { return { diff --git a/src/app/screens/swap/useMasterCoinsList.tsx b/src/app/screens/swap/useMasterCoinsList.tsx index e2e1dfde2..8f291169b 100644 --- a/src/app/screens/swap/useMasterCoinsList.tsx +++ b/src/app/screens/swap/useMasterCoinsList.tsx @@ -1,8 +1,26 @@ import { useRuneFungibleTokensQuery } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; +import type { FungibleToken } from '@secretkeylabs/xverse-core'; import { useMemo } from 'react'; -import { btcFt, stxFt } from './index'; import { useStxCurrencyConversion } from './useStxCurrencyConversion'; +export const btcFt: FungibleToken = { + name: 'Bitcoin', + balance: '', + total_sent: '', + total_received: '', + principal: 'BTC', + assetName: 'Bitcoin', +}; + +export const stxFt: FungibleToken = { + name: 'Stacks', + balance: '', + total_sent: '', + total_received: '', + principal: 'STX', + assetName: 'Stacks', +}; + const useMasterCoinsList = () => { const { acceptableCoinList: sip10FtList } = useStxCurrencyConversion(); const { unfilteredData: runesFtList } = useRuneFungibleTokensQuery(); From e96f5f470a3c7a86b879c7505d25d4faf8ddbe99 Mon Sep 17 00:00:00 2001 From: jordankzf Date: Wed, 7 Aug 2024 00:33:58 +0800 Subject: [PATCH 006/227] complete stacks --- .../components/tokenFromBottomSheet/index.tsx | 24 ++++++++++++++--- .../tokenFromBottomSheet/useFromTokens.ts | 26 +++++++++---------- .../components/tokenToBottomSheet/index.tsx | 6 ++--- .../tokenToBottomSheet/useToTokens.ts | 14 ++++------ src/app/screens/swap/index.tsx | 20 ++++---------- src/app/screens/swap/useMasterCoinsList.tsx | 8 +++++- .../swap/useVisibleMasterCoinsList.tsx | 21 +++++++++++++++ src/app/screens/swap/utils.ts | 10 ++++--- 8 files changed, 81 insertions(+), 48 deletions(-) create mode 100644 src/app/screens/swap/useVisibleMasterCoinsList.tsx diff --git a/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx b/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx index 7bd4adacc..ce8d1c110 100644 --- a/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx +++ b/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx @@ -23,7 +23,7 @@ const StyledTokenTile = styled(TokenTile)` interface Props { visible: boolean; title: string; - onSelectCoin: (token: FungibleToken | 'BTC') => void; + onSelectCoin: (token: FungibleToken) => void; onClose: () => void; to?: Token; } @@ -36,10 +36,10 @@ export default function TokenFromBottomSheet({ visible, title, onSelectCoin, onC {fromTokens.map((token) => { - if (token === 'BTC') { + if (token.principal === 'BTC') { return ( { @@ -52,7 +52,23 @@ export default function TokenFromBottomSheet({ visible, title, onSelectCoin, onC /> ); } - if (token.protocol === 'runes' && 'principal' in token) { + if (token.principal === 'STX') { + return ( + { + onSelectCoin(token); + trackMixPanel(AnalyticsEvents.SelectTokenToSwapFrom, { + token: 'Stacks', + }); + onClose(); + }} + /> + ); + } + if ((token.protocol === 'runes' || token.protocol === 'stacks') && 'principal' in token) { return ( { - const { unfilteredData: runesCoinsList } = useRuneFungibleTokensQuery(); + const tokens = useMasterCoinsList(); const { stxBtcRate, btcFiatRate } = useCoinRates(); - const { btcAddress } = useSelectedAccount(); - // Create a copy of the tokens array to avoid global changes - const tokens: (FungibleToken | 'BTC')[] = [...(runesCoinsList ?? [])].sort((a, b) => - sortFtByFiatBalance(a, b, stxBtcRate, btcFiatRate), - ); - - if (btcAddress && toToken?.protocol !== 'btc') tokens.unshift('BTC'); + // Sort tokens, keeping BTC as the first element, and STX (if enabled) as the second + const sortedTokens = tokens.sort((a, b) => { + if (a.principal === 'BTC') return -1; + if (b.principal === 'BTC') return 1; + if (a.principal === 'STX') return -1; + if (b.principal === 'STX') return 1; + return sortFtByFiatBalance(a, b, stxBtcRate, btcFiatRate); + }); const filteredTokens = toToken - ? tokens.filter((token) => token === 'BTC' || token.principal !== toToken.ticker) - : tokens; + ? sortedTokens.filter((token) => token.principal !== toToken.ticker) + : sortedTokens; return filteredTokens; }; diff --git a/src/app/screens/swap/components/tokenToBottomSheet/index.tsx b/src/app/screens/swap/components/tokenToBottomSheet/index.tsx index 51dfa62e2..bc0fc447a 100644 --- a/src/app/screens/swap/components/tokenToBottomSheet/index.tsx +++ b/src/app/screens/swap/components/tokenToBottomSheet/index.tsx @@ -73,7 +73,7 @@ const ProtocolItem = styled.button<{ selected: boolean }>` interface Props { visible: boolean; title: string; - from?: FungibleToken | 'BTC'; + from?: FungibleToken; onSelectCoin: (token: Token) => void; onClose: () => void; resetFrom: () => void; @@ -96,8 +96,8 @@ export default function TokenToBottomSheet({ const search = useDebounce(query, 500); const fromTokenBasic: TokenBasic | undefined = from ? { - protocol: from === 'BTC' ? 'btc' : mapFTProtocolToSwapProtocol(from.protocol ?? 'runes'), - ticker: from === 'BTC' ? 'BTC' : from.principal, + protocol: mapFTProtocolToSwapProtocol(from), + ticker: from.principal, } : undefined; const { data, error, isLoading } = useToTokens(selectedProtocol, fromTokenBasic, search); diff --git a/src/app/screens/swap/components/tokenToBottomSheet/useToTokens.ts b/src/app/screens/swap/components/tokenToBottomSheet/useToTokens.ts index 71fc4c184..6a03497d9 100644 --- a/src/app/screens/swap/components/tokenToBottomSheet/useToTokens.ts +++ b/src/app/screens/swap/components/tokenToBottomSheet/useToTokens.ts @@ -1,6 +1,5 @@ -import { useVisibleRuneFungibleTokens } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; -import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; +import useVisibleMasterCoinsList from '@screens/swap/useVisibleMasterCoinsList'; import { mapFTProtocolToSwapProtocol } from '@screens/swap/utils'; import { getXverseApiClient, type Protocol, type TokenBasic } from '@secretkeylabs/xverse-core'; import { useQuery } from '@tanstack/react-query'; @@ -8,18 +7,15 @@ import { handleRetries } from '@utils/query'; const useToTokens = (protocol: Protocol, from?: TokenBasic, query?: string) => { const { network } = useWalletSelector(); - const { visible: runesCoinsList } = useVisibleRuneFungibleTokens(); - const { btcAddress } = useSelectedAccount(); + const coinsMasterList = useVisibleMasterCoinsList(); - const runesBasicTokens = - runesCoinsList.map((ft) => ({ + const userTokens = + coinsMasterList.map((ft) => ({ ticker: ft.principal, - protocol: mapFTProtocolToSwapProtocol(ft.protocol ?? 'runes'), + protocol: mapFTProtocolToSwapProtocol(ft), })) ?? []; const search = query?.trim().replace(/\s+/g, ' ').replace(/ /g, '•') ?? ''; - const btcBasicToken: TokenBasic = { protocol: 'btc', ticker: 'BTC' }; - const userTokens = [...(btcAddress ? [btcBasicToken] : [])].concat(runesBasicTokens); const queryFn = async () => { const response = await getXverseApiClient(network.type).swaps.getDestinationTokens({ diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index 4ef8b375c..cf2cbf6c1 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -2,7 +2,6 @@ import ArrowSwap from '@assets/img/icons/ArrowSwap.svg'; import BottomBar from '@components/tabBar'; import TopRow from '@components/topRow'; import useRuneFloorPriceQuery from '@hooks/queries/runes/useRuneFloorPriceQuery'; -import { useRuneFungibleTokensQuery } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; import useGetQuotes from '@hooks/queries/swaps/useGetQuotes'; import useBtcWalletData from '@hooks/queries/useBtcWalletData'; import useCoinRates from '@hooks/queries/useCoinRates'; @@ -11,7 +10,6 @@ import { AnalyticsEvents, btcToSats, getBtcFiatEquivalent, - type BaseToken, type ExecuteOrderRequest, type FungibleToken, type GetUtxosRequest, @@ -28,7 +26,7 @@ import { satsToBtcString } from '@utils/helper'; import { trackMixPanel } from '@utils/mixpanel'; import { getFtBalance } from '@utils/tokens'; import BigNumber from 'bignumber.js'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; @@ -41,7 +39,7 @@ import TokenToBottomSheet from './components/tokenToBottomSheet'; import trackSwapMixPanel from './mixpanel'; import QuoteSummary from './quoteSummary'; import QuotesModal from './quotesModal'; -import { useStxCurrencyConversion } from './useStxCurrencyConversion'; +import useMasterCoinsList from './useMasterCoinsList'; import { mapFTNativeSwapTokenToTokenBasic, mapFTProtocolToSwapProtocol, @@ -118,7 +116,7 @@ const mapFtToSwapToken = (st: FungibleToken): Token => { return { ticker: st.principal ?? '', name: st.name ?? st.assetName ?? '', - protocol: mapFTProtocolToSwapProtocol(st.protocol ?? 'runes'), + protocol: mapFTProtocolToSwapProtocol(st), divisibility: st.decimals ?? 0, logo: st.image ?? st.runeInscriptionId ?? '', symbol: st.runeSymbol ?? '', @@ -148,9 +146,6 @@ export default function SwapScreen() { const { fiatCurrency } = useWalletSelector(); - // Hook for SIP-10 - const { acceptableCoinList: sip10FtList } = useStxCurrencyConversion(); - const { unfilteredData: runesFtList } = useRuneFungibleTokensQuery(); const { data: btcBalance } = useBtcWalletData(); const { btcFiatRate } = useCoinRates(); const navigate = useNavigate(); @@ -160,12 +155,7 @@ export default function SwapScreen() { const defaultFrom = params.get('from'); const { quotes, loading: quotesLoading, error: quotesError, fetchQuotes } = useGetQuotes(); const { data: runeFloorPrice } = useRuneFloorPriceQuery(toToken?.name ?? ''); - - // Combined list - const coinsMasterList = useMemo( - () => [...sip10FtList, ...(runesFtList || []), btcFt, stxFt] ?? [], - [sip10FtList, runesFtList], - ); + const coinsMasterList = useMasterCoinsList(); useEffect(() => { if (defaultFrom) { @@ -523,7 +513,7 @@ export default function SwapScreen() { fromToken?.principal === 'BTC' ? 'btc' : fromToken?.protocol - ? mapFTProtocolToSwapProtocol(fromToken.protocol) + ? mapFTProtocolToSwapProtocol(fromToken) : undefined, decimals: fromToken?.principal === 'BTC' ? 8 : fromToken?.decimals, unit: fromToken?.principal === 'BTC' ? 'BTC' : fromToken?.runeSymbol ?? '', diff --git a/src/app/screens/swap/useMasterCoinsList.tsx b/src/app/screens/swap/useMasterCoinsList.tsx index 8f291169b..f9c5780cc 100644 --- a/src/app/screens/swap/useMasterCoinsList.tsx +++ b/src/app/screens/swap/useMasterCoinsList.tsx @@ -1,4 +1,7 @@ +import IconBitcoin from '@assets/img/dashboard/bitcoin_icon.svg'; +import IconStacks from '@assets/img/dashboard/stx_icon.svg'; import { useRuneFungibleTokensQuery } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; +import useWalletSelector from '@hooks/useWalletSelector'; import type { FungibleToken } from '@secretkeylabs/xverse-core'; import { useMemo } from 'react'; import { useStxCurrencyConversion } from './useStxCurrencyConversion'; @@ -10,6 +13,7 @@ export const btcFt: FungibleToken = { total_received: '', principal: 'BTC', assetName: 'Bitcoin', + image: IconBitcoin, }; export const stxFt: FungibleToken = { @@ -19,14 +23,16 @@ export const stxFt: FungibleToken = { total_received: '', principal: 'STX', assetName: 'Stacks', + image: IconStacks, }; const useMasterCoinsList = () => { const { acceptableCoinList: sip10FtList } = useStxCurrencyConversion(); const { unfilteredData: runesFtList } = useRuneFungibleTokensQuery(); + const { hideStx } = useWalletSelector(); const coinsMasterList = useMemo( - () => [...sip10FtList, ...(runesFtList || []), btcFt, stxFt] ?? [], + () => [...sip10FtList, ...(runesFtList || []), btcFt, ...(hideStx ? [] : [stxFt])] ?? [], [sip10FtList, runesFtList], ); diff --git a/src/app/screens/swap/useVisibleMasterCoinsList.tsx b/src/app/screens/swap/useVisibleMasterCoinsList.tsx new file mode 100644 index 000000000..65ac42291 --- /dev/null +++ b/src/app/screens/swap/useVisibleMasterCoinsList.tsx @@ -0,0 +1,21 @@ +import { useVisibleRuneFungibleTokens } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; +import { useVisibleSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { useMemo } from 'react'; +import { btcFt, stxFt } from './useMasterCoinsList'; + +const useVisibleMasterCoinsList = () => { + const { visible: sip10FtList } = useVisibleSip10FungibleTokens(); + const { visible: runesFtList } = useVisibleRuneFungibleTokens(); + + const { hideStx } = useWalletSelector(); + + const coinsMasterList = useMemo( + () => [...sip10FtList, ...runesFtList, btcFt, ...(hideStx ? [] : [stxFt])] ?? [], + [sip10FtList, runesFtList], + ); + + return coinsMasterList; +}; + +export default useVisibleMasterCoinsList; diff --git a/src/app/screens/swap/utils.ts b/src/app/screens/swap/utils.ts index c495cae9b..98dcb9eaf 100644 --- a/src/app/screens/swap/utils.ts +++ b/src/app/screens/swap/utils.ts @@ -6,13 +6,17 @@ import type { TokenBasic, } from '@secretkeylabs/xverse-core'; -export const mapFTProtocolToSwapProtocol = (protocol: FungibleTokenProtocol): Protocol => { +export const mapFTProtocolToSwapProtocol = (ft: FungibleToken): Protocol => { + if (ft.principal === 'BTC') return 'btc'; + if (ft.principal === 'STX') return 'stx'; + if (!ft.protocol) return 'runes'; + const protocolMap: Record = { stacks: 'sip10', runes: 'runes', 'brc-20': 'brc20', }; - return protocolMap[protocol]; + return protocolMap[ft.protocol]; }; export const mapSwapProtocolToFTProtocol = (protocol: Protocol): FungibleTokenProtocol => { @@ -48,7 +52,7 @@ export const mapFTNativeSwapTokenToTokenBasic = ( // if token is FungibleToken if ('principal' in token && token.protocol) { - return { ticker: token.principal, protocol: mapFTProtocolToSwapProtocol(token.protocol) }; + return { ticker: token.principal, protocol: mapFTProtocolToSwapProtocol(token) }; } // token will never have a principal prop so we can safely cast it as Token From b49f3805860b9f0acfed155d87bd24a581f5e040 Mon Sep 17 00:00:00 2001 From: Christine Pinto Date: Thu, 15 Aug 2024 14:59:30 +0200 Subject: [PATCH 007/227] Adjust worker amount for local transaction e2e execution (#506) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ce18c2076..09af0a788 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "e2etest": "npx playwright test -g \"\" --grep-invert \"#localexecution\"", "e2etest:ui": "npx playwright test --ui", "e2etest:smoketest": "npx playwright test --grep \"#smoketest\"", - "e2etest:skipped": "npx playwright test --grep \"#localexecution\"", + "e2etest:skipped": "npx playwright test --grep \"#localexecution\" --workers=1", "e2etest:report": "playwright show-report", "knip": "knip", "ts-check": "tsc --noEmit", From c65a6db03f65f0f2b8078809dedc46d000effc93 Mon Sep 17 00:00:00 2001 From: jordankzf Date: Thu, 15 Aug 2024 21:09:56 +0800 Subject: [PATCH 008/227] feature flag --- package-lock.json | 14 +++++++------- package.json | 2 +- src/app/screens/coinDashboard/coinHeader.tsx | 9 ++++++--- src/app/screens/swap/useMasterCoinsList.tsx | 11 +++++++++-- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index adadede5e..f121f896c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.2.1", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "18.7.0", + "@secretkeylabs/xverse-core": "18.7.0-49281c0", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.13.1", @@ -1862,9 +1862,9 @@ } }, "node_modules/@secretkeylabs/xverse-core": { - "version": "18.7.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.7.0/e3ab51151b17b6c49fade2a104bb3122f76a1e17", - "integrity": "sha512-hoNCtY7LNBL908Hmh8Yxpo0IPLGg6wnrUYsdVkxyvhn9+IsXZE2GtrafVn5bAibw02wbQdTUX0OUckyd1/ZW+Q==", + "version": "18.7.0-49281c0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.7.0-49281c0/5edda6e48bb4c8ecb7b35a454358600b8a57e684", + "integrity": "sha512-PJSQZlWHMw8t+Y4+PTM2r67qSxYhAYFyNJ9potyH/PKK6NbfQ5bdB3FKE6nSelOAYJuiZPaTMbxqDidmvUkcrw==", "license": "ISC", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", @@ -15328,9 +15328,9 @@ } }, "@secretkeylabs/xverse-core": { - "version": "18.7.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.7.0/e3ab51151b17b6c49fade2a104bb3122f76a1e17", - "integrity": "sha512-hoNCtY7LNBL908Hmh8Yxpo0IPLGg6wnrUYsdVkxyvhn9+IsXZE2GtrafVn5bAibw02wbQdTUX0OUckyd1/ZW+Q==", + "version": "18.7.0-49281c0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.7.0-49281c0/5edda6e48bb4c8ecb7b35a454358600b8a57e684", + "integrity": "sha512-PJSQZlWHMw8t+Y4+PTM2r67qSxYhAYFyNJ9potyH/PKK6NbfQ5bdB3FKE6nSelOAYJuiZPaTMbxqDidmvUkcrw==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", diff --git a/package.json b/package.json index 974c9bc0b..648b128c8 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.2.1", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "18.7.0", + "@secretkeylabs/xverse-core": "18.7.0-49281c0", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.13.1", diff --git a/src/app/screens/coinDashboard/coinHeader.tsx b/src/app/screens/coinDashboard/coinHeader.tsx index f91ff47c7..7b69cfdfd 100644 --- a/src/app/screens/coinDashboard/coinHeader.tsx +++ b/src/app/screens/coinDashboard/coinHeader.tsx @@ -167,12 +167,15 @@ export default function CoinHeader({ currency, fungibleToken }: Props) { }; const isCrossChainSwapsEnabled = useHasFeature(FeatureId.CROSS_CHAIN_SWAPS); - const showRunesSwap = + const isStacksSwapsEnabled = useHasFeature(FeatureId.STACKS_SWAPS); + + const isSwapEligibleCurrency = (currency === 'FT' && - (fungibleToken?.protocol === 'runes' || fungibleToken?.protocol === 'stacks')) || + (fungibleToken?.protocol === 'runes' || + (isStacksSwapsEnabled && fungibleToken?.protocol === 'stacks'))) || currency === 'BTC' || currency === 'STX'; - const showSwaps = isCrossChainSwapsEnabled && showRunesSwap; + const showSwaps = isCrossChainSwapsEnabled && isSwapEligibleCurrency; const navigateToSwaps = () => { if (!showSwaps) { diff --git a/src/app/screens/swap/useMasterCoinsList.tsx b/src/app/screens/swap/useMasterCoinsList.tsx index f9c5780cc..62fec126f 100644 --- a/src/app/screens/swap/useMasterCoinsList.tsx +++ b/src/app/screens/swap/useMasterCoinsList.tsx @@ -1,8 +1,9 @@ import IconBitcoin from '@assets/img/dashboard/bitcoin_icon.svg'; import IconStacks from '@assets/img/dashboard/stx_icon.svg'; import { useRuneFungibleTokensQuery } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; +import useHasFeature from '@hooks/useHasFeature'; import useWalletSelector from '@hooks/useWalletSelector'; -import type { FungibleToken } from '@secretkeylabs/xverse-core'; +import { FeatureId, type FungibleToken } from '@secretkeylabs/xverse-core'; import { useMemo } from 'react'; import { useStxCurrencyConversion } from './useStxCurrencyConversion'; @@ -30,9 +31,15 @@ const useMasterCoinsList = () => { const { acceptableCoinList: sip10FtList } = useStxCurrencyConversion(); const { unfilteredData: runesFtList } = useRuneFungibleTokensQuery(); const { hideStx } = useWalletSelector(); + const isStacksSwapsEnabled = useHasFeature(FeatureId.STACKS_SWAPS); const coinsMasterList = useMemo( - () => [...sip10FtList, ...(runesFtList || []), btcFt, ...(hideStx ? [] : [stxFt])] ?? [], + () => + [ + ...(runesFtList || []), + btcFt, + ...(!hideStx && isStacksSwapsEnabled ? [stxFt, ...sip10FtList] : []), + ] ?? [], [sip10FtList, runesFtList], ); From 83d43d1e45a7df9cc66a6ee197351101e73bfffb Mon Sep 17 00:00:00 2001 From: jordankzf Date: Thu, 15 Aug 2024 21:12:44 +0800 Subject: [PATCH 009/227] fix merge conflicts --- src/app/screens/swap/components/tokenFromBottomSheet/index.tsx | 2 +- src/app/screens/swap/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx b/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx index 19f9d1b7f..4dbbe6566 100644 --- a/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx +++ b/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx @@ -61,7 +61,7 @@ export default function TokenFromBottomSheet({ visible, title, onSelectCoin, onC onPress={() => { onSelectCoin(token); trackMixPanel(AnalyticsEvents.SelectTokenToSwapFrom, { - token: 'Stacks', + selectedToken: 'Stacks', }); onClose(); }} diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index 6dda1eb1c..0c7ca1789 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -188,7 +188,7 @@ export default function SwapScreen() { trackSwapMixPanel(AnalyticsEvents.FetchSwapQuote, { fromToken, toToken, - amount: fromToken === 'BTC' ? btcToSats(new BigNumber(amount)).toString() : amount, + amount: fromToken?.principal === 'BTC' ? btcToSats(new BigNumber(amount)).toString() : amount, quote, btcUsdRate, runeFloorPrice, From e669ced2ebdb888c6b0dc3c5411ad5607f9340c1 Mon Sep 17 00:00:00 2001 From: jordankzf Date: Thu, 15 Aug 2024 21:24:38 +0800 Subject: [PATCH 010/227] remove remaining traces of 'BTC' --- src/app/screens/swap/mixpanel.ts | 6 +++--- src/app/screens/swap/quoteSummary/index.tsx | 14 +++++++------- src/app/screens/swap/utils.ts | 10 ++-------- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/app/screens/swap/mixpanel.ts b/src/app/screens/swap/mixpanel.ts index e40afede5..aace8675d 100644 --- a/src/app/screens/swap/mixpanel.ts +++ b/src/app/screens/swap/mixpanel.ts @@ -20,7 +20,7 @@ function trackSwapMixPanel( runeFloorPrice, }: { provider?: Provider; - fromToken?: FungibleToken | 'BTC'; + fromToken?: FungibleToken; toToken?: Token; amount: string; quote?: Quote; @@ -28,11 +28,11 @@ function trackSwapMixPanel( runeFloorPrice?: number; }, ) { - const from = fromToken === 'BTC' ? 'BTC' : fromToken?.name; + const from = fromToken?.name; const to = toToken?.protocol === 'btc' ? 'BTC' : toToken?.name ?? toToken?.ticker; const fromAmount = - fromToken === 'BTC' + fromToken?.principal === 'BTC' ? getBtcFiatEquivalent(new BigNumber(amount), new BigNumber(btcUsdRate)).toFixed(2) : new BigNumber(fromToken?.tokenFiatRate ?? 0).multipliedBy(amount).toFixed(2); diff --git a/src/app/screens/swap/quoteSummary/index.tsx b/src/app/screens/swap/quoteSummary/index.tsx index cdf918fbe..bd7c268a1 100644 --- a/src/app/screens/swap/quoteSummary/index.tsx +++ b/src/app/screens/swap/quoteSummary/index.tsx @@ -132,7 +132,7 @@ const FeeRate = styled.div` type QuoteSummaryProps = { amount: string; - fromToken?: FungibleToken | 'BTC'; + fromToken?: FungibleToken; toToken?: Token; quote: Quote; onClose: () => void; @@ -183,7 +183,7 @@ export default function QuoteSummary({ const { fiatCurrency } = useWalletSelector(); const fromUnit = - fromToken === 'BTC' + fromToken?.principal === 'BTC' ? 'Sats' : (fromToken as FungibleToken)?.runeSymbol ?? RUNE_DISPLAY_DEFAULTS.symbol; @@ -282,14 +282,14 @@ export default function QuoteSummary({ provider="Amount" price={amount} image={{ - currency: fromToken === 'BTC' ? 'BTC' : 'FT', - ft: fromToken === 'BTC' ? undefined : fromToken, + currency: fromToken?.principal === 'BTC' ? 'BTC' : 'FT', + ft: fromToken?.principal === 'BTC' ? undefined : fromToken, }} - subtitle={fromToken === 'BTC' ? 'Bitcoin' : fromToken?.assetName} + subtitle={fromToken?.principal === 'BTC' ? 'Bitcoin' : fromToken?.assetName} subtitleColor="white_400" - unit={fromToken === 'BTC' ? 'Sats' : fromToken?.runeSymbol ?? ''} + unit={fromToken?.principal === 'BTC' ? 'Sats' : fromToken?.runeSymbol ?? ''} fiatValue={ - fromToken === 'BTC' + fromToken?.principal === 'BTC' ? getBtcFiatEquivalent(new BigNumber(amount), new BigNumber(btcFiatRate)).toFixed( 2, ) diff --git a/src/app/screens/swap/utils.ts b/src/app/screens/swap/utils.ts index 98dcb9eaf..16350cb32 100644 --- a/src/app/screens/swap/utils.ts +++ b/src/app/screens/swap/utils.ts @@ -43,15 +43,9 @@ export const mapSwapTokenToFT = (token: Token): FungibleToken => ({ runeInscriptionId: token.logo, }); -export const mapFTNativeSwapTokenToTokenBasic = ( - token: FungibleToken | 'BTC' | Token, -): TokenBasic => { - if (token === 'BTC') { - return { ticker: 'BTC', protocol: 'btc' }; - } - +export const mapFTNativeSwapTokenToTokenBasic = (token: FungibleToken | Token): TokenBasic => { // if token is FungibleToken - if ('principal' in token && token.protocol) { + if ('principal' in token) { return { ticker: token.principal, protocol: mapFTProtocolToSwapProtocol(token) }; } From b4d8768c425d4422178d0b65f1c92ebc1158ae20 Mon Sep 17 00:00:00 2001 From: jordankzf Date: Tue, 20 Aug 2024 17:43:33 +0800 Subject: [PATCH 011/227] stx swaps --- .../components/tokenToBottomSheet/index.tsx | 11 +++-- src/app/screens/swap/index.tsx | 24 ++++++++++- src/app/screens/swap/quoteSummary/index.tsx | 40 ++++++++++++++++--- .../swap/quoteSummary/usePlaceOrder.tsx | 20 ++++++++-- src/app/screens/swap/quotesModal/index.tsx | 38 +++++++++++++++--- src/app/screens/swap/types.ts | 12 +++++- src/app/screens/swap/utils.ts | 2 +- 7 files changed, 124 insertions(+), 23 deletions(-) diff --git a/src/app/screens/swap/components/tokenToBottomSheet/index.tsx b/src/app/screens/swap/components/tokenToBottomSheet/index.tsx index fc6502bea..ed12b65f7 100644 --- a/src/app/screens/swap/components/tokenToBottomSheet/index.tsx +++ b/src/app/screens/swap/components/tokenToBottomSheet/index.tsx @@ -79,7 +79,12 @@ interface Props { resetFrom: () => void; } -const supportedProtocols: Protocol[] = ['runes']; // add more protocols here +const supportedProtocols: Protocol[] = ['runes', 'sip10']; // add more protocols here + +const mapProtocolName = (protocol: Protocol) => { + if (protocol === 'sip10') return 'SIP-10'; + return protocol.toUpperCase(); +}; export default function TokenToBottomSheet({ visible, @@ -138,7 +143,7 @@ export default function TokenToBottomSheet({ selected={key === selectedProtocol} onClick={onChangeProtocol(key)} > - {key.toUpperCase()} + {mapProtocolName(key)} ))} @@ -168,7 +173,7 @@ export default function TokenToBottomSheet({ /> ); } - if (token.protocol === 'runes') { + if (token.protocol === 'runes' || token.protocol === 'sip10') { return ( (); + const [stxOrderInfo, setStxOrderInfo] = useState(); + const [selectedUtxos, setSelectedUtxos] = useState[]>(); const [utxoProviderSendAmount, setUtxoProviderSendAmount] = useState(); @@ -170,7 +174,10 @@ export default function SwapScreen() { return; } - if ((quotes.amm.length === 0 && quotes.utxo.length === 0) || quotesError) { + if ( + (quotes.amm.length === 0 && quotes.utxo.length === 0 && quotes.stx.length === 0) || + quotesError + ) { return setHasQuoteError(true); } @@ -400,6 +407,7 @@ export default function SwapScreen() { }} ammProviders={quotes?.amm || []} utxoProviders={quotes?.utxo || []} + stxProviders={quotes?.stx || []} toToken={toToken} ammProviderClicked={(provider: Quote) => { setProvider(true, provider); @@ -420,6 +428,17 @@ export default function SwapScreen() { ); } + if (stxOrderInfo?.order.unsignedTransaction) { + const unsignedTransaction = deserializeTransaction(stxOrderInfo.order.unsignedTransaction); + + navigate(RoutePaths.ConfirmStacksTransaction, { + state: { + unsignedTx: unsignedTransaction, + fee: unsignedTransaction.auth.spendingCondition.fee.toString(), + }, + }); + } + if (quote) { return ( <> @@ -433,6 +452,7 @@ export default function SwapScreen() { setGetQuotesModalVisible(true); }} onOrderPlaced={setOrderInfo} + onStxOrderPlaced={setStxOrderInfo} onError={setErrorMessage} selectedIdentifiers={selectedUtxos} /> diff --git a/src/app/screens/swap/quoteSummary/index.tsx b/src/app/screens/swap/quoteSummary/index.tsx index bd7c268a1..a007a12a7 100644 --- a/src/app/screens/swap/quoteSummary/index.tsx +++ b/src/app/screens/swap/quoteSummary/index.tsx @@ -28,7 +28,7 @@ import styled, { useTheme } from 'styled-components'; import trackSwapMixPanel from '../mixpanel'; import QuoteTile from '../quotesModal/quoteTile'; import { SlippageModalContent } from '../slippageModal'; -import type { OrderInfo } from '../types'; +import type { OrderInfo, StxOrderInfo } from '../types'; import { mapFTNativeSwapTokenToTokenBasic } from '../utils'; import EditFee from './EditFee'; import QuoteSummaryTile from './quoteSummaryTile'; @@ -139,6 +139,7 @@ type QuoteSummaryProps = { onChangeProvider: () => void; onError: (errorMessage: string) => void; onOrderPlaced: ({ order, providerCode }: OrderInfo) => void; + onStxOrderPlaced: ({ order, providerCode }: StxOrderInfo) => void; selectedIdentifiers?: Omit[]; }; @@ -150,14 +151,21 @@ export default function QuoteSummary({ onClose, onChangeProvider, onOrderPlaced, + onStxOrderPlaced, onError, selectedIdentifiers, }: QuoteSummaryProps) { const { t } = useTranslation('translation'); const theme = useTheme(); const { btcFiatRate, btcUsdRate } = useCoinRates(); - const { btcAddress, ordinalsAddress, btcPublicKey, ordinalsPublicKey } = useSelectedAccount(); - const { loading: isPlaceOrderLoading, error: placeOrderError, placeOrder } = usePlaceOrder(); + const { btcAddress, ordinalsAddress, btcPublicKey, ordinalsPublicKey, stxAddress, stxPublicKey } = + useSelectedAccount(); + const { + loading: isPlaceOrderLoading, + error: placeOrderError, + placeOrder, + placeStxOrder, + } = usePlaceOrder(); const { loading: isPlaceUtxoOrderLoading, error: placeUtxoOrderError, @@ -185,11 +193,16 @@ export default function QuoteSummary({ const fromUnit = fromToken?.principal === 'BTC' ? 'Sats' - : (fromToken as FungibleToken)?.runeSymbol ?? RUNE_DISPLAY_DEFAULTS.symbol; + : (fromToken as FungibleToken)?.runeSymbol ?? + (fromToken as FungibleToken).assetName ?? + RUNE_DISPLAY_DEFAULTS.symbol; const toUnit = toToken?.protocol === 'btc' ? 'Sats' : toToken?.symbol ?? RUNE_DISPLAY_DEFAULTS.symbol; + const isRunesSwap = fromToken?.protocol === 'runes' || toToken?.protocol === 'runes'; + const isSip10Swap = fromToken?.protocol === 'stacks' || toToken?.protocol === 'sip10'; + const [showSlippageModal, setShowSlippageModal] = useState(false); const [slippage, setSlippage] = useSearchParamsState('slippage', 0.05); @@ -229,7 +242,7 @@ export default function QuoteSummary({ if (placeUtxoOrderResponse?.orders.length === 0) { onError(t('SWAP_SCREEN.ERRORS.NO_UTXOS_FOR_PURCHASE')); } - } else { + } else if (isRunesSwap) { const placeOrderRequest = { providerCode: quote.provider.code, from: mapFTNativeSwapTokenToTokenBasic(fromToken), @@ -248,6 +261,23 @@ export default function QuoteSummary({ if (placeOrderResponse?.psbt) { onOrderPlaced({ order: placeOrderResponse, providerCode: quote.provider.code }); } + } else if (isSip10Swap) { + const placeStxOrderRequest = { + providerCode: quote.provider.code, + from: mapFTNativeSwapTokenToTokenBasic(fromToken), + to: mapFTNativeSwapTokenToTokenBasic(toToken), + sendAmount: amount, + receiveAmount: quote.receiveAmount, + slippage, + feeRate: Number(feeRate), + stxAddress, + stxPublicKey, + }; + const placeOrderResponse = await placeStxOrder(placeStxOrderRequest); + + if (placeOrderResponse?.unsignedTransaction) { + onStxOrderPlaced({ order: placeOrderResponse, providerCode: quote.provider.code }); + } } }; diff --git a/src/app/screens/swap/quoteSummary/usePlaceOrder.tsx b/src/app/screens/swap/quoteSummary/usePlaceOrder.tsx index 4f931e394..617ead829 100644 --- a/src/app/screens/swap/quoteSummary/usePlaceOrder.tsx +++ b/src/app/screens/swap/quoteSummary/usePlaceOrder.tsx @@ -2,12 +2,11 @@ import useWalletSelector from '@hooks/useWalletSelector'; import { getXverseApiClient, type PlaceOrderRequest, - type PlaceOrderResponse, + type PlaceStxOrderRequest, } from '@secretkeylabs/xverse-core'; import { useState } from 'react'; const usePlaceOrder = () => { - const [order, setOrder] = useState(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const { network } = useWalletSelector(); @@ -19,7 +18,6 @@ const usePlaceOrder = () => { setError(null); try { const response = await xverseApiClient.swaps.placeOrder(request); - setOrder(response); return response; } catch (err: any) { setError(err?.response?.data?.message ?? 'Failed to place order'); @@ -28,7 +26,21 @@ const usePlaceOrder = () => { } }; - return { order, loading, error, placeOrder }; + const placeStxOrder = async (request: PlaceStxOrderRequest) => { + setLoading(true); + setError(null); + try { + const response = await xverseApiClient.swaps.placeStxOrder(request); + + return response; + } catch (err: any) { + setError(err?.response?.data?.message ?? 'Failed to place order'); + } finally { + setLoading(false); + } + }; + + return { loading, error, placeOrder, placeStxOrder }; }; export default usePlaceOrder; diff --git a/src/app/screens/swap/quotesModal/index.tsx b/src/app/screens/swap/quotesModal/index.tsx index 41e4e3950..bc3c57bf3 100644 --- a/src/app/screens/swap/quotesModal/index.tsx +++ b/src/app/screens/swap/quotesModal/index.tsx @@ -1,8 +1,10 @@ import useCoinRates from '@hooks/queries/useCoinRates'; import { getBtcFiatEquivalent, + getStxFiatEquivalent, type FungibleToken, type Quote, + type StxQuote, type Token, type UtxoQuote, } from '@secretkeylabs/xverse-core'; @@ -19,6 +21,7 @@ interface Props { onClose: () => void; ammProviders: Quote[]; utxoProviders: UtxoQuote[]; + stxProviders: StxQuote[]; toToken?: Token; ammProviderClicked?: (amm: Quote) => void; utxoProviderClicked?: (utxoProvider: UtxoQuote) => void; @@ -54,13 +57,14 @@ function QuotesModal({ onClose, ammProviders, utxoProviders, + stxProviders, toToken, ammProviderClicked, utxoProviderClicked, }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); - const { btcFiatRate } = useCoinRates(); + const { btcFiatRate, stxBtcRate } = useCoinRates(); const numberOfUtxoProviders = utxoProviders.length; const lowestUtxoQuoteFloorRate = Math.min( @@ -102,11 +106,12 @@ function QuotesModal({ {t('QUOTE_TITLE')} - {ammProviders.length > 0 && ( - - {t('EXCHANGE')} - - )} + {ammProviders.length > 0 || + (stxProviders.length > 0 && ( + + {t('EXCHANGE')} + + ))} {/* todo: get fiat rates of rune from API */} {ammProviders.map((amm) => ( )} + {stxProviders.map((stx) => ( + ammProviderClicked && ammProviderClicked(stx)} + subtitle={t('RECOMMENDED')} + subtitleColor="success_light" + unit={stx.to.protocol === 'stx' ? 'STX' : toToken?.symbol || ''} + fiatValue={ + stx.to.protocol === 'stx' + ? getStxFiatEquivalent( + new BigNumber(stx.receiveAmount), + new BigNumber(stxBtcRate), + new BigNumber(btcFiatRate), + ).toFixed(2) + : '' + } + /> + ))} {utxoQuotes.map((utxoProvider) => { const subTitle = getSubtitle(utxoProvider); let subTitleColour: Color = 'success_light'; diff --git a/src/app/screens/swap/types.ts b/src/app/screens/swap/types.ts index 7077d8730..9a6cc21ec 100644 --- a/src/app/screens/swap/types.ts +++ b/src/app/screens/swap/types.ts @@ -3,6 +3,7 @@ import { type ExecuteOrderRequest, type FungibleToken, type PlaceOrderResponse, + type PlaceStxOrderResponse, type PlaceUtxoOrderResponse, } from '@secretkeylabs/xverse-core'; import { Currency } from 'alex-sdk'; @@ -52,7 +53,14 @@ export type SelectedCurrencyState = { prevFrom?: Currency; }; -export type OrderInfo = { - order: PlaceOrderResponse | PlaceUtxoOrderResponse; +type BaseOrderInfo = { providerCode: ExecuteOrderRequest['providerCode']; }; + +export type OrderInfo = BaseOrderInfo & { + order: PlaceOrderResponse | PlaceUtxoOrderResponse; +}; + +export type StxOrderInfo = BaseOrderInfo & { + order: PlaceStxOrderResponse; +}; diff --git a/src/app/screens/swap/utils.ts b/src/app/screens/swap/utils.ts index 16350cb32..5088bed83 100644 --- a/src/app/screens/swap/utils.ts +++ b/src/app/screens/swap/utils.ts @@ -36,7 +36,7 @@ export const mapSwapTokenToFT = (token: Token): FungibleToken => ({ principal: token.ticker, balance: '0', decimals: token.divisibility, - image: undefined, + image: token.logo, total_received: '0', total_sent: '0', runeSymbol: token.symbol, From 89e626bc7eab407d19bc2f9a37d995dcded50b20 Mon Sep 17 00:00:00 2001 From: jordankzf Date: Thu, 22 Aug 2024 14:49:35 +0800 Subject: [PATCH 012/227] clean up --- .../ContractCallRequest.tsx | 33 +++++++++- .../psbtConfirmation/useExecuteOrder.tsx | 17 +++++- src/app/screens/swap/index.tsx | 61 ++++++++----------- src/app/screens/swap/quoteSummary/index.tsx | 26 +++++--- src/app/screens/swap/utils.ts | 47 ++++++++++++++ src/app/screens/transactionRequest/index.tsx | 13 ++-- .../useStxTransactionRequest/index.ts | 32 ++++++++-- 7 files changed, 174 insertions(+), 55 deletions(-) diff --git a/src/app/components/transactionsRequests/ContractCallRequest.tsx b/src/app/components/transactionsRequests/ContractCallRequest.tsx index a53fe53be..020ca66bd 100644 --- a/src/app/components/transactionsRequests/ContractCallRequest.tsx +++ b/src/app/components/transactionsRequests/ContractCallRequest.tsx @@ -15,6 +15,7 @@ import StxPostConditionCard from '@components/postCondition/stxPostConditionCard import TransactionDetailComponent from '@components/transactionDetailComponent'; import useNetworkSelector from '@hooks/useNetwork'; import useOnOriginTabClose from '@hooks/useOnTabClosed'; +import useExecuteOrder from '@screens/swap/components/psbtConfirmation/useExecuteOrder'; import { addressToString, broadcastSignedTransaction, @@ -26,6 +27,7 @@ import { type Args, type Coin, type ContractFunction, + type ExecuteStxOrderRequest, } from '@secretkeylabs/xverse-core'; import type { ContractCallPayload } from '@stacks/connect'; import { @@ -110,6 +112,8 @@ export default function ContractCallRequest({ const [hasTabClosed, setHasTabClosed] = useState(false); const { t } = useTranslation('translation'); const [fee, setFee] = useState(undefined); + const { executeStxOrder } = useExecuteOrder(); + const [isLoading, setIsLoading] = useState(false); // SignTransaction Params const isMultiSigTx = isMultiSig(unsignedTx); @@ -262,7 +266,32 @@ export default function ContractCallRequest({ } }; - const confirmCallback = (transactions: StacksTransaction[]) => { + const confirmCallback = async (transactions: StacksTransaction[]) => { + if (messageId === 'velar') { + const order: ExecuteStxOrderRequest = { + providerCode: 'velar', + signedTransaction: buf2hex(transactions[0].serialize()), + }; + setIsLoading(true); + const response = await executeStxOrder(order); + setIsLoading(false); + + if (response) { + navigate('/tx-status', { + state: { + txid: response.txid, + currency: 'STX', + error: '', + browserTx: false, + tabId, + messageId, + rpcMethod, + }, + }); + } + return; + } + if (request?.sponsored) { if (rpcMethod && tabId && messageId) { switch (rpcMethod) { @@ -356,7 +385,7 @@ export default function ContractCallRequest({ initialStxTransactions={[unsignedTx]} onConfirmClick={confirmCallback} onCancelClick={cancelCallback} - loading={false} + loading={isLoading} title={request.functionName} subTitle={`Requested by ${request.appDetails?.name}`} hasSignatures={hasSignatures} diff --git a/src/app/screens/swap/components/psbtConfirmation/useExecuteOrder.tsx b/src/app/screens/swap/components/psbtConfirmation/useExecuteOrder.tsx index e4d7e6fc7..afce53d08 100644 --- a/src/app/screens/swap/components/psbtConfirmation/useExecuteOrder.tsx +++ b/src/app/screens/swap/components/psbtConfirmation/useExecuteOrder.tsx @@ -3,6 +3,7 @@ import { getXverseApiClient, type ExecuteOrderRequest, type ExecuteOrderResponse, + type ExecuteStxOrderRequest, } from '@secretkeylabs/xverse-core'; import { useState } from 'react'; @@ -28,7 +29,21 @@ const useExecuteOrder = () => { } }; - return { order, loading, error, executeOrder }; + const executeStxOrder = async (request: ExecuteStxOrderRequest) => { + setLoading(true); + setError(null); + try { + const response = await xverseApiClient.swaps.executeStxOrder(request); + setOrder(response); + return response; + } catch (err: any) { + setError(err?.response?.data?.message ?? 'Failed to execute order'); + } finally { + setLoading(false); + } + }; + + return { order, loading, error, executeOrder, executeStxOrder }; }; export default useExecuteOrder; diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index 6aa52c60e..8d8525de9 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -1,11 +1,14 @@ import ArrowSwap from '@assets/img/icons/ArrowSwap.svg'; +import RequestsRoutes from '@common/utils/route-urls'; import BottomBar from '@components/tabBar'; import TopRow from '@components/topRow'; import useRuneFloorPriceQuery from '@hooks/queries/runes/useRuneFloorPriceQuery'; import useGetQuotes from '@hooks/queries/swaps/useGetQuotes'; import useBtcWalletData from '@hooks/queries/useBtcWalletData'; import useCoinRates from '@hooks/queries/useCoinRates'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; +import type { DataStxSignTransaction } from '@screens/transactionRequest/useStxTransactionRequest'; import { AnalyticsEvents, btcToSats, @@ -17,14 +20,12 @@ import { type Token, type UtxoQuote, } from '@secretkeylabs/xverse-core'; -import { deserializeTransaction } from '@stacks/transactions'; import Button, { LinkButton } from '@ui-library/button'; import { StyledP } from '@ui-library/common.styled'; import SnackBar from '@ui-library/snackBar'; import { satsToBtcString } from '@utils/helper'; import { trackMixPanel } from '@utils/mixpanel'; import { getFtBalance } from '@utils/tokens'; -import RoutePaths from 'app/routes/paths'; import BigNumber from 'bignumber.js'; import { useEffect, useState } from 'react'; import toast from 'react-hot-toast'; @@ -44,6 +45,7 @@ import useMasterCoinsList from './useMasterCoinsList'; import { mapFTNativeSwapTokenToTokenBasic, mapFTProtocolToSwapProtocol, + mapFtToSwapToken, mapSwapProtocolToFTProtocol, mapSwapTokenToFT, } from './utils'; @@ -96,34 +98,6 @@ const Icon = styled.img` rotate: 90deg; `; -const mapFtToSwapToken = (st: FungibleToken): Token => { - if (st.principal === 'BTC') { - return { - ticker: 'BTC', - name: 'Bitcoin', - protocol: 'btc', - divisibility: 8, - }; - } - if (st.principal === 'STX') { - return { - ticker: 'STX', - name: 'Stacks', - protocol: 'stx', - divisibility: 6, - }; - } - - return { - ticker: st.principal ?? '', - name: st.name ?? st.assetName ?? '', - protocol: mapFTProtocolToSwapProtocol(st), - divisibility: st.decimals ?? 0, - logo: st.image ?? st.runeInscriptionId ?? '', - symbol: st.runeSymbol ?? '', - }; -}; - export default function SwapScreen() { const [amount, setAmount] = useState(''); const [quote, setQuote] = useState(); @@ -146,6 +120,7 @@ export default function SwapScreen() { const [utxoProviderSendAmount, setUtxoProviderSendAmount] = useState(); const { fiatCurrency } = useWalletSelector(); + const { stxPublicKey } = useSelectedAccount(); const { data: btcBalance } = useBtcWalletData(); const { btcFiatRate, btcUsdRate } = useCoinRates(); @@ -428,13 +403,29 @@ export default function SwapScreen() { ); } - if (stxOrderInfo?.order.unsignedTransaction) { - const unsignedTransaction = deserializeTransaction(stxOrderInfo.order.unsignedTransaction); + const unsignedTransactionHexString = stxOrderInfo?.order.unsignedTransaction; + + if (unsignedTransactionHexString) { + const dataStxSignTransactionOverride: DataStxSignTransaction = { + context: { + origin: 'extension', + tabId: 0, + }, + data: { + method: 'stx_signTransaction', + params: { + transaction: unsignedTransactionHexString, + pubkey: stxPublicKey, + broadcast: true, + }, + id: 'velar', + jsonrpc: '2.0', + }, + }; - navigate(RoutePaths.ConfirmStacksTransaction, { + navigate(RequestsRoutes.TransactionRequest, { state: { - unsignedTx: unsignedTransaction, - fee: unsignedTransaction.auth.spendingCondition.fee.toString(), + dataStxSignTransactionOverride, }, }); } diff --git a/src/app/screens/swap/quoteSummary/index.tsx b/src/app/screens/swap/quoteSummary/index.tsx index a007a12a7..03448e99c 100644 --- a/src/app/screens/swap/quoteSummary/index.tsx +++ b/src/app/screens/swap/quoteSummary/index.tsx @@ -29,7 +29,12 @@ import trackSwapMixPanel from '../mixpanel'; import QuoteTile from '../quotesModal/quoteTile'; import { SlippageModalContent } from '../slippageModal'; import type { OrderInfo, StxOrderInfo } from '../types'; -import { mapFTNativeSwapTokenToTokenBasic } from '../utils'; +import { + mapFTNativeSwapTokenToTokenBasic, + mapFtToCurrencyType, + mapSwapTokenToFT, + mapTokenToCurrencyType, +} from '../utils'; import EditFee from './EditFee'; import QuoteSummaryTile from './quoteSummaryTile'; import usePlaceOrder from './usePlaceOrder'; @@ -312,8 +317,11 @@ export default function QuoteSummary({ provider="Amount" price={amount} image={{ - currency: fromToken?.principal === 'BTC' ? 'BTC' : 'FT', - ft: fromToken?.principal === 'BTC' ? undefined : fromToken, + currency: mapFtToCurrencyType(fromToken), + ft: + fromToken?.principal === 'BTC' || fromToken?.principal === 'STX' + ? undefined + : fromToken, }} subtitle={fromToken?.principal === 'BTC' ? 'Bitcoin' : fromToken?.assetName} subtitleColor="white_400" @@ -335,16 +343,18 @@ export default function QuoteSummary({ provider="Amount" price={quote.receiveAmount} image={{ - currency: toToken?.protocol === 'btc' ? 'BTC' : 'FT', + currency: mapTokenToCurrencyType(toToken), ft: - toToken?.protocol === 'btc' - ? undefined - : ({ + toToken?.protocol === 'runes' + ? ({ runeSymbol: toToken?.symbol, runeInscriptionId: toToken?.logo, ticker: toToken?.name, protocol: 'runes', - } as FungibleToken), + } as FungibleToken) + : toToken?.protocol === 'sip10' + ? mapSwapTokenToFT(toToken) + : undefined, }} subtitle={toToken?.protocol === 'btc' ? 'Bitcoin' : toToken?.name} subtitleColor="white_400" diff --git a/src/app/screens/swap/utils.ts b/src/app/screens/swap/utils.ts index 5088bed83..811dc795a 100644 --- a/src/app/screens/swap/utils.ts +++ b/src/app/screens/swap/utils.ts @@ -5,6 +5,25 @@ import type { Token, TokenBasic, } from '@secretkeylabs/xverse-core'; +import type { CurrencyTypes } from '@utils/constants'; + +export const mapFtToCurrencyType = (ft?: FungibleToken): CurrencyTypes => { + const principalToCurrencyTypeMap: Record = { + BTC: 'BTC', + STX: 'STX', + }; + + return ft ? principalToCurrencyTypeMap[ft.principal] ?? 'FT' : 'FT'; +}; + +export const mapTokenToCurrencyType = (t?: Token): CurrencyTypes => { + const protocolToCurrencyTypeMap: Record = { + btc: 'BTC', + stx: 'STX', + }; + + return t ? protocolToCurrencyTypeMap[t.protocol] ?? 'FT' : 'FT'; +}; export const mapFTProtocolToSwapProtocol = (ft: FungibleToken): Protocol => { if (ft.principal === 'BTC') return 'btc'; @@ -53,3 +72,31 @@ export const mapFTNativeSwapTokenToTokenBasic = (token: FungibleToken | Token): const safeTypeToken = token as Token; return { ticker: safeTypeToken.ticker, protocol: safeTypeToken.protocol }; }; + +export const mapFtToSwapToken = (st: FungibleToken): Token => { + if (st.principal === 'BTC') { + return { + ticker: 'BTC', + name: 'Bitcoin', + protocol: 'btc', + divisibility: 8, + }; + } + if (st.principal === 'STX') { + return { + ticker: 'STX', + name: 'Stacks', + protocol: 'stx', + divisibility: 6, + }; + } + + return { + ticker: st.principal ?? '', + name: st.name ?? st.assetName ?? '', + protocol: mapFTProtocolToSwapProtocol(st), + divisibility: st.decimals ?? 0, + logo: st.image ?? st.runeInscriptionId ?? '', + symbol: st.runeSymbol ?? '', + }; +}; diff --git a/src/app/screens/transactionRequest/index.tsx b/src/app/screens/transactionRequest/index.tsx index 6c265f56a..718149ea9 100644 --- a/src/app/screens/transactionRequest/index.tsx +++ b/src/app/screens/transactionRequest/index.tsx @@ -25,9 +25,9 @@ import RoutePaths from 'app/routes/paths'; import { useEffect, useState } from 'react'; import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import styled from 'styled-components'; -import useStxTransactionRequest from './useStxTransactionRequest'; +import useStxTransactionRequest, { type DataStxSignTransaction } from './useStxTransactionRequest'; const LoaderContainer = styled.div((props) => ({ display: 'flex', @@ -40,7 +40,11 @@ const LoaderContainer = styled.div((props) => ({ function TransactionRequest() { const selectedAccount = useSelectedAccount(); const { network, feeMultipliers, accountsList } = useWalletSelector(); - const txReq = useStxTransactionRequest(); + const location = useLocation(); + const { dataStxSignTransactionOverride } = (location.state || {}) as { + dataStxSignTransactionOverride?: DataStxSignTransaction; + }; + const txReq = useStxTransactionRequest(dataStxSignTransactionOverride); const navigate = useNavigate(); const selectedNetwork = useNetworkSelector(); const { switchAccount } = useWalletReducer(); @@ -84,11 +88,12 @@ function TransactionRequest() { transaction?.auth, ); setUnsignedTx(unsignedSendStxTx); + navigate(RoutePaths.ConfirmStacksTransaction, { state: { unsignedTx: buf2hex(unsignedSendStxTx.serialize()), sponsored: tokenTransferPayload.sponsored, - isBrowserTx: true, + isBrowserTx: !dataStxSignTransactionOverride, tabId, messageId, requestToken, diff --git a/src/app/screens/transactionRequest/useStxTransactionRequest/index.ts b/src/app/screens/transactionRequest/useStxTransactionRequest/index.ts index d0f430f47..081a3f897 100644 --- a/src/app/screens/transactionRequest/useStxTransactionRequest/index.ts +++ b/src/app/screens/transactionRequest/useStxTransactionRequest/index.ts @@ -1,5 +1,5 @@ import { parseData } from '@common/utils'; -import { getPopupPayload } from '@common/utils/popup'; +import { getPopupPayload, type Context } from '@common/utils/popup'; import { callContractParamsSchema } from '@common/utils/rpc/stx/callContract/paramsSchema'; import { deployContractParamsSchema } from '@common/utils/rpc/stx/deployContract/paramsSchema'; import useNetworkSelector from '@hooks/useNetwork'; @@ -18,7 +18,23 @@ import * as v from 'valibot'; import type { Return } from './types'; import { getPayload, isDeployContractPayload } from './utils'; -const useStxTransactionRequest = (): Return => { +export type DataStxSignTransaction = { + context: Context; + data: { + method: 'stx_signTransaction'; + params: { + transaction: string; + pubkey?: string | undefined; + broadcast?: boolean | undefined; + }; + id: string; + jsonrpc: '2.0'; + }; +} | null; + +const useStxTransactionRequest = ( + dataStxSignTransactionOverride?: DataStxSignTransaction, +): Return => { // Params const { search } = useLocation(); const params = new URLSearchParams(search); @@ -30,12 +46,18 @@ const useStxTransactionRequest = (): Return => { // Common to all WebBTC RPC methods const tabId = Number(params.get('tabId')) ?? 0; const messageId = params.get('messageId') ?? ''; - const rpcMethod = params.get('rpcMethod') ?? ''; + let rpcMethod = params.get('rpcMethod') ?? ''; - const [errorStxSignTransaction, dataStxSignTransaction] = getPopupPayload((data) => + const [errorStxSignTransaction, payloadDataStxSignTransaction] = getPopupPayload((data) => v.parse(stxSignTransactionRequestMessageSchema, data), ); - if (!errorStxSignTransaction) { + + const dataStxSignTransaction = dataStxSignTransactionOverride ?? payloadDataStxSignTransaction; + + if (dataStxSignTransaction) { + if (dataStxSignTransaction.data.method) { + rpcMethod = dataStxSignTransaction.data.method; + } const { transaction: transactionHex } = dataStxSignTransaction.data.params; const transaction = deserializeTransaction(transactionHex); From 149bf4ff74c6e02ee595ca22165cebc9801da5fa Mon Sep 17 00:00:00 2001 From: jordankzf Date: Thu, 22 Aug 2024 21:50:15 +0800 Subject: [PATCH 013/227] more improvements --- .../swap/components/routeItem/index.tsx | 17 +++-- .../tokenFromBottomSheet/useFromTokens.ts | 7 +- .../components/tokenToBottomSheet/index.tsx | 36 ++++++++-- .../tokenToBottomSheet/useToTokens.ts | 8 ++- src/app/screens/swap/index.tsx | 25 +++++-- src/app/screens/swap/quoteSummary/index.tsx | 68 +++++++++++++++---- src/app/screens/swap/quotesModal/index.tsx | 6 +- src/app/screens/swap/useMasterCoinsList.tsx | 2 + src/app/screens/swap/utils.ts | 10 +++ 9 files changed, 147 insertions(+), 32 deletions(-) diff --git a/src/app/screens/swap/components/routeItem/index.tsx b/src/app/screens/swap/components/routeItem/index.tsx index 9438489f7..793f10d77 100644 --- a/src/app/screens/swap/components/routeItem/index.tsx +++ b/src/app/screens/swap/components/routeItem/index.tsx @@ -1,6 +1,6 @@ import TokenImage from '@components/tokenImage'; import { CaretDown } from '@phosphor-icons/react'; -import { mapSwapTokenToFT } from '@screens/swap/utils'; +import { mapFtToCurrencyType, mapSwapTokenToFT, mapTokenToCurrencyType } from '@screens/swap/utils'; import type { FungibleToken, Token } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import { useTranslation } from 'react-i18next'; @@ -37,14 +37,15 @@ const TokenName = styled(StyledP)` type RouteItemProps = { label: string; - token?: FungibleToken | Token | 'BTC'; + token?: FungibleToken | Token; onClick: () => void; }; export default function RouteItem({ label, token, onClick }: RouteItemProps) { const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); - const isBtcToken = token === 'BTC' || token?.protocol === 'btc'; + const currency = + token && 'principal' in token ? mapFtToCurrencyType(token) : mapTokenToCurrencyType(token); return (
@@ -54,16 +55,20 @@ export default function RouteItem({ label, token, onClick }: RouteItemProps) { {token && ( )} - {isBtcToken ? 'BTC' : token?.name ?? t('SELECT_COIN')} + {currency !== 'FT' ? currency : token?.name ?? t('SELECT_COIN')} { }); const filteredTokens = toToken - ? sortedTokens.filter((token) => token.principal !== toToken.ticker) + ? sortedTokens.filter( + (token) => + token.principal !== toToken.ticker && + mapSwapProtocolToFTProtocol(toToken.protocol) === token.protocol, + ) : sortedTokens; return filteredTokens; diff --git a/src/app/screens/swap/components/tokenToBottomSheet/index.tsx b/src/app/screens/swap/components/tokenToBottomSheet/index.tsx index ed12b65f7..0c0f8ddda 100644 --- a/src/app/screens/swap/components/tokenToBottomSheet/index.tsx +++ b/src/app/screens/swap/components/tokenToBottomSheet/index.tsx @@ -1,7 +1,11 @@ import TokenTile from '@components/tokenTile'; import useDebounce from '@hooks/useDebounce'; import { MagnifyingGlass } from '@phosphor-icons/react'; -import { mapFTProtocolToSwapProtocol, mapSwapTokenToFT } from '@screens/swap/utils'; +import { + mapFTMotherProtocolToSwapProtocol, + mapFTProtocolToSwapProtocol, + mapSwapTokenToFT, +} from '@screens/swap/utils'; import { AnalyticsEvents, type FungibleToken, @@ -14,7 +18,7 @@ import Input from '@ui-library/input'; import Sheet from '@ui-library/sheet'; import Spinner from '@ui-library/spinner'; import { trackMixPanel } from '@utils/mixpanel'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import Theme from 'theme'; @@ -95,8 +99,15 @@ export default function TokenToBottomSheet({ resetFrom, }: Props) { const [query, setQuery] = useState(''); + const [selectedProtocol, setSelectedProtocol] = useState(supportedProtocols[0]); + useEffect(() => { + if (from) { + setSelectedProtocol(mapFTMotherProtocolToSwapProtocol(from)); + } + }, [from]); + const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); const search = useDebounce(query, 500); const fromTokenBasic: TokenBasic | undefined = from @@ -111,7 +122,7 @@ export default function TokenToBottomSheet({ if (selectedProtocol === protocol) { return; } - resetFrom(); + // resetFrom(); setQuery(''); setSelectedProtocol(protocol); }; @@ -156,7 +167,7 @@ export default function TokenToBottomSheet({ )} {!!(data && !isLoading) && data.map((token) => { - if (token.protocol === 'btc') { + if (token.protocol === 'btc' && selectedProtocol === 'runes') { return ( ); } + if (token.protocol === 'stx' && selectedProtocol === 'sip10') { + return ( + { + onSelectCoin(token); + trackMixPanel(AnalyticsEvents.SelectTokenToSwapTo, { + selectedToken: 'Stacks', + }); + handleClose(); + }} + hideBalance + /> + ); + } if (token.protocol === 'runes' || token.protocol === 'sip10') { return ( { userTokens, }); - const sortedResponse = response.items.sort((a) => (a.protocol === 'btc' ? -1 : 1)); + const sortedResponse = response.items.sort((a, b) => { + if (a.protocol === 'btc') return -1; + if (b.protocol === 'btc') return 1; + if (a.protocol === 'stx') return -1; + if (b.protocol === 'stx') return 1; + return 0; + }); const filteredResponse = from ? sortedResponse.filter((s) => s.ticker !== from.ticker) diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index 4fdcb01a9..9bce1af86 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -3,9 +3,11 @@ import RequestsRoutes from '@common/utils/route-urls'; import BottomBar from '@components/tabBar'; import TopRow from '@components/topRow'; import useRuneFloorPriceQuery from '@hooks/queries/runes/useRuneFloorPriceQuery'; +import { useGetSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens'; import useGetQuotes from '@hooks/queries/swaps/useGetQuotes'; import useBtcWalletData from '@hooks/queries/useBtcWalletData'; import useCoinRates from '@hooks/queries/useCoinRates'; +import useStxWalletData from '@hooks/queries/useStxWalletData'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import type { DataStxSignTransaction } from '@screens/transactionRequest/useStxTransactionRequest'; @@ -13,6 +15,7 @@ import { AnalyticsEvents, btcToSats, getBtcFiatEquivalent, + microstacksToStx, type FungibleToken, type GetUtxosRequest, type MarketUtxo, @@ -23,7 +26,7 @@ import { import Button, { LinkButton } from '@ui-library/button'; import { StyledP } from '@ui-library/common.styled'; import SnackBar from '@ui-library/snackBar'; -import { satsToBtcString } from '@utils/helper'; +import { formatNumber, satsToBtcString } from '@utils/helper'; import { trackMixPanel } from '@utils/mixpanel'; import { getFtBalance } from '@utils/tokens'; import BigNumber from 'bignumber.js'; @@ -103,6 +106,7 @@ export default function SwapScreen() { const [quote, setQuote] = useState(); const [selectedUtxoProvider, setSelectedUtxoProvider] = useState(); const [errorMessage, setErrorMessage] = useState(''); + const { data: sip10CoinsList } = useGetSip10FungibleTokens(); const [getQuotesModalVisible, setGetQuotesModalVisible] = useState(false); const [tokenSelectionBottomSheet, setTokenSelectionBottomSheet] = useState<'from' | 'to' | null>( @@ -123,6 +127,8 @@ export default function SwapScreen() { const { stxPublicKey } = useSelectedAccount(); const { data: btcBalance } = useBtcWalletData(); + const { data: stxData } = useStxWalletData(); + const { btcFiatRate, btcUsdRate } = useCoinRates(); const navigate = useNavigate(); const { t } = useTranslation('translation'); @@ -196,6 +202,8 @@ export default function SwapScreen() { switch (ftProtocol) { case 'runes': return coinsMasterList.find((coin) => coin.principal === ticker); + case 'stacks': + return coinsMasterList.find((coin) => coin.principal === ticker); default: return undefined; } @@ -264,7 +272,11 @@ export default function SwapScreen() { return satsToBtcString(BigNumber(btcBalance ?? 0)); } - if (fromToken.protocol === 'runes') { + if (fromToken.principal === 'STX') { + return formatNumber(microstacksToStx(BigNumber(stxData?.availableBalance ?? 0)).toString()); + } + + if (fromToken.protocol === 'runes' || fromToken.protocol === 'stacks') { return getFtBalance(fromToken); } }; @@ -278,7 +290,7 @@ export default function SwapScreen() { return getBtcFiatEquivalent(amountInSats, new BigNumber(btcFiatRate)).toFixed(2); } - if (fromToken?.protocol !== 'runes' || !fromToken?.tokenFiatRate || !fromToken.decimals) { + if (!fromToken?.tokenFiatRate || !fromToken.decimals) { return '0.00'; } @@ -309,7 +321,10 @@ export default function SwapScreen() { const isGetQuotesDisabled = ifFormInValid || quotesLoading || Boolean(quotesError); const isMaxDisabled = - !fromToken || fromToken.principal === 'BTC' || BigNumber(amount).eq(getFtBalance(fromToken)); + !fromToken || + fromToken.principal === 'BTC' || + fromToken.principal === 'STX' || + BigNumber(amount).eq(getFtBalance(fromToken)); const isRunesToBtcRoute = fromToken?.principal !== 'BTC' && fromToken?.protocol === 'runes' && @@ -527,7 +542,7 @@ export default function SwapScreen() { unit: fromToken?.principal === 'BTC' ? 'BTC' : fromToken?.runeSymbol ?? '', }} max={ - fromToken?.principal === 'BTC' + fromToken?.principal === 'BTC' || fromToken?.principal === 'STX' ? undefined : { isDisabled: isMaxDisabled, onClick: onClickMax } } diff --git a/src/app/screens/swap/quoteSummary/index.tsx b/src/app/screens/swap/quoteSummary/index.tsx index e8dc85047..59e531f27 100644 --- a/src/app/screens/swap/quoteSummary/index.tsx +++ b/src/app/screens/swap/quoteSummary/index.tsx @@ -1,6 +1,7 @@ import SlippageEditIcon from '@assets/img/swap/slippageEdit.svg'; import TopRow from '@components/topRow'; import useRuneFloorPriceQuery from '@hooks/queries/runes/useRuneFloorPriceQuery'; +import { useGetSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens'; import useCoinRates from '@hooks/queries/useCoinRates'; import useBtcFeeRate from '@hooks/useBtcFeeRate'; import useSearchParamsState from '@hooks/useSearchParamsState'; @@ -11,6 +12,8 @@ import { AnalyticsEvents, RUNE_DISPLAY_DEFAULTS, getBtcFiatEquivalent, + getStxFiatEquivalent, + stxToMicrostacks, type FungibleToken, type MarketUtxo, type PlaceUtxoOrderRequest, @@ -167,8 +170,9 @@ export default function QuoteSummary({ selectedIdentifiers, }: QuoteSummaryProps) { const { t } = useTranslation('translation'); + const { data: sip10CoinsList } = useGetSip10FungibleTokens(); const theme = useTheme(); - const { btcFiatRate, btcUsdRate } = useCoinRates(); + const { btcFiatRate, btcUsdRate, stxBtcRate } = useCoinRates(); const { btcAddress, ordinalsAddress, btcPublicKey, ordinalsPublicKey, stxAddress, stxPublicKey } = useSelectedAccount(); const { @@ -201,15 +205,35 @@ export default function QuoteSummary({ const { fiatCurrency } = useWalletSelector(); - const fromUnit = - fromToken?.principal === 'BTC' - ? 'Sats' - : (fromToken as FungibleToken)?.runeSymbol ?? - (fromToken as FungibleToken).assetName ?? - RUNE_DISPLAY_DEFAULTS.symbol; + const fromUnit = (() => { + if (fromToken?.principal === 'BTC') { + return 'Sats'; + } + if (fromToken?.principal === 'STX') { + return 'STX'; + } + if (fromToken?.protocol === 'runes') { + return fromToken?.runeSymbol ?? RUNE_DISPLAY_DEFAULTS.symbol; + } - const toUnit = - toToken?.protocol === 'btc' ? 'Sats' : toToken?.symbol ?? RUNE_DISPLAY_DEFAULTS.symbol; + return fromToken?.ticker; + })(); + + const toUnit = (() => { + if (toToken?.protocol === 'btc') { + return 'Sats'; + } + if (toToken?.symbol) { + return toToken.symbol; + } + if (toToken?.protocol === 'runes') { + return RUNE_DISPLAY_DEFAULTS.symbol; + } + if (toToken?.ticker === 'STX') { + return 'STX'; + } + return toToken?.name; + })(); const isRunesSwap = fromToken?.protocol === 'runes' || toToken?.protocol === 'runes'; const isSip10Swap = fromToken?.protocol === 'stacks' || toToken?.protocol === 'sip10'; @@ -301,6 +325,12 @@ export default function QuoteSummary({ const fromTokenFiatValue = fromToken?.principal === 'BTC' ? getBtcFiatEquivalent(new BigNumber(amount), new BigNumber(btcFiatRate)).toFixed(2) + : fromToken?.principal === 'STX' + ? getStxFiatEquivalent( + stxToMicrostacks(new BigNumber(amount)), + new BigNumber(stxBtcRate), + new BigNumber(btcFiatRate), + ).toFixed(2) : new BigNumber(fromToken?.tokenFiatRate ?? 0).multipliedBy(amount).toFixed(2); const toTokenFiatValue = @@ -309,10 +339,22 @@ export default function QuoteSummary({ new BigNumber(quote.receiveAmount), new BigNumber(btcFiatRate), ).toFixed(2) - : getBtcFiatEquivalent( + : toToken?.protocol === 'runes' + ? getBtcFiatEquivalent( new BigNumber(runeFloorPrice ?? 0).multipliedBy(quote.receiveAmount), new BigNumber(btcFiatRate), - ).toFixed(2); + ).toFixed(2) + : toToken?.protocol === 'stx' + ? getStxFiatEquivalent( + stxToMicrostacks(new BigNumber(quote.receiveAmount)), + new BigNumber(stxBtcRate), + new BigNumber(btcFiatRate), + ).toFixed(2) + : new BigNumber( + sip10CoinsList?.find((s) => s.principal === toToken?.ticker)?.tokenFiatRate ?? 0, + ) + .multipliedBy(quote.receiveAmount) + .toFixed(2); const showBadQuoteWarning = quote.slippageSupported && @@ -370,7 +412,7 @@ export default function QuoteSummary({ }} subtitle={fromToken?.principal === 'BTC' ? 'Bitcoin' : fromToken?.assetName} subtitleColor="white_400" - unit={fromToken?.principal === 'BTC' ? 'Sats' : fromToken?.runeSymbol ?? ''} + unit={fromUnit} fiatValue={fromTokenFiatValue} /> @@ -397,7 +439,7 @@ export default function QuoteSummary({ }} subtitle={toToken?.protocol === 'btc' ? 'Bitcoin' : toToken?.name} subtitleColor="white_400" - unit={toToken?.protocol === 'btc' ? 'Sats' : toToken?.symbol} + unit={toUnit} fiatValue={toTokenFiatValue} /> diff --git a/src/app/screens/swap/quotesModal/index.tsx b/src/app/screens/swap/quotesModal/index.tsx index bc3c57bf3..c358d1521 100644 --- a/src/app/screens/swap/quotesModal/index.tsx +++ b/src/app/screens/swap/quotesModal/index.tsx @@ -1,7 +1,9 @@ +import { microStxToStx } from '@components/postCondition/postConditionView/helper'; import useCoinRates from '@hooks/queries/useCoinRates'; import { getBtcFiatEquivalent, getStxFiatEquivalent, + stxToMicrostacks, type FungibleToken, type Quote, type StxQuote, @@ -147,11 +149,11 @@ function QuotesModal({ onClick={() => ammProviderClicked && ammProviderClicked(stx)} subtitle={t('RECOMMENDED')} subtitleColor="success_light" - unit={stx.to.protocol === 'stx' ? 'STX' : toToken?.symbol || ''} + unit={stx.to.protocol === 'stx' ? 'STX' : toToken?.name || ''} fiatValue={ stx.to.protocol === 'stx' ? getStxFiatEquivalent( - new BigNumber(stx.receiveAmount), + stxToMicrostacks(new BigNumber(stx.receiveAmount)), new BigNumber(stxBtcRate), new BigNumber(btcFiatRate), ).toFixed(2) diff --git a/src/app/screens/swap/useMasterCoinsList.tsx b/src/app/screens/swap/useMasterCoinsList.tsx index 62fec126f..4081dafa5 100644 --- a/src/app/screens/swap/useMasterCoinsList.tsx +++ b/src/app/screens/swap/useMasterCoinsList.tsx @@ -15,6 +15,7 @@ export const btcFt: FungibleToken = { principal: 'BTC', assetName: 'Bitcoin', image: IconBitcoin, + protocol: 'runes', }; export const stxFt: FungibleToken = { @@ -25,6 +26,7 @@ export const stxFt: FungibleToken = { principal: 'STX', assetName: 'Stacks', image: IconStacks, + protocol: 'stacks', }; const useMasterCoinsList = () => { diff --git a/src/app/screens/swap/utils.ts b/src/app/screens/swap/utils.ts index ff60f6aa1..1bb74ba29 100644 --- a/src/app/screens/swap/utils.ts +++ b/src/app/screens/swap/utils.ts @@ -40,8 +40,18 @@ export const mapFTProtocolToSwapProtocol = (ft: FungibleToken): Protocol => { return protocolMap[ft.protocol]; }; +export const mapFTMotherProtocolToSwapProtocol = (ft: FungibleToken): Protocol => { + if (ft.principal === 'BTC') return 'runes'; + if (ft.principal === 'STX') return 'sip10'; + if (ft.protocol === 'stacks') return 'sip10'; + if (ft.protocol === 'runes') return 'runes'; + return 'runes'; +}; + export const mapSwapProtocolToFTProtocol = (protocol: Protocol): FungibleTokenProtocol => { const protocolMap: Partial> = { + stx: 'stacks', + btc: 'runes', sip10: 'stacks', runes: 'runes', brc20: 'brc-20', From 799d737029cdc4e77ca781d6251421b9c9fda799 Mon Sep 17 00:00:00 2001 From: jordankzf Date: Thu, 22 Aug 2024 22:55:02 +0800 Subject: [PATCH 014/227] remove old stacks swaps and add default slippage --- .../ContractCallRequest.tsx | 2 +- src/app/routes/index.tsx | 5 - src/app/screens/swap/quoteSummary/index.tsx | 12 +- src/app/screens/swap/slippageModal/index.tsx | 4 +- src/app/screens/swap/swap-stacks.tsx | 146 --------- src/app/screens/swap/swapInfoBlock/index.tsx | 181 ---------- src/app/screens/swap/useSwap.test.ts | 72 ---- src/app/screens/swap/useSwap.tsx | 310 ------------------ 8 files changed, 14 insertions(+), 718 deletions(-) delete mode 100644 src/app/screens/swap/swap-stacks.tsx delete mode 100644 src/app/screens/swap/swapInfoBlock/index.tsx delete mode 100644 src/app/screens/swap/useSwap.test.ts delete mode 100644 src/app/screens/swap/useSwap.tsx diff --git a/src/app/components/transactionsRequests/ContractCallRequest.tsx b/src/app/components/transactionsRequests/ContractCallRequest.tsx index 020ca66bd..8d06e861e 100644 --- a/src/app/components/transactionsRequests/ContractCallRequest.tsx +++ b/src/app/components/transactionsRequests/ContractCallRequest.tsx @@ -387,7 +387,7 @@ export default function ContractCallRequest({ onCancelClick={cancelCallback} loading={isLoading} title={request.functionName} - subTitle={`Requested by ${request.appDetails?.name}`} + subTitle={request.appDetails?.name ? `Requested by ${request.appDetails.name}` : undefined} hasSignatures={hasSignatures} fee={fee ? microstacksToStx(fee).toString() : undefined} setFeeRate={(feeRate: string) => { diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index 34f38c6c2..ab5c39a67 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -76,7 +76,6 @@ import SignatureRequest from '@screens/signatureRequest'; import SpeedUpTransactionScreen from '@screens/speedUpTransaction'; import Stacking from '@screens/stacking'; import SwapScreen from '@screens/swap'; -import SwapStacksScreen from '@screens/swap/swap-stacks'; import SwapStacksConfirmation from '@screens/swap/swapStacksConfirmation'; import TransactionRequest from '@screens/transactionRequest'; import TransactionStatus from '@screens/transactionStatus'; @@ -154,10 +153,6 @@ const router = createHashRouter([ path: 'swap', element: , }, - { - path: 'swap-stacks', - element: , - }, { path: 'swap-stacks-confirm', element: , diff --git a/src/app/screens/swap/quoteSummary/index.tsx b/src/app/screens/swap/quoteSummary/index.tsx index ace13ad96..bc0960abd 100644 --- a/src/app/screens/swap/quoteSummary/index.tsx +++ b/src/app/screens/swap/quoteSummary/index.tsx @@ -239,7 +239,8 @@ export default function QuoteSummary({ const isSip10Swap = fromToken?.protocol === 'stacks' || toToken?.protocol === 'sip10'; const [showSlippageModal, setShowSlippageModal] = useState(false); - const [slippage, setSlippage] = useSearchParamsState('slippage', 0.05); + const DEFAULT_SLIPPAGE = quote.provider.code === 'velar' ? 0.04 : 0.05; + const [slippage, setSlippage] = useSearchParamsState('slippage', DEFAULT_SLIPPAGE); const handleSwap = async () => { if (!fromToken || !toToken) { @@ -417,7 +418,13 @@ export default function QuoteSummary({ ? undefined : fromToken, }} - subtitle={fromToken?.principal === 'BTC' ? 'Bitcoin' : fromToken?.assetName} + subtitle={ + fromToken?.principal === 'BTC' + ? 'Bitcoin' + : fromToken?.assetName !== '' + ? fromToken?.assetName + : fromToken?.name + } subtitleColor="white_400" unit={fromUnit} fiatValue={fromTokenFiatValue} @@ -554,6 +561,7 @@ export default function QuoteSummary({ onClose={() => setShowSlippageModal(false)} > ({ color: props.theme.colors.white_400, })); -const DEFAULT_SLIPPAGE = '5'; export function SlippageModalContent({ + defaultSlippage, slippage, slippageThreshold, slippageDecimals, onChange, }: { + defaultSlippage: number; slippage: number; slippageThreshold?: number; slippageDecimals?: number; @@ -53,6 +54,7 @@ export function SlippageModalContent({ const result = Number(percentage); const invalid = !Number.isNaN(result) && result >= 100; const showSlippageWarning = slippageThreshold ? result > slippageThreshold * 100 : false; + const DEFAULT_SLIPPAGE = (defaultSlippage * 100).toString(); const allowDecimalsNumber = slippageDecimals ?? 2; diff --git a/src/app/screens/swap/swap-stacks.tsx b/src/app/screens/swap/swap-stacks.tsx deleted file mode 100644 index ba9bc0a2f..000000000 --- a/src/app/screens/swap/swap-stacks.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import BottomBar from '@components/tabBar'; -import TopRow from '@components/topRow'; -import { ArrowDown } from '@phosphor-icons/react'; -import CoinSelectModal from '@screens/home/coinSelectModal'; -import { SwapInfoBlock } from '@screens/swap/swapInfoBlock'; -import SwapTokenBlock from '@screens/swap/swapTokenBlock'; -import { useSwap } from '@screens/swap/useSwap'; -import Button from '@ui-library/button'; -import { isFungibleToken } from '@utils/helper'; -import { useCallback, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useLocation, useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; - -const ScrollContainer = styled.div` - display: flex; - flex: 1; - flex-direction: column; - row-gap: 16px; - overflow-y: auto; - &::-webkit-scrollbar { - display: none; - } - margin-left: 5%; - margin-right: 5%; - padding-bottom: 16px; -`; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - rowGap: props.theme.spacing(8), - marginTop: props.theme.spacing(16), -})); - -const DownArrowButton = styled.button((props) => ({ - alignSelf: 'center', - borderRadius: props.theme.radius(2), - width: props.theme.spacing(18), - height: props.theme.spacing(18), - background: props.theme.colors.elevation3, - transition: 'all 0.2s ease', - ':hover': { - opacity: 0.8, - }, -})); - -const SendButtonContainer = styled.div((props) => ({ - paddingBottom: props.theme.spacing(12), - paddingTop: props.theme.spacing(4), - marginLeft: '5%', - marginRight: '5%', -})); - -function SwapStacksScreen() { - const navigate = useNavigate(); - const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); - const swap = useSwap(); - const location = useLocation(); - const params = new URLSearchParams(location.search); - const defaultFrom = params.get('from'); - - const [selecting, setSelecting] = useState<'from' | 'to'>(); - const [loading, setLoading] = useState(false); - - useEffect(() => { - if ( - !swap.selectedFromToken && - !swap.selectedToToken && - defaultFrom && - (defaultFrom === 'STX' || isFungibleToken(defaultFrom)) - ) { - swap.onSelectToken(defaultFrom, 'from'); - } - }, [defaultFrom, swap]); - - const handleGoBack = () => { - navigate(-1); - }; - - const handleClickContinue = useCallback(async () => { - if (swap.submitError || !swap.onSwap) { - return; - } - try { - setLoading(true); - await swap.onSwap(); - } finally { - setLoading(false); - } - }, [swap, setLoading]); - - return ( - <> - - - - setSelecting('from')} - /> - - - - setSelecting('to')} - /> - - - - {selecting != null && ( - { - swap.onSelectToken('STX', selecting); - }} - onClose={() => setSelecting(undefined)} - onSelectCoin={(coin) => { - swap.onSelectToken(coin, selecting); - }} - visible={!!selecting} - coins={swap.coinsList} - title={selecting === 'from' ? t('ASSET_TO_CONVERT_FROM') : t('ASSET_TO_CONVERT_TO')} - loadingWalletData={swap.isLoadingWalletData} - /> - )} - - - { - setShowModal(false); - setShowFeeSettings(false); - }} - onApplyClick={onApplyClick} - showFeeSettings={showFeeSettings} - setShowFeeSettings={setShowFeeSettings} - /> - - ); -} diff --git a/src/app/screens/swap/swapStacksConfirmation/feesBlock/index.tsx b/src/app/screens/swap/swapStacksConfirmation/feesBlock/index.tsx deleted file mode 100644 index a424bbf01..000000000 --- a/src/app/screens/swap/swapStacksConfirmation/feesBlock/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import useWalletSelector from '@hooks/useWalletSelector'; -import { Container, TitleText } from '@screens/swap/swapStacksConfirmation/stxInfoBlock'; -import { EstimateUSDText } from '@screens/swap/swapTokenBlock'; -import { currencySymbolMap } from '@secretkeylabs/xverse-core'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; - -const RowContainer = styled.div({ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', -}); - -const FeeText = styled.p((props) => ({ - ...props.theme.body_m, - fontSize: 14, - fontWeight: 500, -})); - -interface FeeTextProps { - txFee: number; - txFeeFiatAmount?: number; -} - -export default function FeesBlock({ txFee, txFeeFiatAmount }: FeeTextProps) { - const { t } = useTranslation('translation', { keyPrefix: 'SWAP_CONFIRM_SCREEN' }); - const { fiatCurrency } = useWalletSelector(); - return ( - - - {t('FEES')} - {`${txFee.toFixed(6)} STX`} - - {` ~ ${currencySymbolMap[fiatCurrency]} ${txFeeFiatAmount} ${fiatCurrency}`} - - ); -} diff --git a/src/app/screens/swap/swapStacksConfirmation/functionBlock/index.tsx b/src/app/screens/swap/swapStacksConfirmation/functionBlock/index.tsx deleted file mode 100644 index 48f9e441a..000000000 --- a/src/app/screens/swap/swapStacksConfirmation/functionBlock/index.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { - Container, - TitleContainer, - TitleText, -} from '@screens/swap/swapStacksConfirmation/stxInfoBlock'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; - -const FunctionName = styled.div((props) => ({ - ...props.theme.body_medium_m, - marginLeft: 10, - color: props.theme.colors.white_0, - textAlign: 'right', -})); - -interface FunctionBlockProps { - name: string; -} - -export default function FunctionBlock({ name }: FunctionBlockProps) { - const { t } = useTranslation('translation', { keyPrefix: 'SWAP_CONFIRM_SCREEN' }); - return ( - - - {t('FUNCTION')} - {name} - - - ); -} diff --git a/src/app/screens/swap/swapStacksConfirmation/index.tsx b/src/app/screens/swap/swapStacksConfirmation/index.tsx deleted file mode 100644 index bf3a003f1..000000000 --- a/src/app/screens/swap/swapStacksConfirmation/index.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import SponsoredTransactionIcon from '@assets/img/transactions/CircleWavyCheck.svg'; -import InfoContainer from '@components/infoContainer'; -import BottomBar from '@components/tabBar'; -import TopRow from '@components/topRow'; -import { Container } from '@screens/home/index.styled'; -import { AdvanceSettings } from '@screens/swap/swapStacksConfirmation/advanceSettings'; -import FeesBlock from '@screens/swap/swapStacksConfirmation/feesBlock'; -import FunctionBlock from '@screens/swap/swapStacksConfirmation/functionBlock'; -import RouteBlock from '@screens/swap/swapStacksConfirmation/routeBlock'; -import StxInfoBlock from '@screens/swap/swapStacksConfirmation/stxInfoBlock'; -import { useConfirmSwap } from '@screens/swap/swapStacksConfirmation/useConfirmSwap'; -import Button from '@ui-library/button'; -import { SUPPORT_URL_TAB_TARGET, SWAP_SPONSOR_DISABLED_SUPPORT_URL } from '@utils/constants'; -import { useCallback, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useLocation, useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; - -const TitleText = styled.div((props) => ({ - fontSize: 21, - fontWeight: 700, - color: props.theme.colors.white_0, - marginBottom: props.theme.spacing(12), - marginTop: props.theme.spacing(12), -})); - -const ButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - columnGap: props.theme.space.s, - marginTop: props.theme.space.m, - position: 'sticky', - bottom: 0, - background: props.theme.colors.elevation0, - padding: `${props.theme.space.s} 0`, -})); - -const SponsoredTransactionText = styled.div((props) => ({ - ...props.theme.typography.body_m, - color: props.theme.colors.white_200, - marginTop: props.theme.spacing(10), - display: 'flex', - gap: props.theme.spacing(4), -})); - -const Icon = styled.img((props) => ({ - marginTop: props.theme.spacing(1), - width: 24, - height: 24, -})); - -const StyledInfoContainer = styled.div((props) => ({ - marginBottom: props.theme.spacing(4), -})); - -export default function SwapStacksConfirmation() { - const { t } = useTranslation('translation', { keyPrefix: 'SWAP_CONFIRM_SCREEN' }); - const location = useLocation(); - const navigate = useNavigate(); - const swap = useConfirmSwap(location.state); - - const handleGoBack = () => { - navigate(-1); - }; - - const [confirming, setConfirming] = useState(false); - const onConfirm = useCallback(() => { - setConfirming(true); - swap.onConfirm().finally(() => { - setConfirming(false); - }); - }, [swap]); - - const handleClickLearnMore = () => { - window.open(SWAP_SPONSOR_DISABLED_SUPPORT_URL, SUPPORT_URL_TAB_TARGET, 'noreferrer noopener'); - }; - - return ( - <> - navigate(-1)} /> - - {t('TOKEN_SWAP')} - {swap.isSponsorDisabled && ( - - - - )} - - - - - {!swap.isSponsored && ( - - )} - {swap.isSponsored ? ( - - - {t('THIS_IS_A_SPONSORED_TRANSACTION')} - - ) : ( - - )} - -
{!hideListActions ? ( - } - onPress={onCreateAccount} - text={t('NEW_ACCOUNT')} - transparent + onClick={onCreateAccount} + title={t('NEW_ACCOUNT')} + variant="secondary" /> - } - onPress={onImportLedgerAccount} - text={t('LEDGER_ACCOUNT')} - transparent + onClick={onImportLedgerAccount} + title={t('LEDGER_ACCOUNT')} + variant="secondary" /> ) : null} From 7106a1b88200089c00c1e7aa7306e57d5ee21932 Mon Sep 17 00:00:00 2001 From: Den <36603049+dhriaznov@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:35:18 +0200 Subject: [PATCH 043/227] [ENG-4722] fix: Naming disappeared after clicking on the account (#510) --- src/app/hooks/useWalletReducer.ts | 37 ++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/app/hooks/useWalletReducer.ts b/src/app/hooks/useWalletReducer.ts index 2fcb62330..3afd0e8b9 100644 --- a/src/app/hooks/useWalletReducer.ts +++ b/src/app/hooks/useWalletReducer.ts @@ -181,12 +181,14 @@ const useWalletReducer = () => { // Load custom account names for the new network const savedCustomAccountNames = savedNames[currentNetwork.type]; - walletAccounts.forEach((account) => { - const savedAccount = savedCustomAccountNames?.find((acc) => acc.id === account.id); - if (savedAccount) { - account.accountName = savedAccount.name; - } - }); + if (savedCustomAccountNames?.length) { + walletAccounts.forEach((account) => { + const savedAccount = savedCustomAccountNames.find((acc) => acc.id === account.id); + if (savedAccount) { + account.accountName = savedAccount.name; + } + }); + } // ledger accounts initially didn't have a deviceAccountIndex // this is a migration to add the deviceAccountIndex to the ledger accounts without them @@ -221,6 +223,19 @@ const useWalletReducer = () => { }); }; + const loadAccountNames = () => { + const updatedSavedNames = softwareAccountsList.reduce<{ id: number; name: string }[]>( + (acc, account) => { + if (account.accountName) { + acc.push({ id: account.id, name: account.accountName }); + } + return acc; + }, + [], + ); + dispatch(updateSavedNamesAction(network.type, updatedSavedNames)); + }; + const loadWallet = async () => { const seedPhrase = await seedVault.getSeed(); const currentAccounts = softwareAccountsList || []; @@ -244,6 +259,16 @@ const useWalletReducer = () => { await ensureSelectedAccountValid(); + if ( + !savedNames[network.type]?.length && + softwareAccountsList.some((account) => !!account.accountName) + ) { + // there was no savedNames store object initially + // this is a migration to add the savedNames if there are custom account names that are not saved + // it should only fire once if ever + loadAccountNames(); + } + dispatch(setWalletUnlockedAction(true)); setSessionStartTimeAndMigrate(); }; From 94419635fdb464fbbf1d8ce739e094234edd4b1c Mon Sep 17 00:00:00 2001 From: jordankzf Date: Thu, 29 Aug 2024 17:31:02 +0800 Subject: [PATCH 044/227] Remove vite --- .github/workflows/build.yml | 1 - package-lock.json | 2086 +---------------------------------- package.json | 4 - vitest.config.js | 7 - 4 files changed, 31 insertions(+), 2067 deletions(-) delete mode 100644 vitest.config.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a706c7343..2e265923a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,6 @@ jobs: npm run knip npx eslint . npx tsc --noEmit - npm test - name: Build env: TRANSAC_API_KEY: ${{ secrets.TRANSAC_API_KEY }} diff --git a/package-lock.json b/package-lock.json index 8b0517cbc..a234c6208 100644 --- a/package-lock.json +++ b/package-lock.json @@ -124,9 +124,6 @@ "type-fest": "^2.19.0", "typescript": "^5.5.4", "typescript-plugin-styled-components": "^3.0.0", - "vite": "5.3.2", - "vite-tsconfig-paths": "4.3.2", - "vitest": "0.34.6", "webpack": "^5.89.0", "webpack-dev-server": "^4.11.0" }, @@ -659,374 +656,6 @@ "vlq": "^0.2.1" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -1165,18 +794,6 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -1543,214 +1160,6 @@ "node": ">=14" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", - "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", - "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", - "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", - "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", - "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", - "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", - "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", - "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", - "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", - "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", - "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", - "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", - "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", - "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", - "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", - "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@sats-connect/core": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.2.1.tgz", @@ -1985,12 +1394,6 @@ "randombytes": "^2.0.1" } }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, "node_modules/@snyk/github-codeowners": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@snyk/github-codeowners/-/github-codeowners-1.1.0.tgz", @@ -2446,21 +1849,6 @@ "@types/node": "*" } }, - "node_modules/@types/chai": { - "version": "4.3.11", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", - "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==", - "dev": true - }, - "node_modules/@types/chai-subset": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.5.tgz", - "integrity": "sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==", - "dev": true, - "dependencies": { - "@types/chai": "*" - } - }, "node_modules/@types/chrome": { "version": "0.0.237", "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.237.tgz", @@ -3102,162 +2490,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@vitest/expect": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", - "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", - "dev": true, - "dependencies": { - "@vitest/spy": "0.34.6", - "@vitest/utils": "0.34.6", - "chai": "^4.3.10" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", - "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", - "dev": true, - "dependencies": { - "@vitest/utils": "0.34.6", - "p-limit": "^4.0.0", - "pathe": "^1.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/snapshot": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", - "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", - "dev": true, - "dependencies": { - "magic-string": "^0.30.1", - "pathe": "^1.1.1", - "pretty-format": "^29.5.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/snapshot/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/spy": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", - "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", - "dev": true, - "dependencies": { - "tinyspy": "^2.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", - "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", - "dev": true, - "dependencies": { - "diff-sequences": "^29.4.3", - "loupe": "^2.3.6", - "pretty-format": "^29.5.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/utils/node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/utils/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -3536,15 +2768,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", - "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -3915,15 +3138,6 @@ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -4589,15 +3803,6 @@ "node": ">=8" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -4662,24 +3867,6 @@ } ] }, - "node_modules/chai": { - "version": "4.3.10", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", - "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", - "dev": true, - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -4693,18 +3880,6 @@ "node": ">=4" } }, - "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "node": "*" - } - }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -5481,18 +4656,6 @@ } } }, - "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", - "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/deep-equal": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", @@ -6191,44 +5354,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -7505,15 +6630,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -7640,12 +6756,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true - }, "node_modules/goober": { "version": "2.1.13", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.13.tgz", @@ -8868,12 +7978,6 @@ "node": ">=6" } }, - "node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -9421,18 +8525,6 @@ "node": ">=8.9.0" } }, - "node_modules/local-pkg": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", - "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -9624,15 +8716,6 @@ "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.12.2.tgz", "integrity": "sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==" }, - "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.1" - } - }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -9651,18 +8734,6 @@ "yallist": "^3.0.2" } }, - "node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/map-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", @@ -9855,18 +8926,6 @@ "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.47.0.tgz", "integrity": "sha512-Ldrva0fRBEIFWmEibBQO1PulfpJVF3pf28Guk09lDirDaSQqqU/xs9zQLwN2rL5VwVtsP1aD3JaCgaa98EjojQ==" }, - "node_modules/mlly": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", - "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", - "dev": true, - "dependencies": { - "acorn": "^8.10.0", - "pathe": "^1.1.1", - "pkg-types": "^1.0.3", - "ufo": "^1.3.0" - } - }, "node_modules/more-entropy": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/more-entropy/-/more-entropy-0.0.7.tgz", @@ -10536,21 +9595,6 @@ "inherits": "2.0.3" } }, - "node_modules/pathe": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", - "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", - "dev": true - }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/pbkdf2": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", @@ -10618,21 +9662,10 @@ "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "dev": true, "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pkg-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", - "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", - "dev": true, - "dependencies": { - "jsonc-parser": "^3.2.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0" + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/playwright": { @@ -11211,7 +10244,8 @@ "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "peer": true }, "node_modules/react-is-visible": { "version": "1.2.0", @@ -11659,41 +10693,6 @@ "node": ">=8" } }, - "node_modules/rollup": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", - "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.18.0", - "@rollup/rollup-android-arm64": "4.18.0", - "@rollup/rollup-darwin-arm64": "4.18.0", - "@rollup/rollup-darwin-x64": "4.18.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", - "@rollup/rollup-linux-arm-musleabihf": "4.18.0", - "@rollup/rollup-linux-arm64-gnu": "4.18.0", - "@rollup/rollup-linux-arm64-musl": "4.18.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", - "@rollup/rollup-linux-riscv64-gnu": "4.18.0", - "@rollup/rollup-linux-s390x-gnu": "4.18.0", - "@rollup/rollup-linux-x64-gnu": "4.18.0", - "@rollup/rollup-linux-x64-musl": "4.18.0", - "@rollup/rollup-win32-arm64-msvc": "4.18.0", - "@rollup/rollup-win32-ia32-msvc": "4.18.0", - "@rollup/rollup-win32-x64-msvc": "4.18.0", - "fsevents": "~2.3.2" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -12114,12 +11113,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true - }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -12240,12 +11233,6 @@ "wbuf": "^1.7.3" } }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true - }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -12261,12 +11248,6 @@ "node": ">= 0.8" } }, - "node_modules/std-env": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.5.0.tgz", - "integrity": "sha512-JGUEaALvL0Mf6JCfYnJOTcobY+Nc7sG/TemDRBqCA0wEr4DER7zDchaaixTlmOxAjG1uRJmX82EQcxwTQTkqVA==", - "dev": true - }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -12473,18 +11454,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", - "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", - "dev": true, - "dependencies": { - "acorn": "^8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/style-loader": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", @@ -12697,30 +11666,6 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, - "node_modules/tinybench": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.1.tgz", - "integrity": "sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==", - "dev": true - }, - "node_modules/tinypool": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", - "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", - "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -12930,26 +11875,6 @@ "typescript": ">=3" } }, - "node_modules/tsconfck": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.0.tgz", - "integrity": "sha512-CMjc5zMnyAjcS9sPLytrbFmj89st2g+JYtY/c02ug4Q+CZaAtCgbyviI0n1YvjZE/pzoc6FbNsINS13DOL1B9w==", - "dev": true, - "bin": { - "tsconfck": "bin/tsconfck.js" - }, - "engines": { - "node": "^18 || >=20" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -13110,15 +12035,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", @@ -13236,12 +12152,6 @@ "typescript": "~4.8 || 5" } }, - "node_modules/ufo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", - "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", - "dev": true - }, "node_modules/uglify-js": { "version": "3.19.1", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.1.tgz", @@ -13431,180 +12341,6 @@ "node": ">= 0.8" } }, - "node_modules/vite": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz", - "integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==", - "dev": true, - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.38", - "rollup": "^4.13.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", - "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", - "dev": true, - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "mlly": "^1.4.0", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": ">=v14.18.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-tsconfig-paths": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", - "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "globrex": "^0.1.2", - "tsconfck": "^3.0.3" - }, - "peerDependencies": { - "vite": "*" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", - "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", - "dev": true, - "dependencies": { - "@types/chai": "^4.3.5", - "@types/chai-subset": "^1.3.3", - "@types/node": "*", - "@vitest/expect": "0.34.6", - "@vitest/runner": "0.34.6", - "@vitest/snapshot": "0.34.6", - "@vitest/spy": "0.34.6", - "@vitest/utils": "0.34.6", - "acorn": "^8.9.0", - "acorn-walk": "^8.2.0", - "cac": "^6.7.14", - "chai": "^4.3.10", - "debug": "^4.3.4", - "local-pkg": "^0.4.3", - "magic-string": "^0.30.1", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.3.3", - "strip-literal": "^1.0.1", - "tinybench": "^2.5.0", - "tinypool": "^0.7.0", - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", - "vite-node": "0.34.6", - "why-is-node-running": "^2.2.2" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": ">=v14.18.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@vitest/browser": "*", - "@vitest/ui": "*", - "happy-dom": "*", - "jsdom": "*", - "playwright": "*", - "safaridriver": "*", - "webdriverio": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "playwright": { - "optional": true - }, - "safaridriver": { - "optional": true - }, - "webdriverio": { - "optional": true - } - } - }, "node_modules/vlq": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", @@ -14016,22 +12752,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", - "dev": true, - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wif": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", @@ -14590,190 +13310,29 @@ "filter-obj": "^1.1.0", "has-own-property": "^0.1.0", "identity-function": "^1.0.0", - "is-iterable": "^1.1.0", - "iterable-lookahead": "^1.0.0", - "lodash.curry": "^4.1.1", - "magic-string": "^0.16.0", - "map-obj": "^2.0.0", - "object-pairs": "^0.1.0", - "object-values": "^1.0.0", - "reverse-arguments": "^1.0.0", - "shell-quote-word": "^1.0.1", - "to-pascal-case": "^1.0.0", - "unescape-js": "^1.0.5" - }, - "dependencies": { - "magic-string": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.16.0.tgz", - "integrity": "sha512-c4BEos3y6G2qO0B9X7K0FVLOPT9uGrjYwYRLFmDqyl5YMboUviyecnXWp94fJTSMwPw2/sf+CEYt5AGpmklkkQ==", - "dev": true, - "requires": { - "vlq": "^0.2.1" - } - } - } - }, - "@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "dev": true, - "optional": true + "is-iterable": "^1.1.0", + "iterable-lookahead": "^1.0.0", + "lodash.curry": "^4.1.1", + "magic-string": "^0.16.0", + "map-obj": "^2.0.0", + "object-pairs": "^0.1.0", + "object-values": "^1.0.0", + "reverse-arguments": "^1.0.0", + "shell-quote-word": "^1.0.1", + "to-pascal-case": "^1.0.0", + "unescape-js": "^1.0.5" + }, + "dependencies": { + "magic-string": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.16.0.tgz", + "integrity": "sha512-c4BEos3y6G2qO0B9X7K0FVLOPT9uGrjYwYRLFmDqyl5YMboUviyecnXWp94fJTSMwPw2/sf+CEYt5AGpmklkkQ==", + "dev": true, + "requires": { + "vlq": "^0.2.1" + } + } + } }, "@eslint-community/eslint-utils": { "version": "4.4.0", @@ -14875,15 +13434,6 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, - "@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "requires": { - "@sinclair/typebox": "^0.27.8" - } - }, "@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -15139,118 +13689,6 @@ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.7.2.tgz", "integrity": "sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==" }, - "@rollup/rollup-android-arm-eabi": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", - "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-android-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", - "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-darwin-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", - "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", - "dev": true, - "optional": true - }, - "@rollup/rollup-darwin-x64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", - "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", - "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", - "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", - "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", - "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", - "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", - "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", - "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-x64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", - "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-x64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", - "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", - "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", - "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-x64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", - "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", - "dev": true, - "optional": true - }, "@sats-connect/core": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.2.1.tgz", @@ -15439,12 +13877,6 @@ } } }, - "@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, "@snyk/github-codeowners": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@snyk/github-codeowners/-/github-codeowners-1.1.0.tgz", @@ -15797,21 +14229,6 @@ "@types/node": "*" } }, - "@types/chai": { - "version": "4.3.11", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", - "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==", - "dev": true - }, - "@types/chai-subset": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.5.tgz", - "integrity": "sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==", - "dev": true, - "requires": { - "@types/chai": "*" - } - }, "@types/chrome": { "version": "0.0.237", "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.237.tgz", @@ -16343,120 +14760,6 @@ "eslint-visitor-keys": "^3.3.0" } }, - "@vitest/expect": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", - "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", - "dev": true, - "requires": { - "@vitest/spy": "0.34.6", - "@vitest/utils": "0.34.6", - "chai": "^4.3.10" - } - }, - "@vitest/runner": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", - "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", - "dev": true, - "requires": { - "@vitest/utils": "0.34.6", - "p-limit": "^4.0.0", - "pathe": "^1.1.1" - }, - "dependencies": { - "p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "requires": { - "yocto-queue": "^1.0.0" - } - }, - "yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true - } - } - }, - "@vitest/snapshot": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", - "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", - "dev": true, - "requires": { - "magic-string": "^0.30.1", - "pathe": "^1.1.1", - "pretty-format": "^29.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - } - } - }, - "@vitest/spy": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", - "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", - "dev": true, - "requires": { - "tinyspy": "^2.1.1" - } - }, - "@vitest/utils": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", - "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", - "dev": true, - "requires": { - "diff-sequences": "^29.4.3", - "loupe": "^2.3.6", - "pretty-format": "^29.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - } - } - }, "@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -16721,12 +15024,6 @@ "dev": true, "requires": {} }, - "acorn-walk": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", - "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", - "dev": true - }, "aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -17005,12 +15302,6 @@ } } }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, "ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -17551,12 +15842,6 @@ "base-x": "^4.0.0" } }, - "cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true - }, "call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -17595,21 +15880,6 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001559.tgz", "integrity": "sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA==" }, - "chai": { - "version": "4.3.10", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", - "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" - } - }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -17620,15 +15890,6 @@ "supports-color": "^5.3.0" } }, - "check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "requires": { - "get-func-name": "^2.0.2" - } - }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -18228,15 +16489,6 @@ "ms": "2.1.2" } }, - "deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, "deep-equal": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", @@ -18800,37 +17052,6 @@ "is-symbol": "^1.0.2" } }, - "esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "requires": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -19771,12 +17992,6 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, - "get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true - }, "get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -19861,12 +18076,6 @@ "slash": "^3.0.0" } }, - "globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true - }, "goober": { "version": "2.1.13", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.13.tgz", @@ -20718,12 +18927,6 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" }, - "jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -21107,12 +19310,6 @@ "json5": "^2.1.2" } }, - "local-pkg": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", - "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", - "dev": true - }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -21266,15 +19463,6 @@ "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.12.2.tgz", "integrity": "sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==" }, - "loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "requires": { - "get-func-name": "^2.0.1" - } - }, "lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -21293,15 +19481,6 @@ "yallist": "^3.0.2" } }, - "magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", - "dev": true, - "requires": { - "@jridgewell/sourcemap-codec": "^1.4.15" - } - }, "map-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", @@ -21451,18 +19630,6 @@ "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.47.0.tgz", "integrity": "sha512-Ldrva0fRBEIFWmEibBQO1PulfpJVF3pf28Guk09lDirDaSQqqU/xs9zQLwN2rL5VwVtsP1aD3JaCgaa98EjojQ==" }, - "mlly": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", - "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", - "dev": true, - "requires": { - "acorn": "^8.10.0", - "pathe": "^1.1.1", - "pkg-types": "^1.0.3", - "ufo": "^1.3.0" - } - }, "more-entropy": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/more-entropy/-/more-entropy-0.0.7.tgz", @@ -21969,18 +20136,6 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, - "pathe": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", - "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", - "dev": true - }, - "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true - }, "pbkdf2": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", @@ -22030,17 +20185,6 @@ "pinkie": "^2.0.0" } }, - "pkg-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", - "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", - "dev": true, - "requires": { - "jsonc-parser": "^3.2.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0" - } - }, "playwright": { "version": "1.46.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz", @@ -22432,7 +20576,8 @@ "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "peer": true }, "react-is-visible": { "version": "1.2.0", @@ -22759,32 +20904,6 @@ "resolved": "https://registry.npmjs.org/ripemd160-min/-/ripemd160-min-0.0.6.tgz", "integrity": "sha512-+GcJgQivhs6S9qvLogusiTcS9kQUfgR75whKuy5jIhuiOfQuJ8fjqxV6EGD5duH1Y/FawFUMtMhyeq3Fbnib8A==" }, - "rollup": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", - "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", - "dev": true, - "requires": { - "@rollup/rollup-android-arm-eabi": "4.18.0", - "@rollup/rollup-android-arm64": "4.18.0", - "@rollup/rollup-darwin-arm64": "4.18.0", - "@rollup/rollup-darwin-x64": "4.18.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", - "@rollup/rollup-linux-arm-musleabihf": "4.18.0", - "@rollup/rollup-linux-arm64-gnu": "4.18.0", - "@rollup/rollup-linux-arm64-musl": "4.18.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", - "@rollup/rollup-linux-riscv64-gnu": "4.18.0", - "@rollup/rollup-linux-s390x-gnu": "4.18.0", - "@rollup/rollup-linux-x64-gnu": "4.18.0", - "@rollup/rollup-linux-x64-musl": "4.18.0", - "@rollup/rollup-win32-arm64-msvc": "4.18.0", - "@rollup/rollup-win32-ia32-msvc": "4.18.0", - "@rollup/rollup-win32-x64-msvc": "4.18.0", - "@types/estree": "1.0.5", - "fsevents": "~2.3.2" - } - }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -23122,12 +21241,6 @@ "object-inspect": "^1.9.0" } }, - "siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true - }, "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -23219,12 +21332,6 @@ "wbuf": "^1.7.3" } }, - "stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true - }, "stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -23237,12 +21344,6 @@ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true }, - "std-env": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.5.0.tgz", - "integrity": "sha512-JGUEaALvL0Mf6JCfYnJOTcobY+Nc7sG/TemDRBqCA0wEr4DER7zDchaaixTlmOxAjG1uRJmX82EQcxwTQTkqVA==", - "dev": true - }, "stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -23397,15 +21498,6 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, - "strip-literal": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", - "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", - "dev": true, - "requires": { - "acorn": "^8.10.0" - } - }, "style-loader": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", @@ -23543,24 +21635,6 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, - "tinybench": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.1.tgz", - "integrity": "sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==", - "dev": true - }, - "tinypool": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", - "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", - "dev": true - }, - "tinyspy": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", - "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", - "dev": true - }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -23719,13 +21793,6 @@ "dev": true, "requires": {} }, - "tsconfck": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.0.tgz", - "integrity": "sha512-CMjc5zMnyAjcS9sPLytrbFmj89st2g+JYtY/c02ug4Q+CZaAtCgbyviI0n1YvjZE/pzoc6FbNsINS13DOL1B9w==", - "dev": true, - "requires": {} - }, "tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -23853,12 +21920,6 @@ "prelude-ls": "^1.2.1" } }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, "type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", @@ -23939,12 +22000,6 @@ "dev": true, "requires": {} }, - "ufo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", - "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", - "dev": true - }, "uglify-js": { "version": "3.19.1", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.1.tgz", @@ -24088,75 +22143,6 @@ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true }, - "vite": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz", - "integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==", - "dev": true, - "requires": { - "esbuild": "^0.21.3", - "fsevents": "~2.3.3", - "postcss": "^8.4.38", - "rollup": "^4.13.0" - } - }, - "vite-node": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", - "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", - "dev": true, - "requires": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "mlly": "^1.4.0", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" - } - }, - "vite-tsconfig-paths": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", - "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "globrex": "^0.1.2", - "tsconfck": "^3.0.3" - } - }, - "vitest": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", - "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", - "dev": true, - "requires": { - "@types/chai": "^4.3.5", - "@types/chai-subset": "^1.3.3", - "@types/node": "*", - "@vitest/expect": "0.34.6", - "@vitest/runner": "0.34.6", - "@vitest/snapshot": "0.34.6", - "@vitest/spy": "0.34.6", - "@vitest/utils": "0.34.6", - "acorn": "^8.9.0", - "acorn-walk": "^8.2.0", - "cac": "^6.7.14", - "chai": "^4.3.10", - "debug": "^4.3.4", - "local-pkg": "^0.4.3", - "magic-string": "^0.30.1", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.3.3", - "strip-literal": "^1.0.1", - "tinybench": "^2.5.0", - "tinypool": "^0.7.0", - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", - "vite-node": "0.34.6", - "why-is-node-running": "^2.2.2" - } - }, "vlq": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", @@ -24451,16 +22437,6 @@ "has-tostringtag": "^1.0.0" } }, - "why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", - "dev": true, - "requires": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - } - }, "wif": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", diff --git a/package.json b/package.json index ea7f76948..770833278 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "build-named": "npm run clean && NODE_ENV=production node scripts/build-named.js", "start": "npx node webpack/utils/devServer.js", "clean": "rimraf build", - "test": "vitest ./src", "style": "prettier --write \"src/**/*.{ts,tsx}\"", "prepare": "husky install", "e2etest": "npx playwright test -g \"\" --grep-invert \"#localexecution\"", @@ -153,9 +152,6 @@ "type-fest": "^2.19.0", "typescript": "^5.5.4", "typescript-plugin-styled-components": "^3.0.0", - "vite": "5.3.2", - "vite-tsconfig-paths": "4.3.2", - "vitest": "0.34.6", "webpack": "^5.89.0", "webpack-dev-server": "^4.11.0" } diff --git a/vitest.config.js b/vitest.config.js deleted file mode 100644 index 43d27dc46..000000000 --- a/vitest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -/// -import { defineConfig } from 'vite'; -import tsconfigPaths from 'vite-tsconfig-paths'; - -export default defineConfig({ - plugins: [tsconfigPaths()], -}); From e3005eb88f6126d7a934f199b5cd59740bea24f0 Mon Sep 17 00:00:00 2001 From: Den <36603049+dhriaznov@users.noreply.github.com> Date: Thu, 29 Aug 2024 12:23:00 +0200 Subject: [PATCH 045/227] [ENG-4848] fix: Navbar UI glitch on the Update Password screen (#516) * [ENG-4848] fix: Navbar UI glitch on the Update Password screen * Update the UI of the Update password screen * Change the password error message element role * Change the error element role in e2e --------- Co-authored-by: Terence Ng --- .../components/passwordInput/index.styled.ts | 91 +++++++ src/app/components/passwordInput/index.tsx | 238 +++++------------- src/app/screens/buy/index.tsx | 2 +- .../security/changePassword/index.tsx | 68 ++--- src/app/ui-library/input.tsx | 2 +- src/locales/en.json | 1 - tests/pages/onboarding.ts | 4 +- 7 files changed, 175 insertions(+), 231 deletions(-) create mode 100644 src/app/components/passwordInput/index.styled.ts diff --git a/src/app/components/passwordInput/index.styled.ts b/src/app/components/passwordInput/index.styled.ts new file mode 100644 index 000000000..d99a5b785 --- /dev/null +++ b/src/app/components/passwordInput/index.styled.ts @@ -0,0 +1,91 @@ +import { animated } from '@react-spring/web'; +import styled from 'styled-components'; + +export const Container = styled.div({ + display: 'flex', + flexDirection: 'column', + flex: 1, +}); + +export const HeaderText = styled.h1((props) => ({ + ...props.theme.typography.body_medium_l, + textAlign: 'center', + marginTop: props.theme.spacing(15), +})); + +export const HeaderContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', +}); + +export const PasswordInputLabel = styled.h2((props) => ({ + ...props.theme.typography.body_medium_m, + marginTop: props.theme.spacing(24), + marginBottom: props.theme.space.xs, + textAlign: 'left', +})); + +export const ButtonsContainer = styled.div<{ + stackButtonAlignment: boolean; + ifError: boolean; +}>((props) => ({ + display: 'flex', + flexDirection: props.stackButtonAlignment ? 'column-reverse' : 'row', + alignItems: props.stackButtonAlignment ? 'center' : 'flex-end', + flex: 1, + marginTop: props.ifError ? props.theme.spacing(30) : props.theme.spacing(40), + marginBottom: props.theme.spacing(8), +})); + +export const StyledButton = styled.button({ + background: 'none', + display: 'flex', + transition: 'opacity 0.1s ease', + '&:hover, &:focus': { + opacity: 0.8, + }, +}); + +export const PasswordStrengthContainer = styled.div((props) => ({ + ...props.theme.body_medium_m, + display: 'flex', + alignItems: 'center', + width: '100%', + marginTop: props.theme.spacing(8), + span: { + opacity: 0.6, + }, + p: { + justifySelf: 'flex-end', + }, +})); + +export const StrengthBar = styled(animated.div)<{ + $strengthColor: string; + $strengthWidth: string; +}>((props) => ({ + display: 'flex', + flex: '1 0', + alignItems: 'center', + backgroundColor: props.theme.colors.white_600, + marginLeft: props.theme.spacing(6), + marginRight: props.theme.spacing(9), + borderRadius: props.theme.radius(1), + width: '50%', + div: { + width: props.$strengthWidth, + height: 4, + backgroundColor: props.$strengthColor, + borderRadius: props.theme.radius(1), + }, +})); + +export const ButtonContainer = styled.div<{ + stackButtonAlignment: boolean; +}>((props) => ({ + marginLeft: props.stackButtonAlignment ? 0 : 3, + marginRight: props.stackButtonAlignment ? 0 : 3, + marginTop: props.theme.spacing(4), + width: '100%', +})); diff --git a/src/app/components/passwordInput/index.tsx b/src/app/components/passwordInput/index.tsx index 7d6ec021b..26adcae7d 100644 --- a/src/app/components/passwordInput/index.tsx +++ b/src/app/components/passwordInput/index.tsx @@ -3,14 +3,38 @@ import EyeSlash from '@assets/img/createPassword/EyeSlash.svg'; import PasswordIcon from '@assets/img/createPassword/Password.svg'; import { animated, useTransition } from '@react-spring/web'; import Button from '@ui-library/button'; +import Input from '@ui-library/input'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import styled, { useTheme } from 'styled-components'; +import { useTheme } from 'styled-components'; import zxcvbn from 'zxcvbn'; +import { + ButtonContainer, + ButtonsContainer, + Container, + HeaderContainer, + HeaderText, + PasswordInputLabel, + PasswordStrengthContainer, + StrengthBar, + StyledButton, +} from './index.styled'; -interface PasswordInputProps { +const REQUIRED_PASSWORD_LENGTH = 5; + +enum PasswordStrength { + NoScore, + PoorScore, + WeakScore, + AverageScore, + StrongScore, + MeetsAllRequirements, +} + +type Props = { title: string; inputLabel: string; + submitButtonText?: string; enteredPassword: string; setEnteredPassword: (enteredPassword: string) => void; handleContinue: () => void; @@ -21,162 +45,26 @@ interface PasswordInputProps { loading?: boolean; createPasswordFlow?: boolean; autoFocus?: boolean; -} - -interface StrengthBarProps { - $strengthColor: string; - $strengthWidth: string; -} - -const Container = styled.div({ - display: 'flex', - flexDirection: 'column', - flex: 1, -}); - -const HeaderText = styled.h1((props) => ({ - ...props.theme.body_bold_l, - textAlign: 'center', - marginTop: props.theme.spacing(15), -})); - -const HeaderContainer = styled.div({ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', -}); - -interface PasswordInputContainerProps { - hasError: boolean; -} - -const PasswordInputContainer = styled.div((props) => ({ - display: 'flex', - alignItems: 'center', - width: '100%', - border: `1px solid ${props.hasError ? 'rgba(211, 60, 60, 0.3)' : props.theme.colors.elevation3}`, - backgroundColor: props.theme.colors.elevation_n1, - borderRadius: props.theme.radius(1), - paddingLeft: props.theme.spacing(4), - paddingRight: props.theme.spacing(4), - marginTop: props.theme.spacing(4), - marginBottom: props.theme.spacing(3), -})); - -const PasswordInputLabel = styled.h2((props) => ({ - ...props.theme.body_medium_m, - marginTop: props.theme.spacing(24), - textAlign: 'left', -})); - -const Input = styled.input((props) => ({ - ...props.theme.body_medium_m, - height: 44, - backgroundColor: props.theme.colors.elevation_n1, - color: props.theme.colors.white_0, - width: '100%', - border: 'none', -})); - -interface ButtonContainerProps { - stackButtonAlignment: boolean; - ifError: boolean; -} -const ButtonsContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: props.stackButtonAlignment ? 'column-reverse' : 'row', - alignItems: props.stackButtonAlignment ? 'center' : 'flex-end', - flex: 1, - marginTop: props.ifError ? props.theme.spacing(30) : props.theme.spacing(40), - marginBottom: props.theme.spacing(8), -})); - -const StyledButton = styled.button({ - background: 'none', - display: 'flex', - transition: 'opacity 0.1s ease', - '&:hover, &:focus': { - opacity: 0.8, - }, -}); - -const ErrorMessage = styled.h2((props) => ({ - ...props.theme.body_xs, - textAlign: 'left', - color: props.theme.colors.feedback.error, -})); - -const PasswordStrengthContainer = styled.div((props) => ({ - ...props.theme.body_medium_m, - display: 'flex', - alignItems: 'center', - width: '100%', - marginTop: props.theme.spacing(8), - span: { - opacity: 0.6, - }, - p: { - justifySelf: 'flex-end', - }, -})); - -const StrengthBar = styled(animated.div)((props) => ({ - display: 'flex', - flex: '1 0', - alignItems: 'center', - backgroundColor: props.theme.colors.white_600, - marginLeft: props.theme.spacing(6), - marginRight: props.theme.spacing(9), - borderRadius: props.theme.radius(1), - width: '50%', - div: { - width: props.$strengthWidth, - height: 4, - backgroundColor: props.$strengthColor, - borderRadius: props.theme.radius(1), - }, -})); - -const REQUIRED_PASSWORD_LENGTH = 5; - -enum PasswordStrength { - NoScore, - PoorScore, - WeakScore, - AverageScore, - StrongScore, - MeetsAllRequirements, -} - -interface TransparentButtonContainerProps { - stackButtonAlignment: boolean; -} -const ButtonContainer = styled.div((props) => ({ - marginLeft: props.stackButtonAlignment ? 0 : 3, - marginRight: props.stackButtonAlignment ? 0 : 3, - marginTop: props.theme.spacing(4), - width: '100%', -})); - -function PasswordInput(props: PasswordInputProps): JSX.Element { - const { - title, - inputLabel, - enteredPassword, - passwordError, - setEnteredPassword, - handleContinue, - handleBack, - checkPasswordStrength, - stackButtonAlignment = false, - loading, - createPasswordFlow, - autoFocus = false, - } = props; - +}; + +function PasswordInput({ + title, + inputLabel, + submitButtonText, + enteredPassword, + passwordError, + setEnteredPassword, + handleContinue, + handleBack, + checkPasswordStrength, + stackButtonAlignment = false, + loading, + createPasswordFlow, + autoFocus = false, +}: Props): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'CREATE_PASSWORD_SCREEN' }); const theme = useTheme(); - const [isPasswordVisible, setIsPasswordVisible] = useState(false); + const [isPasswordVisible, setIsPasswordVisible] = useState(false); const [passwordStrength, setPasswordStrength] = useState( PasswordStrength.NoScore, ); @@ -304,26 +192,24 @@ function PasswordInput(props: PasswordInputProps): JSX.Element { {title} {inputLabel} - + toggle password visibility + } - > - - - show-password - - - {error && {error}} + feedback={error !== '' ? [{ message: error, variant: 'danger' }] : undefined} + hideClear + /> {checkPasswordStrength ? renderStrengthBar() : null} @@ -335,7 +221,7 @@ function PasswordInput(props: PasswordInputProps): JSX.Element { disabled={ !enteredPassword || (!!checkPasswordStrength && score <= PasswordStrength.WeakScore) } - title={t('CONTINUE_BUTTON')} + title={submitButtonText || t('CONTINUE_BUTTON')} onClick={handleContinue} /> diff --git a/src/app/screens/buy/index.tsx b/src/app/screens/buy/index.tsx index ef302015b..4753fd87f 100644 --- a/src/app/screens/buy/index.tsx +++ b/src/app/screens/buy/index.tsx @@ -10,7 +10,7 @@ import useWalletSelector from '@hooks/useWalletSelector'; import { FeatureId, getMoonPaySignedUrl } from '@secretkeylabs/xverse-core'; import Spinner from '@ui-library/spinner'; import { MOON_PAY_API_KEY, MOON_PAY_URL, TRANSAC_API_KEY, TRANSAC_URL } from '@utils/constants'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router-dom'; import styled from 'styled-components'; diff --git a/src/app/screens/settings/security/changePassword/index.tsx b/src/app/screens/settings/security/changePassword/index.tsx index b21e0d743..b99d64f9a 100644 --- a/src/app/screens/settings/security/changePassword/index.tsx +++ b/src/app/screens/settings/security/changePassword/index.tsx @@ -1,4 +1,3 @@ -import Check from '@assets/img/settings/check_circle.svg'; import PasswordInput from '@components/passwordInput'; import BottomBar from '@components/tabBar'; import TopRow from '@components/topRow'; @@ -9,57 +8,30 @@ import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; -const PasswordContainer = styled.div((props) => ({ +const Container = styled.div((props) => ({ + ...props.theme.scrollbar, display: 'flex', flexDirection: 'column', flex: 1, - marginTop: props.theme.spacing(20), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), -})); - -const ToastContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - background: props.theme.colors.feedback.success, - borderRadius: 12, - boxShadow: '0px 7px 16px -4px rgba(25, 25, 48, 0.25)', - height: 44, - padding: '12px 20px 12px 16px', - width: 306, - flex: 1, -})); - -const ToastMessage = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.elevation0, - marginLeft: props.theme.spacing(7), -})); - -const ToastDismissButton = styled.button((props) => ({ - background: 'transparent', - marginLeft: props.theme.spacing(12), + padding: `0 ${props.theme.space.s}`, + paddingTop: props.theme.space.xxl, })); function ChangePasswordScreen() { const { t } = useTranslation('translation'); const { unlockVault, changePassword } = useSeedVault(); - const [password, setPassword] = useState(''); - const [oldPassword, setOldPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [error, setError] = useState(''); - const [loading, setLoading] = useState(false); - const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [password, setPassword] = useState(''); + const [oldPassword, setOldPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + const [currentStepIndex, setCurrentStepIndex] = useState(0); const navigate = useNavigate(); const handleBackButtonClick = () => { navigate(-1); }; - const dismissToast = () => { - toast.dismiss(); - }; - const handleConfirmCurrentPasswordNextClick = async () => { try { setLoading(true); @@ -74,14 +46,6 @@ function ChangePasswordScreen() { } }; - const ToastContent = ( - - Check - {t('SETTING_SCREEN.UPDATE_PASSWORD_SUCCESS')} - {t('OK')} - - ); - const handleEnterNewPasswordNextClick = () => { setCurrentStepIndex(2); }; @@ -90,7 +54,7 @@ function ChangePasswordScreen() { if (confirmPassword === password) { setError(''); await changePassword(oldPassword, confirmPassword); - toast.custom(ToastContent); + toast.success(t('SETTING_SCREEN.UPDATE_PASSWORD_SUCCESS')); navigate('/settings'); } else { setError(t('CREATE_PASSWORD_SCREEN.CONFIRM_PASSWORD_MATCH_ERROR')); @@ -104,11 +68,11 @@ function ChangePasswordScreen() { return ( <> - + {currentStepIndex === 0 && ( )} {currentStepIndex === 1 && ( @@ -129,21 +94,24 @@ function ChangePasswordScreen() { checkPasswordStrength stackButtonAlignment createPasswordFlow + autoFocus /> )} {currentStepIndex === 2 && ( )} - +
); diff --git a/src/app/ui-library/input.tsx b/src/app/ui-library/input.tsx index 77edf4757..1ce68ee64 100644 --- a/src/app/ui-library/input.tsx +++ b/src/app/ui-library/input.tsx @@ -180,7 +180,7 @@ type Props = { dataTestID?: string; onChange: (event: ChangeEvent) => void; onBlur?: (event: ChangeEvent) => void; - type?: 'text' | 'number'; + type?: 'text' | 'number' | 'password'; hideClear?: boolean; infoPanel?: React.ReactNode; complications?: React.ReactNode; diff --git a/src/locales/en.json b/src/locales/en.json index 328ccb63b..36a3ea2e2 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -550,7 +550,6 @@ "CONFIRM_PASSWORD_TITLE": "Confirm your password", "ENTER_PASSWORD": "Enter your current password", "TEXT_INPUT_NEW_PASSWORD_LABEL": "Password", - "TEXT_INPUT_ENTER_PASSWORD_LABEL": "Enter your current password", "TEXT_INPUT_CONFIRM_PASSWORD_LABEL": "Confirm Password", "CONTINUE_BUTTON": "Continue", "BACK_BUTTON": "Back", diff --git a/tests/pages/onboarding.ts b/tests/pages/onboarding.ts index e1748a0bb..0fdbe194c 100644 --- a/tests/pages/onboarding.ts +++ b/tests/pages/onboarding.ts @@ -89,8 +89,8 @@ export default class Onboarding { this.buttonSeedWords = page.locator('button[value]:not([value=""])'); this.header = page.locator('#app h3'); this.inputPassword = page.locator('input[type="password"]'); - this.errorMessage = page.getByRole('heading', { name: 'Your password should be at' }); - this.errorMessage2 = page.getByRole('heading', { name: 'Please make sure your' }); + this.errorMessage = page.locator('p').filter({ hasText: 'Your password should be at' }); + this.errorMessage2 = page.locator('p').filter({ hasText: 'Please make sure your' }); this.errorMessageSeedPhrase = page.locator('p').filter({ hasText: 'Invalid seed phrase' }); this.labelSecurityLevelWeak = page.locator('p').filter({ hasText: 'Weak' }); this.labelSecurityLevelMedium = page.locator('p').filter({ hasText: 'Medium' }); From 8a26894bb823f331cd935eb8fe338ff17efa4f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduard=20Bardaj=C3=AD=20Puig?= Date: Thu, 29 Aug 2024 15:44:34 +0200 Subject: [PATCH 046/227] Ensure unsigned transactions use latest nonce (#534) * Ensure unsigned transactions use latest nonce * Update @stacks/transactions * Fix missing rune ID --- package-lock.json | 403 +++++++++++------- package.json | 4 +- .../confirmStxTransactionComponent/index.tsx | 8 +- src/app/screens/mintRune/index.tsx | 1 + 4 files changed, 248 insertions(+), 168 deletions(-) diff --git a/package-lock.json b/package-lock.json index a234c6208..7efd6dc38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,10 +15,10 @@ "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.2.1", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "18.7.2", + "@secretkeylabs/xverse-core": "18.8.0", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", - "@stacks/transactions": "6.13.1", + "@stacks/transactions": "6.16.1", "@tanstack/query-sync-storage-persister": "^4.29.1", "@tanstack/react-query": "^4.29.3", "@tanstack/react-query-devtools": "^4.29.3", @@ -568,6 +568,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@bitcoinerlab/descriptors/-/descriptors-1.1.1.tgz", "integrity": "sha512-AMFkbBBg9T1iWtEmWB23oADk7zaOQix6wUPLXalhTyFDjhkFXDd6pCRfto/HAdaPg/ccM4GMTVgYLee9WdYFyQ==", + "license": "MIT", "dependencies": { "@bitcoinerlab/miniscript": "^1.2.1", "@bitcoinerlab/secp256k1": "^1.0.5", @@ -588,6 +589,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/@bitcoinerlab/miniscript/-/miniscript-1.4.0.tgz", "integrity": "sha512-BsG3dmwQmgKHnRZecDgUsPjwcpnf1wgaZbolcMTByS10k1zYzIx97W51LzG7GvokRJ+wnzTX/GhC8Y3L2X0CQA==", + "license": "MIT", "dependencies": { "bip68": "^1.0.4" } @@ -596,6 +598,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.1.1.tgz", "integrity": "sha512-uhjW51WfVLpnHN7+G0saDcM/k9IqcyTbZ+bDgLF3AX8V/a3KXSE9vn7UPBrcdU72tp0J4YPR7BHp2m7MLAZ/1Q==", + "license": "MIT", "dependencies": { "@noble/hashes": "^1.1.5", "@noble/secp256k1": "^1.7.1" @@ -1199,6 +1202,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "license": "MIT", "dependencies": { "@noble/curves": "~1.4.0", "@noble/hashes": "~1.4.0", @@ -1212,6 +1216,7 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "license": "MIT", "dependencies": { "@noble/hashes": "1.4.0" }, @@ -1223,6 +1228,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", "engines": { "node": ">= 16" }, @@ -1240,6 +1246,7 @@ "url": "https://paulmillr.com/funding/" } ], + "license": "MIT", "dependencies": { "@noble/hashes": "~1.1.1", "@scure/base": "~1.1.0" @@ -1254,7 +1261,8 @@ "type": "individual", "url": "https://paulmillr.com/funding/" } - ] + ], + "license": "MIT" }, "node_modules/@scure/btc-signer": { "version": "1.2.1", @@ -1271,9 +1279,9 @@ } }, "node_modules/@secretkeylabs/xverse-core": { - "version": "18.7.2", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.7.2/0dc4ba04ad9b58f27031a976faf112a6648ea945", - "integrity": "sha512-B07GqC77UFeelKAjnpW0ZNjv7jc/m12PdVJJhRJdB7PRhsl+KHD8Z33JMHazdR6P3VbNlzUeEZ5R8WX+eBQWaA==", + "version": "18.8.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.0/ddb1e79eace136115e86ca0952613efb900e4501", + "integrity": "sha512-BgWjO5TQiHteiFqb5pAjxFcgU0mSo8XhT9CxXkWV3YnjEb+Z9l9MX0wTrJMKEwswyFpTE8WJEtKU3bWR4uuyJA==", "license": "ISC", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", @@ -1283,14 +1291,14 @@ "@scure/bip32": "^1.4.0", "@scure/bip39": "^1.3.0", "@scure/btc-signer": "1.2.1", - "@stacks/auth": "6.13.1", + "@stacks/auth": "6.16.1", "@stacks/connect": "7.7.1", - "@stacks/encryption": "6.13.1", - "@stacks/network": "6.13.0", - "@stacks/stacking": "6.13.2", - "@stacks/storage": "6.13.1", - "@stacks/transactions": "6.13.1", - "@stacks/wallet-sdk": "6.13.1", + "@stacks/encryption": "6.16.1", + "@stacks/network": "6.16.0", + "@stacks/stacking": "6.16.1", + "@stacks/storage": "6.16.1", + "@stacks/transactions": "6.16.1", + "@stacks/wallet-sdk": "6.16.1", "@tanstack/react-query": "^4.29.3", "@zondax/ledger-stacks": "^1.0.4", "async-mutex": "^0.4.0", @@ -1330,6 +1338,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", "engines": { "node": ">= 16" }, @@ -1341,6 +1350,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "license": "MIT", "dependencies": { "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" @@ -1353,6 +1363,7 @@ "version": "7.7.1", "resolved": "https://registry.npmjs.org/@stacks/connect/-/connect-7.7.1.tgz", "integrity": "sha512-MwLRhgRLOGo0Y4IDC0qp9RUX2SZubgse1aI2TN/fz2abNIh1LgmOKUua3w17YiBEZxDD9nyQ4KW1c33bdnrOPw==", + "license": "MIT", "dependencies": { "@stacks/auth": "^6.1.1", "@stacks/connect-ui": "6.4.1", @@ -1366,6 +1377,7 @@ "version": "6.4.1", "resolved": "https://registry.npmjs.org/@stacks/connect-ui/-/connect-ui-6.4.1.tgz", "integrity": "sha512-Y6Fp26MUsMQq08zFSWus40rS7RNHrHj6VDJrFUqM9VsksV3wftpsRcy7psQusUvW1DS7fPza67IlM1dcN4rvSg==", + "license": "MIT", "dependencies": { "@stencil/core": "^2.17.1" } @@ -1373,12 +1385,14 @@ "node_modules/@secretkeylabs/xverse-core/node_modules/@types/node": { "version": "11.11.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", - "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" + "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==", + "license": "MIT" }, "node_modules/@secretkeylabs/xverse-core/node_modules/async-mutex": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "license": "MIT", "dependencies": { "tslib": "^2.4.0" } @@ -1387,6 +1401,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.3.tgz", "integrity": "sha512-P0dKrz4g0V0BjXfx7d9QNkJ/Txcz/k+hM9TnjqjUaXtuOfAvxXSw2rJw8DX0e3ZPwnK/IgDxoRqf0bvoVCqbMg==", + "license": "ISC", "dependencies": { "@types/node": "11.11.6", "create-hash": "^1.1.0", @@ -1436,22 +1451,24 @@ } }, "node_modules/@stacks/auth": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-6.13.1.tgz", - "integrity": "sha512-nr5VeLIJBVI72eWYs/oQ7nrmuZxe89AlS5Lt/WxCcUDK9Z+oqQR1qsxBo86yVDlmrxoNWncHWD38LX54HsOEzA==", - "dependencies": { - "@stacks/common": "^6.13.0", - "@stacks/encryption": "^6.13.1", - "@stacks/network": "^6.13.0", - "@stacks/profile": "^6.13.1", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-6.16.1.tgz", + "integrity": "sha512-6Co7VZTlfvWVKEYXh+x2TD1e+3XmCPumJut1jRgW6pYPGruZ4Dz/R4xfkf9qX3MEJabLe4AfasSfoqftg0twTg==", + "license": "MIT", + "dependencies": { + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.1", + "@stacks/network": "^6.16.0", + "@stacks/profile": "^6.16.1", "cross-fetch": "^3.1.5", "jsontokens": "^4.0.1" } }, "node_modules/@stacks/common": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.13.0.tgz", - "integrity": "sha512-wwzyihjaSdmL6NxKvDeayy3dqM0L0Q2sawmdNtzJDi0FnXuJGm5PeapJj7bEfcI9XwI7Bw5jZoC6mCn9nc5YIw==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.16.0.tgz", + "integrity": "sha512-PnzvhrdGRMVZvxTulitlYafSK4l02gPCBBoI9QEoTqgSnv62oaOXhYAUUkTMFKxdHW1seVEwZsrahuXiZPIAwg==", + "license": "MIT", "dependencies": { "@types/bn.js": "^5.1.0", "@types/node": "^18.0.4" @@ -1487,14 +1504,15 @@ } }, "node_modules/@stacks/encryption": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.13.1.tgz", - "integrity": "sha512-y5IFX3/nGI3fCk70gE0JwH70GpshD8RhUfvhMLcL96oNaec1cCdj1ZUiQupeicfYTHuraaVBYU9xLls4TRmypg==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.16.1.tgz", + "integrity": "sha512-DtVNNW/iipyxxRDz73S9DbLfRmBMqQCCog89F1Q1i6JUnl2kBB1PR9SPQfYv9zcAJ37oHoNB4i4b2tJWYr01vg==", + "license": "MIT", "dependencies": { "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.1", "@scure/bip39": "1.1.0", - "@stacks/common": "^6.13.0", + "@stacks/common": "^6.16.0", "@types/node": "^18.0.4", "base64-js": "^1.5.1", "bs58": "^5.0.0", @@ -1511,50 +1529,55 @@ "type": "individual", "url": "https://paulmillr.com/funding/" } - ] + ], + "license": "MIT" }, "node_modules/@stacks/encryption/node_modules/@types/node": { - "version": "18.19.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.31.tgz", - "integrity": "sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==", + "version": "18.19.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.47.tgz", + "integrity": "sha512-1f7dB3BL/bpd9tnDJrrHb66Y+cVrhxSOTGorRNdHwYTUlTay3HuTDPKo9a/4vX9pMQkhYBcAbL4jQdNlhCFP9A==", + "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@stacks/network": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.13.0.tgz", - "integrity": "sha512-Ss/Da4BNyPBBj1OieM981fJ7SkevKqLPkzoI1+Yo7cYR2df+0FipIN++Z4RfpJpc8ne60vgcx7nJZXQsiGhKBQ==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.16.0.tgz", + "integrity": "sha512-uqz9Nb6uf+SeyCKENJN+idt51HAfEeggQKrOMfGjpAeFgZV2CR66soB/ci9+OVQR/SURvasncAz2ScI1blfS8A==", + "license": "MIT", "dependencies": { - "@stacks/common": "^6.13.0", + "@stacks/common": "^6.16.0", "cross-fetch": "^3.1.5" } }, "node_modules/@stacks/profile": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-6.13.1.tgz", - "integrity": "sha512-GCDE13hwoUYZvZKTb5c0Tr74DcxIP/n4bffcYrKa5UabITPQ7JwsJIOyDoAwdtl3lu7fi9aBsKrdfHpBBUSQIQ==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-6.16.1.tgz", + "integrity": "sha512-FJcwKN6oVDfmwymivkZfm1PcO7gv5fcWN4HI+jtYdMftrl6yxAB4/XD63FSvshIeLPzBJjnCmaZSnPAV2mtdeA==", + "license": "MIT", "dependencies": { - "@stacks/common": "^6.13.0", - "@stacks/network": "^6.13.0", - "@stacks/transactions": "^6.13.1", + "@stacks/common": "^6.16.0", + "@stacks/network": "^6.16.0", + "@stacks/transactions": "^6.16.1", "jsontokens": "^4.0.1", "schema-inspector": "^2.0.2", "zone-file": "^2.0.0-beta.3" } }, "node_modules/@stacks/stacking": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.13.2.tgz", - "integrity": "sha512-4h1UQuL2+Xdra9zMqzUElvKG9X9fenuNE7hD9sIqyxyLFxeQ7gRqczmTYPsmaj4wY5004JNj+efzGJ0VmpOcAA==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.16.1.tgz", + "integrity": "sha512-Bv7TSyoMrb1wYOfKrPxwDQPSjsohyKCduN1HYMlKL9hHF0J8+MvlJFw9eIj1c2SEOyYaElT+Ly3CI56J/acVxw==", + "license": "MIT", "dependencies": { "@noble/hashes": "1.1.5", "@scure/base": "1.1.1", - "@stacks/common": "^6.13.0", - "@stacks/encryption": "^6.13.1", - "@stacks/network": "^6.13.0", + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.1", + "@stacks/network": "^6.16.0", "@stacks/stacks-blockchain-api-types": "^0.61.0", - "@stacks/transactions": "^6.13.1", + "@stacks/transactions": "^6.16.1", "bs58": "^5.0.0" } }, @@ -1567,7 +1590,8 @@ "type": "individual", "url": "https://paulmillr.com/funding/" } - ] + ], + "license": "MIT" }, "node_modules/@stacks/stacking/node_modules/@scure/base": { "version": "1.1.1", @@ -1578,12 +1602,14 @@ "type": "individual", "url": "https://paulmillr.com/funding/" } - ] + ], + "license": "MIT" }, "node_modules/@stacks/stacking/node_modules/@stacks/stacks-blockchain-api-types": { "version": "0.61.0", "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-0.61.0.tgz", - "integrity": "sha512-yPOfTUboo5eA9BZL/hqMcM71GstrFs9YWzOrJFPeP4cOO1wgYvAcckgBRbgiE3NqeX0A7SLZLDAXLZbATuRq9w==" + "integrity": "sha512-yPOfTUboo5eA9BZL/hqMcM71GstrFs9YWzOrJFPeP4cOO1wgYvAcckgBRbgiE3NqeX0A7SLZLDAXLZbATuRq9w==", + "license": "ISC" }, "node_modules/@stacks/stacks-blockchain-api-types": { "version": "6.1.1", @@ -1591,27 +1617,29 @@ "integrity": "sha512-Mw5dBPx3DySPupwaq0iBdm1WdEVXIfhjUVaTjI2iSyzWz4Fgs3U7JCaAezLbgNu7Q69c/ZN4JUDWuo9FVjy7oA==" }, "node_modules/@stacks/storage": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.13.1.tgz", - "integrity": "sha512-XnzRAETDKW7Ij3cuUNllrrnLOCwctV/XrIHgVWnD04LNL5R9apkwp9IkzaFbyJZ+XrkUBKwtdCgVYype2XgT6w==", - "dependencies": { - "@stacks/auth": "^6.13.1", - "@stacks/common": "^6.13.0", - "@stacks/encryption": "^6.13.1", - "@stacks/network": "^6.13.0", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.16.1.tgz", + "integrity": "sha512-bElxB03dg3XGTX8r/J8Os2tIsodcTglg7zeq6RU4FVUU7tUA+Agpmn9FKl8MmnsWfO4ls5UgeujyBaxUJ//yAw==", + "license": "MIT", + "dependencies": { + "@stacks/auth": "^6.16.1", + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.1", + "@stacks/network": "^6.16.0", "base64-js": "^1.5.1", "jsontokens": "^4.0.1" } }, "node_modules/@stacks/transactions": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.13.1.tgz", - "integrity": "sha512-PWw2I+2Fj3CaFYQIoVcqQN6E2qGHNhFv03nuR0CxMq0sx8stPgYZbdzUlnlBcJQdsFiHrw3sPeqnXDZt+Hg5YQ==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.16.1.tgz", + "integrity": "sha512-yCtUM+8IN0QJbnnlFhY1wBW7Q30Cxje3Zmy8DgqdBoM/EPPWadez/8wNWFANVAMyUZeQ9V/FY+8MAw4E+pCReA==", + "license": "MIT", "dependencies": { "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.1", - "@stacks/common": "^6.13.0", - "@stacks/network": "^6.13.0", + "@stacks/common": "^6.16.0", + "@stacks/network": "^6.16.0", "c32check": "^2.0.0", "lodash.clonedeep": "^4.5.0" } @@ -1628,19 +1656,20 @@ ] }, "node_modules/@stacks/wallet-sdk": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/wallet-sdk/-/wallet-sdk-6.13.1.tgz", - "integrity": "sha512-262CYKAm1j8oVxfGUIJrHp867j9gm5NrqPM85s0TfCv2QhfLDkvme6nKgmvtL2TecAZkBa5tu8M5DQ7z92WvAQ==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/wallet-sdk/-/wallet-sdk-6.16.1.tgz", + "integrity": "sha512-QYm/BfRpHZfD5FjvaOJp0hy8yeqde6qrEmSrcIl06IC2SNr4R5NOy0xK/RPUgJFB1/Gd16HbqS9EdyV3o6ybwg==", + "license": "MIT", "dependencies": { "@scure/bip32": "1.1.3", "@scure/bip39": "1.1.0", - "@stacks/auth": "^6.13.1", - "@stacks/common": "^6.13.0", - "@stacks/encryption": "^6.13.1", - "@stacks/network": "^6.13.0", - "@stacks/profile": "^6.13.1", - "@stacks/storage": "^6.13.1", - "@stacks/transactions": "^6.13.1", + "@stacks/auth": "^6.16.1", + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.1", + "@stacks/network": "^6.16.0", + "@stacks/profile": "^6.16.1", + "@stacks/storage": "^6.16.1", + "@stacks/transactions": "^6.16.1", "buffer": "^6.0.3", "c32check": "^2.0.0", "jsontokens": "^4.0.1", @@ -1657,7 +1686,8 @@ "type": "individual", "url": "https://paulmillr.com/funding/" } - ] + ], + "license": "MIT" }, "node_modules/@stacks/wallet-sdk/node_modules/@scure/bip32": { "version": "1.1.3", @@ -1669,6 +1699,7 @@ "url": "https://paulmillr.com/funding/" } ], + "license": "MIT", "dependencies": { "@noble/hashes": "~1.1.3", "@noble/secp256k1": "~1.7.0", @@ -2153,6 +2184,7 @@ "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/sha.js/-/sha.js-2.4.4.tgz", "integrity": "sha512-Qukd+D6S2Hm0wLVt2Vh+/eWBIoUt+wF8jWjBsG4F8EFQRwKtYvtXCPcNl2OEUQ1R+eTr3xuSaBYUyM3WD1x/Qw==", + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -2180,7 +2212,8 @@ "node_modules/@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", - "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "license": "MIT" }, "node_modules/@types/webextension-polyfill": { "version": "0.9.2", @@ -2652,6 +2685,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/@zondax/ledger-stacks/-/ledger-stacks-1.0.4.tgz", "integrity": "sha512-R8CB0CZ2poTzpcG0jhzzXZvXF7axIsmZFhp06aHCUjgz+1df63YbC4tUzyzmseekwqNWnaebWFejQKJ99WiHZA==", + "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.12.5", "@ledgerhq/hw-transport": "^6.28.1", @@ -2663,6 +2697,7 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/@stacks/common/-/common-4.3.5.tgz", "integrity": "sha512-UuViiQ7fn3vdtTe3739aRzbl+wbukekeQuXgqt8d7nB2HC2HodD7GcHhpUga165cO35CD6lQUtj3vXxJb5Ga+A==", + "license": "MIT", "dependencies": { "@types/bn.js": "^5.1.0", "@types/node": "^18.0.4", @@ -2673,6 +2708,7 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/@stacks/network/-/network-4.3.5.tgz", "integrity": "sha512-TC4+AkuT6qi3MoEGxTftA+4BNp99QvGnI+qtKQkoA1m0KDr8b9hSBUhugJHRhQbWuo7D6q0+JagYEGxLID29Kw==", + "license": "MIT", "dependencies": { "@stacks/common": "^4.3.5", "cross-fetch": "^3.1.5" @@ -2682,6 +2718,7 @@ "version": "4.3.8", "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-4.3.8.tgz", "integrity": "sha512-5xYYv2TdXXM9PVixB79Pr99symQ8fhbVATjempGUxtL23/XUiRiLvJZohDxIE4VQ2EzbB4g4j8Y7oqPjj0h09Q==", + "license": "MIT", "dependencies": { "@noble/hashes": "^1.0.0", "@noble/secp256k1": "^1.5.5", @@ -2697,9 +2734,10 @@ } }, "node_modules/@zondax/ledger-stacks/node_modules/@types/node": { - "version": "18.19.42", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.42.tgz", - "integrity": "sha512-d2ZFc/3lnK2YCYhos8iaNIYu9Vfhr92nHiyJHRltXWjXUBjEE+A4I58Tdbnw4VhggSW+2j5y5gTrLs4biNnubg==", + "version": "18.19.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.47.tgz", + "integrity": "sha512-1f7dB3BL/bpd9tnDJrrHb66Y+cVrhxSOTGorRNdHwYTUlTay3HuTDPKo9a/4vX9pMQkhYBcAbL4jQdNlhCFP9A==", + "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } @@ -2708,6 +2746,7 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.10.tgz", "integrity": "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==", + "license": "MIT", "dependencies": { "safe-buffer": "^5.0.1" } @@ -2716,6 +2755,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/c32check/-/c32check-1.1.3.tgz", "integrity": "sha512-ADADE/PjAbJRlwpG3ShaOMbBUlJJZO7xaYSRD5Tub6PixQlgR4s36y9cvMf/YRGpkqX+QOxIdMw216iC320q9A==", + "license": "MIT", "dependencies": { "base-x": "^3.0.8", "buffer": "^5.6.0", @@ -3298,6 +3338,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -3351,6 +3392,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", "dependencies": { "file-uri-to-path": "1.0.0" } @@ -3359,6 +3401,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.1.1.tgz", "integrity": "sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ==", + "license": "MIT", "engines": { "node": ">=8.0.0" } @@ -3367,6 +3410,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/bip32/-/bip32-4.0.0.tgz", "integrity": "sha512-aOGy88DDlVUhspIXJN+dVEtclhIsfAUppD43V0j40cPTld3pv/0X/MlrZSZ6jowIaQQzFwP8M6rFU2z2mVYjDQ==", + "license": "MIT", "dependencies": { "@noble/hashes": "^1.2.0", "@scure/base": "^1.1.1", @@ -3380,7 +3424,8 @@ "node_modules/bip32-path": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/bip32-path/-/bip32-path-0.4.2.tgz", - "integrity": "sha512-ZBMCELjJfcNMkz5bDuJ1WrYvjlhEF5k6mQ8vUr4N7MbVRsXei7ZOg8VhhwMfNiW68NWmLkgkc6WvTickrLGprQ==" + "integrity": "sha512-ZBMCELjJfcNMkz5bDuJ1WrYvjlhEF5k6mQ8vUr4N7MbVRsXei7ZOg8VhhwMfNiW68NWmLkgkc6WvTickrLGprQ==", + "license": "MIT" }, "node_modules/bip39": { "version": "3.1.0", @@ -3394,6 +3439,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", "integrity": "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==", + "license": "MIT", "dependencies": { "safe-buffer": "^5.0.1" } @@ -3402,6 +3448,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/bip68/-/bip68-1.0.4.tgz", "integrity": "sha512-O1htyufFTYy3EO0JkHg2CLykdXEtV2ssqw47Gq9A0WByp662xpJnMEB9m43LZjsSDjIAOozWRExlFQk2hlV1XQ==", + "license": "ISC", "engines": { "node": ">=4.5.0" } @@ -3420,6 +3467,7 @@ "version": "6.1.6", "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.6.tgz", "integrity": "sha512-Fk8+Vc+e2rMoDU5gXkW9tD+313rhkm5h6N9HfZxXvYU9LedttVvmXKTgd9k5rsQJjkSfsv6XRM8uhJv94SrvcA==", + "license": "MIT", "dependencies": { "@noble/hashes": "^1.2.0", "bech32": "^2.0.0", @@ -3436,6 +3484,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/bitcoinjs-message/-/bitcoinjs-message-2.2.0.tgz", "integrity": "sha512-103Wy3xg8Y9o+pdhGP4M3/mtQQuUWs6sPuOp1mYphSUoSMHjHTlkj32K4zxU8qMH0Ckv23emfkGlFWtoWZ7YFA==", + "license": "MIT", "dependencies": { "bech32": "^1.1.3", "bs58check": "^2.1.2", @@ -3452,6 +3501,7 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.10.tgz", "integrity": "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==", + "license": "MIT", "dependencies": { "safe-buffer": "^5.0.1" } @@ -3459,12 +3509,14 @@ "node_modules/bitcoinjs-message/node_modules/bech32": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", - "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "license": "MIT" }, "node_modules/bitcoinjs-message/node_modules/bs58": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", "dependencies": { "base-x": "^3.0.2" } @@ -3473,6 +3525,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "license": "MIT", "dependencies": { "bs58": "^4.0.0", "create-hash": "^1.1.0", @@ -3735,6 +3788,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", + "license": "MIT", "dependencies": { "@noble/hashes": "^1.2.0", "bs58": "^5.0.0" @@ -3767,6 +3821,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/buffer-equals/-/buffer-equals-1.0.4.tgz", "integrity": "sha512-99MsCq0j5+RhubVEtKQgKaD6EM+UP3xJgIvQqwJ3SOLDUekzxMX1ylXBng+Wa2sh7mGT0W6RUly8ojjr1Tt6nA==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4458,6 +4513,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/cross-sha256/-/cross-sha256-1.2.0.tgz", "integrity": "sha512-KViLNMDZKV7jwFqjFx+rNhG26amnFYYQ0S+VaFlVvpk8tM+2XbFia/don/SjGHg9WQxnFVi6z64CGPuF3T+nNw==", + "license": "(MIT AND BSD-3-Clause)", "dependencies": { "buffer": "^5.6.0" } @@ -5071,6 +5127,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", "integrity": "sha512-F4wZ06PvqxYLFEZKkFxTDcns9oFNk34hvmJSEwdzsxVQ8YI5YaxtACgQatkYgv2VI2CFkUd2Y+xosPQnHv809g==", + "license": "MIT", "dependencies": { "browserify-aes": "^1.0.6", "create-hash": "^1.1.2", @@ -5102,6 +5159,7 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" } @@ -5110,6 +5168,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/ecpair/-/ecpair-2.1.0.tgz", "integrity": "sha512-cL/mh3MtJutFOvFc27GPZE2pWL3a3k4YvzUWEOvilnfZVlH3Jwgx/7d6tlD7/75tNk8TG2m+7Kgtz0SI1tWcqw==", + "license": "MIT", "dependencies": { "randombytes": "^2.1.0", "typeforce": "^1.18.0", @@ -6233,7 +6292,8 @@ "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" }, "node_modules/fill-range": { "version": "7.0.1", @@ -7554,6 +7614,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -7945,6 +8006,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", "dependencies": { "bignumber.js": "^9.0.0" } @@ -8168,6 +8230,7 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/ledger-bitcoin/-/ledger-bitcoin-0.2.3.tgz", "integrity": "sha512-sWdvMTR5CkebNlM0Mam9ROdpsD7Y4087kj4cbIaCCq8IXShCQ44vE3j0wTmt+sHp13eETgY63OWN1rkuIfMfuQ==", + "license": "Apache-2.0", "dependencies": { "@bitcoinerlab/descriptors": "^1.0.2", "@bitcoinerlab/secp256k1": "^1.0.5", @@ -8955,7 +9018,8 @@ "node_modules/nan": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", - "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==" + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "license": "MIT" }, "node_modules/nano-time": { "version": "1.0.0", @@ -10827,6 +10891,7 @@ "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.0.tgz", "integrity": "sha512-k5ke5avRZbtl9Tqx/SA7CbY3NF6Ro+Sj9cZxezFzuBlLDmyqPiL8hJJ+EmzD8Ig4LUDByHJ3/iPOVoRixs/hmw==", "hasInstallScript": true, + "license": "MIT", "dependencies": { "bindings": "^1.5.0", "bip66": "^1.1.5", @@ -10844,7 +10909,8 @@ "node_modules/secp256k1/node_modules/bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "license": "MIT" }, "node_modules/select-hose": { "version": "2.0.0", @@ -11160,6 +11226,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -12128,7 +12195,8 @@ "node_modules/typeforce": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", - "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" + "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==", + "license": "MIT" }, "node_modules/typescript": { "version": "5.5.4", @@ -12153,9 +12221,10 @@ } }, "node_modules/uglify-js": { - "version": "3.19.1", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.1.tgz", - "integrity": "sha512-y/2wiW+ceTYR2TSSptAhfnEtpLaQ4Ups5zrjB2d3kuVxHj16j/QJwPl5PvuGy9uARb39J0+iKxcRPvtpsx4A4A==", + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.2.tgz", + "integrity": "sha512-S8KA6DDI47nQXJSi2ctQ629YzwOVs+bQML6DAtvy0wgNdpi+0ySpQK0g2pxBq2xfF2z3YCscu7NNA8nXT9PlIQ==", + "license": "BSD-2-Clause", "bin": { "uglifyjs": "bin/uglifyjs" }, @@ -12269,6 +12338,7 @@ "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", @@ -12309,6 +12379,7 @@ "version": "6.2.13", "resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.13.tgz", "integrity": "sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ==", + "license": "MIT", "dependencies": { "@types/uuid": "8.3.4", "uuid": "8.3.2" @@ -12756,6 +12827,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", "integrity": "sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==", + "license": "MIT", "dependencies": { "bs58check": "<3.0.0" } @@ -12764,6 +12836,7 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.10.tgz", "integrity": "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==", + "license": "MIT", "dependencies": { "safe-buffer": "^5.0.1" } @@ -12772,6 +12845,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", "dependencies": { "base-x": "^3.0.2" } @@ -12780,6 +12854,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "license": "MIT", "dependencies": { "bs58": "^4.0.0", "create-hash": "^1.1.0", @@ -13771,9 +13846,9 @@ } }, "@secretkeylabs/xverse-core": { - "version": "18.7.2", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.7.2/0dc4ba04ad9b58f27031a976faf112a6648ea945", - "integrity": "sha512-B07GqC77UFeelKAjnpW0ZNjv7jc/m12PdVJJhRJdB7PRhsl+KHD8Z33JMHazdR6P3VbNlzUeEZ5R8WX+eBQWaA==", + "version": "18.8.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.0/ddb1e79eace136115e86ca0952613efb900e4501", + "integrity": "sha512-BgWjO5TQiHteiFqb5pAjxFcgU0mSo8XhT9CxXkWV3YnjEb+Z9l9MX0wTrJMKEwswyFpTE8WJEtKU3bWR4uuyJA==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", @@ -13782,14 +13857,14 @@ "@scure/bip32": "^1.4.0", "@scure/bip39": "^1.3.0", "@scure/btc-signer": "1.2.1", - "@stacks/auth": "6.13.1", + "@stacks/auth": "6.16.1", "@stacks/connect": "7.7.1", - "@stacks/encryption": "6.13.1", - "@stacks/network": "6.13.0", - "@stacks/stacking": "6.13.2", - "@stacks/storage": "6.13.1", - "@stacks/transactions": "6.13.1", - "@stacks/wallet-sdk": "6.13.1", + "@stacks/encryption": "6.16.1", + "@stacks/network": "6.16.0", + "@stacks/stacking": "6.16.1", + "@stacks/storage": "6.16.1", + "@stacks/transactions": "6.16.1", + "@stacks/wallet-sdk": "6.16.1", "@tanstack/react-query": "^4.29.3", "@zondax/ledger-stacks": "^1.0.4", "async-mutex": "^0.4.0", @@ -13906,22 +13981,22 @@ } }, "@stacks/auth": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-6.13.1.tgz", - "integrity": "sha512-nr5VeLIJBVI72eWYs/oQ7nrmuZxe89AlS5Lt/WxCcUDK9Z+oqQR1qsxBo86yVDlmrxoNWncHWD38LX54HsOEzA==", - "requires": { - "@stacks/common": "^6.13.0", - "@stacks/encryption": "^6.13.1", - "@stacks/network": "^6.13.0", - "@stacks/profile": "^6.13.1", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-6.16.1.tgz", + "integrity": "sha512-6Co7VZTlfvWVKEYXh+x2TD1e+3XmCPumJut1jRgW6pYPGruZ4Dz/R4xfkf9qX3MEJabLe4AfasSfoqftg0twTg==", + "requires": { + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.1", + "@stacks/network": "^6.16.0", + "@stacks/profile": "^6.16.1", "cross-fetch": "^3.1.5", "jsontokens": "^4.0.1" } }, "@stacks/common": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.13.0.tgz", - "integrity": "sha512-wwzyihjaSdmL6NxKvDeayy3dqM0L0Q2sawmdNtzJDi0FnXuJGm5PeapJj7bEfcI9XwI7Bw5jZoC6mCn9nc5YIw==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.16.0.tgz", + "integrity": "sha512-PnzvhrdGRMVZvxTulitlYafSK4l02gPCBBoI9QEoTqgSnv62oaOXhYAUUkTMFKxdHW1seVEwZsrahuXiZPIAwg==", "requires": { "@types/bn.js": "^5.1.0", "@types/node": "^18.0.4" @@ -13959,14 +14034,14 @@ } }, "@stacks/encryption": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.13.1.tgz", - "integrity": "sha512-y5IFX3/nGI3fCk70gE0JwH70GpshD8RhUfvhMLcL96oNaec1cCdj1ZUiQupeicfYTHuraaVBYU9xLls4TRmypg==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.16.1.tgz", + "integrity": "sha512-DtVNNW/iipyxxRDz73S9DbLfRmBMqQCCog89F1Q1i6JUnl2kBB1PR9SPQfYv9zcAJ37oHoNB4i4b2tJWYr01vg==", "requires": { "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.1", "@scure/bip39": "1.1.0", - "@stacks/common": "^6.13.0", + "@stacks/common": "^6.16.0", "@types/node": "^18.0.4", "base64-js": "^1.5.1", "bs58": "^5.0.0", @@ -13980,9 +14055,9 @@ "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==" }, "@types/node": { - "version": "18.19.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.31.tgz", - "integrity": "sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==", + "version": "18.19.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.47.tgz", + "integrity": "sha512-1f7dB3BL/bpd9tnDJrrHb66Y+cVrhxSOTGorRNdHwYTUlTay3HuTDPKo9a/4vX9pMQkhYBcAbL4jQdNlhCFP9A==", "requires": { "undici-types": "~5.26.4" } @@ -13990,39 +14065,39 @@ } }, "@stacks/network": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.13.0.tgz", - "integrity": "sha512-Ss/Da4BNyPBBj1OieM981fJ7SkevKqLPkzoI1+Yo7cYR2df+0FipIN++Z4RfpJpc8ne60vgcx7nJZXQsiGhKBQ==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.16.0.tgz", + "integrity": "sha512-uqz9Nb6uf+SeyCKENJN+idt51HAfEeggQKrOMfGjpAeFgZV2CR66soB/ci9+OVQR/SURvasncAz2ScI1blfS8A==", "requires": { - "@stacks/common": "^6.13.0", + "@stacks/common": "^6.16.0", "cross-fetch": "^3.1.5" } }, "@stacks/profile": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-6.13.1.tgz", - "integrity": "sha512-GCDE13hwoUYZvZKTb5c0Tr74DcxIP/n4bffcYrKa5UabITPQ7JwsJIOyDoAwdtl3lu7fi9aBsKrdfHpBBUSQIQ==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-6.16.1.tgz", + "integrity": "sha512-FJcwKN6oVDfmwymivkZfm1PcO7gv5fcWN4HI+jtYdMftrl6yxAB4/XD63FSvshIeLPzBJjnCmaZSnPAV2mtdeA==", "requires": { - "@stacks/common": "^6.13.0", - "@stacks/network": "^6.13.0", - "@stacks/transactions": "^6.13.1", + "@stacks/common": "^6.16.0", + "@stacks/network": "^6.16.0", + "@stacks/transactions": "^6.16.1", "jsontokens": "^4.0.1", "schema-inspector": "^2.0.2", "zone-file": "^2.0.0-beta.3" } }, "@stacks/stacking": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.13.2.tgz", - "integrity": "sha512-4h1UQuL2+Xdra9zMqzUElvKG9X9fenuNE7hD9sIqyxyLFxeQ7gRqczmTYPsmaj4wY5004JNj+efzGJ0VmpOcAA==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.16.1.tgz", + "integrity": "sha512-Bv7TSyoMrb1wYOfKrPxwDQPSjsohyKCduN1HYMlKL9hHF0J8+MvlJFw9eIj1c2SEOyYaElT+Ly3CI56J/acVxw==", "requires": { "@noble/hashes": "1.1.5", "@scure/base": "1.1.1", - "@stacks/common": "^6.13.0", - "@stacks/encryption": "^6.13.1", - "@stacks/network": "^6.13.0", + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.1", + "@stacks/network": "^6.16.0", "@stacks/stacks-blockchain-api-types": "^0.61.0", - "@stacks/transactions": "^6.13.1", + "@stacks/transactions": "^6.16.1", "bs58": "^5.0.0" }, "dependencies": { @@ -14049,27 +14124,27 @@ "integrity": "sha512-Mw5dBPx3DySPupwaq0iBdm1WdEVXIfhjUVaTjI2iSyzWz4Fgs3U7JCaAezLbgNu7Q69c/ZN4JUDWuo9FVjy7oA==" }, "@stacks/storage": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.13.1.tgz", - "integrity": "sha512-XnzRAETDKW7Ij3cuUNllrrnLOCwctV/XrIHgVWnD04LNL5R9apkwp9IkzaFbyJZ+XrkUBKwtdCgVYype2XgT6w==", - "requires": { - "@stacks/auth": "^6.13.1", - "@stacks/common": "^6.13.0", - "@stacks/encryption": "^6.13.1", - "@stacks/network": "^6.13.0", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.16.1.tgz", + "integrity": "sha512-bElxB03dg3XGTX8r/J8Os2tIsodcTglg7zeq6RU4FVUU7tUA+Agpmn9FKl8MmnsWfO4ls5UgeujyBaxUJ//yAw==", + "requires": { + "@stacks/auth": "^6.16.1", + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.1", + "@stacks/network": "^6.16.0", "base64-js": "^1.5.1", "jsontokens": "^4.0.1" } }, "@stacks/transactions": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.13.1.tgz", - "integrity": "sha512-PWw2I+2Fj3CaFYQIoVcqQN6E2qGHNhFv03nuR0CxMq0sx8stPgYZbdzUlnlBcJQdsFiHrw3sPeqnXDZt+Hg5YQ==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.16.1.tgz", + "integrity": "sha512-yCtUM+8IN0QJbnnlFhY1wBW7Q30Cxje3Zmy8DgqdBoM/EPPWadez/8wNWFANVAMyUZeQ9V/FY+8MAw4E+pCReA==", "requires": { "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.1", - "@stacks/common": "^6.13.0", - "@stacks/network": "^6.13.0", + "@stacks/common": "^6.16.0", + "@stacks/network": "^6.16.0", "c32check": "^2.0.0", "lodash.clonedeep": "^4.5.0" }, @@ -14082,19 +14157,19 @@ } }, "@stacks/wallet-sdk": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/wallet-sdk/-/wallet-sdk-6.13.1.tgz", - "integrity": "sha512-262CYKAm1j8oVxfGUIJrHp867j9gm5NrqPM85s0TfCv2QhfLDkvme6nKgmvtL2TecAZkBa5tu8M5DQ7z92WvAQ==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/wallet-sdk/-/wallet-sdk-6.16.1.tgz", + "integrity": "sha512-QYm/BfRpHZfD5FjvaOJp0hy8yeqde6qrEmSrcIl06IC2SNr4R5NOy0xK/RPUgJFB1/Gd16HbqS9EdyV3o6ybwg==", "requires": { "@scure/bip32": "1.1.3", "@scure/bip39": "1.1.0", - "@stacks/auth": "^6.13.1", - "@stacks/common": "^6.13.0", - "@stacks/encryption": "^6.13.1", - "@stacks/network": "^6.13.0", - "@stacks/profile": "^6.13.1", - "@stacks/storage": "^6.13.1", - "@stacks/transactions": "^6.13.1", + "@stacks/auth": "^6.16.1", + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.1", + "@stacks/network": "^6.16.0", + "@stacks/profile": "^6.16.1", + "@stacks/storage": "^6.16.1", + "@stacks/transactions": "^6.16.1", "buffer": "6.0.3", "c32check": "^2.0.0", "jsontokens": "^4.0.1", @@ -14967,9 +15042,9 @@ } }, "@types/node": { - "version": "18.19.42", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.42.tgz", - "integrity": "sha512-d2ZFc/3lnK2YCYhos8iaNIYu9Vfhr92nHiyJHRltXWjXUBjEE+A4I58Tdbnw4VhggSW+2j5y5gTrLs4biNnubg==", + "version": "18.19.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.47.tgz", + "integrity": "sha512-1f7dB3BL/bpd9tnDJrrHb66Y+cVrhxSOTGorRNdHwYTUlTay3HuTDPKo9a/4vX9pMQkhYBcAbL4jQdNlhCFP9A==", "requires": { "undici-types": "~5.26.4" } @@ -22001,9 +22076,9 @@ "requires": {} }, "uglify-js": { - "version": "3.19.1", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.1.tgz", - "integrity": "sha512-y/2wiW+ceTYR2TSSptAhfnEtpLaQ4Ups5zrjB2d3kuVxHj16j/QJwPl5PvuGy9uARb39J0+iKxcRPvtpsx4A4A==" + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.2.tgz", + "integrity": "sha512-S8KA6DDI47nQXJSi2ctQ629YzwOVs+bQML6DAtvy0wgNdpi+0ySpQK0g2pxBq2xfF2z3YCscu7NNA8nXT9PlIQ==" }, "unbox-primitive": { "version": "1.0.2", diff --git a/package.json b/package.json index 770833278..330e06879 100644 --- a/package.json +++ b/package.json @@ -43,10 +43,10 @@ "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.2.1", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "18.7.2", + "@secretkeylabs/xverse-core": "18.8.0", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", - "@stacks/transactions": "6.13.1", + "@stacks/transactions": "6.16.1", "@tanstack/query-sync-storage-persister": "^4.29.1", "@tanstack/react-query": "^4.29.3", "@tanstack/react-query-devtools": "^4.29.3", diff --git a/src/app/components/confirmStxTransactionComponent/index.tsx b/src/app/components/confirmStxTransactionComponent/index.tsx index e1c4c6531..5b53921cf 100644 --- a/src/app/components/confirmStxTransactionComponent/index.tsx +++ b/src/app/components/confirmStxTransactionComponent/index.tsx @@ -18,6 +18,7 @@ import { getNonce, getStxFiatEquivalent, microstacksToStx, + possiblNexteNonce, signLedgerStxTransaction, signMultiStxTransactions, signTransaction, @@ -99,7 +100,7 @@ function ConfirmStxTransactionComponent({ const { getSeed } = useSeedVault(); const [showFeeSettings, setShowFeeSettings] = useState(false); const selectedAccount = useSelectedAccount(); - const { feeMultipliers, fiatCurrency } = useWalletSelector(); + const { feeMultipliers, fiatCurrency, network } = useWalletSelector(); const [openTransactionSettingModal, setOpenTransactionSettingModal] = useState(false); const [buttonLoading, setButtonLoading] = useState(loading); const [isModalVisible, setIsModalVisible] = useState(false); @@ -220,8 +221,11 @@ function ConfirmStxTransactionComponent({ } if (initialStxTransactions.length === 1) { + const transaction = initialStxTransactions[0]; + const nonce = await possiblNexteNonce(selectedAccount.stxAddress, network); + transaction.setNonce(nonce); const signedContractCall = await signTransaction( - initialStxTransactions[0], + transaction, seed, selectedAccount?.id ?? 0, selectedNetwork, diff --git a/src/app/screens/mintRune/index.tsx b/src/app/screens/mintRune/index.tsx index 69c43d161..019ae1c96 100644 --- a/src/app/screens/mintRune/index.tsx +++ b/src/app/screens/mintRune/index.tsx @@ -81,6 +81,7 @@ function MintRune() { receipts: [], transfers: [], mint: { + runeId: runeInfo.id, runeName: runeInfo.entry.spaced_rune, amount: BigInt(runeInfo.entry.terms.amount?.toNumber() ?? 0), divisibility: runeInfo.entry.divisibility.toNumber(), From f8276766528c3090d2e600b0911b2c7721f71a0c Mon Sep 17 00:00:00 2001 From: Terence Ng Date: Thu, 29 Aug 2024 22:08:36 +0800 Subject: [PATCH 047/227] Rune Fiat Amount Selector (#533) * commit fix * commit fixes * fix state * remove redundant code * fix lock * commit new method using query * touchup conditional * resolve * fix up logic --------- Co-authored-by: Den <36603049+dhriaznov@users.noreply.github.com> --- .../itemRow/runeAmount.tsx | 31 ++-- .../queries/runes/useRuneFiatRateQuery.ts | 27 +++ src/app/screens/listRune/index.tsx | 36 +++- src/app/screens/listRune/reducer.tsx | 12 +- .../screens/listRune/setCustomPriceModal.tsx | 14 +- src/app/screens/mintRune/index.tsx | 2 +- src/app/screens/sendRune/amountSelector.tsx | 6 + src/app/screens/sendRune/index.tsx | 3 + .../screens/sendRune/runeAmountSelector.tsx | 155 +++++++++++++++--- src/app/screens/sendRune/stepDisplay.tsx | 6 + 10 files changed, 246 insertions(+), 46 deletions(-) create mode 100644 src/app/hooks/queries/runes/useRuneFiatRateQuery.ts diff --git a/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx b/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx index 76aa9d1ad..3c88ac06f 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx @@ -1,8 +1,12 @@ import { mapRuneNameToPlaceholder } from '@components/confirmBtcTransaction/utils'; +import { StyledFiatAmountText } from '@components/fiatAmountText'; import TokenImage from '@components/tokenImage'; +import useRuneFiatRateQuery from '@hooks/queries/runes/useRuneFiatRateQuery'; +import useWalletSelector from '@hooks/useWalletSelector'; import type { RuneBase } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import { ftDecimals } from '@utils/helper'; +import BigNumber from 'bignumber.js'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; @@ -36,11 +40,6 @@ const Column = styled.div` overflow: hidden; `; -const StyledPRight = styled(StyledP)` - word-break: break-all; - text-align: end; -`; - type Props = { rune: RuneBase; hasSufficientBalance?: boolean; @@ -55,6 +54,8 @@ export default function RuneAmount({ const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const { runeName, amount, divisibility, symbol, inscriptionId } = rune; const amountWithDecimals = ftDecimals(String(amount), divisibility); + const { fiatCurrency } = useWalletSelector(); + const { data: runeFiatRate } = useRuneFiatRateQuery(rune); return ( @@ -77,19 +78,29 @@ export default function RuneAmount({ thousandSeparator suffix={` ${symbol}`} renderText={(value: string) => ( - {value} - + )} /> - - {runeName} - + + + {runeName} + + {typeof runeFiatRate === 'number' && runeFiatRate > 0 && ( + + )} + ); diff --git a/src/app/hooks/queries/runes/useRuneFiatRateQuery.ts b/src/app/hooks/queries/runes/useRuneFiatRateQuery.ts new file mode 100644 index 000000000..0526db632 --- /dev/null +++ b/src/app/hooks/queries/runes/useRuneFiatRateQuery.ts @@ -0,0 +1,27 @@ +import useRunesApi from '@hooks/apiClients/useRunesApi'; +import { useRuneFungibleTokensQuery } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; +import useWalletSelector from '@hooks/useWalletSelector'; +import type { RuneBase } from '@secretkeylabs/xverse-core'; +import { useQuery } from '@tanstack/react-query'; +import { useCallback } from 'react'; + +export default function useRuneFiatRateQuery(rune: RuneBase) { + const { fiatCurrency } = useWalletSelector(); + const { data: runesCoinsList } = useRuneFungibleTokensQuery(); + const runesApi = useRunesApi(); + const queryFn = useCallback(async (): Promise => { + if (runesCoinsList) { + const fungibleToken = runesCoinsList.find((coin) => coin.principal === rune.runeId); + if (fungibleToken && fungibleToken.tokenFiatRate) { + return fungibleToken.tokenFiatRate; + } + } + const runeFiatRates = await runesApi.getRuneFiatRatesByRuneIds([rune.runeId], fiatCurrency); + return runeFiatRates[rune.runeId]?.[fiatCurrency] ?? 0; + }, [runesCoinsList, runesApi, rune.runeId, fiatCurrency]); + return useQuery({ + queryKey: ['get-rune-fiat-rate', rune.runeId, fiatCurrency], + enabled: Boolean(rune), + queryFn, + }); +} diff --git a/src/app/screens/listRune/index.tsx b/src/app/screens/listRune/index.tsx index cbfa4fe0f..258a5d06c 100644 --- a/src/app/screens/listRune/index.tsx +++ b/src/app/screens/listRune/index.tsx @@ -418,7 +418,13 @@ export default function ListRuneScreen() { - dispatch({ type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL', payload: true }) + dispatch({ + type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL', + payload: { + title: t('SET_PRICES'), + visible: true, + }, + }) } variant={ runePriceOption === 'custom' && !individualCustomPriceUsed @@ -458,7 +464,13 @@ export default function ListRuneScreen() { } handleShowCustomPriceModal={() => { dispatch({ type: 'SET_INDIVIDUAL_CUSTOM_ITEM', payload: fullTxId }); - dispatch({ type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL', payload: true }); + dispatch({ + type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL', + payload: { + title: t('EDIT_PRICE'), + visible: true, + }, + }); }} /> ))} @@ -539,8 +551,8 @@ export default function ListRuneScreen() { { dispatch({ type: 'SET_INDIVIDUAL_CUSTOM_ITEM', payload: null }); - dispatch({ type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL', payload: false }); + dispatch({ + type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL', + payload: { + ...toggleCustomPriceModal, + visible: false, + }, + }); }} onApplyPrice={(priceSats: number) => { if (individualCustomItem) { @@ -565,7 +583,13 @@ export default function ListRuneScreen() { dispatch({ type: 'SET_RUNE_PRICE_OPTION', payload: 'custom' }); } dispatch({ type: 'SET_INDIVIDUAL_CUSTOM_ITEM', payload: null }); - dispatch({ type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL', payload: false }); + dispatch({ + type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL', + payload: { + ...toggleCustomPriceModal, + visible: false, + }, + }); }} /> diff --git a/src/app/screens/listRune/reducer.tsx b/src/app/screens/listRune/reducer.tsx index 3867f8d45..bbc1454c0 100644 --- a/src/app/screens/listRune/reducer.tsx +++ b/src/app/screens/listRune/reducer.tsx @@ -7,7 +7,10 @@ interface ListRunesState { runePriceOption: 1 | 1.05 | 1.1 | 1.2 | 'custom'; customRunePrice: number | null; individualCustomItem: string | null; - toggleCustomPriceModal: boolean; + toggleCustomPriceModal: { + title: string; + visible: boolean; + }; } type Action = @@ -20,7 +23,7 @@ type Action = | { type: 'SET_RUNE_PRICE_OPTION'; payload: 1 | 1.05 | 1.1 | 1.2 | 'custom' } | { type: 'SET_CUSTOM_RUNE_PRICE'; payload: number | null } | { type: 'SET_INDIVIDUAL_CUSTOM_ITEM'; payload: string | null } - | { type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL'; payload: boolean } + | { type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL'; payload: { title: string; visible: boolean } } | { type: 'RESTORE_STATE_FROM_PSBT'; payload: ListRunesState }; export const initialListRunesState: ListRunesState = { @@ -30,7 +33,10 @@ export const initialListRunesState: ListRunesState = { runePriceOption: 1, customRunePrice: null, individualCustomItem: null, - toggleCustomPriceModal: false, + toggleCustomPriceModal: { + title: '', + visible: false, + }, }; export function ListRunesReducer(state: ListRunesState, action: Action): ListRunesState { diff --git a/src/app/screens/listRune/setCustomPriceModal.tsx b/src/app/screens/listRune/setCustomPriceModal.tsx index 09e1853ee..bbd8f094d 100644 --- a/src/app/screens/listRune/setCustomPriceModal.tsx +++ b/src/app/screens/listRune/setCustomPriceModal.tsx @@ -4,7 +4,7 @@ import { StyledP } from '@ui-library/common.styled'; import Input from '@ui-library/input'; import Sheet from '@ui-library/sheet'; import { formatToXDecimalPlaces } from '@utils/helper'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; @@ -61,6 +61,17 @@ function SetCustomPriceModal({ const lowError: boolean = priceSats.length !== 0 && Number(priceSats) < minPriceSats; const highError: boolean = priceSats.length !== 0 && Number(priceSats) > maxPriceSats; + const inputFeedback = useMemo(() => { + if (lowError || highError) { + return [ + { + variant: 'danger' as const, + message: '', + }, + ]; + } + }, [highError, lowError]); + return ( } + feedback={inputFeedback} hideClear autoFocus /> diff --git a/src/app/screens/mintRune/index.tsx b/src/app/screens/mintRune/index.tsx index 019ae1c96..134ccb7c8 100644 --- a/src/app/screens/mintRune/index.tsx +++ b/src/app/screens/mintRune/index.tsx @@ -81,8 +81,8 @@ function MintRune() { receipts: [], transfers: [], mint: { - runeId: runeInfo.id, runeName: runeInfo.entry.spaced_rune, + runeId: runeInfo.id, amount: BigInt(runeInfo.entry.terms.amount?.toNumber() ?? 0), divisibility: runeInfo.entry.divisibility.toNumber(), symbol: runeInfo.entry.symbol, diff --git a/src/app/screens/sendRune/amountSelector.tsx b/src/app/screens/sendRune/amountSelector.tsx index 2be6d04f1..877d2a9bf 100644 --- a/src/app/screens/sendRune/amountSelector.tsx +++ b/src/app/screens/sendRune/amountSelector.tsx @@ -31,6 +31,8 @@ type Props = { token: FungibleToken; amountToSend: string; setAmountToSend: (amount: string) => void; + useTokenValue: boolean; + setUseTokenValue: (toggle: boolean) => void; amountError: string; feeRate: string; setFeeRate: (feeRate: string) => void; @@ -49,6 +51,8 @@ function AmountSelector({ token, amountToSend, setAmountToSend, + useTokenValue, + setUseTokenValue, amountError, feeRate, setFeeRate, @@ -86,6 +90,8 @@ function AmountSelector({ token={token} amountToSend={amountToSend} setAmountToSend={setAmountToSend} + useTokenValue={useTokenValue} + setUseTokenValue={setUseTokenValue} amountError={amountError} sendMax={sendMax} setSendMax={setSendMax} diff --git a/src/app/screens/sendRune/index.tsx b/src/app/screens/sendRune/index.tsx index 842e4ac33..7507b4c22 100644 --- a/src/app/screens/sendRune/index.tsx +++ b/src/app/screens/sendRune/index.tsx @@ -42,6 +42,7 @@ function SendRuneScreen() { ); const [amountError, setAmountError] = useState(''); const [amountToSend, setAmountToSend] = useState(location.state?.amount || ''); + const [useTokenValue, setUseTokenValue] = useState(true); const [feeRate, setFeeRate] = useState(''); const [sendMax, setSendMax] = useState(false); const [currentStep, setCurrentStep] = useState(Step.SelectRecipient); @@ -235,6 +236,8 @@ function SendRuneScreen() { token={fungibleToken} amountToSend={amountToSend} setAmountToSend={setAmount} + useTokenValue={useTokenValue} + setUseTokenValue={setUseTokenValue} amountError={amountError} currentStep={currentStep} setCurrentStep={setCurrentStep} diff --git a/src/app/screens/sendRune/runeAmountSelector.tsx b/src/app/screens/sendRune/runeAmountSelector.tsx index e99fc02f3..fea1e4543 100644 --- a/src/app/screens/sendRune/runeAmountSelector.tsx +++ b/src/app/screens/sendRune/runeAmountSelector.tsx @@ -1,26 +1,58 @@ -import type { FungibleToken } from '@secretkeylabs/xverse-core'; -import Input, { MaxButton, VertRule } from '@ui-library/input'; -import { getFtBalance } from '@utils/tokens'; -import { useEffect } from 'react'; +import FiatAmountText from '@components/fiatAmountText'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { ArrowsDownUp } from '@phosphor-icons/react'; +import { currencySymbolMap, type FungibleToken } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import Input, { ConvertComplication, MaxButton, VertRule } from '@ui-library/input'; +import { ftDecimals } from '@utils/helper'; +import { getFtTicker } from '@utils/tokens'; +import BigNumber from 'bignumber.js'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; +import Theme from 'theme'; + +const BalanceTextWrapper = styled.div` + text-align: right; +`; const BalanceText = styled.span` ${(props) => props.theme.typography.body_medium_m} color: ${(props) => props.theme.colors.white_200}; `; -const BalanceDiv = styled.div` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; +const AmountText = styled(StyledP)` + margin-right: ${(props) => props.theme.space.xxs}; +`; + +const StyledFiatAmountText = styled(FiatAmountText)` + ${(props) => props.theme.typography.body_medium_s} + color: ${(props) => props.theme.colors.white_200}; `; +const fiatInputValidator = /^[0-9]+[.]?[0-9]{0,2}$/; +const tokenInputValidator = (decimals: number) => { + // If no decimals are allowed, only allow whole numbers (no decimal point) + if (decimals === 0) { + return /^[0-9]*$/; // Match only whole numbers + } + // Otherwise, allow numbers with the specified number of decimal places + return new RegExp(`^[0-9]+([.][0-9]{0,${decimals}})?$`); +}; + +const getTokenFiatEquivalent = (amount: BigNumber, fungibleToken: FungibleToken): BigNumber => + amount.multipliedBy(fungibleToken.tokenFiatRate ?? 0); + +const getFiatTokenEquivalent = (fiatAmount: BigNumber, fungibleToken: FungibleToken): BigNumber => + fiatAmount.dividedBy(fungibleToken.tokenFiatRate ?? 1); + type Props = { token: FungibleToken; amountToSend: string; setAmountToSend: (amount: string) => void; + useTokenValue: boolean; + setUseTokenValue: (toggle: boolean) => void; sendMax: boolean; setSendMax: (sendMax: boolean) => void; amountError: string; @@ -30,26 +62,68 @@ function RuneAmountSelector({ token, amountToSend, setAmountToSend, + useTokenValue, + setUseTokenValue, sendMax, setSendMax, amountError, }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'SEND' }); - // construct regex based on runes divisibility (decimals) - const tokenInputValidator = new RegExp(`^(?:[0-9]+(?:\\.[0-9]{0,${token.decimals}})?)?$`); - const balance = getFtBalance(token); + const { fiatCurrency } = useWalletSelector(); + const [displayAmount, setDisplayAmount] = useState(amountToSend); + const tokenDecimals = Number(token.decimals ?? 0); + const tokenBalance = new BigNumber(ftDecimals(token.balance, tokenDecimals)); + const displayBalance = useTokenValue + ? Number(tokenBalance) + : getTokenFiatEquivalent(tokenBalance, token).toNumber().toFixed(2); useEffect(() => { - if (sendMax) { - setAmountToSend(String(balance)); - } - }, [sendMax]); // eslint-disable-line react-hooks/exhaustive-deps + if (!sendMax) return; + setDisplayAmount( + useTokenValue + ? tokenBalance.toString() + : getTokenFiatEquivalent(tokenBalance, token).toNumber().toFixed(2), + ); + setAmountToSend(tokenBalance.toString()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sendMax]); const handleAmountChange = (newAmount: string) => { - const isValidInput = tokenInputValidator.test(newAmount); - if (isValidInput) { - setAmountToSend(newAmount); + if (!newAmount) { + setAmountToSend('0'); + setDisplayAmount(''); setSendMax(false); + return; + } + const isValidInput = useTokenValue + ? tokenInputValidator(tokenDecimals).test(newAmount) + : fiatInputValidator.test(newAmount); + + if (!isValidInput) return; + setDisplayAmount(newAmount); + setSendMax(false); + if (useTokenValue) { + setAmountToSend(BigNumber(newAmount).toString()); + } else { + // strip to exact decimals the rune support + const runeAmount = getFiatTokenEquivalent(BigNumber(newAmount), token) + .toNumber() + .toFixed(tokenDecimals); + setAmountToSend(runeAmount); + } + }; + + const handleUseTokenValueChange = () => { + const shouldUseTokenValue = !useTokenValue; + setUseTokenValue(shouldUseTokenValue); + if (!displayAmount || Number.isNaN(+displayAmount)) return; + if (shouldUseTokenValue) { + setDisplayAmount(amountToSend); + } else { + const fiatAmount = getTokenFiatEquivalent(BigNumber(amountToSend), token) + .toNumber() + .toFixed(2); + setDisplayAmount(fiatAmount); } }; @@ -57,19 +131,50 @@ function RuneAmountSelector({ return ( handleAmountChange(e.target.value)} placeholder="0" infoPanel={ - - {t('BALANCE')} - - + ( + + {t('BALANCE')} {value}{' '} + {useTokenValue ? getFtTicker(token) : fiatCurrency} + + )} + /> } complications={ <> + {token.tokenFiatRate && ( + + {useTokenValue ? ( + + ) : ( + ( +
+ + {value} {getFtTicker(token)} + +
+ )} + /> + )} + +
+ )} MAX diff --git a/src/app/screens/sendRune/stepDisplay.tsx b/src/app/screens/sendRune/stepDisplay.tsx index 7d8d9fe57..34335609d 100644 --- a/src/app/screens/sendRune/stepDisplay.tsx +++ b/src/app/screens/sendRune/stepDisplay.tsx @@ -34,6 +34,8 @@ type Props = { runeSummary: RuneSummary | undefined; amountToSend: string; setAmountToSend: (amount: string) => void; + useTokenValue: boolean; + setUseTokenValue: (toggle: boolean) => void; amountError: string; currentStep: Step; setCurrentStep: (step: Step) => void; @@ -57,6 +59,8 @@ function StepDisplay({ runeSummary, amountToSend, setAmountToSend, + useTokenValue, + setUseTokenValue, amountError, currentStep, setCurrentStep, @@ -106,6 +110,8 @@ function StepDisplay({ header={header} amountToSend={amountToSend} setAmountToSend={setAmountToSend} + useTokenValue={useTokenValue} + setUseTokenValue={setUseTokenValue} amountError={amountError} feeRate={feeRate} setFeeRate={setFeeRate} From 85c7f7512142dd10f32016ed1676d9c59a07f41d Mon Sep 17 00:00:00 2001 From: Terence Ng Date: Thu, 29 Aug 2024 22:35:17 +0800 Subject: [PATCH 048/227] Terence/fix bug (#539) * commit fix * fix bug --- src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx b/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx index 3c88ac06f..97d9bf939 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx @@ -92,7 +92,7 @@ export default function RuneAmount({ {runeName} - {typeof runeFiatRate === 'number' && runeFiatRate > 0 && ( + {runeFiatRate !== undefined && runeFiatRate > 0 && ( Date: Thu, 29 Aug 2024 16:35:35 +0200 Subject: [PATCH 049/227] fix: version (#538) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7efd6dc38..3a18c7e62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "xverse-web-extension", - "version": "0.41.0", + "version": "0.40.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "xverse-web-extension", - "version": "0.41.0", + "version": "0.40.1", "dependencies": { "@aryzing/superqs": "0.0.6", "@ledgerhq/hw-transport-webusb": "^6.27.13", diff --git a/package.json b/package.json index 330e06879..ac07bbf0b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xverse-web-extension", "description": "A Bitcoin wallet for Web3", - "version": "0.41.0", + "version": "0.40.1", "private": true, "engines": { "node": "^18.18.2" From 396dc6b89fbce64a0ede5340af0479bf819b51e6 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Thu, 29 Aug 2024 14:43:03 +0000 Subject: [PATCH 050/227] release: v0.41.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3a18c7e62..7efd6dc38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "xverse-web-extension", - "version": "0.40.1", + "version": "0.41.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "xverse-web-extension", - "version": "0.40.1", + "version": "0.41.0", "dependencies": { "@aryzing/superqs": "0.0.6", "@ledgerhq/hw-transport-webusb": "^6.27.13", diff --git a/package.json b/package.json index ac07bbf0b..330e06879 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xverse-web-extension", "description": "A Bitcoin wallet for Web3", - "version": "0.40.1", + "version": "0.41.0", "private": true, "engines": { "node": "^18.18.2" From fda4bb21fb20c4ea5b4f51737a787b708514371e Mon Sep 17 00:00:00 2001 From: fede erbes Date: Thu, 29 Aug 2024 16:52:03 +0200 Subject: [PATCH 051/227] fix: remove test step which is not longer used (#542) --- .github/workflows/build-rc.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build-rc.yml b/.github/workflows/build-rc.yml index b2a30ffc1..29ba056be 100644 --- a/.github/workflows/build-rc.yml +++ b/.github/workflows/build-rc.yml @@ -34,7 +34,6 @@ jobs: npm run knip npx eslint . npx tsc --noEmit - npm test - name: Build env: TRANSAC_API_KEY: ${{ secrets.TRANSAC_API_KEY }} From a42247c4c134ef80d4bd8789321e8c5d18ae24cd Mon Sep 17 00:00:00 2001 From: fede erbes Date: Thu, 29 Aug 2024 18:26:48 +0200 Subject: [PATCH 052/227] fix: e2e v0.41.0 (#543) * fix: missing send-input * fixed selectors for update password flow --------- Co-authored-by: DuskaT021 --- src/app/screens/sendRune/runeAmountSelector.tsx | 1 + tests/pages/wallet.ts | 4 ++-- tests/specs/tabSettings.spec.ts | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/app/screens/sendRune/runeAmountSelector.tsx b/src/app/screens/sendRune/runeAmountSelector.tsx index fea1e4543..86b36c135 100644 --- a/src/app/screens/sendRune/runeAmountSelector.tsx +++ b/src/app/screens/sendRune/runeAmountSelector.tsx @@ -133,6 +133,7 @@ function RuneAmountSelector({ handleAmountChange(e.target.value)} placeholder="0" infoPanel={ diff --git a/tests/pages/wallet.ts b/tests/pages/wallet.ts index 2b2ffe8f7..5c7725849 100644 --- a/tests/pages/wallet.ts +++ b/tests/pages/wallet.ts @@ -428,9 +428,9 @@ export default class Wallet { this.inputBTCURL = page.getByTestId('BTC URL'); this.inputFallbackBTCURL = page.getByTestId('Fallback BTC URL'); this.buttonUpdatePassword = page.getByRole('button', { name: 'Update Password' }); - this.errorMessage = page.getByRole('heading', { name: 'Incorrect password' }); + this.errorMessage = page.getByText(/incorrect password/i); this.headerNewPassword = page.getByRole('heading', { name: 'Enter your new password' }); - this.infoUpdatePassword = page.getByRole('heading', { name: 'Password successfully updated' }); + this.infoUpdatePassword = page.getByText(/password successfully updated/i); this.buttonCurrency = page.getByRole('button', { name: 'Fiat Currency' }); this.buttonShowSeedphrase = page.getByRole('button', { name: 'Show Seedphrase' }); this.selectCurrency = page.getByTestId('currency-button'); diff --git a/tests/specs/tabSettings.spec.ts b/tests/specs/tabSettings.spec.ts index fef6ccd37..d814122cc 100644 --- a/tests/specs/tabSettings.spec.ts +++ b/tests/specs/tabSettings.spec.ts @@ -90,10 +90,10 @@ test.describe('Settings Tab', () => { await expect(onboardingPage.buttonContinue).toBeEnabled(); await onboardingPage.buttonContinue.click(); await expect(onboardingPage.inputPassword).toBeVisible(); - await expect(onboardingPage.buttonContinue).toBeDisabled(); + await expect(wallet.buttonConfirm).toBeDisabled(); await onboardingPage.inputPassword.fill(`${strongPW}ABC`); - await expect(onboardingPage.buttonContinue).toBeEnabled(); - await onboardingPage.buttonContinue.click(); + await expect(wallet.buttonConfirm).toBeEnabled(); + await wallet.buttonConfirm.click(); await expect(wallet.infoUpdatePassword).toBeVisible(); }); test('Show Seedphrase', async ({ page, extensionId }) => { From dad818a49f3a4008dc4a0548be952752ea19b9bc Mon Sep 17 00:00:00 2001 From: Terence Ng Date: Fri, 30 Aug 2024 11:38:55 +0800 Subject: [PATCH 053/227] commit --- src/app/components/tokenImage/index.tsx | 47 +++++++++---------------- 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/src/app/components/tokenImage/index.tsx b/src/app/components/tokenImage/index.tsx index 3243ed5f5..378a737c7 100644 --- a/src/app/components/tokenImage/index.tsx +++ b/src/app/components/tokenImage/index.tsx @@ -94,6 +94,17 @@ export default function TokenImage({ } }, [currency]); + const ticker = + fungibleToken?.runeSymbol || + fungibleToken?.ticker || + (fungibleToken?.name ? getTicker(fungibleToken.name) : fungibleToken?.assetName || ''); + + const tickerComponent = () => ( + + {ticker.substring(0, 4)} + + ); + const getProtocolIcon = () => { if (!ftProtocol) { return null; @@ -111,18 +122,6 @@ export default function TokenImage({ }; const renderIcon = () => { - const ticker = - fungibleToken?.ticker || - (fungibleToken?.name ? getTicker(fungibleToken.name) : fungibleToken?.assetName || ''); - - if (imageError) { - return ( - - {ticker.substring(0, 4)} - - ); - } - if (!fungibleToken) { return ( ); } - if (fungibleToken?.image) { + if (fungibleToken.runeInscriptionId) { return ( setImageError(true)} /> ); } - if (fungibleToken.runeInscriptionId) { + if (fungibleToken?.image) { return ( setImageError(true)} /> ); } - if (fungibleToken.runeSymbol) { - return ( - - {fungibleToken.runeSymbol} - - ); - } - - return ( - - {ticker.substring(0, 4)} - - ); + return tickerComponent(); }; if (loading) { @@ -180,7 +167,7 @@ export default function TokenImage({ return ( - {renderIcon()} + {imageError ? tickerComponent() : renderIcon()} {ftProtocol && showProtocolIcon && ( {getProtocolIcon()} )} From f1a0069c50f618576bbdcd8e1839369e91033f3c Mon Sep 17 00:00:00 2001 From: Terence Ng Date: Fri, 30 Aug 2024 12:23:12 +0800 Subject: [PATCH 054/227] fix logic for non-runes --- src/app/components/tokenImage/index.tsx | 30 ++++++++++++++++--------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/app/components/tokenImage/index.tsx b/src/app/components/tokenImage/index.tsx index 378a737c7..933d78c83 100644 --- a/src/app/components/tokenImage/index.tsx +++ b/src/app/components/tokenImage/index.tsx @@ -95,7 +95,6 @@ export default function TokenImage({ }, [currency]); const ticker = - fungibleToken?.runeSymbol || fungibleToken?.ticker || (fungibleToken?.name ? getTicker(fungibleToken.name) : fungibleToken?.assetName || ''); @@ -132,15 +131,26 @@ export default function TokenImage({ /> ); } - if (fungibleToken.runeInscriptionId) { - return ( - setImageError(true)} - /> - ); + if (fungibleToken.protocol === 'runes') { + if (fungibleToken.runeInscriptionId) { + return ( + setImageError(true)} + /> + ); + } + if (fungibleToken?.runeSymbol) { + return ( + + {fungibleToken.runeSymbol} + + ); + } } if (fungibleToken?.image) { return ( From 11bce92c3c0aa3416e4fe86fb7857ac008d142bc Mon Sep 17 00:00:00 2001 From: Terence Ng Date: Fri, 30 Aug 2024 12:51:27 +0800 Subject: [PATCH 055/227] fix --- .../components/confirmBtcTransaction/itemRow/runeAmount.tsx | 4 ++-- src/app/components/fiatAmountText/index.tsx | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx b/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx index 97d9bf939..d10e0072d 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx @@ -1,5 +1,5 @@ import { mapRuneNameToPlaceholder } from '@components/confirmBtcTransaction/utils'; -import { StyledFiatAmountText } from '@components/fiatAmountText'; +import { RightAlignedStyledFiatAmountText } from '@components/fiatAmountText'; import TokenImage from '@components/tokenImage'; import useRuneFiatRateQuery from '@hooks/queries/runes/useRuneFiatRateQuery'; import useWalletSelector from '@hooks/useWalletSelector'; @@ -93,7 +93,7 @@ export default function RuneAmount({ {runeName} {runeFiatRate !== undefined && runeFiatRate > 0 && ( - props.theme.colors.white_400}; `; +export const RightAlignedStyledFiatAmountText = styled(StyledFiatAmountText)` + text-align: right; +`; + export default FiatAmountText; From ed2d360968d4f2c95adad6d2c20bbff42b1c9792 Mon Sep 17 00:00:00 2001 From: "Jordan K." <65149726+jordankzf@users.noreply.github.com> Date: Fri, 30 Aug 2024 18:18:33 +0800 Subject: [PATCH 056/227] Stacks Swaps Fixes (#535) * Remove fee rate for SIP-10 swaps * Remove feature flag hardcode * Add Mixpanel tracking * Bump xverse-core * hide fee rate if not btc/runes * small tweak * small tweak * Improve mixpanel tracking --------- Co-authored-by: Terence Ng --- package-lock.json | 14 ++-- package.json | 2 +- .../itemRow/runeAmount.tsx | 6 +- .../components/confirmBtcTransaction/utils.ts | 21 +++--- src/app/hooks/useHasFeature.ts | 4 - src/app/screens/coinDashboard/coinHeader.tsx | 7 +- .../psbtConfirmation/useExecuteOrder.tsx | 1 + .../components/tokenFromBottomSheet/index.tsx | 2 + .../components/tokenToBottomSheet/index.tsx | 4 + src/app/screens/swap/index.tsx | 14 +++- src/app/screens/swap/mixpanel.ts | 75 +++++++++++++++---- src/app/screens/swap/quoteSummary/index.tsx | 61 ++++++++------- src/app/screens/swap/utils.ts | 24 ++++++ 13 files changed, 163 insertions(+), 72 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7efd6dc38..28aa9806f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.2.1", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "18.8.0", + "@secretkeylabs/xverse-core": "18.8.1", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.16.1", @@ -1279,9 +1279,9 @@ } }, "node_modules/@secretkeylabs/xverse-core": { - "version": "18.8.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.0/ddb1e79eace136115e86ca0952613efb900e4501", - "integrity": "sha512-BgWjO5TQiHteiFqb5pAjxFcgU0mSo8XhT9CxXkWV3YnjEb+Z9l9MX0wTrJMKEwswyFpTE8WJEtKU3bWR4uuyJA==", + "version": "18.8.1", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.1/537d8964f3673681cfe0e1afe5c0acecb83729d4", + "integrity": "sha512-NEy5LN1+oj8acsqRGkOmuDfVFfoe1Inwt7wNBaqZQGGSaMac6mgYlaaxwrZUsyOtLZ9e1kzbht1ng3Lzjq/ynQ==", "license": "ISC", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", @@ -13846,9 +13846,9 @@ } }, "@secretkeylabs/xverse-core": { - "version": "18.8.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.0/ddb1e79eace136115e86ca0952613efb900e4501", - "integrity": "sha512-BgWjO5TQiHteiFqb5pAjxFcgU0mSo8XhT9CxXkWV3YnjEb+Z9l9MX0wTrJMKEwswyFpTE8WJEtKU3bWR4uuyJA==", + "version": "18.8.1", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.1/537d8964f3673681cfe0e1afe5c0acecb83729d4", + "integrity": "sha512-NEy5LN1+oj8acsqRGkOmuDfVFfoe1Inwt7wNBaqZQGGSaMac6mgYlaaxwrZUsyOtLZ9e1kzbht1ng3Lzjq/ynQ==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", diff --git a/package.json b/package.json index 330e06879..fede151db 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.2.1", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "18.8.0", + "@secretkeylabs/xverse-core": "18.8.1", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.16.1", diff --git a/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx b/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx index d10e0072d..979f1849b 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx @@ -1,4 +1,4 @@ -import { mapRuneNameToPlaceholder } from '@components/confirmBtcTransaction/utils'; +import { mapRuneBaseToFungibleToken } from '@components/confirmBtcTransaction/utils'; import { RightAlignedStyledFiatAmountText } from '@components/fiatAmountText'; import TokenImage from '@components/tokenImage'; import useRuneFiatRateQuery from '@hooks/queries/runes/useRuneFiatRateQuery'; @@ -52,7 +52,7 @@ export default function RuneAmount({ topMargin = false, }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const { runeName, amount, divisibility, symbol, inscriptionId } = rune; + const { runeName, amount, divisibility, symbol } = rune; const amountWithDecimals = ftDecimals(String(amount), divisibility); const { fiatCurrency } = useWalletSelector(); const { data: runeFiatRate } = useRuneFiatRateQuery(rune); @@ -61,7 +61,7 @@ export default function RuneAmount({ ({ +export const mapRuneBaseToFungibleToken = (rune: RuneBase): FungibleToken => ({ protocol: 'runes', - name: runeName, + name: rune.runeName, + principal: rune.runeId, assetName: '', balance: '', - principal: '', total_received: '', total_sent: '', - runeSymbol: symbol, - runeInscriptionId: inscriptionId, + runeSymbol: rune.symbol, + runeInscriptionId: rune.inscriptionId, }); diff --git a/src/app/hooks/useHasFeature.ts b/src/app/hooks/useHasFeature.ts index 298ace532..9435a0198 100644 --- a/src/app/hooks/useHasFeature.ts +++ b/src/app/hooks/useHasFeature.ts @@ -17,9 +17,5 @@ const useAppFeatures = () => { export default function useHasFeature(feature: FeatureId): boolean { const { data } = useAppFeatures(); - - // Remove before merging - if (feature === FeatureId.STACKS_SWAPS) return true; - return data?.[feature]?.enabled ?? false; } diff --git a/src/app/screens/coinDashboard/coinHeader.tsx b/src/app/screens/coinDashboard/coinHeader.tsx index a523f9f35..2f0f56e8f 100644 --- a/src/app/screens/coinDashboard/coinHeader.tsx +++ b/src/app/screens/coinDashboard/coinHeader.tsx @@ -182,7 +182,12 @@ export default function CoinHeader({ currency, fungibleToken }: Props) { return; } trackMixPanel(AnalyticsEvents.InitiateSwapFlow, { - selectedToken: fungibleToken ? fungibleToken.name ?? fungibleToken.principal : currency, + selectedToken: fungibleToken ? fungibleToken.name : currency, + principal: fungibleToken + ? fungibleToken?.principal + : currency === 'STX' + ? currency + : undefined, }); navigate(`/swap?from=${fungibleToken ? fungibleToken.principal : currency}`); }; diff --git a/src/app/screens/swap/components/psbtConfirmation/useExecuteOrder.tsx b/src/app/screens/swap/components/psbtConfirmation/useExecuteOrder.tsx index afce53d08..94e99675b 100644 --- a/src/app/screens/swap/components/psbtConfirmation/useExecuteOrder.tsx +++ b/src/app/screens/swap/components/psbtConfirmation/useExecuteOrder.tsx @@ -35,6 +35,7 @@ const useExecuteOrder = () => { try { const response = await xverseApiClient.swaps.executeStxOrder(request); setOrder(response); + return response; } catch (err: any) { setError(err?.response?.data?.message ?? 'Failed to execute order'); diff --git a/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx b/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx index 4dbbe6566..05234fa21 100644 --- a/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx +++ b/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx @@ -62,6 +62,7 @@ export default function TokenFromBottomSheet({ visible, title, onSelectCoin, onC onSelectCoin(token); trackMixPanel(AnalyticsEvents.SelectTokenToSwapFrom, { selectedToken: 'Stacks', + principal: 'STX', }); onClose(); }} @@ -78,6 +79,7 @@ export default function TokenFromBottomSheet({ visible, title, onSelectCoin, onC onSelectCoin(token); trackMixPanel(AnalyticsEvents.SelectTokenToSwapFrom, { selectedToken: token.name, + principal: token.protocol === 'stacks' ? token.principal : undefined, }); onClose(); }} diff --git a/src/app/screens/swap/components/tokenToBottomSheet/index.tsx b/src/app/screens/swap/components/tokenToBottomSheet/index.tsx index 4d914d140..2953d0f5a 100644 --- a/src/app/screens/swap/components/tokenToBottomSheet/index.tsx +++ b/src/app/screens/swap/components/tokenToBottomSheet/index.tsx @@ -2,8 +2,10 @@ import TokenTile from '@components/tokenTile'; import useDebounce from '@hooks/useDebounce'; import { MagnifyingGlass } from '@phosphor-icons/react'; import { + isStxTx, mapFTMotherProtocolToSwapProtocol, mapFTProtocolToSwapProtocol, + mapSwapProtocolToFTProtocol, mapSwapTokenToFT, } from '@screens/swap/utils'; import { @@ -194,6 +196,7 @@ export default function TokenToBottomSheet({ onSelectCoin(token); trackMixPanel(AnalyticsEvents.SelectTokenToSwapTo, { selectedToken: 'Stacks', + principal: 'STX', }); handleClose(); }} @@ -211,6 +214,7 @@ export default function TokenToBottomSheet({ onSelectCoin(token); trackMixPanel(AnalyticsEvents.SelectTokenToSwapTo, { selectedToken: token.name ?? token.ticker, + principal: isStxTx({ toToken: token }) ? token.ticker : undefined, }); handleClose(); }} diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index cafe50622..f01e5941e 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -46,7 +46,9 @@ import QuoteSummary from './quoteSummary'; import QuotesModal from './quotesModal'; import type { OrderInfo, Side, StxOrderInfo } from './types'; import useMasterCoinsList from './useMasterCoinsList'; +import { useStxCurrencyConversion } from './useStxCurrencyConversion'; import { + isStxTx, mapFTNativeSwapTokenToTokenBasic, mapFTProtocolToSwapProtocol, mapFtToSwapToken, @@ -136,6 +138,7 @@ export default function SwapScreen() { const { quotes, loading: quotesLoading, error: quotesError, fetchQuotes } = useGetQuotes(); const { data: runeFloorPrice } = useRuneFloorPriceQuery(toToken?.name ?? ''); const coinsMasterList = useMasterCoinsList(); + const { acceptableCoinList: sip10CoinsList } = useStxCurrencyConversion(); useEffect(() => { if (defaultFrom) { @@ -178,6 +181,8 @@ export default function SwapScreen() { quote, btcUsdRate, runeFloorPrice, + stxBtcRate, + sip10CoinsList, }); fetchQuotes({ @@ -213,7 +218,6 @@ export default function SwapScreen() { if (isSwapRouteDisabled) { return; } - setInputError(''); setAmount(''); setHasQuoteError(false); @@ -356,8 +360,10 @@ export default function SwapScreen() { trackMixPanel(AnalyticsEvents.SelectSwapQuote, { provider: provider.provider.name, - from: fromToken.principal === 'BTC' ? 'BTC' : fromToken.name, - to: toToken.protocol === 'btc' ? 'BTC' : toToken.name ?? toToken.ticker, + from: fromToken.name, + to: toToken.name ?? toToken.ticker, + fromPrincipal: isStxTx({ fromToken, toToken }) ? fromToken.principal : undefined, + toPrincipal: isStxTx({ fromToken, toToken }) ? toToken.ticker : undefined, }); if (isAmm) { @@ -393,6 +399,8 @@ export default function SwapScreen() { quote, btcUsdRate, runeFloorPrice, + stxBtcRate, + sip10CoinsList, }); }; diff --git a/src/app/screens/swap/mixpanel.ts b/src/app/screens/swap/mixpanel.ts index aace8675d..32da62cf4 100644 --- a/src/app/screens/swap/mixpanel.ts +++ b/src/app/screens/swap/mixpanel.ts @@ -1,5 +1,7 @@ import { getBtcFiatEquivalent, + getStxFiatEquivalent, + stxToMicrostacks, type FungibleToken, type Provider, type Quote, @@ -7,6 +9,7 @@ import { } from '@secretkeylabs/xverse-core'; import { trackMixPanel } from '@utils/mixpanel'; import BigNumber from 'bignumber.js'; +import { isRunesTx } from './utils'; function trackSwapMixPanel( eventName, @@ -18,6 +21,8 @@ function trackSwapMixPanel( quote, btcUsdRate, runeFloorPrice, + stxBtcRate, + sip10CoinsList, }: { provider?: Provider; fromToken?: FungibleToken; @@ -26,30 +31,68 @@ function trackSwapMixPanel( quote?: Quote; btcUsdRate: string; runeFloorPrice?: number; + stxBtcRate?: string; + sip10CoinsList?: any; }, ) { + let fromAmount; + let receiveAmount; + let toAmount; + let fromPrincipal; + let toPrincipal; + const from = fromToken?.name; - const to = toToken?.protocol === 'btc' ? 'BTC' : toToken?.name ?? toToken?.ticker; - - const fromAmount = - fromToken?.principal === 'BTC' - ? getBtcFiatEquivalent(new BigNumber(amount), new BigNumber(btcUsdRate)).toFixed(2) - : new BigNumber(fromToken?.tokenFiatRate ?? 0).multipliedBy(amount).toFixed(2); - - const receiveAmount = quote?.receiveAmount ? new BigNumber(quote?.receiveAmount) : undefined; - const toAmount = receiveAmount - ? getBtcFiatEquivalent( - toToken?.protocol === 'btc' - ? receiveAmount - : receiveAmount.multipliedBy(runeFloorPrice ?? 0), - new BigNumber(btcUsdRate), - ).toFixed(2) - : undefined; + const to = toToken?.name ?? toToken?.ticker; + + if (isRunesTx({ fromToken, toToken })) { + fromAmount = + fromToken?.principal === 'BTC' + ? getBtcFiatEquivalent(new BigNumber(amount), new BigNumber(btcUsdRate)).toFixed(2) + : new BigNumber(fromToken?.tokenFiatRate ?? 0).multipliedBy(amount).toFixed(2); + + receiveAmount = quote?.receiveAmount ? new BigNumber(quote?.receiveAmount) : undefined; + toAmount = receiveAmount + ? getBtcFiatEquivalent( + toToken?.protocol === 'btc' + ? receiveAmount + : receiveAmount.multipliedBy(runeFloorPrice ?? 0), + new BigNumber(btcUsdRate), + ).toFixed(2) + : undefined; + } else if (stxBtcRate) { + fromPrincipal = fromToken?.principal; + toPrincipal = toToken?.ticker; + + fromAmount = + fromToken?.principal === 'STX' + ? getStxFiatEquivalent( + stxToMicrostacks(new BigNumber(amount)), + new BigNumber(stxBtcRate), + new BigNumber(btcUsdRate), + ).toFixed(2) + : new BigNumber(fromToken?.tokenFiatRate ?? 0).multipliedBy(amount).toFixed(2); + + receiveAmount = quote?.receiveAmount ? new BigNumber(quote?.receiveAmount) : undefined; + toAmount = + toToken?.protocol === 'stx' + ? getStxFiatEquivalent( + stxToMicrostacks(receiveAmount), + new BigNumber(stxBtcRate), + new BigNumber(btcUsdRate), + ).toFixed(2) + : new BigNumber( + sip10CoinsList?.find((s) => s.principal === toToken?.ticker)?.tokenFiatRate ?? 0, + ) + .multipliedBy(receiveAmount) + .toFixed(2); + } trackMixPanel(eventName, { ...(provider ? { provider: provider?.name } : {}), ...(from ? { from } : {}), ...(to ? { to } : {}), + ...(fromPrincipal ? { fromPrincipal } : {}), + ...(toPrincipal ? { toPrincipal } : {}), fromAmount, ...(toAmount ? { toAmount } : {}), }); diff --git a/src/app/screens/swap/quoteSummary/index.tsx b/src/app/screens/swap/quoteSummary/index.tsx index e9f8c522c..3debaf0b5 100644 --- a/src/app/screens/swap/quoteSummary/index.tsx +++ b/src/app/screens/swap/quoteSummary/index.tsx @@ -33,8 +33,10 @@ import trackSwapMixPanel from '../mixpanel'; import QuoteTile from '../quotesModal/quoteTile'; import { SlippageModalContent } from '../slippageModal'; import type { OrderInfo, StxOrderInfo } from '../types'; +import { useStxCurrencyConversion } from '../useStxCurrencyConversion'; import { BAD_QUOTE_PERCENTAGE, + isRunesTx, mapFTNativeSwapTokenToTokenBasic, mapFtToCurrencyType, mapSwapTokenToFT, @@ -175,6 +177,7 @@ export default function QuoteSummary({ const { btcFiatRate, btcUsdRate, stxBtcRate } = useCoinRates(); const { btcAddress, ordinalsAddress, btcPublicKey, ordinalsPublicKey, stxAddress, stxPublicKey } = useSelectedAccount(); + const { loading: isPlaceOrderLoading, error: placeOrderError, @@ -255,6 +258,8 @@ export default function QuoteSummary({ quote, btcUsdRate, runeFloorPrice, + stxBtcRate, + sip10CoinsList, }); if (selectedIdentifiers) { @@ -524,33 +529,35 @@ export default function QuoteSummary({ - - - {t('TRANSACTION_SETTING.FEE_RATE')} - - ''} - fiatUnit={fiatCurrency} - getFeeForFeeRate={(fee) => Promise.resolve(fee)} - feeRates={{ - medium: recommendedFees?.regular, - high: recommendedFees?.priority, - }} - feeRateLimits={{ ...recommendedFees?.limits, min: recommendedFees?.regular }} - onFeeChange={setFeeRate} - /> - - - - {feeRate} {t('UNITS.SATS_PER_VB')} - - + {isRunesTx({ toToken }) && ( + + + {t('TRANSACTION_SETTING.FEE_RATE')} + + ''} + fiatUnit={fiatCurrency} + getFeeForFeeRate={(fee) => Promise.resolve(fee)} + feeRates={{ + medium: recommendedFees?.regular, + high: recommendedFees?.priority, + }} + feeRateLimits={{ ...recommendedFees?.limits, min: recommendedFees?.regular }} + onFeeChange={setFeeRate} + /> + + + + {feeRate} {t('UNITS.SATS_PER_VB')} + + + )} diff --git a/src/app/screens/swap/utils.ts b/src/app/screens/swap/utils.ts index 21e82cbba..283d6d0d9 100644 --- a/src/app/screens/swap/utils.ts +++ b/src/app/screens/swap/utils.ts @@ -103,3 +103,27 @@ export const mapFtToSwapToken = (st: FungibleToken): Token => { symbol: st.runeSymbol ?? '', }; }; + +export const isRunesTx = ({ + fromToken, + toToken, +}: { + fromToken?: FungibleToken; + toToken?: Token; +}): boolean => + fromToken?.protocol === 'runes' || + fromToken?.principal === 'BTC' || + toToken?.protocol === 'runes' || + toToken?.ticker === 'BTC'; + +export const isStxTx = ({ + fromToken, + toToken, +}: { + fromToken?: FungibleToken; + toToken?: Token; +}): boolean => + fromToken?.protocol === 'stacks' || + fromToken?.principal === 'STX' || + toToken?.protocol === 'sip10' || + toToken?.ticker === 'STX'; From 153754d1c563ea8431f930537c6ea29f241c76d1 Mon Sep 17 00:00:00 2001 From: "Jordan K." <65149726+jordankzf@users.noreply.github.com> Date: Fri, 30 Aug 2024 19:32:55 +0800 Subject: [PATCH 057/227] Fix mixpanel crash (#549) * Remove fee rate for SIP-10 swaps * Remove feature flag hardcode * Add Mixpanel tracking * Bump xverse-core * hide fee rate if not btc/runes * small tweak * small tweak * Improve mixpanel tracking * Fix MixPanel crash --------- Co-authored-by: Terence Ng --- src/app/screens/swap/mixpanel.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/app/screens/swap/mixpanel.ts b/src/app/screens/swap/mixpanel.ts index 32da62cf4..a66e35ad1 100644 --- a/src/app/screens/swap/mixpanel.ts +++ b/src/app/screens/swap/mixpanel.ts @@ -73,18 +73,20 @@ function trackSwapMixPanel( : new BigNumber(fromToken?.tokenFiatRate ?? 0).multipliedBy(amount).toFixed(2); receiveAmount = quote?.receiveAmount ? new BigNumber(quote?.receiveAmount) : undefined; - toAmount = - toToken?.protocol === 'stx' - ? getStxFiatEquivalent( - stxToMicrostacks(receiveAmount), - new BigNumber(stxBtcRate), - new BigNumber(btcUsdRate), - ).toFixed(2) - : new BigNumber( - sip10CoinsList?.find((s) => s.principal === toToken?.ticker)?.tokenFiatRate ?? 0, - ) - .multipliedBy(receiveAmount) - .toFixed(2); + if (receiveAmount) { + toAmount = + toToken?.protocol === 'stx' + ? getStxFiatEquivalent( + stxToMicrostacks(receiveAmount), + new BigNumber(stxBtcRate), + new BigNumber(btcUsdRate), + ).toFixed(2) + : new BigNumber( + sip10CoinsList?.find((s) => s.principal === toToken?.ticker)?.tokenFiatRate ?? 0, + ) + .multipliedBy(receiveAmount) + .toFixed(2); + } } trackMixPanel(eventName, { From bb181422cab18d7e393b84f26640bec445d5568f Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Fri, 30 Aug 2024 17:17:31 +0300 Subject: [PATCH 058/227] update core version --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 28aa9806f..ff7f4a308 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.2.1", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "18.8.1", + "@secretkeylabs/xverse-core": "18.8.1-7f289bb", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.16.1", @@ -1279,9 +1279,9 @@ } }, "node_modules/@secretkeylabs/xverse-core": { - "version": "18.8.1", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.1/537d8964f3673681cfe0e1afe5c0acecb83729d4", - "integrity": "sha512-NEy5LN1+oj8acsqRGkOmuDfVFfoe1Inwt7wNBaqZQGGSaMac6mgYlaaxwrZUsyOtLZ9e1kzbht1ng3Lzjq/ynQ==", + "version": "18.8.1-7f289bb", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.1-7f289bb/c769239023b0c763dd84649a661b80d47111cc54", + "integrity": "sha512-4DcpbvTDXEPIp/TXz1w6bJflgsaYheipsZkOl4Lf5pAM5rtbzDzIoCobRBwPgBUtF46matrVxAex4Mo77nfPJQ==", "license": "ISC", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", @@ -13846,9 +13846,9 @@ } }, "@secretkeylabs/xverse-core": { - "version": "18.8.1", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.1/537d8964f3673681cfe0e1afe5c0acecb83729d4", - "integrity": "sha512-NEy5LN1+oj8acsqRGkOmuDfVFfoe1Inwt7wNBaqZQGGSaMac6mgYlaaxwrZUsyOtLZ9e1kzbht1ng3Lzjq/ynQ==", + "version": "18.8.1-7f289bb", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.1-7f289bb/c769239023b0c763dd84649a661b80d47111cc54", + "integrity": "sha512-4DcpbvTDXEPIp/TXz1w6bJflgsaYheipsZkOl4Lf5pAM5rtbzDzIoCobRBwPgBUtF46matrVxAex4Mo77nfPJQ==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", diff --git a/package.json b/package.json index fede151db..92d60002c 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.2.1", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "18.8.1", + "@secretkeylabs/xverse-core": "18.8.1-7f289bb", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.16.1", From 7603deba41d5fc2a98941866abdf9f43007d0820 Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Fri, 30 Aug 2024 19:43:07 +0300 Subject: [PATCH 059/227] fix-incorrect fee showing and update estimate api --- package-lock.json | 98 +++++++++++++------ package.json | 2 +- .../confirmStxTransactionComponent/index.tsx | 4 +- src/app/hooks/apiClients/useStacksApi.ts | 19 ++++ src/app/hooks/queries/useFeeMultipliers.ts | 5 +- src/app/hooks/queries/useStxWalletData.ts | 17 ++-- src/app/utils/transactions/transactions.ts | 2 +- 7 files changed, 103 insertions(+), 44 deletions(-) create mode 100644 src/app/hooks/apiClients/useStacksApi.ts diff --git a/package-lock.json b/package-lock.json index 28aa9806f..e7f4a673e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.2.1", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "18.8.1", + "@secretkeylabs/xverse-core": "18.8.1-95f903a", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.16.1", @@ -1279,9 +1279,9 @@ } }, "node_modules/@secretkeylabs/xverse-core": { - "version": "18.8.1", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.1/537d8964f3673681cfe0e1afe5c0acecb83729d4", - "integrity": "sha512-NEy5LN1+oj8acsqRGkOmuDfVFfoe1Inwt7wNBaqZQGGSaMac6mgYlaaxwrZUsyOtLZ9e1kzbht1ng3Lzjq/ynQ==", + "version": "18.8.1-95f903a", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.1-95f903a/2253e7fbd54a34d53a196032b3120be0819938fe", + "integrity": "sha512-57NGEcdZnywDUBwkghffvlztaZRDOwSPmTReEUktN+gVc/EhLQG0S+68XZZVCf3M4h4KGLwCZPCRE3a6R07wAQ==", "license": "ISC", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", @@ -1293,12 +1293,13 @@ "@scure/btc-signer": "1.2.1", "@stacks/auth": "6.16.1", "@stacks/connect": "7.7.1", - "@stacks/encryption": "6.16.1", + "@stacks/encryption": "6.16.1 ", "@stacks/network": "6.16.0", - "@stacks/stacking": "6.16.1", - "@stacks/storage": "6.16.1", + "@stacks/stacking": "6.16.0", + "@stacks/stacks-blockchain-api-types": "^7.14.1", + "@stacks/storage": "6.16.0", "@stacks/transactions": "6.16.1", - "@stacks/wallet-sdk": "6.16.1", + "@stacks/wallet-sdk": "6.16.1 ", "@tanstack/react-query": "^4.29.3", "@zondax/ledger-stacks": "^1.0.4", "async-mutex": "^0.4.0", @@ -1382,6 +1383,24 @@ "@stencil/core": "^2.17.1" } }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/stacks-blockchain-api-types": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-7.14.1.tgz", + "integrity": "sha512-65hvhXxC+EUqHJAQsqlBCqXB+zwfxZICSKYJugdg6BCp9I9qniyfz5XyQeC4RMVo0tgEoRdS/b5ZCFo5kLWmxA==" + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/storage": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.16.0.tgz", + "integrity": "sha512-MLOMddIKyOcbzZo7ViW9tm6dhFMfLKf38yhNXpWohCnoxHRaUkCHP1prIT7hd9oLMI22vwtOvWgboJ+/e2uxKg==", + "dependencies": { + "@stacks/auth": "^6.16.0", + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.0", + "@stacks/network": "^6.16.0", + "base64-js": "^1.5.1", + "jsontokens": "^4.0.1" + } + }, "node_modules/@secretkeylabs/xverse-core/node_modules/@types/node": { "version": "11.11.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", @@ -1566,18 +1585,17 @@ } }, "node_modules/@stacks/stacking": { - "version": "6.16.1", - "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.16.1.tgz", - "integrity": "sha512-Bv7TSyoMrb1wYOfKrPxwDQPSjsohyKCduN1HYMlKL9hHF0J8+MvlJFw9eIj1c2SEOyYaElT+Ly3CI56J/acVxw==", - "license": "MIT", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.16.0.tgz", + "integrity": "sha512-FjhKYyaCu/KytP/khWB9KhNOOrZdqM56xn9yxvZCR6McGSZ+hBeLud1E2WugIOzRX0muZbygCz9c1GBTz+pLiA==", "dependencies": { "@noble/hashes": "1.1.5", "@scure/base": "1.1.1", "@stacks/common": "^6.16.0", - "@stacks/encryption": "^6.16.1", + "@stacks/encryption": "^6.16.0", "@stacks/network": "^6.16.0", "@stacks/stacks-blockchain-api-types": "^0.61.0", - "@stacks/transactions": "^6.16.1", + "@stacks/transactions": "^6.16.0", "bs58": "^5.0.0" } }, @@ -1590,8 +1608,7 @@ "type": "individual", "url": "https://paulmillr.com/funding/" } - ], - "license": "MIT" + ] }, "node_modules/@stacks/stacking/node_modules/@scure/base": { "version": "1.1.1", @@ -1602,14 +1619,12 @@ "type": "individual", "url": "https://paulmillr.com/funding/" } - ], - "license": "MIT" + ] }, "node_modules/@stacks/stacking/node_modules/@stacks/stacks-blockchain-api-types": { "version": "0.61.0", "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-0.61.0.tgz", - "integrity": "sha512-yPOfTUboo5eA9BZL/hqMcM71GstrFs9YWzOrJFPeP4cOO1wgYvAcckgBRbgiE3NqeX0A7SLZLDAXLZbATuRq9w==", - "license": "ISC" + "integrity": "sha512-yPOfTUboo5eA9BZL/hqMcM71GstrFs9YWzOrJFPeP4cOO1wgYvAcckgBRbgiE3NqeX0A7SLZLDAXLZbATuRq9w==" }, "node_modules/@stacks/stacks-blockchain-api-types": { "version": "6.1.1", @@ -13846,9 +13861,9 @@ } }, "@secretkeylabs/xverse-core": { - "version": "18.8.1", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.1/537d8964f3673681cfe0e1afe5c0acecb83729d4", - "integrity": "sha512-NEy5LN1+oj8acsqRGkOmuDfVFfoe1Inwt7wNBaqZQGGSaMac6mgYlaaxwrZUsyOtLZ9e1kzbht1ng3Lzjq/ynQ==", + "version": "18.8.1-95f903a", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.1-95f903a/2253e7fbd54a34d53a196032b3120be0819938fe", + "integrity": "sha512-57NGEcdZnywDUBwkghffvlztaZRDOwSPmTReEUktN+gVc/EhLQG0S+68XZZVCf3M4h4KGLwCZPCRE3a6R07wAQ==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", @@ -13859,12 +13874,13 @@ "@scure/btc-signer": "1.2.1", "@stacks/auth": "6.16.1", "@stacks/connect": "7.7.1", - "@stacks/encryption": "6.16.1", + "@stacks/encryption": "6.16.1 ", "@stacks/network": "6.16.0", - "@stacks/stacking": "6.16.1", - "@stacks/storage": "6.16.1", + "@stacks/stacking": "6.16.0", + "@stacks/stacks-blockchain-api-types": "^7.14.1", + "@stacks/storage": "6.16.0", "@stacks/transactions": "6.16.1", - "@stacks/wallet-sdk": "6.16.1", + "@stacks/wallet-sdk": "6.16.1 ", "@tanstack/react-query": "^4.29.3", "@zondax/ledger-stacks": "^1.0.4", "async-mutex": "^0.4.0", @@ -13926,6 +13942,24 @@ "@stencil/core": "^2.17.1" } }, + "@stacks/stacks-blockchain-api-types": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-7.14.1.tgz", + "integrity": "sha512-65hvhXxC+EUqHJAQsqlBCqXB+zwfxZICSKYJugdg6BCp9I9qniyfz5XyQeC4RMVo0tgEoRdS/b5ZCFo5kLWmxA==" + }, + "@stacks/storage": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.16.0.tgz", + "integrity": "sha512-MLOMddIKyOcbzZo7ViW9tm6dhFMfLKf38yhNXpWohCnoxHRaUkCHP1prIT7hd9oLMI22vwtOvWgboJ+/e2uxKg==", + "requires": { + "@stacks/auth": "^6.16.0", + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.0", + "@stacks/network": "^6.16.0", + "base64-js": "^1.5.1", + "jsontokens": "^4.0.1" + } + }, "@types/node": { "version": "11.11.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", @@ -14087,17 +14121,17 @@ } }, "@stacks/stacking": { - "version": "6.16.1", - "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.16.1.tgz", - "integrity": "sha512-Bv7TSyoMrb1wYOfKrPxwDQPSjsohyKCduN1HYMlKL9hHF0J8+MvlJFw9eIj1c2SEOyYaElT+Ly3CI56J/acVxw==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.16.0.tgz", + "integrity": "sha512-FjhKYyaCu/KytP/khWB9KhNOOrZdqM56xn9yxvZCR6McGSZ+hBeLud1E2WugIOzRX0muZbygCz9c1GBTz+pLiA==", "requires": { "@noble/hashes": "1.1.5", "@scure/base": "1.1.1", "@stacks/common": "^6.16.0", - "@stacks/encryption": "^6.16.1", + "@stacks/encryption": "^6.16.0", "@stacks/network": "^6.16.0", "@stacks/stacks-blockchain-api-types": "^0.61.0", - "@stacks/transactions": "^6.16.1", + "@stacks/transactions": "^6.16.0", "bs58": "^5.0.0" }, "dependencies": { diff --git a/package.json b/package.json index fede151db..a1b885ad3 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.2.1", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "18.8.1", + "@secretkeylabs/xverse-core": "18.8.1-95f903a", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.16.1", diff --git a/src/app/components/confirmStxTransactionComponent/index.tsx b/src/app/components/confirmStxTransactionComponent/index.tsx index 5b53921cf..b9d9bafbc 100644 --- a/src/app/components/confirmStxTransactionComponent/index.tsx +++ b/src/app/components/confirmStxTransactionComponent/index.tsx @@ -148,7 +148,7 @@ function ConfirmStxTransactionComponent({ medium: microstacksToStx(BigNumber(modifiedFees.medium)).toNumber(), high: microstacksToStx(BigNumber(modifiedFees.high)).toNumber(), }); - if (!fee) setFeeRate?.(Number(microstacksToStx(BigNumber(medium.fee))).toString()); + if (!fee) setFeeRate?.(Number(microstacksToStx(BigNumber(modifiedFees.low))).toString()); } catch (e) { console.error(e); } finally { @@ -157,7 +157,7 @@ function ConfirmStxTransactionComponent({ }; fetchStxFees(); - }, [selectedNetwork, initialStxTransactions]); + }, [selectedNetwork, initialStxTransactions, feeMultipliers, fee, setFeeRate]); useEffect(() => { const stxTxFee = BigNumber(initialStxTransactions[0].auth.spendingCondition.fee.toString()); diff --git a/src/app/hooks/apiClients/useStacksApi.ts b/src/app/hooks/apiClients/useStacksApi.ts new file mode 100644 index 000000000..a0051e7b9 --- /dev/null +++ b/src/app/hooks/apiClients/useStacksApi.ts @@ -0,0 +1,19 @@ +import useNetworkSelector from '@hooks/useNetwork'; +import { StacksApiProvider } from '@secretkeylabs/xverse-core'; +import { useMemo } from 'react'; + +const useStacksAPI = () => { + const network = useNetworkSelector(); + + const StacksAPI = useMemo( + () => + new StacksApiProvider({ + network, + }), + [network], + ); + + return StacksAPI; +}; + +export default useStacksAPI; diff --git a/src/app/hooks/queries/useFeeMultipliers.ts b/src/app/hooks/queries/useFeeMultipliers.ts index 7a5430932..14ca0543c 100644 --- a/src/app/hooks/queries/useFeeMultipliers.ts +++ b/src/app/hooks/queries/useFeeMultipliers.ts @@ -1,16 +1,17 @@ import useWalletSelector from '@hooks/useWalletSelector'; import type { AppInfo } from '@secretkeylabs/xverse-core'; -import { fetchAppInfo } from '@secretkeylabs/xverse-core'; +import { getXverseApiClient } from '@secretkeylabs/xverse-core'; import { setFeeMultiplierAction } from '@stores/wallet/actions/actionCreators'; import { useQuery } from '@tanstack/react-query'; import { useDispatch } from 'react-redux'; const useFeeMultipliers = () => { const { network } = useWalletSelector(); + const xverseApi = getXverseApiClient(network.type); const dispatch = useDispatch(); const fetchFeeMultiplierData = async (): Promise => { - const response = await fetchAppInfo(network.type); + const response = await xverseApi.fetchAppInfo(); if (!response) throw new Error('Failed to fetch fee multipliers'); dispatch(setFeeMultiplierAction(response)); diff --git a/src/app/hooks/queries/useStxWalletData.ts b/src/app/hooks/queries/useStxWalletData.ts index c12ec2362..2759e1242 100644 --- a/src/app/hooks/queries/useStxWalletData.ts +++ b/src/app/hooks/queries/useStxWalletData.ts @@ -1,15 +1,20 @@ +import useStacksAPI from '@hooks/apiClients/useStacksApi'; import useSelectedAccount from '@hooks/useSelectedAccount'; import type { StxAddressData } from '@secretkeylabs/xverse-core'; -import { fetchStxAddressData } from '@secretkeylabs/xverse-core'; import { useQuery } from '@tanstack/react-query'; -import { PAGINATION_LIMIT } from '@utils/constants'; -import useNetworkSelector from '../useNetwork'; const useStxWalletData = () => { const { stxAddress } = useSelectedAccount(); - const currentNetworkInstance = useNetworkSelector(); - const fetchStxWalletData = async (): Promise => - fetchStxAddressData(stxAddress, currentNetworkInstance, 0, PAGINATION_LIMIT); + const StacksAPI = useStacksAPI(); + const fetchStxWalletData = async (): Promise => { + const response = await StacksAPI.getAddressBalance(stxAddress); + return { + ...response, + balance: response.totalBalance, + locked: response.lockedBalance, + transactions: [], + }; + }; return useQuery({ queryKey: ['stx-wallet-data', stxAddress], diff --git a/src/app/utils/transactions/transactions.ts b/src/app/utils/transactions/transactions.ts index b0a0cb8c1..72f03b068 100644 --- a/src/app/utils/transactions/transactions.ts +++ b/src/app/utils/transactions/transactions.ts @@ -146,7 +146,7 @@ export const modifyRecommendedStxFees = ( }, appInfo: AppInfo | undefined | null, ): { low: number; medium: number; high: number } => { - const multiplier = appInfo?.stxSendTxMultiplier || 1; + const multiplier = appInfo?.otherTxMultiplier || 1; const highCap = appInfo?.thresholdHighStacksFee; let adjustedLow = Math.round(baseFees.low * multiplier); From 809a1bb1ee5729af30d46f2c106e32d1634ae4c5 Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Sun, 1 Sep 2024 17:31:06 +0300 Subject: [PATCH 060/227] apply multiplier based on txType --- .../components/confirmStxTransactionComponent/index.tsx | 1 + src/app/screens/sendStx/steps/Step2SelectAmount.tsx | 6 +++++- src/app/utils/transactions/transactions.ts | 8 +++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/app/components/confirmStxTransactionComponent/index.tsx b/src/app/components/confirmStxTransactionComponent/index.tsx index b9d9bafbc..e7e865990 100644 --- a/src/app/components/confirmStxTransactionComponent/index.tsx +++ b/src/app/components/confirmStxTransactionComponent/index.tsx @@ -141,6 +141,7 @@ function ConfirmStxTransactionComponent({ high: high.fee, }, feeMultipliers, + initialStxTransactions[0].payload.payloadType, ); setFeeRates({ diff --git a/src/app/screens/sendStx/steps/Step2SelectAmount.tsx b/src/app/screens/sendStx/steps/Step2SelectAmount.tsx index dfadfe07a..511d8e056 100644 --- a/src/app/screens/sendStx/steps/Step2SelectAmount.tsx +++ b/src/app/screens/sendStx/steps/Step2SelectAmount.tsx @@ -218,7 +218,11 @@ function Step2SelectAmount({ }; if (feeMultipliers?.thresholdHighStacksFee) { - stxFees = modifyRecommendedStxFees(stxFees, feeMultipliers); + stxFees = modifyRecommendedStxFees( + stxFees, + feeMultipliers, + unsignedTx.payload.payloadType, + ); } setFees({ low: Number(microstacksToStx(new BigNumber(stxFees.low))), diff --git a/src/app/utils/transactions/transactions.ts b/src/app/utils/transactions/transactions.ts index 72f03b068..2111d8b01 100644 --- a/src/app/utils/transactions/transactions.ts +++ b/src/app/utils/transactions/transactions.ts @@ -15,6 +15,7 @@ import { type MempoolTransactionListResponse, type Transaction, } from '@stacks/stacks-blockchain-api-types'; +import { PayloadType } from '@stacks/transactions'; import axios from 'axios'; interface PaginatedResults { @@ -145,8 +146,13 @@ export const modifyRecommendedStxFees = ( high: number; }, appInfo: AppInfo | undefined | null, + txType: PayloadType, ): { low: number; medium: number; high: number } => { - const multiplier = appInfo?.otherTxMultiplier || 1; + const multiplier = appInfo + ? txType === PayloadType.ContractCall + ? appInfo.otherTxMultiplier + : appInfo.stxSendTxMultiplier + : 1; const highCap = appInfo?.thresholdHighStacksFee; let adjustedLow = Math.round(baseFees.low * multiplier); From c9821bd9045dd8ab70d4603f20bffa39f39f383a Mon Sep 17 00:00:00 2001 From: jordankzf Date: Mon, 2 Sep 2024 18:49:18 +0800 Subject: [PATCH 061/227] Remove Alex filtering --- src/app/screens/swap/index.tsx | 3 ++- src/app/screens/swap/useMasterCoinsList.tsx | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index f01e5941e..25d6044a5 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -3,6 +3,7 @@ import RequestsRoutes from '@common/utils/route-urls'; import BottomBar from '@components/tabBar'; import TopRow from '@components/topRow'; import useRuneFloorPriceQuery from '@hooks/queries/runes/useRuneFloorPriceQuery'; +import { useGetSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens'; import useGetQuotes from '@hooks/queries/swaps/useGetQuotes'; import useBtcWalletData from '@hooks/queries/useBtcWalletData'; import useCoinRates from '@hooks/queries/useCoinRates'; @@ -138,7 +139,7 @@ export default function SwapScreen() { const { quotes, loading: quotesLoading, error: quotesError, fetchQuotes } = useGetQuotes(); const { data: runeFloorPrice } = useRuneFloorPriceQuery(toToken?.name ?? ''); const coinsMasterList = useMasterCoinsList(); - const { acceptableCoinList: sip10CoinsList } = useStxCurrencyConversion(); + const { data: sip10CoinsList } = useGetSip10FungibleTokens(); useEffect(() => { if (defaultFrom) { diff --git a/src/app/screens/swap/useMasterCoinsList.tsx b/src/app/screens/swap/useMasterCoinsList.tsx index 891046ff6..51cfd440e 100644 --- a/src/app/screens/swap/useMasterCoinsList.tsx +++ b/src/app/screens/swap/useMasterCoinsList.tsx @@ -1,11 +1,11 @@ import IconBitcoin from '@assets/img/dashboard/bitcoin_icon.svg'; import IconStacks from '@assets/img/dashboard/stx_icon.svg'; import { useRuneFungibleTokensQuery } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; +import { useGetSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens'; import useHasFeature from '@hooks/useHasFeature'; import useWalletSelector from '@hooks/useWalletSelector'; import { FeatureId, type FungibleToken } from '@secretkeylabs/xverse-core'; import { useMemo } from 'react'; -import { useStxCurrencyConversion } from './useStxCurrencyConversion'; export const btcFt: FungibleToken = { name: 'Bitcoin', @@ -30,7 +30,7 @@ export const stxFt: FungibleToken = { }; const useMasterCoinsList = () => { - const { acceptableCoinList: sip10FtList } = useStxCurrencyConversion(); + const { data: sip10FtList } = useGetSip10FungibleTokens(); const { unfilteredData: runesFtList } = useRuneFungibleTokensQuery(); const { hideStx } = useWalletSelector(); const isStacksSwapsEnabled = useHasFeature(FeatureId.STACKS_SWAPS); @@ -40,7 +40,7 @@ const useMasterCoinsList = () => { [ ...(runesFtList || []), btcFt, - ...(!hideStx && isStacksSwapsEnabled ? [stxFt, ...sip10FtList] : []), + ...(!hideStx && isStacksSwapsEnabled ? [stxFt, ...(sip10FtList ?? [])] : []), ] ?? [], [runesFtList, hideStx, isStacksSwapsEnabled, sip10FtList], ); From d68f2018c5dad8aa14294ded065acdbd31522e6e Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Mon, 2 Sep 2024 19:02:51 +0300 Subject: [PATCH 062/227] fix fee cap warning doesn't show --- .../confirmStxTransactionComponent/index.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/app/components/confirmStxTransactionComponent/index.tsx b/src/app/components/confirmStxTransactionComponent/index.tsx index e7e865990..75dab0db7 100644 --- a/src/app/components/confirmStxTransactionComponent/index.tsx +++ b/src/app/components/confirmStxTransactionComponent/index.tsx @@ -161,12 +161,13 @@ function ConfirmStxTransactionComponent({ }, [selectedNetwork, initialStxTransactions, feeMultipliers, fee, setFeeRate]); useEffect(() => { - const stxTxFee = BigNumber(initialStxTransactions[0].auth.spendingCondition.fee.toString()); + if (!feeMultipliers || !fee) return; - if ( - feeMultipliers && - stxTxFee.isGreaterThan(BigNumber(feeMultipliers.thresholdHighStacksFee)) - ) { + const feeExceedsThreshold = stxToMicrostacks(new BigNumber(fee)).isGreaterThan( + BigNumber(feeMultipliers.thresholdHighStacksFee), + ); + + if (feeExceedsThreshold) { setShowFeeWarning(true); } else if (showFeeWarning) { setShowFeeWarning(false); From d6a249299920b5346d65b34d4fd7457bd1258776 Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Mon, 2 Sep 2024 20:27:12 +0300 Subject: [PATCH 063/227] update core version --- package-lock.json | 76 ++++++++++++++++------------------------------- package.json | 2 +- 2 files changed, 26 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index e7f4a673e..b16ca1e4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.2.1", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "18.8.1-95f903a", + "@secretkeylabs/xverse-core": "18.9.0", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.16.1", @@ -1279,9 +1279,9 @@ } }, "node_modules/@secretkeylabs/xverse-core": { - "version": "18.8.1-95f903a", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.1-95f903a/2253e7fbd54a34d53a196032b3120be0819938fe", - "integrity": "sha512-57NGEcdZnywDUBwkghffvlztaZRDOwSPmTReEUktN+gVc/EhLQG0S+68XZZVCf3M4h4KGLwCZPCRE3a6R07wAQ==", + "version": "18.9.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.9.0/d25852a70a53ae29de94773ab01883e1350cbd5f", + "integrity": "sha512-v0r+eFHezlBmdlms13KH6et6QHFhEBqxGGnI7cyA0oFgL8vtEt94Rkq0SnRwARYXG2yyAH0VRdCp+l/lKVJ6fQ==", "license": "ISC", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", @@ -1293,13 +1293,13 @@ "@scure/btc-signer": "1.2.1", "@stacks/auth": "6.16.1", "@stacks/connect": "7.7.1", - "@stacks/encryption": "6.16.1 ", + "@stacks/encryption": "6.16.1", "@stacks/network": "6.16.0", - "@stacks/stacking": "6.16.0", + "@stacks/stacking": "6.16.1", "@stacks/stacks-blockchain-api-types": "^7.14.1", - "@stacks/storage": "6.16.0", + "@stacks/storage": "6.16.1", "@stacks/transactions": "6.16.1", - "@stacks/wallet-sdk": "6.16.1 ", + "@stacks/wallet-sdk": "6.16.1", "@tanstack/react-query": "^4.29.3", "@zondax/ledger-stacks": "^1.0.4", "async-mutex": "^0.4.0", @@ -1388,19 +1388,6 @@ "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-7.14.1.tgz", "integrity": "sha512-65hvhXxC+EUqHJAQsqlBCqXB+zwfxZICSKYJugdg6BCp9I9qniyfz5XyQeC4RMVo0tgEoRdS/b5ZCFo5kLWmxA==" }, - "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/storage": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.16.0.tgz", - "integrity": "sha512-MLOMddIKyOcbzZo7ViW9tm6dhFMfLKf38yhNXpWohCnoxHRaUkCHP1prIT7hd9oLMI22vwtOvWgboJ+/e2uxKg==", - "dependencies": { - "@stacks/auth": "^6.16.0", - "@stacks/common": "^6.16.0", - "@stacks/encryption": "^6.16.0", - "@stacks/network": "^6.16.0", - "base64-js": "^1.5.1", - "jsontokens": "^4.0.1" - } - }, "node_modules/@secretkeylabs/xverse-core/node_modules/@types/node": { "version": "11.11.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", @@ -1585,17 +1572,17 @@ } }, "node_modules/@stacks/stacking": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.16.0.tgz", - "integrity": "sha512-FjhKYyaCu/KytP/khWB9KhNOOrZdqM56xn9yxvZCR6McGSZ+hBeLud1E2WugIOzRX0muZbygCz9c1GBTz+pLiA==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.16.1.tgz", + "integrity": "sha512-Bv7TSyoMrb1wYOfKrPxwDQPSjsohyKCduN1HYMlKL9hHF0J8+MvlJFw9eIj1c2SEOyYaElT+Ly3CI56J/acVxw==", "dependencies": { "@noble/hashes": "1.1.5", "@scure/base": "1.1.1", "@stacks/common": "^6.16.0", - "@stacks/encryption": "^6.16.0", + "@stacks/encryption": "^6.16.1", "@stacks/network": "^6.16.0", "@stacks/stacks-blockchain-api-types": "^0.61.0", - "@stacks/transactions": "^6.16.0", + "@stacks/transactions": "^6.16.1", "bs58": "^5.0.0" } }, @@ -13861,9 +13848,9 @@ } }, "@secretkeylabs/xverse-core": { - "version": "18.8.1-95f903a", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.1-95f903a/2253e7fbd54a34d53a196032b3120be0819938fe", - "integrity": "sha512-57NGEcdZnywDUBwkghffvlztaZRDOwSPmTReEUktN+gVc/EhLQG0S+68XZZVCf3M4h4KGLwCZPCRE3a6R07wAQ==", + "version": "18.9.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.9.0/d25852a70a53ae29de94773ab01883e1350cbd5f", + "integrity": "sha512-v0r+eFHezlBmdlms13KH6et6QHFhEBqxGGnI7cyA0oFgL8vtEt94Rkq0SnRwARYXG2yyAH0VRdCp+l/lKVJ6fQ==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", @@ -13874,13 +13861,13 @@ "@scure/btc-signer": "1.2.1", "@stacks/auth": "6.16.1", "@stacks/connect": "7.7.1", - "@stacks/encryption": "6.16.1 ", + "@stacks/encryption": "6.16.1", "@stacks/network": "6.16.0", - "@stacks/stacking": "6.16.0", + "@stacks/stacking": "6.16.1", "@stacks/stacks-blockchain-api-types": "^7.14.1", - "@stacks/storage": "6.16.0", + "@stacks/storage": "6.16.1", "@stacks/transactions": "6.16.1", - "@stacks/wallet-sdk": "6.16.1 ", + "@stacks/wallet-sdk": "6.16.1", "@tanstack/react-query": "^4.29.3", "@zondax/ledger-stacks": "^1.0.4", "async-mutex": "^0.4.0", @@ -13947,19 +13934,6 @@ "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-7.14.1.tgz", "integrity": "sha512-65hvhXxC+EUqHJAQsqlBCqXB+zwfxZICSKYJugdg6BCp9I9qniyfz5XyQeC4RMVo0tgEoRdS/b5ZCFo5kLWmxA==" }, - "@stacks/storage": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.16.0.tgz", - "integrity": "sha512-MLOMddIKyOcbzZo7ViW9tm6dhFMfLKf38yhNXpWohCnoxHRaUkCHP1prIT7hd9oLMI22vwtOvWgboJ+/e2uxKg==", - "requires": { - "@stacks/auth": "^6.16.0", - "@stacks/common": "^6.16.0", - "@stacks/encryption": "^6.16.0", - "@stacks/network": "^6.16.0", - "base64-js": "^1.5.1", - "jsontokens": "^4.0.1" - } - }, "@types/node": { "version": "11.11.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", @@ -14121,17 +14095,17 @@ } }, "@stacks/stacking": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.16.0.tgz", - "integrity": "sha512-FjhKYyaCu/KytP/khWB9KhNOOrZdqM56xn9yxvZCR6McGSZ+hBeLud1E2WugIOzRX0muZbygCz9c1GBTz+pLiA==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.16.1.tgz", + "integrity": "sha512-Bv7TSyoMrb1wYOfKrPxwDQPSjsohyKCduN1HYMlKL9hHF0J8+MvlJFw9eIj1c2SEOyYaElT+Ly3CI56J/acVxw==", "requires": { "@noble/hashes": "1.1.5", "@scure/base": "1.1.1", "@stacks/common": "^6.16.0", - "@stacks/encryption": "^6.16.0", + "@stacks/encryption": "^6.16.1", "@stacks/network": "^6.16.0", "@stacks/stacks-blockchain-api-types": "^0.61.0", - "@stacks/transactions": "^6.16.0", + "@stacks/transactions": "^6.16.1", "bs58": "^5.0.0" }, "dependencies": { diff --git a/package.json b/package.json index a1b885ad3..9d7ef2988 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.2.1", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "18.8.1-95f903a", + "@secretkeylabs/xverse-core": "18.9.0", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.16.1", From 00436ee2ca91197adbc067764e8d248cc4949faf Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Mon, 2 Sep 2024 17:46:15 +0000 Subject: [PATCH 064/227] release: v0.41.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b16ca1e4f..c3f119e2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "xverse-web-extension", - "version": "0.41.0", + "version": "0.41.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "xverse-web-extension", - "version": "0.41.0", + "version": "0.41.1", "dependencies": { "@aryzing/superqs": "0.0.6", "@ledgerhq/hw-transport-webusb": "^6.27.13", diff --git a/package.json b/package.json index 9d7ef2988..057a6678b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xverse-web-extension", "description": "A Bitcoin wallet for Web3", - "version": "0.41.0", + "version": "0.41.1", "private": true, "engines": { "node": "^18.18.2" From e6c82b6c138353058c864262d8335ea60c0bb96c Mon Sep 17 00:00:00 2001 From: Den <36603049+dhriaznov@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:14:18 +0200 Subject: [PATCH 065/227] Add the E2E test result section to the PR template and updated the readme (#544) * Add the E2E test result section to the PR template * updated the pr description for running e2e tests * moved the E2E section from readme to PR description * updated the node version in the readme to 18 --------- Co-authored-by: DuskaT021 --- .github/PULL_REQUEST_TEMPLATE.md | 23 +++++++++++++++++++++++ README.md | 29 +---------------------------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0aa70c0a1..504fed181 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,5 @@ # 🔘 PR Type + What kind of change does this PR introduce? @@ -13,22 +14,44 @@ What kind of change does this PR introduce? - [ ] Other... Please describe: # 📜 Background + Provide a brief explanation of why this pull request is needed. Include the problem you are solving or the functionality you are adding. Reference any related issues. Issue Link: #[issue_number] Context Link (if applicable): # 🔄 Changes + Enumerate the changes made in this pull request, detailing what has been modified, added, or removed. Include technical details and implications if necessary. Impact: + - Explain the broader impact of these changes. - How it improves performance, fixes bugs, adds functionality, etc. +# 🧪 E2E Test Result + +Include a screenshot of the e2e test result. + +`Run E2E Tests` +Our End-to-end (E2E) test suite is build with Playwright. To run the whole E2E test suite, run: +`npm run e2etest` + +If you only want to run the smoke test suite, run +`npm run e2etest:smoketest` + +If you want to run the e2e test in UI Mode: +`npm run e2etest:ui` + +To generate test report, run: +`npm run e2etest:report` + # 🖼 Screenshot / 📹 Video + Include screenshots or a video demonstrating the changes. This is especially helpful for UI changes. # ✅ Review checklist + Please ensure the following are true before merging: - [ ] Code Style is consistent with the project guidelines. diff --git a/README.md b/README.md index 1bca9fcc6..3237ba9bc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ### Procedures -1. Check if your [Node.js](https://nodejs.org/) version is >= **14**. +1. Check if your [Node.js](https://nodejs.org/) version is >= **18**. 2. Clone this repository. 3. Make sure you're logged in to the @secretkeylabs scope on the GitHub NPM package registry. See the [Guide](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#authenticating-with-a-personal-access-token) 1. Create a GitHub personal access token (classic) @@ -32,30 +32,3 @@ make or pull your local changes to xverse-core, then: cd ../xverse-core && npm i && npm run build:esm && \ cd $OLDPWD && npm i --legacy-peer-deps @secretkeylabs/xverse-core@../xverse-core && npm start ``` - -### Run E2E Tests - -Our End-to-end (E2E) test suite is build with Playwright. -To run the whole E2E test suite, run: - -``` -npm run e2etest -``` - -If you only want to run the smoke test suite, run - -``` -npm run e2etest:smoketest -``` - -If you want to run the e2e test in UI Mode: - -``` -npm run e2etest:ui -``` - -To generate test report, run: - -``` -npm run e2etest:report -``` From 4314523ddb1c2757205be2c51ef8ecee668a662d Mon Sep 17 00:00:00 2001 From: Den <36603049+dhriaznov@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:47:49 +0200 Subject: [PATCH 066/227] [ENG-5094] Remove the Reset wallet option from the Home screen three dots menu (#557) * [ENG-5094] Remove the Reset wallet option from the Home screen three dots menu * Update e2e tests * Update e2e tests --- src/app/components/accountHeader/index.tsx | 125 ++++----------------- tests/pages/wallet.ts | 3 - tests/specs/createWallet.spec.ts | 19 ++-- 3 files changed, 33 insertions(+), 114 deletions(-) diff --git a/src/app/components/accountHeader/index.tsx b/src/app/components/accountHeader/index.tsx index 477418fee..17ba73762 100644 --- a/src/app/components/accountHeader/index.tsx +++ b/src/app/components/accountHeader/index.tsx @@ -1,6 +1,4 @@ import AccountRow from '@components/accountRow'; -import PasswordInput from '@components/passwordInput'; -import ResetWalletPrompt from '@components/resetWallet'; import useWalletReducer from '@hooks/useWalletReducer'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,7 +6,6 @@ import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import OptionsDialog from '@components/optionsDialog/optionsDialog'; -import useSeedVault from '@hooks/useSeedVault'; import useSelectedAccount from '@hooks/useSelectedAccount'; import { DotsThreeVertical } from '@phosphor-icons/react'; import { OPTIONS_DIALOG_WIDTH } from '@utils/constants'; @@ -25,21 +22,6 @@ const SelectedAccountContainer = styled.div<{ showBorderBottom?: boolean }>((pro : 'none', })); -const ResetWalletContainer = styled.div((props) => ({ - width: '100%', - height: '100%', - top: 0, - left: 0, - bottom: 0, - right: 0, - position: 'fixed', - zIndex: 10, - background: 'rgba(25, 25, 48, 0.5)', - backdropFilter: 'blur(16px)', - padding: props.theme.spacing(8), - paddingTop: props.theme.spacing(30), -})); - const OptionsButton = styled.button(() => ({ display: 'flex', alignItems: 'center', @@ -67,10 +49,6 @@ const ButtonRow = styled.button` } `; -const WarningButton = styled(ButtonRow)` - color: ${(props) => props.theme.colors.feedback.error}; -`; - type Props = { disableMenuOption?: boolean; disableAccountSwitch?: boolean; @@ -85,49 +63,15 @@ function AccountHeaderComponent({ const navigate = useNavigate(); const selectedAccount = useSelectedAccount(); - const { t } = useTranslation('translation', { keyPrefix: 'SETTING_SCREEN' }); const { t: optionsDialogTranslation } = useTranslation('translation', { keyPrefix: 'OPTIONS_DIALOG', }); const [showOptionsDialog, setShowOptionsDialog] = useState(false); - const [showResetWalletPrompt, setShowResetWalletPrompt] = useState(false); - const [showResetWalletDisplay, setShowResetWalletDisplay] = useState(false); - const [password, setPassword] = useState(''); - const { lockWallet, resetWallet } = useWalletReducer(); - const { unlockVault } = useSeedVault(); - const [error, setError] = useState(''); + const { lockWallet } = useWalletReducer(); const [optionsDialogIndents, setOptionsDialogIndents] = useState< { top: string; left: string } | undefined >(); - const handlePasswordNextClick = async () => { - try { - await unlockVault(password); - setPassword(''); - setError(''); - await resetWallet(); - } catch (e) { - setError(t('INCORRECT_PASSWORD_ERROR')); - } - }; - - const onGoBack = () => { - navigate(0); - }; - - const onResetWalletPromptClose = () => { - setShowResetWalletPrompt(false); - }; - - const handleResetWalletPromptOpen = () => { - setShowResetWalletPrompt(true); - }; - - const openResetWalletScreen = () => { - setShowResetWalletPrompt(false); - setShowResetWalletDisplay(true); - }; - const handleAccountSelect = () => { if (!disableAccountSwitch) { navigate('/account-list'); @@ -152,54 +96,27 @@ function AccountHeaderComponent({ }; return ( - <> - {showResetWalletDisplay && ( - - - - )} - - - {!disableMenuOption && ( - - - - )} - {showOptionsDialog && ( - - - {optionsDialogTranslation('SWITCH_ACCOUNT')} - - {optionsDialogTranslation('LOCK')} - - {optionsDialogTranslation('RESET_WALLET')} - - - )} - - + - + {!disableMenuOption && ( + + + + )} + {showOptionsDialog && ( + + + {optionsDialogTranslation('SWITCH_ACCOUNT')} + + {optionsDialogTranslation('LOCK')} + + )} + ); } diff --git a/tests/pages/wallet.ts b/tests/pages/wallet.ts index 5c7725849..1e4d13aa8 100644 --- a/tests/pages/wallet.ts +++ b/tests/pages/wallet.ts @@ -36,8 +36,6 @@ export default class Wallet { readonly buttonConfirm: Locator; - readonly buttonResetWallet: Locator; - readonly buttonDenyDataCollection: Locator; readonly buttonNetwork: Locator; @@ -385,7 +383,6 @@ export default class Wallet { this.buttonMenu = page.getByRole('button', { name: 'Open Header Options' }); this.buttonLock = page.getByRole('button', { name: 'Lock' }); this.buttonConfirm = page.getByRole('button', { name: 'Confirm' }); - this.buttonResetWallet = page.getByRole('button', { name: 'Reset Wallet' }); this.buttonDenyDataCollection = page.getByRole('button', { name: 'Deny' }); this.labelBalanceAmountSelector = page.getByTestId('balance-label'); this.buttonClose = page.getByRole('button', { name: 'Close' }); diff --git a/tests/specs/createWallet.spec.ts b/tests/specs/createWallet.spec.ts index d38d57bfc..e0f530738 100644 --- a/tests/specs/createWallet.spec.ts +++ b/tests/specs/createWallet.spec.ts @@ -90,16 +90,21 @@ test.describe('Create and Restore Wallet Flow', () => { // Write the file fs.writeFileSync(filePathAddresses, dataAddress, 'utf8'); }); - await test.step('reset Wallet via Menu', async () => { - await expect(wallet.buttonMenu).toBeVisible(); - await wallet.buttonMenu.click(); - await expect(wallet.buttonResetWallet).toBeVisible(); - await wallet.buttonResetWallet.click(); - await wallet.buttonResetWallet.click(); - await expect(onboardingPage.inputPassword).toBeVisible(); + + await test.step('Reset Wallet via Settings', async () => { + // Go to Settings -> Security + await wallet.navigationSettings.click(); + await wallet.buttonSecurity.click(); + + // Confirm reset + await page.getByRole('button', { name: 'Reset Wallet' }).first().click(); + await page.getByRole('dialog').getByRole('button', { name: 'Reset Wallet' }).click(); + + // Enter password to confirm reset await onboardingPage.inputPassword.fill(strongPW); await onboardingPage.buttonContinue.click(); }); + await test.step('Restore wallet with 12 word seed phrase', async () => { const landingPage = new Landing(page); await expect(landingPage.buttonRestoreWallet).toBeVisible(); From 0588fee5cde2fdaa42c748d3b1c3cf36d3dbfca3 Mon Sep 17 00:00:00 2001 From: Den <36603049+dhriaznov@users.noreply.github.com> Date: Tue, 3 Sep 2024 17:28:17 +0200 Subject: [PATCH 067/227] Add timeouts in e2e before changing network to avoid rate limiting (#558) --- tests/pages/wallet.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/pages/wallet.ts b/tests/pages/wallet.ts index 1e4d13aa8..dfdcdaee7 100644 --- a/tests/pages/wallet.ts +++ b/tests/pages/wallet.ts @@ -1097,6 +1097,9 @@ export default class Wallet { await this.checkTestnetUrls(true); + // Wait for the network to be switched so that API doesn't fail because of the rate limiting + await this.page.waitForTimeout(15000); + await this.buttonSave.click(); await expect(this.buttonNetwork).toBeVisible({ timeout: 30000 }); await expect(this.buttonNetwork).toHaveText('NetworkTestnet'); @@ -1116,6 +1119,9 @@ export default class Wallet { await this.checkTestnetUrls(false); + // Wait for the network to be switched so that API doesn't fail because of the rate limiting + await this.page.waitForTimeout(15000); + await this.buttonSave.click(); await expect(this.buttonNetwork).toBeVisible({ timeout: 30000 }); await expect(this.buttonNetwork).toHaveText('NetworkMainnet'); From c972cfb299f14decf3ae63377e8ad56deabd1ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduard=20Bardaj=C3=AD=20Puig?= Date: Wed, 4 Sep 2024 07:49:49 +0200 Subject: [PATCH 068/227] Use better nonce (#553) Co-authored-by: Tim Man --- package-lock.json | 10 +++++----- package.json | 2 +- .../confirmStxTransactionComponent/index.tsx | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index ff7f4a308..297e0e7d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.2.1", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "18.8.1-7f289bb", + "@secretkeylabs/xverse-core": "19.0.0", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.16.1", @@ -1279,9 +1279,9 @@ } }, "node_modules/@secretkeylabs/xverse-core": { - "version": "18.8.1-7f289bb", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.1-7f289bb/c769239023b0c763dd84649a661b80d47111cc54", - "integrity": "sha512-4DcpbvTDXEPIp/TXz1w6bJflgsaYheipsZkOl4Lf5pAM5rtbzDzIoCobRBwPgBUtF46matrVxAex4Mo77nfPJQ==", + "version": "19.0.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.1-e391063/57c20779656abba3cdc7b17becba1ac2334c0355", + "integrity": "sha512-Cuj0ZSY+RBGiW2mjKsih67vs5wf2Qj5mql8i3E3KeRkfyYP2r6PtsFgBnTxR9vIhYgqx27PUjRHWpbGEz9M5pQ==", "license": "ISC", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", @@ -13846,7 +13846,7 @@ } }, "@secretkeylabs/xverse-core": { - "version": "18.8.1-7f289bb", + "version": "19.0.0", "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.1-7f289bb/c769239023b0c763dd84649a661b80d47111cc54", "integrity": "sha512-4DcpbvTDXEPIp/TXz1w6bJflgsaYheipsZkOl4Lf5pAM5rtbzDzIoCobRBwPgBUtF46matrVxAex4Mo77nfPJQ==", "requires": { diff --git a/package.json b/package.json index 92d60002c..3c24e8085 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.2.1", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "18.8.1-7f289bb", + "@secretkeylabs/xverse-core": "19.0.0", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.16.1", diff --git a/src/app/components/confirmStxTransactionComponent/index.tsx b/src/app/components/confirmStxTransactionComponent/index.tsx index 5b53921cf..d27dc7f65 100644 --- a/src/app/components/confirmStxTransactionComponent/index.tsx +++ b/src/app/components/confirmStxTransactionComponent/index.tsx @@ -18,7 +18,7 @@ import { getNonce, getStxFiatEquivalent, microstacksToStx, - possiblNexteNonce, + nextBestNonce, signLedgerStxTransaction, signMultiStxTransactions, signTransaction, @@ -222,7 +222,7 @@ function ConfirmStxTransactionComponent({ if (initialStxTransactions.length === 1) { const transaction = initialStxTransactions[0]; - const nonce = await possiblNexteNonce(selectedAccount.stxAddress, network); + const nonce = await nextBestNonce(selectedAccount.stxAddress, network); transaction.setNonce(nonce); const signedContractCall = await signTransaction( transaction, From 7f15a2c674c0acd22c335feabca1da0a749cdcd5 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Wed, 4 Sep 2024 13:55:12 +0800 Subject: [PATCH 069/227] chore: run npm i for core 19.0.0 --- package-lock.json | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 297e0e7d0..ce9a8666c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1280,9 +1280,8 @@ }, "node_modules/@secretkeylabs/xverse-core": { "version": "19.0.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.1-e391063/57c20779656abba3cdc7b17becba1ac2334c0355", - "integrity": "sha512-Cuj0ZSY+RBGiW2mjKsih67vs5wf2Qj5mql8i3E3KeRkfyYP2r6PtsFgBnTxR9vIhYgqx27PUjRHWpbGEz9M5pQ==", - "license": "ISC", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/19.0.0/7b2d47d225739051b88873932cd373fa5672b647", + "integrity": "sha512-3TBd/JeH0PCvqmPKsFJee8XwvBTAUWkrqNgkA3jUCmj1lpdBn6Uw6MTAsTdMBIiOvT8A4PdNf44BrPplTgHZdw==", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", @@ -1296,6 +1295,7 @@ "@stacks/encryption": "6.16.1", "@stacks/network": "6.16.0", "@stacks/stacking": "6.16.1", + "@stacks/stacks-blockchain-api-types": "^7.14.1", "@stacks/storage": "6.16.1", "@stacks/transactions": "6.16.1", "@stacks/wallet-sdk": "6.16.1", @@ -1382,6 +1382,11 @@ "@stencil/core": "^2.17.1" } }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/stacks-blockchain-api-types": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-7.14.1.tgz", + "integrity": "sha512-65hvhXxC+EUqHJAQsqlBCqXB+zwfxZICSKYJugdg6BCp9I9qniyfz5XyQeC4RMVo0tgEoRdS/b5ZCFo5kLWmxA==" + }, "node_modules/@secretkeylabs/xverse-core/node_modules/@types/node": { "version": "11.11.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", @@ -13847,8 +13852,8 @@ }, "@secretkeylabs/xverse-core": { "version": "19.0.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.8.1-7f289bb/c769239023b0c763dd84649a661b80d47111cc54", - "integrity": "sha512-4DcpbvTDXEPIp/TXz1w6bJflgsaYheipsZkOl4Lf5pAM5rtbzDzIoCobRBwPgBUtF46matrVxAex4Mo77nfPJQ==", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/19.0.0/7b2d47d225739051b88873932cd373fa5672b647", + "integrity": "sha512-3TBd/JeH0PCvqmPKsFJee8XwvBTAUWkrqNgkA3jUCmj1lpdBn6Uw6MTAsTdMBIiOvT8A4PdNf44BrPplTgHZdw==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", @@ -13862,6 +13867,7 @@ "@stacks/encryption": "6.16.1", "@stacks/network": "6.16.0", "@stacks/stacking": "6.16.1", + "@stacks/stacks-blockchain-api-types": "^7.14.1", "@stacks/storage": "6.16.1", "@stacks/transactions": "6.16.1", "@stacks/wallet-sdk": "6.16.1", @@ -13926,6 +13932,11 @@ "@stencil/core": "^2.17.1" } }, + "@stacks/stacks-blockchain-api-types": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-7.14.1.tgz", + "integrity": "sha512-65hvhXxC+EUqHJAQsqlBCqXB+zwfxZICSKYJugdg6BCp9I9qniyfz5XyQeC4RMVo0tgEoRdS/b5ZCFo5kLWmxA==" + }, "@types/node": { "version": "11.11.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", From 9ac72fd34d60ee7c192ea6cfcf00da2047126a05 Mon Sep 17 00:00:00 2001 From: jordankzf Date: Wed, 4 Sep 2024 15:44:44 +0800 Subject: [PATCH 070/227] Fix Slippage rounding issue --- src/app/screens/swap/index.tsx | 1 - src/app/screens/swap/quoteSummary/index.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index 25d6044a5..072d25d1d 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -47,7 +47,6 @@ import QuoteSummary from './quoteSummary'; import QuotesModal from './quotesModal'; import type { OrderInfo, Side, StxOrderInfo } from './types'; import useMasterCoinsList from './useMasterCoinsList'; -import { useStxCurrencyConversion } from './useStxCurrencyConversion'; import { isStxTx, mapFTNativeSwapTokenToTokenBasic, diff --git a/src/app/screens/swap/quoteSummary/index.tsx b/src/app/screens/swap/quoteSummary/index.tsx index 3debaf0b5..0d849098c 100644 --- a/src/app/screens/swap/quoteSummary/index.tsx +++ b/src/app/screens/swap/quoteSummary/index.tsx @@ -484,7 +484,7 @@ export default function QuoteSummary({ {showSlippageWarning && ( )} - {slippage * 100}% + {formatNumber(slippage * 100)}% {t('SLIPPAGE')} From e40e10d220c94d5bb719ce3fcc5eec7e1aeae7c4 Mon Sep 17 00:00:00 2001 From: "Duska.T" <55587184+DuskaT021@users.noreply.github.com> Date: Wed, 4 Sep 2024 10:07:53 +0200 Subject: [PATCH 071/227] Added the e2e test for stacks swap test on mainnet - velar (#559) * added the first stacks swap test on mainnet - velar * improve after review * improved locator --- tests/specs/swapVelar.spec.ts | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/specs/swapVelar.spec.ts diff --git a/tests/specs/swapVelar.spec.ts b/tests/specs/swapVelar.spec.ts new file mode 100644 index 000000000..733a9fe34 --- /dev/null +++ b/tests/specs/swapVelar.spec.ts @@ -0,0 +1,57 @@ +import { expect, test } from '../fixtures/base'; +import { enableCrossChainSwaps } from '../fixtures/helpers'; +import Wallet from '../pages/wallet'; +//* swap STX for runes using Velar on mainnet(not supported on testnet) + +test.describe('Velar sip-10 swap flow', () => { + test.beforeAll(async ({ page }) => { + await enableCrossChainSwaps(page); + }); + test('Check the Velar sip-10 flow on mainnet #localexecution', async ({ page, extensionId }) => { + // set up the testing wallet + const wallet = new Wallet(page); + await wallet.setupTest(extensionId, 'SEED_WORDS1', false); + + // click the swap button and choose STX->Velar route + await page.getByRole('button', { name: 'Swap' }).click(); + + await page + .getByRole('button', { name: /select asset/i }) + .first() + .click(); + await page.getByText(/stacks/i).click(); + page + .getByRole('button', { name: /select asset/i }) + .last() + .click(); + + await page.getByText(/velar/i).first().click(); + page.getByRole('textbox', { name: '0' }).fill('0.1'); + page.getByRole('button', { name: /get quotes/i }).click(); + page.getByRole('heading', { name: /rates/i }).isVisible(); + await page.getByText(/^\d+\.\d+\s+Velar$/i).click(); + + // Arrive to Quotes page + await expect(page.getByText(/quote/i)).toBeVisible(); + await expect(page.getByText(/4%/i)).toBeVisible(); + + page.getByRole('img', { name: /velar logo/i }).isVisible(); + page.getByRole('button', { name: /swap/i }).click(); + + // Arrive to the final step - swap contract page + await expect(page.getByText(/swap-exact-tokens-for-tokens/i)).toHaveCount(2); + page + .getByText(/network fee/i) + .getByText(/^\d+\.\d+\s+STX$/i) + .isVisible(); + page.getByRole('button', { name: /edit nonce/i }).isVisible(); + + // User clicks confirm + page.getByRole('button', { name: /confirm/i }).click(); + await expect(page.getByText(/transaction broadcasted/i)).toBeVisible(); + page.getByRole('button', { name: /close/i }).click(); + + // After closing user should arrive to homepage + await expect(page).toHaveURL(/popup\.html/); + }); +}); From 09d6ec4c391f4a94a497ac0d03e585166ac1370b Mon Sep 17 00:00:00 2001 From: jordankzf Date: Wed, 4 Sep 2024 16:47:30 +0800 Subject: [PATCH 072/227] Use BigNumber --- src/app/screens/swap/quoteSummary/index.tsx | 3 +-- src/app/screens/swap/slippageModal/index.tsx | 25 +++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/app/screens/swap/quoteSummary/index.tsx b/src/app/screens/swap/quoteSummary/index.tsx index 0d849098c..1fdc543e4 100644 --- a/src/app/screens/swap/quoteSummary/index.tsx +++ b/src/app/screens/swap/quoteSummary/index.tsx @@ -31,9 +31,8 @@ import { useTranslation } from 'react-i18next'; import styled, { useTheme } from 'styled-components'; import trackSwapMixPanel from '../mixpanel'; import QuoteTile from '../quotesModal/quoteTile'; -import { SlippageModalContent } from '../slippageModal'; +import SlippageModalContent from '../slippageModal'; import type { OrderInfo, StxOrderInfo } from '../types'; -import { useStxCurrencyConversion } from '../useStxCurrencyConversion'; import { BAD_QUOTE_PERCENTAGE, isRunesTx, diff --git a/src/app/screens/swap/slippageModal/index.tsx b/src/app/screens/swap/slippageModal/index.tsx index d4aa86be4..52efc39c3 100644 --- a/src/app/screens/swap/slippageModal/index.tsx +++ b/src/app/screens/swap/slippageModal/index.tsx @@ -1,7 +1,7 @@ -/* eslint-disable import/prefer-default-export */ import ActionButton from '@components/button'; import { StyledP } from '@ui-library/common.styled'; import Input from '@ui-library/input'; +import BigNumber from 'bignumber.js'; import { useState, type ChangeEvent } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; @@ -16,6 +16,7 @@ const TitleRow = styled.div` display: flex; justify-content: space-between; `; + const ResetButton = styled.button((props) => ({ display: 'inline', background: 'transparent', @@ -36,7 +37,7 @@ const Description = styled.p((props) => ({ color: props.theme.colors.white_400, })); -export function SlippageModalContent({ +function SlippageModalContent({ defaultSlippage, slippage, slippageThreshold, @@ -50,11 +51,15 @@ export function SlippageModalContent({ onChange: (slippage: number) => void; }) { const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); - const [percentage, setPercentage] = useState(`${(slippage * 100).toString()}`); - const result = Number(percentage); - const invalid = !Number.isNaN(result) && result >= 100; - const showSlippageWarning = slippageThreshold ? result > slippageThreshold * 100 : false; - const DEFAULT_SLIPPAGE = (defaultSlippage * 100).toString(); + const [percentage, setPercentage] = useState(new BigNumber(slippage).times(100).toString()); + + const result = new BigNumber(percentage); + const invalid = result.isNaN() || result.gte(100); + const showSlippageWarning = slippageThreshold + ? result.gt(new BigNumber(slippageThreshold).times(100)) + : false; + + const DEFAULT_SLIPPAGE = new BigNumber(defaultSlippage).times(100).toString(); const allowDecimalsNumber = slippageDecimals ?? 2; @@ -108,11 +113,13 @@ export function SlippageModalContent({ /> {t('SLIPPAGE_DESC')} onChange(result / 100)} + onPress={() => onChange(result.div(100).toNumber())} /> ); } + +export default SlippageModalContent; From aba9ec090bddd828d29e9e804e7cd0ae25118b20 Mon Sep 17 00:00:00 2001 From: Den <36603049+dhriaznov@users.noreply.github.com> Date: Wed, 4 Sep 2024 11:19:28 +0200 Subject: [PATCH 073/227] [ENG-4652] Add keyboard navigation throughout the app (#532) * [ENG-4651] fix: Difficult to differentiate selected vs not selected accounts * Fix the html props for styling * Add a separate Props type * [ENG-4652] Add keyboard navigation throughout the app * Add elements focus for keyboard on full screen * Move tabs component to ui-library, use it on the Collectibles screen * Refactor the SquareButton component * Update the ordinal detail screen action buttons * Refactor the resetWallet component * Refactor tabBar component to improve accessibility * Improve the scrollbar indent on the Home screen * Improve accessibility for the fee Edit button * Make Home screen banner clickable via keyboard * Improve accessibility on the Swap flow * Update the Save button on the ChangeNetworkScreen * Update the Modal component on the ReceiveNftModal screen * Make Sheet component accessible via keyboard * Make input clear button accessible via keyboard * Add accessibility for the AlertMessage component * Improve the Forgot password flow accessibility * Fix caret icon color in the header * Remove the unused styles * Improve the ledger account import flow accessibility * Fix value submit on Enter when other buttons are in focus * Add Toggle component to ui-library, replace and remove react-switch package, update e2e * Update the toggle and settings buttons * Fix the converted value overflow in the amount inputs * Update e2e tests --- package-lock.json | 21 ---- package.json | 1 - src/app/components/accountHeader/index.tsx | 8 +- src/app/components/accountRow/index.styled.ts | 31 +++--- src/app/components/accountRow/index.tsx | 11 +- src/app/components/alertMessage/index.tsx | 27 ++++- .../index.styled.ts | 4 - src/app/components/explore/FeaturedCard.tsx | 6 +- .../components/explore/RecommendedApps.tsx | 13 ++- .../ledger/ledgerAssetSelectCard/index.tsx | 51 ++++----- .../components/passwordInput/index.styled.ts | 24 ++-- src/app/components/passwordInput/index.tsx | 11 +- .../components/receiveCardComponent/index.tsx | 4 +- src/app/components/resetWallet/index.tsx | 48 +++----- src/app/components/squareButton/index.tsx | 64 +++++------ src/app/components/tabBar/index.tsx | 50 ++++++--- .../components/updatedBottomModal/index.tsx | 101 ----------------- src/app/components/webGalleryButton/index.tsx | 1 + src/app/screens/explore/index.tsx | 5 +- src/app/screens/forgotPassword/index.tsx | 36 +++--- src/app/screens/home/banner.tsx | 5 +- src/app/screens/home/index.styled.ts | 28 +++-- src/app/screens/home/index.tsx | 2 +- .../ledger/importLedgerAccount/index.tsx | 4 +- .../importLedgerAccount/stepControls.tsx | 87 ++++++++------- .../importLedgerAccount/steps/index.styled.ts | 13 +-- .../importLedgerAccount/steps/index.tsx | 24 ++-- src/app/screens/legal/index.tsx | 11 +- src/app/screens/login/index.tsx | 32 +++--- .../screens/manageTokens/coinItem/index.tsx | 21 +--- .../screens/nftDashboard/collectiblesTabs.tsx | 27 +++-- src/app/screens/nftDashboard/index.tsx | 10 +- .../screens/nftDashboard/receiveNft/index.tsx | 24 ++-- .../screens/nftDashboard/useNftDashboard.tsx | 12 +- src/app/screens/ordinalDetail/index.styled.ts | 47 +++----- src/app/screens/ordinalDetail/index.tsx | 47 ++++---- src/app/screens/ordinalsCollection/index.tsx | 12 +- src/app/screens/sendStx/stxAmountSelector.tsx | 47 ++++---- .../screens/settings/changeNetwork/index.tsx | 10 +- .../preferences/privacyPreferences/index.tsx | 22 +--- src/app/screens/settings/security/index.tsx | 2 +- .../settings/settingComponent/index.tsx | 30 ++--- src/app/screens/stacking/index.tsx | 4 +- .../stackingProgress/components/index.tsx | 21 ++-- .../stacking/stackingProgress/index.tsx | 13 ++- .../screens/stacking/startStacking/index.tsx | 5 +- .../swap/components/amountInput/index.tsx | 8 +- .../swap/components/routeItem/index.tsx | 5 +- src/app/screens/swap/quoteSummary/EditFee.tsx | 25 +---- src/app/ui-components/btcAmountSelector.tsx | 11 +- src/app/ui-components/selectFeeRate/index.tsx | 32 +++--- src/app/ui-library/button.tsx | 10 +- src/app/ui-library/common.styled.ts | 26 +---- src/app/ui-library/input.tsx | 26 +++-- src/app/ui-library/sheet.tsx | 2 +- .../tabs/index.tsx => ui-library/tabs.tsx} | 12 +- src/app/ui-library/toggle.tsx | 104 ++++++++++++++++++ src/locales/en.json | 1 + src/pages/Options/index.css | 15 +++ src/pages/Popup/index.css | 15 +++ tests/pages/onboarding.ts | 2 +- tests/pages/wallet.ts | 14 +-- 62 files changed, 685 insertions(+), 700 deletions(-) delete mode 100644 src/app/components/updatedBottomModal/index.tsx rename src/app/{components/tabs/index.tsx => ui-library/tabs.tsx} (86%) create mode 100644 src/app/ui-library/toggle.tsx diff --git a/package-lock.json b/package-lock.json index c3f119e2a..28949fb56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,6 @@ "react-qr-code": "^2.0.8", "react-redux": "^7.2.1", "react-router-dom": "^6.4.0", - "react-switch": "^7.0.0", "react-tabs": "^6.0.2", "react-tooltip": "^5.4.0", "redux": "^4.0.5", @@ -10472,18 +10471,6 @@ "react-dom": ">=16.8" } }, - "node_modules/react-switch": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/react-switch/-/react-switch-7.0.0.tgz", - "integrity": "sha512-KkDeW+cozZXI6knDPyUt3KBN1rmhoVYgAdCJqAh7st7tk8YE6N0iR89zjCWO8T8dUTeJGTR0KU+5CHCRMRffiA==", - "dependencies": { - "prop-types": "^15.7.2" - }, - "peerDependencies": { - "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-tabs": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-6.0.2.tgz", @@ -20763,14 +20750,6 @@ "react-router": "6.14.2" } }, - "react-switch": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/react-switch/-/react-switch-7.0.0.tgz", - "integrity": "sha512-KkDeW+cozZXI6knDPyUt3KBN1rmhoVYgAdCJqAh7st7tk8YE6N0iR89zjCWO8T8dUTeJGTR0KU+5CHCRMRffiA==", - "requires": { - "prop-types": "^15.7.2" - } - }, "react-tabs": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-6.0.2.tgz", diff --git a/package.json b/package.json index 057a6678b..a9f15f516 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,6 @@ "react-qr-code": "^2.0.8", "react-redux": "^7.2.1", "react-router-dom": "^6.4.0", - "react-switch": "^7.0.0", "react-tabs": "^6.0.2", "react-tooltip": "^5.4.0", "redux": "^4.0.5", diff --git a/src/app/components/accountHeader/index.tsx b/src/app/components/accountHeader/index.tsx index 17ba73762..4a77ed6b6 100644 --- a/src/app/components/accountHeader/index.tsx +++ b/src/app/components/accountHeader/index.tsx @@ -16,10 +16,8 @@ const SelectedAccountContainer = styled.div<{ showBorderBottom?: boolean }>((pro position: 'relative', alignItems: 'center', justifyContent: 'space-between', - padding: `${props.theme.spacing(10)}px ${props.theme.spacing(8)}px`, - borderBottom: props.showBorderBottom - ? `0.5px solid ${props.theme.colors.background.elevation3}` - : 'none', + padding: `${props.theme.spacing(10)}px ${props.theme.space.m}`, + borderBottom: props.showBorderBottom ? `0.5px solid ${props.theme.colors.elevation3}` : 'none', })); const OptionsButton = styled.button(() => ({ @@ -38,7 +36,7 @@ const ButtonRow = styled.button` padding-bottom: ${(props) => props.theme.space.s}; padding-left: ${(props) => props.theme.space.l}; padding-right: ${(props) => props.theme.space.l}; - font: ${(props) => props.theme.body_medium_m}; + font: ${(props) => props.theme.typography.body_medium_m}; color: ${(props) => props.theme.colors.white_0}; transition: background-color 0.2s ease; :hover { diff --git a/src/app/components/accountRow/index.styled.ts b/src/app/components/accountRow/index.styled.ts index d4d6757d3..2d969fdc1 100644 --- a/src/app/components/accountRow/index.styled.ts +++ b/src/app/components/accountRow/index.styled.ts @@ -1,6 +1,22 @@ import Button from '@ui-library/button'; import styled from 'styled-components'; +export const Container = styled.div({ + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: 'transparent', +}); + +export const AccountInfoContainer = styled.button<{ $disableClick?: boolean }>((props) => ({ + width: '100%', + display: 'flex', + alignItems: 'center', + backgroundColor: 'transparent', + cursor: props.$disableClick ? 'initial' : 'pointer', +})); + export const GradientCircle = styled.div<{ $firstGradient: string; $secondGradient: string; @@ -15,21 +31,6 @@ export const GradientCircle = styled.div<{ opacity: props.$isSelected ? 1 : 0.5, })); -export const TopSectionContainer = styled.div<{ disableClick?: boolean }>((props) => ({ - position: 'relative', - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - backgroundColor: 'transparent', - cursor: props.disableClick ? 'initial' : 'pointer', -})); - -export const AccountInfoContainer = styled.div({ - width: '100%', - display: 'flex', - alignItems: 'center', -}); - export const CurrentAccountContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'column', diff --git a/src/app/components/accountRow/index.tsx b/src/app/components/accountRow/index.tsx index 5f873694a..6e8354717 100644 --- a/src/app/components/accountRow/index.tsx +++ b/src/app/components/accountRow/index.tsx @@ -16,12 +16,14 @@ import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import 'react-tooltip/dist/react-tooltip.css'; +import Theme from 'theme'; import { AccountInfoContainer, AccountName, Balance, BarLoaderContainer, ButtonRow, + Container, CurrentAccountContainer, CurrentAccountTextContainer, GradientCircle, @@ -32,7 +34,6 @@ import { ModalDescription, OptionsButton, StyledButton, - TopSectionContainer, TransparentSpan, } from './index.styled'; @@ -193,8 +194,8 @@ function AccountRow({ }; return ( - - + + {isLedgerAccount(account) && Ledger icon} {isSelected && !disabledAccountSelect && !isAccountListView && ( - + )} {isAccountListView && totalBalance && ( @@ -337,7 +338,7 @@ function AccountRow({
)} - + ); } diff --git a/src/app/components/alertMessage/index.tsx b/src/app/components/alertMessage/index.tsx index 6287416aa..1621bdbdf 100644 --- a/src/app/components/alertMessage/index.tsx +++ b/src/app/components/alertMessage/index.tsx @@ -1,6 +1,8 @@ import Cross from '@assets/img/dashboard/X.svg'; import ActionButton from '@components/button'; +import Button from '@ui-library/button'; import Checkbox from '@ui-library/checkbox'; +import { useEffect, useRef } from 'react'; import styled from 'styled-components'; const Container = styled.div((props) => ({ @@ -75,7 +77,7 @@ const OuterContainer = styled.div((props) => ({ opacity: 0.6, })); -interface Props { +type Props = { onClose: () => void; title: string; description: string; @@ -87,7 +89,7 @@ interface Props { onSecondButtonClick?: () => void; tickMarkButtonClick?: (e: React.ChangeEvent) => void; tickMarkButtonChecked?: boolean; -} +}; function AlertMessage({ onClose, @@ -102,13 +104,30 @@ function AlertMessage({ tickMarkButtonClick, tickMarkButtonChecked, }: Props) { + const buttonRef = useRef(null); + const previousFocusRef = useRef(null); + + useEffect(() => { + previousFocusRef.current = document.activeElement as HTMLElement; + + if (buttonRef.current) { + buttonRef.current.focus(); + } + + return () => { + if (previousFocusRef.current) { + previousFocusRef.current.focus(); + } + }; + }, []); + return ( <> {title} - + cross @@ -127,7 +146,7 @@ function AlertMessage({ )} {!onSecondButtonClick && onButtonClick && ( - + - {text} - + {icon && {icon}} + + {text} + ); } diff --git a/src/app/components/tabBar/index.tsx b/src/app/components/tabBar/index.tsx index 64846f5dc..28b71659e 100644 --- a/src/app/components/tabBar/index.tsx +++ b/src/app/components/tabBar/index.tsx @@ -4,6 +4,9 @@ import { isInOptions } from '@utils/helper'; import { useNavigate } from 'react-router-dom'; import styled, { useTheme } from 'styled-components'; +const BUTTON_WIDTH = 56; +const BUTTON_HEIGHT = 32; + const RowContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'row', @@ -11,46 +14,61 @@ const RowContainer = styled.div((props) => ({ width: '100%', minHeight: 64, justifyContent: 'space-between', - paddingLeft: props.theme.spacing(30), - paddingRight: props.theme.spacing(30), + paddingLeft: props.theme.space.xl, + paddingRight: props.theme.space.xl, borderTop: `1px solid ${props.theme.colors.elevation3}`, })); const MovingDiv = styled(animated.div)((props) => ({ - width: 56, - height: 32, + width: BUTTON_WIDTH, + height: BUTTON_HEIGHT, backgroundColor: props.theme.colors.white_900, position: 'absolute', - bottom: 18, - left: 20, + bottom: BUTTON_HEIGHT / 2, borderRadius: 16, })); const Button = styled.button({ - backgroundColor: 'transparent', zIndex: 2, + width: BUTTON_WIDTH, + height: BUTTON_HEIGHT, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 16, + backgroundColor: 'transparent', }); export type Tab = 'dashboard' | 'nft' | 'stacking' | 'explore' | 'settings'; -interface Props { +type Props = { tab: Tab; -} +}; + function BottomTabBar({ tab }: Props) { const navigate = useNavigate(); const theme = useTheme(); const getPosition = () => { - if (tab === 'nft') return 78; - if (tab === 'stacking') return 132; - if (tab === 'explore') return 186; - if (tab === 'settings') return 239; - return 25; + const containerPadding = parseInt(theme.space.xl, 10); + const gap = (window.innerWidth - 2 * containerPadding - 5 * BUTTON_WIDTH) / 4; + + switch (tab) { + case 'nft': + return containerPadding + BUTTON_WIDTH + gap; + case 'stacking': + return containerPadding + 2 * (BUTTON_WIDTH + gap); + case 'explore': + return containerPadding + 3 * (BUTTON_WIDTH + gap); + case 'settings': + return containerPadding + 4 * (BUTTON_WIDTH + gap); + default: // 'dashboard' + return containerPadding; + } }; const styles = useSpring({ - from: { x: getPosition() }, // TODO: enable slide animation - to: { x: getPosition() }, + left: getPosition(), // TODO: enable slide animation config: { duration: 200, easing: easings.easeOutCirc, diff --git a/src/app/components/updatedBottomModal/index.tsx b/src/app/components/updatedBottomModal/index.tsx deleted file mode 100644 index 5040b69ea..000000000 --- a/src/app/components/updatedBottomModal/index.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { XCircle } from '@phosphor-icons/react'; -import Modal from 'react-modal'; -import styled, { useTheme } from 'styled-components'; - -const BottomModalHeaderText = styled.h1((props) => ({ - ...props.theme.typography.body_bold_l, - flex: 1, -})); - -const RowContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - padding: props.theme.space.m, -})); - -const ButtonImage = styled.button((props) => ({ - display: 'flex', - alignItems: 'center', - color: props.theme.colors.white_400, - background: 'transparent', -})); - -interface Props { - header: string; - visible: boolean; - children: React.ReactNode; - onClose: () => void; - overlayStylesOverriding?: {}; - contentStylesOverriding?: {}; -} - -const CustomisedModal = styled(Modal)` - overflow-y: auto; - &::-webkit-scrollbar { - display: none; - } - position: absolute; -`; - -function UpdatedBottomModal({ - header, - children, - visible, - onClose, - overlayStylesOverriding, - contentStylesOverriding, -}: Props) { - const theme = useTheme(); - const isGalleryOpen: boolean = document.documentElement.clientWidth > 360; - const customStyles = { - overlay: { - backgroundColor: isGalleryOpen ? 'transparent' : theme.colors.background.modalBackdrop, - height: '100%', - width: 360, - margin: 'auto', - zIndex: 4, - ...overlayStylesOverriding, - }, - content: { - inset: 'auto auto 0px auto', - width: '100%', - maxWidth: 360, - maxHeight: '90%', - border: 'transparent', - background: theme.colors.elevation6_600, - backdropFilter: 'blur(24px)', - margin: 0, - padding: 0, - borderTopLeftRadius: isGalleryOpen ? 12 : 16, - borderTopRightRadius: isGalleryOpen ? 12 : 16, - borderBottomRightRadius: isGalleryOpen ? 12 : 0, - borderBottomLeftRadius: isGalleryOpen ? 12 : 0, - ...contentStylesOverriding, - }, - }; - - return ( - document.getElementById('app') as HTMLElement} - ariaHideApp={false} - style={customStyles} - contentLabel="Example Modal" - > - - {header} - - - - - {children} - - ); -} - -/** - * @deprecated use @ui-library/sheet - */ -export default UpdatedBottomModal; diff --git a/src/app/components/webGalleryButton/index.tsx b/src/app/components/webGalleryButton/index.tsx index 5d62f4e8a..ce1f9b6a1 100644 --- a/src/app/components/webGalleryButton/index.tsx +++ b/src/app/components/webGalleryButton/index.tsx @@ -10,6 +10,7 @@ const StyledButton = styled.button` gap: ${(props) => props.theme.space.xs}; background-color: transparent; color: ${(props) => props.theme.colors.white_0}; + transition: color 0.1s ease; :hover:enabled { color: ${(props) => props.theme.colors.white_200}; } diff --git a/src/app/screens/explore/index.tsx b/src/app/screens/explore/index.tsx index 9b6c78380..924b9dabb 100644 --- a/src/app/screens/explore/index.tsx +++ b/src/app/screens/explore/index.tsx @@ -2,11 +2,11 @@ import FeaturedCardCarousel from '@components/explore/FeaturedCarousel'; import RecommendedApps from '@components/explore/RecommendedApps'; import SwiperNavigation from '@components/explore/SwiperNavigation'; import BottomBar from '@components/tabBar'; -import Tabs from '@components/tabs'; import useFeaturedDapps from '@hooks/useFeaturedDapps'; import { ArrowsOut } from '@phosphor-icons/react'; import { StyledHeading } from '@ui-library/common.styled'; import Spinner from '@ui-library/spinner'; +import Tabs from '@ui-library/tabs'; import { XVERSE_EXPLORE_URL } from '@utils/constants'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -38,6 +38,7 @@ const ExternalLink = styled.a` ${({ theme }) => theme.typography.body_medium_m}; display: flex; align-items: center; + align-self: flex-start; column-gap: ${({ theme }) => theme.space.xs}; color: ${({ theme }) => theme.colors.white_0}; margin-top: ${({ theme }) => theme.space.s}; @@ -58,7 +59,7 @@ const LoaderContainer = styled.div((props) => ({ flex: 1, justifyContent: 'center', alignItems: 'center', - marginTop: props.theme.spacing(12), + marginTop: props.theme.space.l, })); function ExploreScreen() { diff --git a/src/app/screens/forgotPassword/index.tsx b/src/app/screens/forgotPassword/index.tsx index 93e2c5303..860719d34 100644 --- a/src/app/screens/forgotPassword/index.tsx +++ b/src/app/screens/forgotPassword/index.tsx @@ -1,7 +1,7 @@ -import ActionButton from '@components/button'; -import CheckBox from '@components/checkBox'; import TopRow from '@components/topRow'; import useWalletReducer from '@hooks/useWalletReducer'; +import Button from '@ui-library/button'; +import Checkbox from '@ui-library/checkbox'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; @@ -12,14 +12,14 @@ const Container = styled.div((props) => ({ flexDirection: 'column', height: '100%', backgroundColor: props.theme.colors.elevation0, - padding: `0 ${props.theme.spacing(8)}px 0 ${props.theme.spacing(8)}px`, + padding: `0 ${props.theme.space.m}`, })); const Paragraph = styled.p((props) => ({ - ...props.theme.body_l, + ...props.theme.typography.body_l, color: props.theme.colors.white_200, textAlign: 'left', - marginTop: props.theme.spacing(12), + marginTop: props.theme.space.l, })); const BottomContainer = styled.div((props) => ({ @@ -30,8 +30,8 @@ const ButtonsContainer = styled.div((props) => ({ display: 'flex', alignItems: 'center', justifyContent: 'space-between', - marginTop: props.theme.spacing(16), - columnGap: props.theme.spacing(8), + marginTop: props.theme.space.xl, + columnGap: props.theme.space.xs, })); const StyledTopRow = styled(TopRow)({ @@ -54,7 +54,7 @@ function ForgotPassword(): JSX.Element { const handleResetWallet = async () => { await resetWallet(); - navigate('/'); + onBack(); }; return ( @@ -63,19 +63,19 @@ function ForgotPassword(): JSX.Element { {t('PARAGRAPH1')} {t('PARAGRAPH2')} - - - + + + diff --git a/src/app/screens/stacking/index.tsx b/src/app/screens/stacking/index.tsx index a38c0b290..8d2d96900 100644 --- a/src/app/screens/stacking/index.tsx +++ b/src/app/screens/stacking/index.tsx @@ -13,11 +13,11 @@ const LoaderContainer = styled.div((props) => ({ flex: 1, justifyContent: 'center', alignItems: 'center', - marginTop: props.theme.spacing(12), + marginTop: props.theme.space.l, })); const Text = styled.h1((props) => ({ - ...props.theme.body_bold_m, + ...props.theme.typography.body_bold_m, color: props.theme.colors.white_200, marginTop: 'auto', marginBottom: 'auto', diff --git a/src/app/screens/stacking/stackingProgress/components/index.tsx b/src/app/screens/stacking/stackingProgress/components/index.tsx index 234d4e51d..25d54dc55 100644 --- a/src/app/screens/stacking/stackingProgress/components/index.tsx +++ b/src/app/screens/stacking/stackingProgress/components/index.tsx @@ -5,9 +5,10 @@ export const Container = styled.button((props) => ({ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - background: props.theme.colors.elevation1, + backgroundColor: 'transparent', + border: `1px solid ${props.theme.colors.white_850}`, borderRadius: props.theme.radius(2), - padding: props.theme.spacing(9), + padding: props.theme.space.m, })); export const TextContainer = styled.div({ @@ -23,17 +24,17 @@ export const StatusContainer = styled.div((props) => ({ alignItems: 'center', background: 'rgba(81, 214, 166, 0.15)', borderRadius: props.theme.radius(7), - paddingLeft: props.theme.spacing(7), - paddingRight: props.theme.spacing(7), - paddingTop: props.theme.spacing(4), - paddingBottom: props.theme.spacing(4), + paddingLeft: props.theme.space.m, + paddingRight: props.theme.space.m, + paddingTop: props.theme.space.xs, + paddingBottom: props.theme.space.xs, })); export const Dot = styled.div((props) => ({ width: 7, height: 7, borderRadius: props.theme.radius(9), - marginRight: props.theme.spacing(4), + marginRight: props.theme.space.xs, background: props.theme.colors.feedback.success, })); @@ -41,16 +42,16 @@ export const ColumnContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', - marginLeft: props.theme.spacing(6), + marginLeft: props.theme.space.s, })); export const BoldText = styled.h1((props) => ({ - ...props.theme.body_bold_m, + ...props.theme.typography.body_bold_m, color: props.theme.colors.white_0, })); export const SubText = styled.h1((props) => ({ - ...props.theme.body_xs, + ...props.theme.typography.body_s, color: props.theme.colors.white_400, })); diff --git a/src/app/screens/stacking/stackingProgress/index.tsx b/src/app/screens/stacking/stackingProgress/index.tsx index 83bd173ce..94d6f7b40 100644 --- a/src/app/screens/stacking/stackingProgress/index.tsx +++ b/src/app/screens/stacking/stackingProgress/index.tsx @@ -6,21 +6,22 @@ const Container = styled.div((props) => ({ display: 'flex', flex: 1, flexDirection: 'column', - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), + marginLeft: props.theme.space.m, + marginRight: props.theme.space.m, })); const TitleText = styled.h1((props) => ({ ...props.theme.headline_s, - marginTop: props.theme.spacing(16), + marginTop: props.theme.space.xl, })); const StackingDescriptionText = styled.h1((props) => ({ - ...props.theme.body_m, - marginTop: props.theme.spacing(4), - marginBottom: props.theme.spacing(18), + ...props.theme.typography.body_m, + marginTop: props.theme.space.xs, + marginBottom: props.theme.space.l, color: props.theme.colors.white_200, })); + function StackingProgress() { const { t } = useTranslation('translation', { keyPrefix: 'STACKING_SCREEN' }); diff --git a/src/app/screens/stacking/startStacking/index.tsx b/src/app/screens/stacking/startStacking/index.tsx index 71eac292d..8edac7426 100644 --- a/src/app/screens/stacking/startStacking/index.tsx +++ b/src/app/screens/stacking/startStacking/index.tsx @@ -1,8 +1,7 @@ -import ArrowSquareOut from '@assets/img/arrow_square_out.svg'; -import ActionButton from '@components/button'; import useStackingData from '@hooks/queries/useStackingData'; import type { Pool } from '@secretkeylabs/xverse-core'; import { microstacksToStx } from '@secretkeylabs/xverse-core'; +import Button from '@ui-library/button'; import { XVERSE_WEB_POOL_URL } from '@utils/constants'; import BigNumber from 'bignumber.js'; import { useTranslation } from 'react-i18next'; @@ -63,7 +62,7 @@ function StartStacking() { - + + + + )} + + + ); +} + +export default BannerCarousel; diff --git a/src/app/screens/home/index.styled.ts b/src/app/screens/home/index.styled.ts index be06d44bb..b2f4fbb47 100644 --- a/src/app/screens/home/index.styled.ts +++ b/src/app/screens/home/index.styled.ts @@ -2,7 +2,7 @@ import TokenTile from '@components/tokenTile'; import Divider from '@ui-library/divider'; import styled from 'styled-components'; -export const Container = styled.div<{ $applyIndent?: boolean }>` +export const Container = styled.div` display: flex; flex-direction: column; flex: 1; diff --git a/src/app/screens/home/index.tsx b/src/app/screens/home/index.tsx index 4657f92c1..114479b67 100644 --- a/src/app/screens/home/index.tsx +++ b/src/app/screens/home/index.tsx @@ -50,7 +50,7 @@ import { useNavigate } from 'react-router-dom'; import { useTheme } from 'styled-components'; import SquareButton from '../../components/squareButton'; import BalanceCard from './balanceCard'; -import Banner from './banner'; +import BannerCarousel from './bannerCarousel'; import { ButtonImage, ButtonText, @@ -109,7 +109,8 @@ function Home() { const { isInitialLoading: loadingStxWalletData, isRefetching: refetchingStxWalletData } = useStxWalletData(); const { btcFiatRate, stxBtcRate } = useCoinRates(); - const { data: notificationBannersArr } = useNotificationBanners(); + const { data: notificationBannersArr, isFetching: isFetchingNotificationBannersArr } = + useNotificationBanners(); const { unfilteredData: fullSip10CoinsList, visible: sip10CoinsList, @@ -182,10 +183,11 @@ function Home() { .concat(runesCoinsList) .sort((a, b) => sortFtByFiatBalance(a, b, stxBtcRate, btcFiatRate)); - const showNotificationBanner = - notificationBannersArr?.length && - notificationBannersArr.length > 0 && - !notificationBanners[notificationBannersArr[0].id]; + const filteredNotificationBannersArr = notificationBannersArr + ? notificationBannersArr.filter((banner) => !notificationBanners[banner.id]) + : []; + const showBannerCarousel = + !isFetchingNotificationBannersArr && !!filteredNotificationBannersArr?.length; const onReceiveModalOpen = () => { setOpenReceiveModal(true); @@ -422,7 +424,7 @@ function Home() { {isOrdinalReceiveAlertVisible && ( )} - + - {showNotificationBanner ? ( + {showBannerCarousel ? ( <>
- + ) : ( From a95a26a688c126766e69f28a6c305ee59a7a3aae Mon Sep 17 00:00:00 2001 From: jordankzf Date: Thu, 5 Sep 2024 15:00:00 +0800 Subject: [PATCH 080/227] Don't show fiatCurrency if there is no fiat value --- src/app/screens/swap/components/amountInput/index.tsx | 3 ++- src/app/screens/swap/quotesModal/quoteTile.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/screens/swap/components/amountInput/index.tsx b/src/app/screens/swap/components/amountInput/index.tsx index 5766bf04c..0126fb80e 100644 --- a/src/app/screens/swap/components/amountInput/index.tsx +++ b/src/app/screens/swap/components/amountInput/index.tsx @@ -133,9 +133,10 @@ export default function AmountInput({ max, input, balance }: Props) { displayType="text" thousandSeparator prefix={`~${currencySymbolMap[input.fiatCurrency]}`} + suffix={input.fiatCurrency} renderText={(value: string) => ( - {value || '--'} {input.fiatCurrency} + {value || '--'} )} /> diff --git a/src/app/screens/swap/quotesModal/quoteTile.tsx b/src/app/screens/swap/quotesModal/quoteTile.tsx index 5b7445f66..2291578cd 100644 --- a/src/app/screens/swap/quotesModal/quoteTile.tsx +++ b/src/app/screens/swap/quotesModal/quoteTile.tsx @@ -115,9 +115,10 @@ function QuoteTile({ displayType="text" thousandSeparator prefix="~ $" + suffix={fiatCurrency} renderText={(value: string) => ( - {value || '--'} {fiatCurrency} + {value || '--'} )} /> From 97a1e1652dc32ae7059b5514b5d9501166be04be Mon Sep 17 00:00:00 2001 From: jordankzf Date: Fri, 6 Sep 2024 16:59:56 +0800 Subject: [PATCH 081/227] Unified Provider Sorting --- src/app/screens/swap/quoteSummary/index.tsx | 4 +- src/app/screens/swap/quotesModal/index.tsx | 120 ++++++++++-------- .../screens/swap/quotesModal/quoteTile.tsx | 11 +- 3 files changed, 79 insertions(+), 56 deletions(-) diff --git a/src/app/screens/swap/quoteSummary/index.tsx b/src/app/screens/swap/quoteSummary/index.tsx index 1fdc543e4..e0dd19687 100644 --- a/src/app/screens/swap/quoteSummary/index.tsx +++ b/src/app/screens/swap/quoteSummary/index.tsx @@ -436,7 +436,7 @@ export default function QuoteSummary({ ? fromToken?.assetName : fromToken?.name } - subtitleColor="white_400" + subtitleColorOverride="white_400" unit={fromUnit} fiatValue={fromTokenFiatValue} /> @@ -463,7 +463,7 @@ export default function QuoteSummary({ : undefined, }} subtitle={toToken?.protocol === 'btc' ? 'Bitcoin' : toToken?.name} - subtitleColor="white_400" + subtitleColorOverride="white_400" unit={toUnit} fiatValue={toTokenFiatValue} /> diff --git a/src/app/screens/swap/quotesModal/index.tsx b/src/app/screens/swap/quotesModal/index.tsx index ec256e0f5..07ce4ed8b 100644 --- a/src/app/screens/swap/quotesModal/index.tsx +++ b/src/app/screens/swap/quotesModal/index.tsx @@ -1,4 +1,3 @@ -import { microStxToStx } from '@components/postCondition/postConditionView/helper'; import useCoinRates from '@hooks/queries/useCoinRates'; import { getBtcFiatEquivalent, @@ -15,7 +14,6 @@ import Sheet from '@ui-library/sheet'; import BigNumber from 'bignumber.js'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -import type { Color } from 'theme'; import QuoteTile from './quoteTile'; interface Props { @@ -29,10 +27,10 @@ interface Props { utxoProviderClicked?: (utxoProvider: UtxoQuote) => void; } -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', -})); +const Container = styled.div` + display: flex; + flex-direction: column; +`; const Heading = styled(StyledP)` margin-top: ${(props) => props.theme.space.l}; @@ -68,20 +66,61 @@ function QuotesModal({ const { btcFiatRate, stxBtcRate } = useCoinRates(); - const numberOfUtxoProviders = utxoProviders.length; - const lowestUtxoQuoteFloorRate = Math.min( - ...utxoProviders.map((provider) => Number(provider.floorRate)), + const sortQuotesByReceiveAmount = (quotes: T[]): T[] => + [...quotes].sort((a, b) => + BigNumber(b.receiveAmount).gte(a.receiveAmount) + ? 1 + : BigNumber(a.receiveAmount).gte(b.receiveAmount) + ? -1 + : 0, + ); + + const sortedAmmQuotes = sortQuotesByReceiveAmount(ammProviders); + const sortedStxQuotes = sortQuotesByReceiveAmount(stxProviders); + const sortedUtxoQuotes = [...utxoProviders].sort((a, b) => + BigNumber(b.floorRate).gte(a.floorRate) ? -1 : BigNumber(a.floorRate).gte(b.floorRate) ? 1 : 0, ); - const getSubtitle = (provider: UtxoQuote): string => { - if (numberOfUtxoProviders === 1) { + const getReceiveAmountSubtitle = ( + quote: StxQuote | Quote, + quotes: (StxQuote | Quote)[], + ): string => { + const highestReceiveAmount = BigNumber.max( + ...quotes.map((q) => new BigNumber(q.receiveAmount)), + ); + + if (quote.provider.code === 'dotswap') { + return t('RECOMMENDED'); + } + + if (new BigNumber(quote.receiveAmount).eq(highestReceiveAmount)) { + return t('BEST'); + } + + const difference = highestReceiveAmount.minus(quote.receiveAmount); + const percentageDifference = difference.div(highestReceiveAmount).times(100); + + if (percentageDifference.gt(0)) { + return `+${percentageDifference.toFixed(2)}%`; + } + + return `-${percentageDifference.abs().toFixed(2)}%`; + }; + + const getFloorPriceSubtitle = (quote: UtxoQuote, quotes: UtxoQuote[]): string => { + if (quotes.length === 1) { return ''; } - if (Number(provider.floorRate) === lowestUtxoQuoteFloorRate) { + + const lowestUtxoQuoteFloorRate = Math.min( + ...utxoProviders.map((provider) => Number(provider.floorRate)), + ); + + if (Number(quote.floorRate) === lowestUtxoQuoteFloorRate) { return t('BEST'); } const difference = lowestUtxoQuoteFloorRate - ? Number(provider.floorRate) - lowestUtxoQuoteFloorRate + ? Number(quote.floorRate) - lowestUtxoQuoteFloorRate : 0; const percentageDifference = (difference / lowestUtxoQuoteFloorRate) * 100; if (percentageDifference > 0) { @@ -90,18 +129,6 @@ function QuotesModal({ return `-${Math.abs(percentageDifference).toFixed(2)}%`; }; - const ammQuotes = [...ammProviders].sort((a, b) => - BigNumber(b.receiveAmount).gte(a.receiveAmount) - ? 1 - : BigNumber(a.receiveAmount).gte(b.receiveAmount) - ? -1 - : 0, - ); - - const utxoQuotes = [...utxoProviders].sort((a, b) => - BigNumber(b.floorRate).gte(a.floorRate) ? -1 : BigNumber(a.floorRate).gte(b.floorRate) ? 1 : 0, - ); - return ( @@ -114,15 +141,14 @@ function QuotesModal({ {t('EXCHANGE')} ))} - {ammProviders.map((amm) => ( + {sortedAmmQuotes.map((amm) => ( ammProviderClicked && ammProviderClicked(amm)} - subtitle={t('RECOMMENDED')} - subtitleColor="success_light" + subtitle={getReceiveAmountSubtitle(amm, ammProviders)} unit={amm.to.protocol === 'btc' ? 'Sats' : toToken?.symbol || ''} fiatValue={ amm.to.protocol === 'btc' @@ -139,15 +165,14 @@ function QuotesModal({ {t('MARKETPLACE')} )} - {stxProviders.map((stx) => ( + {sortedStxQuotes.map((stx) => ( ammProviderClicked && ammProviderClicked(stx)} - subtitle={t('RECOMMENDED')} - subtitleColor="success_light" + subtitle={getReceiveAmountSubtitle(stx, stxProviders)} unit={stx.to.protocol === 'stx' ? 'STX' : toToken?.name || ''} fiatValue={ stx.to.protocol === 'stx' @@ -160,27 +185,18 @@ function QuotesModal({ } /> ))} - {utxoQuotes.map((utxoProvider) => { - const subTitle = getSubtitle(utxoProvider); - let subTitleColour: Color = 'success_light'; - - if (subTitle.startsWith('+')) { - subTitleColour = 'danger_light'; - } - return ( - utxoProviderClicked && utxoProviderClicked(utxoProvider)} - subtitle={subTitle} - subtitleColor={subTitleColour} - unit={toToken?.symbol ? `Sats / ${toToken.symbol}` : ''} - /> - ); - })} + {sortedUtxoQuotes.map((utxoProvider) => ( + utxoProviderClicked && utxoProviderClicked(utxoProvider)} + subtitle={getFloorPriceSubtitle(utxoProvider, utxoProviders)} + unit={toToken?.symbol ? `Sats / ${toToken.symbol}` : ''} + /> + ))} ); diff --git a/src/app/screens/swap/quotesModal/quoteTile.tsx b/src/app/screens/swap/quotesModal/quoteTile.tsx index fd3280650..531f7c98b 100644 --- a/src/app/screens/swap/quotesModal/quoteTile.tsx +++ b/src/app/screens/swap/quotesModal/quoteTile.tsx @@ -60,7 +60,7 @@ interface Props { ft?: FungibleToken; }; subtitle?: string; - subtitleColor?: Color; + subtitleColorOverride?: Color; fiatValue?: string; floorText?: string; onClick?: () => void; @@ -72,7 +72,7 @@ function QuoteTile({ price, image, subtitle, - subtitleColor, + subtitleColorOverride, fiatValue, floorText, onClick, @@ -81,6 +81,13 @@ function QuoteTile({ const theme = useTheme(); const { fiatCurrency } = useWalletSelector(); + const getSubtitleColor = (): Color | undefined => { + if (!subtitle) return undefined; + return subtitle.startsWith('+') ? 'danger_light' : 'success_light'; + }; + + const subtitleColor = subtitleColorOverride ?? getSubtitleColor(); + return ( From 590ba41456ec805042254771e4bf155da399c297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduard=20Bardaj=C3=AD=20Puig?= Date: Fri, 6 Sep 2024 11:39:21 +0200 Subject: [PATCH 082/227] Add missing return (#566) --- src/common/utils/rpc/btc/getAddresses/getAddresses.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/utils/rpc/btc/getAddresses/getAddresses.ts b/src/common/utils/rpc/btc/getAddresses/getAddresses.ts index 95df6e2f1..7483c78a6 100644 --- a/src/common/utils/rpc/btc/getAddresses/getAddresses.ts +++ b/src/common/utils/rpc/btc/getAddresses/getAddresses.ts @@ -42,6 +42,7 @@ export const handleGetAddresses = async (message: RpcRequestMessage, port: chrom messageId: parseResult.output.id, }), }); + return; } const { From 76305a5e3251cb277c375d50ec2d1a34b3fabdb4 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Fri, 6 Sep 2024 18:18:02 +0800 Subject: [PATCH 083/227] [ENG-4958][ENG-5105] core update - adjust threshold for ordinals split send & 5byte brc20 fix (#560) * chore: bump core version to update split send threshold * chore: bump to core 19.1.0 includes split send threshold change and brc20 5 byte ticker fix --- package-lock.json | 15 +++++++-------- package.json | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 125fb24d8..3bcf90680 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.2.1", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "19.0.0", + "@secretkeylabs/xverse-core": "19.1.0", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.16.1", @@ -1277,10 +1277,9 @@ } }, "node_modules/@secretkeylabs/xverse-core": { - "version": "19.0.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/19.0.0/7b2d47d225739051b88873932cd373fa5672b647", - "integrity": "sha512-3TBd/JeH0PCvqmPKsFJee8XwvBTAUWkrqNgkA3jUCmj1lpdBn6Uw6MTAsTdMBIiOvT8A4PdNf44BrPplTgHZdw==", - "license": "ISC", + "version": "19.1.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/19.1.0/fe6b53e68ea3d66c7fd1bbf8e791f865299811d8", + "integrity": "sha512-ssrdJAb9jpl1mGCfbS4K6S22sKLBXzK5LbYMiNBU8anyps4A79u/c7qkHUDMgDY4pWcuSMs8iS4mcpyshPFiQQ==", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", @@ -13686,9 +13685,9 @@ } }, "@secretkeylabs/xverse-core": { - "version": "19.0.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/19.0.0/7b2d47d225739051b88873932cd373fa5672b647", - "integrity": "sha512-3TBd/JeH0PCvqmPKsFJee8XwvBTAUWkrqNgkA3jUCmj1lpdBn6Uw6MTAsTdMBIiOvT8A4PdNf44BrPplTgHZdw==", + "version": "19.1.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/19.1.0/fe6b53e68ea3d66c7fd1bbf8e791f865299811d8", + "integrity": "sha512-ssrdJAb9jpl1mGCfbS4K6S22sKLBXzK5LbYMiNBU8anyps4A79u/c7qkHUDMgDY4pWcuSMs8iS4mcpyshPFiQQ==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", diff --git a/package.json b/package.json index 346376016..a5b76e4c1 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.2.1", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "19.0.0", + "@secretkeylabs/xverse-core": "19.1.0", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.16.1", From 960980441b0a26cc2f1486c2a28ca66068bdd83c Mon Sep 17 00:00:00 2001 From: Terence Ng Date: Sat, 7 Sep 2024 14:46:33 +0800 Subject: [PATCH 084/227] track rune bundles (#551) * initial commit * add satribute icon * fix sizing * commit ui redesign * commit final logic work * fix up navigations * code review updates * update e2e * remove duplicate await * fix up spacing and margins * add role * add default export * add accessibility * seperate into component * fix up e2e * fix e2e --------- Co-authored-by: Den <36603049+dhriaznov@users.noreply.github.com> --- .../confirmBtcTransaction/itemRow/amount.tsx | 56 +++--- .../amountWithInscriptionSatribute.tsx | 1 + .../itemRow/runeAmount.tsx | 14 +- .../confirmBtcTransaction/sendSection.tsx | 25 ++- .../queries/runes/useRuneFloorPriceQuery.ts | 1 - .../runes/useRuneFungibleTokensQuery.ts | 1 - .../hooks/queries/runes/useRuneUtxosQuery.ts | 11 +- src/app/screens/coinDashboard/index.tsx | 166 +++++++++++------- .../screens/coinDashboard/runes/bundleRow.tsx | 151 ++++++++++++++++ .../screens/rareSatsBundle/bundleContent.tsx | 41 +++++ .../screens/rareSatsBundle/index.styled.ts | 16 +- src/app/screens/rareSatsBundle/index.tsx | 132 +++++++------- .../rareSatsBundle/rareSatsBundleGridItem.tsx | 12 +- src/app/screens/rareSatsBundle/runeAmount.tsx | 108 ++++++++++++ src/app/screens/sendOrdinal/index.tsx | 3 +- src/locales/en.json | 7 +- tests/pages/wallet.ts | 12 +- tests/specs/createWallet.spec.ts | 2 +- tests/specs/managementToken.spec.ts | 4 +- tests/specs/tabCollectiblesRareSats.spec.ts | 2 +- tests/specs/transactionHistory.spec.ts | 12 +- tests/specs/transactionSTX.spec.ts | 2 +- 22 files changed, 570 insertions(+), 209 deletions(-) create mode 100644 src/app/screens/coinDashboard/runes/bundleRow.tsx create mode 100644 src/app/screens/rareSatsBundle/bundleContent.tsx create mode 100644 src/app/screens/rareSatsBundle/runeAmount.tsx diff --git a/src/app/components/confirmBtcTransaction/itemRow/amount.tsx b/src/app/components/confirmBtcTransaction/itemRow/amount.tsx index 469417188..932ab340c 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/amount.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/amount.tsx @@ -9,20 +9,14 @@ import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; -const RowCenter = styled.div<{ spaceBetween?: boolean }>((props) => ({ +const Container = styled.div<{ spaceBetween?: boolean }>((props) => ({ + width: '100%', display: 'flex', - flex: 1, flexDirection: 'row', alignItems: 'center', - justifyContent: props.spaceBetween ? 'space-between' : 'initial', - columnGap: props.theme.space.m, marginBottom: props.theme.space.s, })); -const NumberTypeContainer = styled.div` - text-align: right; -`; - const StyledFiatAmountText = styled(FiatAmountText)` display: block; ${(props) => props.theme.typography.body_medium_s} @@ -30,8 +24,24 @@ const StyledFiatAmountText = styled(FiatAmountText)` margin-top: ${(props) => props.theme.space.xxxs}; `; -const StyledBtcTitle = styled(StyledP)` - margin-top: ${(props) => props.theme.space.xxxs}; +const Column = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 2px; + overflow: hidden; +`; + +const Row = styled.div` + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 24px; +`; + +const AvatarContainer = styled.div` + margin-right: ${(props) => props.theme.space.m}; `; type Props = { @@ -46,18 +56,15 @@ export default function Amount({ amount }: Props) { if (!amount) return null; return ( - - - -
+ + + + + + {t('CONFIRM_TRANSACTION.AMOUNT')} - - Bitcoin - -
- )} /> + + + + Bitcoin + - -
-
+ + +
); } diff --git a/src/app/components/confirmBtcTransaction/itemRow/amountWithInscriptionSatribute.tsx b/src/app/components/confirmBtcTransaction/itemRow/amountWithInscriptionSatribute.tsx index 37710a65e..48738f870 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/amountWithInscriptionSatribute.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/amountWithInscriptionSatribute.tsx @@ -17,6 +17,7 @@ const WarningContainer = styled.div` display: flex; flex-direction: column; border-radius: ${(props) => props.theme.space.s}; + margin-bottom: ${(props) => props.theme.space.s}; `; const WarningButton = styled.button` diff --git a/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx b/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx index 979f1849b..b00a0b802 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx @@ -17,7 +17,6 @@ const Container = styled.div<{ topMargin?: boolean }>((props) => ({ flexDirection: 'row', alignItems: 'center', marginBottom: props.theme.space.s, - marginTop: props.topMargin ? props.theme.space.s : 0, })); const AvatarContainer = styled.div` @@ -43,21 +42,16 @@ const Column = styled.div` type Props = { rune: RuneBase; hasSufficientBalance?: boolean; - topMargin?: boolean; }; -export default function RuneAmount({ - rune, - hasSufficientBalance = true, - topMargin = false, -}: Props) { +export default function RuneAmount({ rune, hasSufficientBalance = true }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const { runeName, amount, divisibility, symbol } = rune; const amountWithDecimals = ftDecimals(String(amount), divisibility); const { fiatCurrency } = useWalletSelector(); const { data: runeFiatRate } = useRuneFiatRateQuery(rune); return ( - + {runeFiatRate !== undefined && runeFiatRate > 0 && ( )} diff --git a/src/app/components/confirmBtcTransaction/sendSection.tsx b/src/app/components/confirmBtcTransaction/sendSection.tsx index e84e28733..833352000 100644 --- a/src/app/components/confirmBtcTransaction/sendSection.tsx +++ b/src/app/components/confirmBtcTransaction/sendSection.tsx @@ -26,9 +26,9 @@ const Container = styled.div((props) => ({ marginBottom: props.theme.space.s, })); -const RowContainer = styled.div<{ noPadding?: boolean; noMargin?: boolean }>((props) => ({ - padding: props.noPadding ? 0 : `0 ${props.theme.space.m}`, - marginBottom: props.noMargin ? 0 : props.theme.space.m, +const RowContainer = styled.div((props) => ({ + padding: `0 ${props.theme.space.m}`, + marginBottom: props.theme.space.s, })); const BundleHeader = styled.div((props) => ({ @@ -57,7 +57,7 @@ function SendSection({ {t('CONFIRM_TRANSACTION.YOU_WILL_SEND')} {Object.entries(bundledOutputs).map(([address, bundle], index) => ( - + {t('COMMON.TO')} @@ -66,20 +66,17 @@ function SendSection({ {getTruncatedAddress(address, 6)} - - - - - {bundle.runeTransfers.map((runeTransfer, i) => ( + + + {bundle.runeTransfers.map((runeTransfer) => ( 0} hasSufficientBalance={runeTransfer.hasSufficientBalance} /> ))} diff --git a/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts b/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts index 39525660d..cec5195b2 100644 --- a/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts +++ b/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts @@ -5,7 +5,6 @@ import { useCallback } from 'react'; export default function useRuneFloorPriceQuery(runeName: string, backgroundRefetch = true) { const { network } = useWalletSelector(); - const runesApi = useRunesApi(); const queryFn = useCallback( async () => diff --git a/src/app/hooks/queries/runes/useRuneFungibleTokensQuery.ts b/src/app/hooks/queries/runes/useRuneFungibleTokensQuery.ts index b220ae91b..e9526fe33 100644 --- a/src/app/hooks/queries/runes/useRuneFungibleTokensQuery.ts +++ b/src/app/hooks/queries/runes/useRuneFungibleTokensQuery.ts @@ -44,7 +44,6 @@ export const useRuneFungibleTokensQuery = (backgroundRefetch = true) => { refetchOnReconnect: backgroundRefetch, queryFn, }); - return { ...query, unfilteredData: query.data, diff --git a/src/app/hooks/queries/runes/useRuneUtxosQuery.ts b/src/app/hooks/queries/runes/useRuneUtxosQuery.ts index ba18c7b7c..847993edd 100644 --- a/src/app/hooks/queries/runes/useRuneUtxosQuery.ts +++ b/src/app/hooks/queries/runes/useRuneUtxosQuery.ts @@ -10,12 +10,12 @@ export default function useRuneUtxosQuery( backgroundRefetch = true, ) { const { network } = useWalletSelector(); - const { ordinalsAddress } = useSelectedAccount(); - if (network.type !== 'Mainnet') { - throw new Error('Only available on Mainnet'); - } const runesApi = useRunesApi(); + const { ordinalsAddress } = useSelectedAccount(); const queryFn = useCallback(async () => { + if (network.type !== 'Mainnet') { + return []; + } const res = await runesApi.getRunesUtxos({ address: ordinalsAddress, rune: runeName }); const sortedRes = res.sort((a, b) => { const amountA = Number(a.runes?.[0][1].amount); @@ -28,7 +28,8 @@ export default function useRuneUtxosQuery( if (filter === 'listed') { return sortedRes.filter((item) => item.listing[0] !== null); } - }, [runesApi, ordinalsAddress, filter, runeName]); + return sortedRes; + }, [network.type, runesApi, ordinalsAddress, runeName, filter]); return useQuery({ refetchOnWindowFocus: backgroundRefetch, refetchOnReconnect: backgroundRefetch, diff --git a/src/app/screens/coinDashboard/index.tsx b/src/app/screens/coinDashboard/index.tsx index 35d8a17b9..56e89c919 100644 --- a/src/app/screens/coinDashboard/index.tsx +++ b/src/app/screens/coinDashboard/index.tsx @@ -5,13 +5,15 @@ import BottomBar from '@components/tabBar'; import TopRow from '@components/topRow'; import { useVisibleBrc20FungibleTokens } from '@hooks/queries/ordinals/useGetBrc20FungibleTokens'; import { useVisibleRuneFungibleTokens } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; +import useRuneUtxosQuery from '@hooks/queries/runes/useRuneUtxosQuery'; import { useVisibleSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens'; -import useBtcWalletData from '@hooks/queries/useBtcWalletData'; import useSpamTokens from '@hooks/queries/useSpamTokens'; import { broadcastResetUserFlow, useResetUserFlow } from '@hooks/useResetUserFlow'; import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed'; import { Flag } from '@phosphor-icons/react'; +import RuneBundleRow from '@screens/coinDashboard/runes/bundleRow'; import type { FungibleToken } from '@secretkeylabs/xverse-core'; +import { mapRareSatsAPIResponseToBundle } from '@secretkeylabs/xverse-core'; import { setBrc20ManageTokensAction, setRunesManageTokensAction, @@ -20,7 +22,9 @@ import { } from '@stores/wallet/actions/actionCreators'; import { StyledP } from '@ui-library/common.styled'; import { SPAM_OPTIONS_WIDTH, type CurrencyTypes } from '@utils/constants'; -import { getExplorerUrl } from '@utils/helper'; +import { ftDecimals, getExplorerUrl, getTruncatedAddress } from '@utils/helper'; +import { getFullTxId, getTxIdFromFullTxId, getVoutFromFullTxId } from '@utils/runes'; +import BigNumber from 'bignumber.js'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; @@ -33,7 +37,7 @@ import TransactionsHistoryList from './transactionsHistoryList'; const Container = styled.div((props) => ({ display: 'flex', flex: 1, - marginTop: props.theme.spacing(4), + marginTop: props.theme.space.xs, flexDirection: 'column', overflowY: 'auto', '&::-webkit-scrollbar': { @@ -41,12 +45,13 @@ const Container = styled.div((props) => ({ }, })); -const TokenContractContainer = styled.div((props) => ({ +const SecondaryContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'column', - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), - marginTop: props.theme.spacing(16), + paddingLeft: props.theme.space.m, + paddingRight: props.theme.space.m, + marginTop: props.theme.space.xl, + marginBottom: props.theme.space.xl, h1: { ...props.theme.typography.body_medium_m, color: props.theme.colors.white_400, @@ -55,7 +60,7 @@ const TokenContractContainer = styled.div((props) => ({ const ContractAddressCopyButton = styled.button((props) => ({ display: 'flex', - marginTop: props.theme.spacing(2), + marginTop: props.theme.space.xxs, background: 'transparent', })); @@ -71,10 +76,9 @@ const FtInfoContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'row', borderTop: `1px solid ${props.theme.colors.elevation2}`, - paddingTop: props.theme.spacing(12), - marginTop: props.theme.spacing(16), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(14), + paddingTop: props.theme.space.l, + marginTop: props.theme.space.xl, + paddingLeft: props.theme.space.m, })); const ShareIcon = styled.img({ @@ -83,22 +87,22 @@ const ShareIcon = styled.img({ }); const CopyButtonContainer = styled.div((props) => ({ - marginRight: props.theme.spacing(2), + marginRight: props.theme.space.xxs, })); const ContractDeploymentButton = styled.button((props) => ({ ...props.theme.typography.body_m, display: 'flex', alignItems: 'center', - marginTop: props.theme.spacing(12), + marginTop: props.theme.space.l, background: 'none', color: props.theme.colors.white_400, span: { color: props.theme.colors.white_0, - marginLeft: props.theme.spacing(3), + marginLeft: props.theme.space.xs, }, img: { - marginLeft: props.theme.spacing(3), + marginLeft: props.theme.space.xs, }, })); @@ -111,9 +115,9 @@ const Button = styled.button<{ justifyContent: 'center', alignItems: 'center', height: 31, - paddingLeft: props.theme.spacing(6), - paddingRight: props.theme.spacing(6), - marginRight: props.theme.spacing(2), + paddingLeft: props.theme.space.s, + paddingRight: props.theme.space.s, + marginRight: props.theme.space.xxs, borderRadius: 44, background: props.isSelected ? props.theme.colors.elevation2 : 'transparent', color: props.theme.colors.white_0, @@ -142,14 +146,23 @@ const TokenText = styled(StyledP)` margin-left: ${(props) => props.theme.space.m}; `; +const RuneBundlesContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${(props) => props.theme.space.s}; +`; + export default function CoinDashboard() { + const [searchParams] = useSearchParams(); + const ftKey = searchParams.get('ftKey') ?? ''; + const protocol = searchParams.get('protocol') ?? ''; const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' }); - const [showFtContractDetails, setShowFtContractDetails] = useState(false); + const fromSecondaryTab = searchParams.get('secondaryTab') === 'true' ? 'secondary' : 'primary'; + const [currentTab, setCurrentTab] = useState<'primary' | 'secondary'>(fromSecondaryTab); const [showOptionsDialog, setShowOptionsDialog] = useState(false); const [optionsDialogIndents, setOptionsDialogIndents] = useState< { top: string; left: string } | undefined >(); - const [searchParams] = useSearchParams(); const { addToSpamTokens } = useSpamTokens(); const dispatch = useDispatch(); const { currency } = useParams(); @@ -157,8 +170,6 @@ export default function CoinDashboard() { const { visible: sip10CoinsList } = useVisibleSip10FungibleTokens(); const { visible: brc20CoinsList } = useVisibleBrc20FungibleTokens(); - const ftKey = searchParams.get('ftKey'); - const protocol = searchParams.get('protocol'); let selectedFt: FungibleToken | undefined; if (ftKey && protocol) { @@ -176,9 +187,16 @@ export default function CoinDashboard() { selectedFt = undefined; } } + const { data: runeUtxos } = useRuneUtxosQuery( + selectedFt?.protocol === 'runes' ? selectedFt?.name : '', + ); + + const showTxHistory = currentTab === 'primary'; + const displayTabs = ['stacks', 'runes'].includes(protocol); + const showStxContract = currentTab === 'secondary' && selectedFt && protocol === 'stacks'; + const showRuneBundles = currentTab === 'secondary' && selectedFt && protocol === 'runes'; useResetUserFlow('/coinDashboard'); - useBtcWalletData(); const handleGoBack = () => broadcastResetUserFlow(); @@ -192,29 +210,13 @@ export default function CoinDashboard() { const openOptionsDialog = (event: React.MouseEvent) => { setShowOptionsDialog(true); - setOptionsDialogIndents({ top: `${(event.target as HTMLElement).parentElement?.getBoundingClientRect().top}px`, left: `calc(100% - ${SPAM_OPTIONS_WIDTH}px)`, }); }; - const closeOptionsDialog = () => { - setShowOptionsDialog(false); - }; - - const openContractDeployment = () => - window.open(getExplorerUrl(selectedFt?.principal as string), '_blank'); - - const onContractClick = () => setShowFtContractDetails(true); - - const handleCopyContractAddress = () => - navigator.clipboard.writeText(selectedFt?.principal as string); - - const onTransactionsClick = () => setShowFtContractDetails(false); - - const formatAddress = (addr: string): string => - addr ? `${addr.substring(0, 20)}...${addr.substring(addr.length - 20, addr.length)}` : ''; + const closeOptionsDialog = () => setShowOptionsDialog(false); return ( <> @@ -262,55 +264,83 @@ export default function CoinDashboard() { )} - {protocol === 'stacks' && ( + {displayTabs && ( )} - {selectedFt && protocol === 'stacks' && showFtContractDetails && ( - + {showTxHistory && ( + + )} + {showStxContract && ( +

{t('FT_CONTRACT_PREFIX')}

- + navigator.clipboard.writeText(selectedFt?.principal as string)} + > - {formatAddress(selectedFt?.principal as string)} + {getTruncatedAddress(selectedFt?.principal as string, 20)} - + window.open(getExplorerUrl(selectedFt?.principal as string), '_blank')} + > {t('OPEN_FT_CONTRACT_DEPLOYMENT')} {t('STACKS_EXPLORER')} -
+ )} - {!showFtContractDetails && ( - + {showRuneBundles && ( + + + {runeUtxos?.map((utxo) => { + const fullTxId = getFullTxId(utxo); + const runeAmount = utxo.runes?.filter((rune) => rune[0] === selectedFt?.name)[0][1] + .amount; + return ( + + ); + })} + + )}
diff --git a/src/app/screens/coinDashboard/runes/bundleRow.tsx b/src/app/screens/coinDashboard/runes/bundleRow.tsx new file mode 100644 index 000000000..6caa33587 --- /dev/null +++ b/src/app/screens/coinDashboard/runes/bundleRow.tsx @@ -0,0 +1,151 @@ +import RareSatIcon from '@components/rareSatIcon/rareSatIcon'; +import useSatBundleDataReducer from '@hooks/stores/useSatBundleReducer'; +import type { Bundle } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { getTruncatedAddress } from '@utils/helper'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + align-items: center; + flex-direction: row; + justify-content: space-between; + flex: 1; + padding: ${(props) => props.theme.space.s}; + padding-left ${(props) => props.theme.space.m}; + border-radius: ${(props) => props.theme.space.xs}; + border: 1px solid 'transparent'; + background-color: ${(props) => props.theme.colors.elevation1}; + gap: ${(props) => props.theme.space.s}; + :hover { + cursor: pointer; + }, +`; + +const InfoContainer = styled.div` + display: flex; + flex: 1; + flex-direction: column; + gap: ${(props) => props.theme.space.xxxs}; +`; + +const SubContainer = styled.div` + display: flex; + flex: 1; + flex-direction: row; + justify-content: space-between; +`; + +const RuneTitle = styled(StyledP)` + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + text-align: left; +`; + +const StyledBundleSub = styled(StyledP)` + width: 100%; + text-align: left; +`; + +const SizeInfoContainer = styled.div` + display: flex; + align-items: center; + column-gap: ${(props) => props.theme.space.xxs}; +`; + +const RangeContainer = styled.div``; + +const Range = styled.div` + display: flex; + border-radius: 6px; + border: 1px solid ${(props) => props.theme.colors.white_800}; + padding: 1px; + flex-wrap: wrap; + flex-direction: row; + align-items: center; +`; + +type Props = { + runeAmount: string; + runeSymbol: string; + runeId: string; + txId: string; + vout: string; + satAmount: number; + bundle: Bundle; +}; + +function RuneBundleRow({ runeAmount, runeSymbol, runeId, txId, vout, satAmount, bundle }: Props) { + const { t } = useTranslation('translation', { keyPrefix: 'COMMON' }); + const navigate = useNavigate(); + const satributesArr = bundle.satributes.flatMap((item) => item); + const { setSelectedSatBundleDetails } = useSatBundleDataReducer(); + + const handleOnClick = () => { + // exotics v1 wont show range details only bundle details + setSelectedSatBundleDetails(bundle); + navigate('/nft-dashboard/rare-sats-bundle', { state: { source: 'RuneBundlesTab', runeId } }); + }; + + const onKeyDown = (event) => { + if (event.key === 'Enter') { + handleOnClick(); + } + }; + + return ( + + + + {satributesArr.map((satribute) => ( + + ))} + + + + ( + + {value} + + )} + /> + + + {`${getTruncatedAddress(txId, 6)}:${vout}`} + + + ( + + {value} + + )} + /> + + + + + ); +} + +export default RuneBundleRow; diff --git a/src/app/screens/rareSatsBundle/bundleContent.tsx b/src/app/screens/rareSatsBundle/bundleContent.tsx new file mode 100644 index 000000000..955050a45 --- /dev/null +++ b/src/app/screens/rareSatsBundle/bundleContent.tsx @@ -0,0 +1,41 @@ +import { useRuneFungibleTokensQuery } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; +import { RuneAmountContainer } from '@screens/rareSatsBundle/index.styled'; +import RareSatsBundleGridItem from '@screens/rareSatsBundle/rareSatsBundleGridItem'; +import RuneAmount from '@screens/rareSatsBundle/runeAmount'; +import type { Bundle, BundleSatRange, FungibleToken } from '@secretkeylabs/xverse-core'; + +export default function BundleContent({ bundle }: { bundle: Bundle }) { + const isGalleryOpen: boolean = document.documentElement.clientWidth > 360; + const { data: runesCoinsList } = useRuneFungibleTokensQuery(); + return ( + <> + {bundle?.satRanges.map((item: BundleSatRange) => ( + + ))} + {bundle?.runes?.map((runeUtxo) => { + const matchedRune: FungibleToken | undefined = (runesCoinsList ?? []).find( + (ft) => ft.name === runeUtxo[0], + ); + if (matchedRune) { + return ( + // eslint-disable-next-line react/jsx-key + + + + ); + } + return null; + })} + + ); +} diff --git a/src/app/screens/rareSatsBundle/index.styled.ts b/src/app/screens/rareSatsBundle/index.styled.ts index 46af82766..49bec85dc 100644 --- a/src/app/screens/rareSatsBundle/index.styled.ts +++ b/src/app/screens/rareSatsBundle/index.styled.ts @@ -7,10 +7,12 @@ interface DetailSectionProps { } /* layout */ -export const Container = styled.div` +export const Container = styled.div` ...${(props) => props.theme.scrollbar}; overflow-y: auto; padding-bottom: ${(props) => props.theme.space.l}; + padding-left: ${(props) => (props.isGalleryOpen ? 0 : props.theme.space.m)}; + padding-right: ${(props) => (props.isGalleryOpen ? 0 : props.theme.space.m)}; `; export const PageHeader = styled.div` @@ -32,7 +34,6 @@ export const PageHeaderContent = styled.div` export const AttributesContainer = styled.div((props) => ({ maxWidth: props.isGalleryOpen ? '285px' : '100%', - padding: props.isGalleryOpen ? 0 : `0 ${props.theme.space.m}`, })); export const StyledSeparator = styled(Separator)` @@ -48,7 +49,7 @@ export const StyledWebGalleryButton = styled(WebGalleryButton)` export const SendButtonContainer = styled.div` margin-top: ${(props) => props.theme.space.l}; - width: ${(props) => (props.isGalleryOpen ? '222px' : '155px')}; + width: ${(props) => (props.isGalleryOpen ? '222px' : '100%')}; `; export const BundleRarityLinkContainer = styled.button` @@ -112,11 +113,11 @@ export const NoCollectiblesText = styled.p((props) => ({ export const Header = styled.div<{ isGalleryOpen: boolean }>((props) => ({ display: props.isGalleryOpen ? 'block' : 'flex', flexDirection: props.isGalleryOpen ? 'row' : 'column', - alignItems: props.isGalleryOpen ? 'flex-start' : 'center', + alignItems: 'flex-start', })); export const SatRangeContainer = styled.div((props) => ({ - marginTop: props.isGalleryOpen ? 0 : props.theme.space.xl, + marginTop: props.isGalleryOpen ? 0 : props.theme.space.s, maxWidth: '1224px', marginLeft: 'auto', marginRight: 'auto', @@ -134,3 +135,8 @@ export const DetailSection = styled.div((props) => ({ export const SeeRarityContainer = styled.div` padding: ${(props) => props.theme.space.l} ${(props) => props.theme.space.m}; `; + +export const RuneAmountContainer = styled.div<{ isGalleryOpen?: boolean }>((props) => ({ + padding: props.isGalleryOpen ? `0 ${props.theme.space.m}` : 0, + marginBottom: props.theme.space.s, +})); diff --git a/src/app/screens/rareSatsBundle/index.tsx b/src/app/screens/rareSatsBundle/index.tsx index a5eb7bd36..39662d69b 100644 --- a/src/app/screens/rareSatsBundle/index.tsx +++ b/src/app/screens/rareSatsBundle/index.tsx @@ -10,7 +10,8 @@ import { useResetUserFlow } from '@hooks/useResetUserFlow'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { ArrowRight, ArrowUp } from '@phosphor-icons/react'; -import type { BundleSatRange } from '@secretkeylabs/xverse-core'; +import BundleContent from '@screens/rareSatsBundle/bundleContent'; +import type { Bundle } from '@secretkeylabs/xverse-core'; import Button from '@ui-library/button'; import { StyledHeading, StyledP } from '@ui-library/common.styled'; import { @@ -21,7 +22,7 @@ import { } from '@utils/helper'; import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import OrdinalAttributeComponent from '../ordinalDetail/ordinalAttributeComponent'; import { AssetDetailButtonText, @@ -42,14 +43,14 @@ import { StyledSeparator, StyledWebGalleryButton, } from './index.styled'; -import { RareSatsBundleGridItem } from './rareSatsBundleGridItem'; function RareSatsBundle() { const { t } = useTranslation('translation'); const navigate = useNavigate(); const location = useLocation(); - const { source } = location.state || {}; + const { source, runeId } = location.state || {}; const selectedAccount = useSelectedAccount(); + const [searchParams] = useSearchParams(); const { network } = useWalletSelector(); const { selectedSatBundle: bundle } = useNftDataSelector(); const { isPending, pendingTxHash } = usePendingOrdinalTxs(bundle?.txid); @@ -57,42 +58,53 @@ function RareSatsBundle() { const { setSelectedSatBundleDetails } = useSatBundleDataReducer(); const isGalleryOpen: boolean = useMemo(() => document.documentElement.clientWidth > 360, []); + const fromRunes = !!searchParams.get('fromRune') || source === 'RuneBundlesTab'; + const fromOrdinals = source === 'OrdinalDetail'; + const isEmpty = !bundle?.satRanges?.length; useResetUserFlow('/rare-sats-bundle'); const handleBackButtonClick = () => { - if (source === 'OrdinalDetail') { - navigate(-1); - } else { - navigate('/nft-dashboard?tab=rareSats'); - } setSelectedSatBundleDetails(null); + if (fromOrdinals) { + return navigate(-1); + } + if (fromRunes) { + return navigate( + `/coinDashboard/FT?ftKey=${ + searchParams.get('fromRune') || runeId + }&protocol=runes&secondaryTab=true`, + ); + } + return navigate('/nft-dashboard?tab=rareSats'); }; const openInGalleryView = async () => { await chrome.tabs.create({ - url: chrome.runtime.getURL('options.html#/nft-dashboard/rare-sats-bundle'), + url: chrome.runtime.getURL( + `options.html#/nft-dashboard/rare-sats-bundle?fromRune=${ + searchParams.get('fromRune') || runeId + }`, + ), }); }; - const onCloseAlert = () => { - setShowSendOrdinalsAlert(false); - }; + const onCloseAlert = () => setShowSendOrdinalsAlert(false); const handleSendOrdinal = async () => { if (isPending) { return setShowSendOrdinalsAlert(true); } - - const link = `/nft-dashboard/ordinal-detail/${bundle?.txid}/send-ordinal?isRareSat=true&vout=${bundle?.vout}`; - + const hasRune = !!(searchParams.get('fromRune') || runeId); + const link = `/nft-dashboard/ordinal-detail/${bundle?.txid}/send-ordinal?isRareSat=true&vout=${ + bundle?.vout + }${hasRune ? `&fromRune=${searchParams.get('fromRune') || runeId}` : ''}`; if (isLedgerAccount(selectedAccount) && !isInOptions()) { await chrome.tabs.create({ url: chrome.runtime.getURL(`options.html#${link}`), }); return; } - navigate(link); }; @@ -102,45 +114,48 @@ function RareSatsBundle() { } }; - const handleRarityScale = () => { - navigate('/nft-dashboard/supported-rarity-scale'); - }; - - const goBackText = - location.state && location.state.source === 'OrdinalDetail' - ? t('SEND.MOVE_TO_ASSET_DETAIL') - : t('NFT_DETAIL_SCREEN.MOVE_TO_ASSET_DETAIL'); + const handleRarityScale = () => navigate('/nft-dashboard/supported-rarity-scale'); - const isEmpty = !bundle?.satRanges?.length; + const goBackText = () => { + if (fromOrdinals) { + return t('SEND.MOVE_TO_ASSET_DETAIL'); + } + if (fromRunes) { + return t('NFT_DETAIL_SCREEN.BACK_TO_RUNES'); + } + return t('NFT_DETAIL_SCREEN.MOVE_TO_ASSET_DETAIL'); + }; return ( <> {isGalleryOpen ? ( ) : ( - + )} - + {isGalleryOpen && ( <> - {goBackText} + {goBackText()} )}
- - {t('NFT_DASHBOARD_SCREEN.RARE_SATS')} - - - {bundle?.totalExoticSats} + + {t('NFT_DASHBOARD_SCREEN.BUNDLE')} {!isGalleryOpen && } +
+ + + + + + + {!isGalleryOpen && ( + + )} + {isEmpty && ( {t('NFT_DASHBOARD_SCREEN.NO_COLLECTIBLES')} )} {!isGalleryOpen && ( - {bundle?.satRanges.map((item: BundleSatRange) => ( - - ))} + )} {!isGalleryOpen && ( @@ -176,25 +211,6 @@ function RareSatsBundle() { /> )} - - - - - - -
{isGalleryOpen && } @@ -203,9 +219,7 @@ function RareSatsBundle() { )} {isGalleryOpen && ( - {bundle?.satRanges.map((item: BundleSatRange) => ( - - ))} + )} {showSendOrdinalsAlert && ( diff --git a/src/app/screens/rareSatsBundle/rareSatsBundleGridItem.tsx b/src/app/screens/rareSatsBundle/rareSatsBundleGridItem.tsx index 8138de4d9..d02c0aedc 100644 --- a/src/app/screens/rareSatsBundle/rareSatsBundleGridItem.tsx +++ b/src/app/screens/rareSatsBundle/rareSatsBundleGridItem.tsx @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import ExoticSatsRow from '@components/exoticSatsRow/exoticSatsRow'; import RareSatIcon from '@components/rareSatIcon/rareSatIcon'; import type { BundleSatRange } from '@secretkeylabs/xverse-core'; @@ -19,17 +18,18 @@ const Range = styled.div` flex-wrap: wrap; `; -const Container = styled.div((props) => ({ +const Container = styled.div<{ isGalleryOpen?: boolean }>((props) => ({ marginBottom: props.theme.space.s, - padding: `0 ${props.theme.space.m}`, + padding: props.isGalleryOpen ? `0 ${props.theme.space.m}` : 0, })); -export function RareSatsBundleGridItem({ item }: { item: BundleSatRange }) { +export default function RareSatsBundleGridItem({ item }: { item: BundleSatRange }) { + const isGalleryOpen: boolean = document.documentElement.clientWidth > 360; return ( - + diff --git a/src/app/screens/rareSatsBundle/runeAmount.tsx b/src/app/screens/rareSatsBundle/runeAmount.tsx new file mode 100644 index 000000000..2fc483e48 --- /dev/null +++ b/src/app/screens/rareSatsBundle/runeAmount.tsx @@ -0,0 +1,108 @@ +import { mapRuneBaseToFungibleToken } from '@components/confirmBtcTransaction/utils'; +import { RightAlignedStyledFiatAmountText } from '@components/fiatAmountText'; +import TokenImage from '@components/tokenImage'; +import useRuneFiatRateQuery from '@hooks/queries/runes/useRuneFiatRateQuery'; +import useWalletSelector from '@hooks/useWalletSelector'; +import type { RuneBase } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { ftDecimals } from '@utils/helper'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; +import styled from 'styled-components'; + +const Container = styled.div<{ topMargin?: boolean }>((props) => ({ + width: '100%', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + marginBottom: props.theme.space.s, + marginTop: props.topMargin ? props.theme.space.s : 0, + backgroundColor: props.theme.colors.elevation1, + padding: props.theme.space.m, + borderRadius: props.theme.space.xs, +})); + +const AvatarContainer = styled.div` + margin-right: ${(props) => props.theme.space.m}; +`; + +const Row = styled.div` + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 24px; +`; + +const Column = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 2px; + overflow: hidden; +`; + +type Props = { + rune: RuneBase; + hasSufficientBalance?: boolean; + topMargin?: boolean; +}; + +export default function RuneAmount({ + rune, + hasSufficientBalance = true, + topMargin = false, +}: Props) { + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + const { runeName, amount, divisibility, symbol } = rune; + const amountWithDecimals = ftDecimals(String(amount), divisibility); + const { fiatCurrency } = useWalletSelector(); + const { data: runeFiatRate } = useRuneFiatRateQuery(rune); + return ( + + + + + + + + {t('AMOUNT')} + + ( + + {value} + + )} + /> + + + + {runeName} + + {runeFiatRate !== undefined && runeFiatRate > 0 && ( + + )} + + + + ); +} diff --git a/src/app/screens/sendOrdinal/index.tsx b/src/app/screens/sendOrdinal/index.tsx index 78a1c746d..4725ceb23 100644 --- a/src/app/screens/sendOrdinal/index.tsx +++ b/src/app/screens/sendOrdinal/index.tsx @@ -31,6 +31,7 @@ function SendOrdinalScreen() { const isRareSatParam = params.get('isRareSat'); const vout = params.get('vout'); const isRareSat = isRareSatParam === 'true'; + const fromRune = params.get('fromRune'); const context = useTransactionContext(); const { data: selectedOrdinal } = useAddressInscription(isRareSat ? undefined : id); @@ -136,7 +137,7 @@ function SendOrdinalScreen() { } navigate( isRareSat - ? `/nft-dashboard/rare-sats-bundle` + ? `/nft-dashboard/rare-sats-bundle${fromRune ? `?fromRune=${fromRune}` : ''}` : `/nft-dashboard/ordinal-detail/${selectedOrdinal?.id}`, ); }; diff --git a/src/locales/en.json b/src/locales/en.json index 1c611bae9..792e1bc98 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -665,6 +665,7 @@ "NO_EARN_OPTION": "No Earn options currently available for this wallet type." }, "NFT_DASHBOARD_SCREEN": { + "BUNDLE": "Bundle", "COLLECTIBLES": "Collectibles", "NO_COLLECTIBLES": "There's nothing here yet.", "INSCRIPTIONS": "Inscriptions", @@ -750,6 +751,7 @@ "TRANSFER_ALL": "Transfer all" }, "NFT_DETAIL_SCREEN": { + "RARE_SATS": "Rare Sats", "NFT_DETAIL": "Item detail", "WEB_GALLERY": "Open gallery", "SEND": "Send", @@ -769,6 +771,7 @@ "GAMMA": "Gamma.io", "MOVE_TO_ASSET_DETAIL": "Back to gallery", "BACK_TO_COLLECTION": "Back to collection", + "BACK_TO_RUNES": "Back to runes", "ORDINALS": "Ordinal", "ORDINAL_PENDING_SEND_TITLE": "Transfer Pending", "ORDINAL_PENDING_SEND_DESCRIPTION": "This Ordinal is already in a pending transfer.", @@ -803,7 +806,8 @@ "SAT": "Sat", "NFT_TYPE": "Asset Type", "DATA": "Data", - "COPIED": "Sharing Link Copied" + "COPIED": "Sharing Link Copied", + "CONTENT": "Content" }, "RESET_WALLET_SCREEN": { "ENTER_PASSWORD": "Enter your password to reset your wallet", @@ -984,6 +988,7 @@ "BALANCE": "Balance", "TRANSACTIONS": "TRANSACTIONS", "CONTRACT": "CONTRACT", + "BUNDLES": "BUNDLES", "COMING_SOON": "Coming soon", "VERIFY_ADDRESS_ON_LEDGER": "Verify address on Ledger", "VIEW_ADDRESS": "View address", diff --git a/tests/pages/wallet.ts b/tests/pages/wallet.ts index 6ce8a2237..2fb0bfa9f 100644 --- a/tests/pages/wallet.ts +++ b/tests/pages/wallet.ts @@ -212,9 +212,9 @@ export default class Wallet { readonly noFundsBTCMessage: Locator; - readonly buttonCoinContract: Locator; + readonly coinSecondaryButton: Locator; - readonly coinContractContainer: Locator; + readonly coinSecondaryContainer: Locator; readonly coinContractAddress: Locator; @@ -266,7 +266,7 @@ export default class Wallet { readonly labelOwnedBy: Locator; - readonly labelRareSats: Locator; + readonly labelBundle: Locator; readonly buttonSupportRarity: Locator; @@ -452,8 +452,8 @@ export default class Wallet { this.containerTransactionHistory = page.getByTestId('transaction-container'); this.transactionHistoryAmount = page.getByTestId('transaction-amount'); this.transactionHistoryInfo = page.getByTestId('transaction-info'); - this.buttonCoinContract = page.getByTestId('coin-contract-button'); - this.coinContractContainer = page.getByTestId('coin-contract-container'); + this.coinSecondaryButton = page.getByTestId('coin-secondary-button'); + this.coinSecondaryContainer = page.getByTestId('coin-secondary-container'); this.coinContractAddress = page.getByTestId('coin-contract-address'); this.textCoinTitle = page.getByTestId('coin-title-text'); @@ -487,9 +487,9 @@ export default class Wallet { this.buttonShare = page.getByRole('button', { name: 'Share' }); this.buttonReceive = page.getByRole('button', { name: 'Receive', exact: true }); this.buttonOpenOrdinalViewer = page.getByRole('button', { name: 'Open in Ordinal Viewer' }); + this.labelBundle = page.locator('h1').filter({ hasText: 'Bundle' }); this.labelSatsValue = page.locator('h1').filter({ hasText: 'Sats value' }); this.labelOwnedBy = page.locator('h1').filter({ hasText: 'Owned by' }); - this.labelRareSats = page.locator('p').filter({ hasText: 'Rare Sats' }); this.buttonSupportRarity = page.getByRole('button', { name: 'See supported rarity scale' }); this.numberInscription = page.getByTestId('inscription-number'); this.numberOrdinal = page.getByTestId('ordinal-number'); diff --git a/tests/specs/createWallet.spec.ts b/tests/specs/createWallet.spec.ts index e0f530738..e323e2b77 100644 --- a/tests/specs/createWallet.spec.ts +++ b/tests/specs/createWallet.spec.ts @@ -142,7 +142,7 @@ test.describe('Create and Restore Wallet Flow', () => { await newWallet.checkVisualsStartpage(); const balanceText = newWallet.balance; - await await expect(balanceText).toHaveText('$0.00'); + await expect(balanceText).toHaveText('$0.00'); // Get the Addresses const addressBitcoinCheck = await newWallet.getAddress('Bitcoin'); diff --git a/tests/specs/managementToken.spec.ts b/tests/specs/managementToken.spec.ts index 154ac4e66..0b559955f 100644 --- a/tests/specs/managementToken.spec.ts +++ b/tests/specs/managementToken.spec.ts @@ -24,7 +24,7 @@ test.describe('Token Management', () => { await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); const amounttokenSIP = await wallet.labelCoinTitle.count(); - await await expect(amounttokenSIP).toBeGreaterThanOrEqual(15); + await expect(amounttokenSIP).toBeGreaterThanOrEqual(15); await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenSIP - 1); await expect(wallet.checkboxTokenActive).toHaveCount(1); await expect(wallet.checkboxToken).toHaveCount(amounttokenSIP); @@ -56,7 +56,7 @@ test.describe('Token Management', () => { await wallet.checkVisualsStartpage(); // Check balances await expect(wallet.balance).toBeVisible(); - await await expect(wallet.balance).toHaveText('$0.00'); + await expect(wallet.balance).toHaveText('$0.00'); let balanceText = await wallet.getBalanceOfAllTokens(); await expect(balanceText).toBe(0); await wallet.manageTokenButton.click(); diff --git a/tests/specs/tabCollectiblesRareSats.spec.ts b/tests/specs/tabCollectiblesRareSats.spec.ts index ed63df9c3..ed135620c 100644 --- a/tests/specs/tabCollectiblesRareSats.spec.ts +++ b/tests/specs/tabCollectiblesRareSats.spec.ts @@ -22,7 +22,7 @@ test.describe('Collectibles Tab - Rare sats', () => { await expect(wallet.buttonSend).toBeVisible(); await expect(wallet.labelSatsValue).toBeVisible(); await expect(wallet.labelOwnedBy).toBeVisible(); - await expect(wallet.labelRareSats).toBeVisible(); + await expect(wallet.labelBundle).toBeVisible(); await expect(wallet.buttonSupportRarity).toBeVisible(); }); diff --git a/tests/specs/transactionHistory.spec.ts b/tests/specs/transactionHistory.spec.ts index 8aa556b40..3d1b6e16d 100644 --- a/tests/specs/transactionHistory.spec.ts +++ b/tests/specs/transactionHistory.spec.ts @@ -17,8 +17,8 @@ test.describe('Transaction', () => { await expect(wallet.textCoinTitle).toContainText(tokenName); // Check contract details are displayed - await wallet.buttonCoinContract.click(); - await expect(wallet.buttonCoinContract).toBeVisible(); + await wallet.coinSecondaryButton.click(); + await expect(wallet.coinSecondaryButton).toBeVisible(); await expect(wallet.coinContractAddress).toBeVisible(); await expect(wallet.coinContractAddress).not.toBeEmpty(); }); @@ -76,15 +76,19 @@ test.describe('Transaction', () => { test('Visual Check Runes Transaction history', async ({ page, extensionId }) => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', false); - // Check if Rune is enabled and if not enable the rune and click on it await wallet.checkAndClickOnSpecificRune('SKIBIDI•OHIO•RIZZ'); - const originalBalanceAmount = await wallet.checkVisualsRunesDashboard('SKIBIDI•OHIO•RIZZ'); await expect(originalBalanceAmount).toBeGreaterThan(0); await expect(wallet.containerTransactionHistory.first()).toBeVisible(); // There should be at least one transaction visible await expect(await wallet.containerTransactionHistory.count()).toBeGreaterThanOrEqual(1); + // check able to see rune bundles + await wallet.coinSecondaryButton.click(); + await expect(wallet.coinSecondaryButton).toBeVisible(); + // can navigate to rare-sats-bundle page + await wallet.runeItem.last().click(); + await expect(page.url()).toContain('rare-sats-bundle'); }); // TODO: add test for sending NFT - https://linear.app/xverseapp/issue/ENG-4321/transaction-send-nft diff --git a/tests/specs/transactionSTX.spec.ts b/tests/specs/transactionSTX.spec.ts index 41b65f0a5..6a1d69b0d 100644 --- a/tests/specs/transactionSTX.spec.ts +++ b/tests/specs/transactionSTX.spec.ts @@ -43,9 +43,9 @@ test.describe('Transaction STX', () => { await expect(wallet.buttonNext).toBeHidden(); // Amount input is visible await expect(wallet.inputField.first()).toBeVisible(); + await expect(wallet.inputField.first()).toBeEnabled(); await expect(wallet.labelBalanceAmountSelector).toBeVisible(); await expect(wallet.imageToken).toBeVisible(); - await expect(wallet.inputField.first()).toBeDisabled(); await expect(wallet.noFundsBTCMessage).toBeVisible(); }); From 7bae215aa4db6f2b4ddcfc88f10cfa15c8405d90 Mon Sep 17 00:00:00 2001 From: Den <36603049+dhriaznov@users.noreply.github.com> Date: Mon, 9 Sep 2024 05:43:24 +0200 Subject: [PATCH 085/227] [ENG-5132] Adjust the banner carousel to have a fixed height (#570) * [ENG-5132] Adjust the banner carousel to have a fixed height * Update the carousel banner height/smoothness --- src/app/screens/home/banner.tsx | 2 ++ src/app/screens/home/bannerCarousel.tsx | 10 ++++-- src/app/screens/home/index.styled.ts | 12 +++++-- src/app/screens/home/index.tsx | 42 +++++++++++++++++++------ 4 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/app/screens/home/banner.tsx b/src/app/screens/home/banner.tsx index f2217ae67..056493311 100644 --- a/src/app/screens/home/banner.tsx +++ b/src/app/screens/home/banner.tsx @@ -10,6 +10,8 @@ const Container = styled.div` const BannerContent = styled.button` cursor: pointer; + width: 100%; + min-height: 60px; display: flex; align-items: center; column-gap: ${({ theme }) => theme.space.s}; diff --git a/src/app/screens/home/bannerCarousel.tsx b/src/app/screens/home/bannerCarousel.tsx index 9d584bb0a..a1f565ec2 100644 --- a/src/app/screens/home/bannerCarousel.tsx +++ b/src/app/screens/home/bannerCarousel.tsx @@ -15,10 +15,15 @@ import Banner from './banner'; const CarouselContainer = styled.div` position: relative; margin-top: ${({ theme }) => theme.space.xxs}; - margin-bottom: ${({ theme }) => theme.space.m}; + margin-bottom: ${({ theme }) => theme.space.xxs}; .swiper { padding: 0; + z-index: 0; + } + + .swiper-wrapper { + align-items: center; } .swiper-pagination { @@ -85,7 +90,7 @@ const PaginationContainer = styled.div` const StyledCrossButton = styled(CrossButton)` z-index: 1; position: absolute; - top: -14px; + top: -8px; right: -6px; `; @@ -116,7 +121,6 @@ function BannerCarousel({ items }: Props) { setActiveIndex(swiper.activeIndex); }} allowTouchMove - autoHeight > {items.map((item) => ( diff --git a/src/app/screens/home/index.styled.ts b/src/app/screens/home/index.styled.ts index b2f4fbb47..ac6a1426f 100644 --- a/src/app/screens/home/index.styled.ts +++ b/src/app/screens/home/index.styled.ts @@ -1,4 +1,5 @@ import TokenTile from '@components/tokenTile'; +import { animated } from '@react-spring/web'; import Divider from '@ui-library/divider'; import styled from 'styled-components'; @@ -15,6 +16,7 @@ export const ColumnContainer = styled.div((props) => ({ flexDirection: 'column', alignItems: 'space-between', justifyContent: 'space-between', + marginTop: props.theme.space.xs, marginBottom: props.theme.space.s, })); @@ -155,13 +157,19 @@ export const IconBackground = styled.div((props) => ({ alignItems: 'center', })); -export const StyledDivider = styled(Divider)` +export const StyledDivider = styled(Divider)<{ $noMarginBottom?: boolean }>` flex: 1 0 auto; width: calc(100% + ${(props) => props.theme.space.xl}); margin-left: -${(props) => props.theme.space.m}; margin-right: -${(props) => props.theme.space.m}; + transition: margin-bottom 0.1s ease; + ${(props) => + props.$noMarginBottom && + ` + margin-bottom: 0; + `} `; export const StyledDividerSingle = styled(StyledDivider)` - margin-top: ${(props) => props.theme.space.xl}; + margin-bottom: 0; `; diff --git a/src/app/screens/home/index.tsx b/src/app/screens/home/index.tsx index 114479b67..08af56b25 100644 --- a/src/app/screens/home/index.tsx +++ b/src/app/screens/home/index.tsx @@ -25,6 +25,7 @@ import useSelectedAccount from '@hooks/useSelectedAccount'; import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed'; import useWalletSelector from '@hooks/useWalletSelector'; import { ArrowDown, ArrowUp, Plus } from '@phosphor-icons/react'; +import { animated, useTransition } from '@react-spring/web'; import CoinSelectModal from '@screens/home/coinSelectModal'; import { AnalyticsEvents, FeatureId, type FungibleToken } from '@secretkeylabs/xverse-core'; import { @@ -415,6 +416,21 @@ function Home() { const isCrossChainSwapsEnabled = useHasFeature(FeatureId.CROSS_CHAIN_SWAPS); const showSwaps = isCrossChainSwapsEnabled; + const transitions = useTransition(showBannerCarousel, { + from: { maxHeight: '1000px', opacity: 0.5 }, + enter: { maxHeight: '1000px', opacity: 1 }, + leave: { maxHeight: '0px', opacity: 0 }, + config: (item, index, phase) => + phase === 'leave' + ? { + duration: 300, + easing: (progress) => 1 - (1 - progress) ** 4, + } + : { + duration: 200, + }, + }); + return ( <> @@ -454,15 +470,23 @@ function Home() { /> - {showBannerCarousel ? ( - <> -
- - - - - ) : ( - + {transitions((style, item) => + item ? ( + +
+ + + +
+ ) : ( + + + + ), )} From c4c215d9976c44603b8d2541dcef8bbd652a12ee Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Mon, 9 Sep 2024 06:46:07 +0300 Subject: [PATCH 086/227] mahmoud/eng-5115-ext-sendtransfer-fails-with-user-rejection-error-even-if-successfull (#564) * re-add request handling for `sendTransfer` * remove unnecessary log --- src/app/screens/btcSendRequest/index.tsx | 39 ++++++++++++++----- .../useBtcSendRequestPayload.ts | 4 +- src/common/utils/rpc/btc/signPsbt.ts | 2 +- src/common/utils/rpc/runes/etch.ts | 2 +- src/common/utils/rpc/runes/mint.ts | 2 +- 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/app/screens/btcSendRequest/index.tsx b/src/app/screens/btcSendRequest/index.tsx index 50db52f51..2fd7caf8b 100644 --- a/src/app/screens/btcSendRequest/index.tsx +++ b/src/app/screens/btcSendRequest/index.tsx @@ -1,4 +1,4 @@ -import { makeRPCError, sendRpcResponse } from '@common/utils/rpc/helpers'; +import { makeRPCError, makeRpcSuccessResponse, sendRpcResponse } from '@common/utils/rpc/helpers'; import ConfirmBtcTransaction from '@components/confirmBtcTransaction'; import useBtcFeeRate from '@hooks/useBtcFeeRate'; import useHasFeature from '@hooks/useHasFeature'; @@ -166,6 +166,15 @@ function BtcSendRequest() { checkIfValidAmount(); }, [payload]); + const handleCancel = () => { + const response = makeRPCError(requestId, { + code: RpcErrorCode.USER_REJECTION, + message: 'User rejected request to send transfer', + }); + sendRpcResponse(+tabId, response); + window.close(); + }; + const handleSubmit = async (ledgerTransport?: Transport) => { try { setIsSubmitting(true); @@ -175,16 +184,26 @@ function BtcSendRequest() { action: 'transfer', wallet_type: selectedAccount.accountType || 'software', }); - navigate('/tx-status', { - state: { + if (txnId) { + const sendTransferResponse = makeRpcSuccessResponse<'sendTransfer'>(requestId, { txid: txnId, - currency: 'BTC', - error: '', - browserTx: true, - }, - }); + }); + sendRpcResponse(+tabId, sendTransferResponse); + navigate('/tx-status', { + state: { + txid: txnId, + currency: 'BTC', + error: '', + browserTx: true, + }, + }); + } } catch (e) { - console.error(e); + const response = makeRPCError(requestId, { + code: RpcErrorCode.INTERNAL_ERROR, + message: (e as any).message, + }); + sendRpcResponse(+tabId, response); navigate('/tx-status', { state: { txid: '', @@ -216,7 +235,7 @@ function BtcSendRequest() { isLoading={isLoading} confirmText={t('COMMON.CONFIRM')} cancelText={t('COMMON.CANCEL')} - onCancel={() => window.close()} + onCancel={handleCancel} onConfirm={handleSubmit} getFeeForFeeRate={calculateFeeForFeeRate} onFeeRateSet={(newFeeRate) => setFeeRate(newFeeRate.toString())} diff --git a/src/app/screens/btcSendRequest/useBtcSendRequestPayload.ts b/src/app/screens/btcSendRequest/useBtcSendRequestPayload.ts index 3486a7e7d..6ee2bb7b2 100644 --- a/src/app/screens/btcSendRequest/useBtcSendRequestPayload.ts +++ b/src/app/screens/btcSendRequest/useBtcSendRequestPayload.ts @@ -10,7 +10,7 @@ import { useLocation } from 'react-router-dom'; const useBtcSendRequestPayload = (btcAddress: string, network: SettingsNetwork) => { const { search } = useLocation(); - const params = new URLSearchParams(search); + const params = useMemo(() => new URLSearchParams(search), [search]); const tabId = params.get('tabId') ?? '0'; const requestId = params.get('requestId') ?? ''; @@ -38,7 +38,7 @@ const useBtcSendRequestPayload = (btcAddress: string, network: SettingsNetwork) payload: rpcPayload, requestToken: null, }; - }, []); + }, [params, btcAddress, network]); return { payload, tabId, requestToken, requestId }; }; diff --git a/src/common/utils/rpc/btc/signPsbt.ts b/src/common/utils/rpc/btc/signPsbt.ts index 67811c67c..b9007ba7f 100644 --- a/src/common/utils/rpc/btc/signPsbt.ts +++ b/src/common/utils/rpc/btc/signPsbt.ts @@ -51,7 +51,7 @@ export const handleSignPsbt = async ( id, response: makeRPCError(message.id, { code: RpcErrorCode.USER_REJECTION, - message: 'User rejected request to send transfer', + message: 'User rejected request to sign a psbt', }), }); diff --git a/src/common/utils/rpc/runes/etch.ts b/src/common/utils/rpc/runes/etch.ts index 02cd3fe51..140ae02bd 100644 --- a/src/common/utils/rpc/runes/etch.ts +++ b/src/common/utils/rpc/runes/etch.ts @@ -68,7 +68,7 @@ const handleEtchRune = async (message: WebBtcMessage<'runes_etch'>, port: chrome id, response: makeRPCError(message.id, { code: RpcErrorCode.USER_REJECTION, - message: 'User rejected request to send transfer', + message: 'User rejected request to etch a rune', }), }); listenForOriginTabClose({ tabId }); diff --git a/src/common/utils/rpc/runes/mint.ts b/src/common/utils/rpc/runes/mint.ts index 3919b8f8b..35713f5f7 100644 --- a/src/common/utils/rpc/runes/mint.ts +++ b/src/common/utils/rpc/runes/mint.ts @@ -49,7 +49,7 @@ const handleMintRune = async (message: WebBtcMessage<'runes_mint'>, port: chrome id, response: makeRPCError(message.id, { code: RpcErrorCode.USER_REJECTION, - message: 'User rejected request to send transfer', + message: 'User rejected request to mint a rune', }), }); listenForOriginTabClose({ tabId }); From 7a1dc01db38d76aaf0d218a51db4b7123d30f769 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Mon, 9 Sep 2024 03:48:32 +0000 Subject: [PATCH 087/227] release: v0.42.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3bcf90680..e2f5dc011 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "xverse-web-extension", - "version": "0.41.1", + "version": "0.42.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "xverse-web-extension", - "version": "0.41.1", + "version": "0.42.0", "dependencies": { "@aryzing/superqs": "0.0.6", "@ledgerhq/hw-transport-webusb": "^6.27.13", diff --git a/package.json b/package.json index a5b76e4c1..5bd0f9313 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xverse-web-extension", "description": "A Bitcoin wallet for Web3", - "version": "0.41.1", + "version": "0.42.0", "private": true, "engines": { "node": "^18.18.2" From cfa2d52dc3add87f36500b55b3a31dfaf02ad875 Mon Sep 17 00:00:00 2001 From: "Duska.T" <55587184+DuskaT021@users.noreply.github.com> Date: Mon, 9 Sep 2024 06:41:48 +0200 Subject: [PATCH 088/227] Added the e2e for swap flow from sip-10 to stx and improved some assertions in a previous test (#568) * Added the e2e for swap flow from sip-10 to stx and improved some assertions in a previous test * Renamed the file --- tests/specs/swapSip10Velar.spec.ts | 80 ++++++++++++++++++++++++++++++ tests/specs/swapVelar.spec.ts | 10 ++-- 2 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 tests/specs/swapSip10Velar.spec.ts diff --git a/tests/specs/swapSip10Velar.spec.ts b/tests/specs/swapSip10Velar.spec.ts new file mode 100644 index 000000000..111ef61b6 --- /dev/null +++ b/tests/specs/swapSip10Velar.spec.ts @@ -0,0 +1,80 @@ +import { expect, test } from '../fixtures/base'; +import { enableCrossChainSwaps } from '../fixtures/helpers'; +import Wallet from '../pages/wallet'; + +// swap sip-10 to STX on Velar - mainnet + +test.describe('Swap sip-10 token to STX', () => { + test.beforeAll(async ({ page }) => { + await enableCrossChainSwaps(page); + }); + + test('Swap sip-10 to STX #localexecution', async ({ page, extensionId }) => { + // set up the testing wallet + const wallet = new Wallet(page); + await wallet.setupTest(extensionId, 'SEED_WORDS1', false); + + // click swap button on the homepage + await page.getByRole('button', { name: 'Swap' }).click(); + + // choose the tokens + await page + .getByRole('button', { name: /select asset/i }) + .first() + .click(); + await page.getByText('VELAR', { exact: true }).first().click(); + await expect(page.getByText(/velar/i)).toBeVisible(); + await page + .getByRole('button', { name: /select asset/i }) + .last() + .click(); + await page.getByText('STX', { exact: true }).first().click(); + + // assert chosen tokens populated the dropdown inputs velar -> stx + await expect(page.getByText(/velar/i)).toBeVisible(); + await expect(page.getByText(/stx/i)).toBeVisible(); + + // input the amount for swapping + await page.getByRole('textbox', { name: '0' }).fill('0.1'); + await expect(page.getByText(/^~\$\d+\.\d{2}\sUSD$/)).toBeVisible(); + + // assert presence balance and amount + await expect(page.getByText(/balance:/i)).toBeVisible(); + await expect(page.getByText(/^\d+\.\d+$/i)).toBeVisible(); + + // assert presence of max button + await expect(page.getByRole('button', { name: 'MAX' })).toBeVisible(); + + page.getByRole('button', { name: /get quotes/i }).click(); + + await expect(page.getByText('Rates', { exact: true })).toBeVisible(); + await page.getByText(/^\d+\.\d+\s+STX$/i).click(); + + // Quotes page velar -> stacks + await expect(page.getByText(/quote/i)).toBeVisible(); + await expect(page.getByText(/4%/i)).toBeVisible(); + page.getByRole('button', { name: /4%/i }).click(); + + // edit slippage tolerance + await page.getByRole('textbox', { name: '4' }).fill('1.22'); + page.getByRole('button', { name: /apply/i }).click(); + await expect(page.getByText('1.22')).toBeVisible(); + await expect(page.getByRole('img', { name: /velar logo/i })).toBeVisible(); + + page.getByRole('button', { name: /swap/i }).click(); + + // Arrive to the final step - swap contract page + await expect(page.getByText(/swap-exact-tokens-for-tokens/i)).toHaveCount(2); + await expect(page.getByText(/network fee/i)).toBeVisible(); + await expect(page.getByText(/^\d+\.\d+\s+STX$/i).last()).toBeVisible(); + await expect(page.getByRole('button', { name: /edit nonce/i })).toBeVisible(); + + // User clicks confirm + page.getByRole('button', { name: /confirm/i }).click(); + await expect(page.getByText(/transaction broadcasted/i)).toBeVisible(); + page.getByRole('button', { name: /close/i }).click(); + + // After closing user should arrive to homepage + await expect(page).toHaveURL(/popup\.html/); + }); +}); diff --git a/tests/specs/swapVelar.spec.ts b/tests/specs/swapVelar.spec.ts index 733a9fe34..0c0b478ee 100644 --- a/tests/specs/swapVelar.spec.ts +++ b/tests/specs/swapVelar.spec.ts @@ -28,7 +28,7 @@ test.describe('Velar sip-10 swap flow', () => { await page.getByText(/velar/i).first().click(); page.getByRole('textbox', { name: '0' }).fill('0.1'); page.getByRole('button', { name: /get quotes/i }).click(); - page.getByRole('heading', { name: /rates/i }).isVisible(); + await expect(page.getByText('Rates', { exact: true })).toBeVisible(); await page.getByText(/^\d+\.\d+\s+Velar$/i).click(); // Arrive to Quotes page @@ -40,11 +40,9 @@ test.describe('Velar sip-10 swap flow', () => { // Arrive to the final step - swap contract page await expect(page.getByText(/swap-exact-tokens-for-tokens/i)).toHaveCount(2); - page - .getByText(/network fee/i) - .getByText(/^\d+\.\d+\s+STX$/i) - .isVisible(); - page.getByRole('button', { name: /edit nonce/i }).isVisible(); + await expect(page.getByText(/network fee/i)).toBeVisible(); + await expect(page.getByText(/^\d+\.\d+\s+STX$/i)).toBeVisible(); + await expect(page.getByRole('button', { name: /edit nonce/i })).toBeVisible(); // User clicks confirm page.getByRole('button', { name: /confirm/i }).click(); From 9c4f55a35b78c0cc8bb78f8ed220e18dd9ff2a0f Mon Sep 17 00:00:00 2001 From: jordankzf Date: Mon, 9 Sep 2024 14:26:31 +0800 Subject: [PATCH 089/227] Use comparedTo --- src/app/screens/swap/quotesModal/index.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/app/screens/swap/quotesModal/index.tsx b/src/app/screens/swap/quotesModal/index.tsx index 07ce4ed8b..f5e8a87ce 100644 --- a/src/app/screens/swap/quotesModal/index.tsx +++ b/src/app/screens/swap/quotesModal/index.tsx @@ -67,19 +67,14 @@ function QuotesModal({ const { btcFiatRate, stxBtcRate } = useCoinRates(); const sortQuotesByReceiveAmount = (quotes: T[]): T[] => - [...quotes].sort((a, b) => - BigNumber(b.receiveAmount).gte(a.receiveAmount) - ? 1 - : BigNumber(a.receiveAmount).gte(b.receiveAmount) - ? -1 - : 0, - ); + [...quotes].sort((a, b) => BigNumber(b.receiveAmount).comparedTo(a.receiveAmount)); + + const sortQuotesByFloorPrice = (quotes: T[]): T[] => + [...quotes].sort((a, b) => BigNumber(a.floorRate).comparedTo(b.floorRate)); const sortedAmmQuotes = sortQuotesByReceiveAmount(ammProviders); const sortedStxQuotes = sortQuotesByReceiveAmount(stxProviders); - const sortedUtxoQuotes = [...utxoProviders].sort((a, b) => - BigNumber(b.floorRate).gte(a.floorRate) ? -1 : BigNumber(a.floorRate).gte(b.floorRate) ? 1 : 0, - ); + const sortedUtxoQuotes = sortQuotesByFloorPrice(utxoProviders); const getReceiveAmountSubtitle = ( quote: StxQuote | Quote, From 688f877fa970ecfc1f5cbc4bf7a28a9da683cfe1 Mon Sep 17 00:00:00 2001 From: "Duska.T" <55587184+DuskaT021@users.noreply.github.com> Date: Mon, 9 Sep 2024 10:34:21 +0200 Subject: [PATCH 090/227] Fixed a failing test in the stx flow (#574) * Fixed a failing test in the stx flow * removed a commented line * improved an existing locator :) --- tests/specs/transactionSTX.spec.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/specs/transactionSTX.spec.ts b/tests/specs/transactionSTX.spec.ts index 6a1d69b0d..fae696298 100644 --- a/tests/specs/transactionSTX.spec.ts +++ b/tests/specs/transactionSTX.spec.ts @@ -6,7 +6,10 @@ const STXTest = `STN2AMZQ54Y0NN4H5Z4S0DGMWP27CTXY5QEDCQAN`; const amountSTXSend = 10; test.describe('Transaction STX', () => { - test('Send STX Page Visual Check without funds Mainnet', async ({ page, extensionId }) => { + test('Send STX Page Visual Check with insufficient funds Mainnet', async ({ + page, + extensionId, + }) => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', false); @@ -42,11 +45,12 @@ test.describe('Transaction STX', () => { // No funds on mainnet in this wallet -->Page opens and Next button is hidden and info message is shown await expect(wallet.buttonNext).toBeHidden(); // Amount input is visible - await expect(wallet.inputField.first()).toBeVisible(); - await expect(wallet.inputField.first()).toBeEnabled(); + await expect(page.getByRole('textbox', { name: '0' })).toBeVisible(); + await expect(page.getByRole('textbox', { name: '0' })).toBeEnabled(); await expect(wallet.labelBalanceAmountSelector).toBeVisible(); await expect(wallet.imageToken).toBeVisible(); - await expect(wallet.noFundsBTCMessage).toBeVisible(); + page.getByRole('textbox', { name: '0' }).fill('200000000'); + await expect(page.getByRole('button', { name: /insufficient funds/i })).toBeVisible(); }); test('Send STX - Cancel transaction testnet', async ({ page, extensionId }) => { From 28c3be0b3ce3c29bf85a541d0604c982358af5d1 Mon Sep 17 00:00:00 2001 From: "Jordan K." <65149726+jordankzf@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:49:07 +0800 Subject: [PATCH 091/227] ENG-5062: Non-blocking Stacks Swaps fixes (#565) * More consistent Mixpanel token names * DRY * Pass both fromToken and toToken * Simplify conditional principal * Make code less brittle --- src/app/screens/coinDashboard/coinHeader.tsx | 9 +++---- .../components/tokenFromBottomSheet/index.tsx | 7 +++--- .../components/tokenToBottomSheet/index.tsx | 4 +-- src/app/screens/swap/index.tsx | 5 ++-- src/app/screens/swap/mixpanel.ts | 8 +++--- src/app/screens/swap/quoteSummary/index.tsx | 2 +- src/app/screens/swap/utils.ts | 25 ++++++++++++++----- 7 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/app/screens/coinDashboard/coinHeader.tsx b/src/app/screens/coinDashboard/coinHeader.tsx index 2f0f56e8f..8dcdedd08 100644 --- a/src/app/screens/coinDashboard/coinHeader.tsx +++ b/src/app/screens/coinDashboard/coinHeader.tsx @@ -14,6 +14,7 @@ import useStxWalletData from '@hooks/queries/useStxWalletData'; import useHasFeature from '@hooks/useHasFeature'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; +import { getTrackingIdentifier, isMotherToken } from '@screens/swap/utils'; import { AnalyticsEvents, FeatureId, @@ -182,12 +183,8 @@ export default function CoinHeader({ currency, fungibleToken }: Props) { return; } trackMixPanel(AnalyticsEvents.InitiateSwapFlow, { - selectedToken: fungibleToken ? fungibleToken.name : currency, - principal: fungibleToken - ? fungibleToken?.principal - : currency === 'STX' - ? currency - : undefined, + selectedToken: fungibleToken ? getTrackingIdentifier(fungibleToken) : currency, + principal: isMotherToken(fungibleToken) ? getTrackingIdentifier(fungibleToken) : undefined, }); navigate(`/swap?from=${fungibleToken ? fungibleToken.principal : currency}`); }; diff --git a/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx b/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx index 05234fa21..9f8f6dc28 100644 --- a/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx +++ b/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx @@ -1,4 +1,5 @@ import TokenTile from '@components/tokenTile'; +import { getTrackingIdentifier } from '@screens/swap/utils'; import { AnalyticsEvents, type FungibleToken, type Token } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import Sheet from '@ui-library/sheet'; @@ -45,7 +46,7 @@ export default function TokenFromBottomSheet({ visible, title, onSelectCoin, onC onPress={() => { onSelectCoin(token); trackMixPanel(AnalyticsEvents.SelectTokenToSwapFrom, { - selectedToken: 'Bitcoin', + selectedToken: 'BTC', }); onClose(); }} @@ -61,7 +62,7 @@ export default function TokenFromBottomSheet({ visible, title, onSelectCoin, onC onPress={() => { onSelectCoin(token); trackMixPanel(AnalyticsEvents.SelectTokenToSwapFrom, { - selectedToken: 'Stacks', + selectedToken: 'STX', principal: 'STX', }); onClose(); @@ -78,7 +79,7 @@ export default function TokenFromBottomSheet({ visible, title, onSelectCoin, onC onPress={() => { onSelectCoin(token); trackMixPanel(AnalyticsEvents.SelectTokenToSwapFrom, { - selectedToken: token.name, + selectedToken: getTrackingIdentifier(token), principal: token.protocol === 'stacks' ? token.principal : undefined, }); onClose(); diff --git a/src/app/screens/swap/components/tokenToBottomSheet/index.tsx b/src/app/screens/swap/components/tokenToBottomSheet/index.tsx index 2953d0f5a..5a0830d88 100644 --- a/src/app/screens/swap/components/tokenToBottomSheet/index.tsx +++ b/src/app/screens/swap/components/tokenToBottomSheet/index.tsx @@ -178,7 +178,7 @@ export default function TokenToBottomSheet({ onPress={() => { onSelectCoin(token); trackMixPanel(AnalyticsEvents.SelectTokenToSwapTo, { - selectedToken: 'Bitcoin', + selectedToken: 'BTC', }); handleClose(); }} @@ -195,7 +195,7 @@ export default function TokenToBottomSheet({ onPress={() => { onSelectCoin(token); trackMixPanel(AnalyticsEvents.SelectTokenToSwapTo, { - selectedToken: 'Stacks', + selectedToken: 'STX', principal: 'STX', }); handleClose(); diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index fcabb2f45..810711700 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -48,6 +48,7 @@ import QuotesModal from './quotesModal'; import type { OrderInfo, Side, StxOrderInfo } from './types'; import useMasterCoinsList from './useMasterCoinsList'; import { + getTrackingIdentifier, isStxTx, mapFTNativeSwapTokenToTokenBasic, mapFTProtocolToSwapProtocol, @@ -360,8 +361,8 @@ export default function SwapScreen() { trackMixPanel(AnalyticsEvents.SelectSwapQuote, { provider: provider.provider.name, - from: fromToken.name, - to: toToken.name ?? toToken.ticker, + from: getTrackingIdentifier(fromToken), + to: getTrackingIdentifier(toToken), fromPrincipal: isStxTx({ fromToken, toToken }) ? fromToken.principal : undefined, toPrincipal: isStxTx({ fromToken, toToken }) ? toToken.ticker : undefined, }); diff --git a/src/app/screens/swap/mixpanel.ts b/src/app/screens/swap/mixpanel.ts index 7928c6a21..665a23a77 100644 --- a/src/app/screens/swap/mixpanel.ts +++ b/src/app/screens/swap/mixpanel.ts @@ -10,7 +10,7 @@ import { } from '@secretkeylabs/xverse-core'; import { trackMixPanel } from '@utils/mixpanel'; import BigNumber from 'bignumber.js'; -import { isRunesTx } from './utils'; +import { getTrackingIdentifier, isRunesTx } from './utils'; function trackSwapMixPanel( eventName, @@ -42,10 +42,10 @@ function trackSwapMixPanel( let fromPrincipal; let toPrincipal; - const from = fromToken?.name; - const to = toToken?.name ?? toToken?.ticker; + const from = getTrackingIdentifier(fromToken); + const to = getTrackingIdentifier(toToken); - if (isRunesTx({ fromToken, toToken })) { + if (fromToken && toToken && isRunesTx({ fromToken, toToken })) { fromAmount = fromToken?.principal === 'BTC' ? getBtcFiatEquivalent(new BigNumber(amount), new BigNumber(btcUsdRate)).toFixed(2) diff --git a/src/app/screens/swap/quoteSummary/index.tsx b/src/app/screens/swap/quoteSummary/index.tsx index 4c54f8032..38db3fa72 100644 --- a/src/app/screens/swap/quoteSummary/index.tsx +++ b/src/app/screens/swap/quoteSummary/index.tsx @@ -529,7 +529,7 @@ export default function QuoteSummary({ - {isRunesTx({ toToken }) && ( + {fromToken && toToken && isRunesTx({ fromToken, toToken }) && ( {t('TRANSACTION_SETTING.FEE_RATE')} diff --git a/src/app/screens/swap/utils.ts b/src/app/screens/swap/utils.ts index 283d6d0d9..e415ebb54 100644 --- a/src/app/screens/swap/utils.ts +++ b/src/app/screens/swap/utils.ts @@ -108,13 +108,11 @@ export const isRunesTx = ({ fromToken, toToken, }: { - fromToken?: FungibleToken; - toToken?: Token; + fromToken: FungibleToken; + toToken: Token; }): boolean => - fromToken?.protocol === 'runes' || - fromToken?.principal === 'BTC' || - toToken?.protocol === 'runes' || - toToken?.ticker === 'BTC'; + (fromToken?.protocol === 'runes' || toToken?.protocol === 'runes') && + (fromToken?.principal === 'BTC' || toToken?.ticker === 'BTC'); export const isStxTx = ({ fromToken, @@ -127,3 +125,18 @@ export const isStxTx = ({ fromToken?.principal === 'STX' || toToken?.protocol === 'sip10' || toToken?.ticker === 'STX'; + +const getIdentifier = (token?: Token | FungibleToken) => { + if (!token) return ''; + return 'principal' in token ? token.principal : token.ticker; +}; + +export const isMotherToken = (token?: Token | FungibleToken) => { + const identifier = getIdentifier(token); + return identifier === 'BTC' || identifier === 'STX'; +}; + +export const getTrackingIdentifier = (token?: Token | FungibleToken) => { + const identifier = getIdentifier(token); + return isMotherToken(token) ? identifier : token?.name ?? identifier; +}; From 69268e2262abc948e7847b33cb4bd2f3d42535a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduard=20Bardaj=C3=AD=20Puig?= Date: Mon, 9 Sep 2024 14:44:32 +0200 Subject: [PATCH 092/227] Avoid switching accounts when current account is target account (#575) --- .../signMessageRequest/useSignMessageRequest.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/app/screens/signMessageRequest/useSignMessageRequest.ts b/src/app/screens/signMessageRequest/useSignMessageRequest.ts index 3f59bb949..548a86cb0 100644 --- a/src/app/screens/signMessageRequest/useSignMessageRequest.ts +++ b/src/app/screens/signMessageRequest/useSignMessageRequest.ts @@ -59,6 +59,7 @@ export const useSignMessageValidation = (requestPayload: SignMessagePayload | un const { t } = useTranslation('translation', { keyPrefix: 'REQUEST_ERRORS' }); const selectedAccount = useSelectedAccount(); const { accountsList, network } = useWalletSelector(); + const { btcAddress } = useSelectedAccount(); const { switchAccount } = useWalletReducer(); const checkAddressAvailability = () => { @@ -83,13 +84,17 @@ export const useSignMessageValidation = (requestPayload: SignMessagePayload | un return; } const account = checkAddressAvailability(); - if (account) { - switchAccount(account); - } else { + + if (!account) { setValidationError({ error: t('ADDRESS_MISMATCH'), }); + return; } + + if (btcAddress === account.btcAddress) return; + + switchAccount(account); }; useEffect(() => { From fa13cb74b91a895790161fe7e3bf1cf304338f8b Mon Sep 17 00:00:00 2001 From: Christos Maris Date: Tue, 20 Aug 2024 21:47:04 +0300 Subject: [PATCH 093/227] [ENG-4887] Start the Runes Listing Flow --- package-lock.json | 15 +++ package.json | 1 + src/app/components/tokenImage/index.tsx | 36 +++--- src/app/hooks/apiClients/useXverseApi.ts | 10 ++ .../useRuneFloorPricePerMarketplaceQuery.ts | 36 ++++++ .../hooks/queries/runes/useRuneSellPsbt.ts | 3 +- .../runes/useRuneSellPsbtPerMarketplace.ts | 85 ++++++++++++ .../queries/runes/useSubmitRuneSellPsbt.ts | 3 +- src/app/hooks/useSearchParamsState.ts | 15 ++- src/app/hooks/useSignBatchPsbtTx.ts | 13 +- src/app/routes/index.tsx | 5 + src/app/screens/listRune/index.tsx | 122 +++++++++++++++--- .../screens/listRune/listMarketplaceItem.tsx | 86 ++++++++++++ src/app/screens/listRune/reducer.tsx | 6 +- .../screens/signBatchPsbtRequest/index.tsx | 112 +++++++++++++--- src/app/screens/transactionStatus/index.tsx | 83 +++++++++--- .../marketplaceRuneListingResults.tsx | 104 +++++++++++++++ .../multipleMarketplaceListingResult.tsx | 82 ++++++++++++ src/app/utils/constants.ts | 2 + src/app/utils/helper.ts | 2 + src/assets/img/listings/CheckCircle.svg | 3 + src/assets/img/listings/XCircle.svg | 3 + src/assets/img/send/info_circle.svg | 3 + src/locales/en.json | 17 ++- 24 files changed, 772 insertions(+), 75 deletions(-) create mode 100644 src/app/hooks/apiClients/useXverseApi.ts create mode 100644 src/app/hooks/queries/runes/useRuneFloorPricePerMarketplaceQuery.ts create mode 100644 src/app/hooks/queries/runes/useRuneSellPsbtPerMarketplace.ts create mode 100644 src/app/screens/listRune/listMarketplaceItem.tsx create mode 100644 src/app/screens/transactionStatus/marketplaceRuneListingResults.tsx create mode 100644 src/app/screens/transactionStatus/multipleMarketplaceListingResult.tsx create mode 100644 src/assets/img/listings/CheckCircle.svg create mode 100644 src/assets/img/listings/XCircle.svg create mode 100644 src/assets/img/send/info_circle.svg diff --git a/package-lock.json b/package-lock.json index 3bcf90680..24cfdbdb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "jsontokens": "^4.0.1", "mixpanel-browser": "^2.47.0", "nanoid": "^4.0.0", + "object-hash": "^3.0.0", "p-queue": "^7.3.4", "path": "^0.12.7", "qr-code-styling": "^1.5.0", @@ -1280,6 +1281,7 @@ "version": "19.1.0", "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/19.1.0/fe6b53e68ea3d66c7fd1bbf8e791f865299811d8", "integrity": "sha512-ssrdJAb9jpl1mGCfbS4K6S22sKLBXzK5LbYMiNBU8anyps4A79u/c7qkHUDMgDY4pWcuSMs8iS4mcpyshPFiQQ==", + "license": "ISC", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", @@ -9097,6 +9099,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -19637,6 +19647,11 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" + }, "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", diff --git a/package.json b/package.json index a5b76e4c1..bd87877d9 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "jsontokens": "^4.0.1", "mixpanel-browser": "^2.47.0", "nanoid": "^4.0.0", + "object-hash": "^3.0.0", "p-queue": "^7.3.4", "path": "^0.12.7", "qr-code-styling": "^1.5.0", diff --git a/src/app/components/tokenImage/index.tsx b/src/app/components/tokenImage/index.tsx index c63b083c0..0a8bb3202 100644 --- a/src/app/components/tokenImage/index.tsx +++ b/src/app/components/tokenImage/index.tsx @@ -7,7 +7,7 @@ import useWalletSelector from '@hooks/useWalletSelector'; import type { FungibleToken } from '@secretkeylabs/xverse-core'; import { XVERSE_ORDIVIEW_URL, type CurrencyTypes } from '@utils/constants'; import { getTicker } from '@utils/helper'; -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; const DEFAULT_SIZE = 40; @@ -71,6 +71,7 @@ interface TokenImageProps { size?: number; round?: boolean; showProtocolIcon?: boolean; + customProtocolIcon?: string; } export default function TokenImage({ @@ -80,6 +81,7 @@ export default function TokenImage({ size, round, showProtocolIcon = true, + customProtocolIcon, }: TokenImageProps) { const { network } = useWalletSelector(); const ftProtocol = fungibleToken?.protocol; @@ -104,21 +106,23 @@ export default function TokenImage({ ); - const getProtocolIcon = () => { - if (!ftProtocol) { + const protocolIcon = useMemo(() => { + if (!ftProtocol && !customProtocolIcon) { return null; } - switch (ftProtocol) { - case 'stacks': - return ; - case 'brc-20': - return ; - case 'runes': - return ; - default: - return null; - } - }; + + const protocolToIcon = { + 'brc-20': , + stacks: , + runes: , + }; + + return ftProtocol ? ( + protocolToIcon[ftProtocol] + ) : ( + + ); + }, [ftProtocol, customProtocolIcon]); const renderIcon = () => { if (!fungibleToken) { @@ -178,8 +182,8 @@ export default function TokenImage({ return ( {imageError ? tickerComponent() : renderIcon()} - {ftProtocol && showProtocolIcon && ( - {getProtocolIcon()} + {showProtocolIcon && protocolIcon && ( + {protocolIcon} )} ); diff --git a/src/app/hooks/apiClients/useXverseApi.ts b/src/app/hooks/apiClients/useXverseApi.ts new file mode 100644 index 000000000..16b2cbf05 --- /dev/null +++ b/src/app/hooks/apiClients/useXverseApi.ts @@ -0,0 +1,10 @@ +import { getXverseApiClient } from '@secretkeylabs/xverse-core'; +import { useMemo } from 'react'; +import useWalletSelector from '../useWalletSelector'; + +const useXverseApi = () => { + const { network } = useWalletSelector(); + return useMemo(() => getXverseApiClient(network.type), [network.type]); +}; + +export default useXverseApi; diff --git a/src/app/hooks/queries/runes/useRuneFloorPricePerMarketplaceQuery.ts b/src/app/hooks/queries/runes/useRuneFloorPricePerMarketplaceQuery.ts new file mode 100644 index 000000000..bb3fafb4b --- /dev/null +++ b/src/app/hooks/queries/runes/useRuneFloorPricePerMarketplaceQuery.ts @@ -0,0 +1,36 @@ +import useXverseApi from '@hooks/apiClients/useXverseApi'; +import useWalletSelector from '@hooks/useWalletSelector'; +import type { Marketplace, TokenId } from '@secretkeylabs/xverse-core'; +import { useQuery } from '@tanstack/react-query'; +import { useCallback } from 'react'; + +export default function useRuneFloorPricePerMarketplaceQuery( + token: TokenId, + marketplaces: Marketplace[], + backgroundRefetch = true, +) { + const { network } = useWalletSelector(); + const xverseApi = useXverseApi(); + + const queryFn = useCallback( + () => + xverseApi.listings.getRuneMarketData(token, marketplaces).then((res) => + res + .filter((data) => marketplaces.includes(data.marketplace.name)) + .sort((a, b) => (a.marketplace.name < b.marketplace.name ? -1 : 1)) + .map((data) => ({ + floorPrice: Number(data.floorPrice ?? '0'), + marketplace: data.marketplace, + })), + ), + [token, marketplaces, xverseApi], + ); + + return useQuery({ + refetchOnWindowFocus: backgroundRefetch, + refetchOnReconnect: backgroundRefetch, + queryKey: ['get-rune-floor-price-per-marketplace', token], + enabled: Boolean(Object.keys(token).length) && network.type === 'Mainnet', + queryFn, + }); +} diff --git a/src/app/hooks/queries/runes/useRuneSellPsbt.ts b/src/app/hooks/queries/runes/useRuneSellPsbt.ts index 8efb17536..d2236b97d 100644 --- a/src/app/hooks/queries/runes/useRuneSellPsbt.ts +++ b/src/app/hooks/queries/runes/useRuneSellPsbt.ts @@ -3,6 +3,7 @@ import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { BitcoinNetworkType } from '@sats-connect/core'; import type { RuneSellRequest } from '@secretkeylabs/xverse-core'; +import { sanitizeRuneName } from '@utils/helper'; import type { RuneItem } from '@utils/runes'; import { useCallback, useState } from 'react'; @@ -20,7 +21,7 @@ const useRuneSellPsbt = (runeName: string, listingUtxos: Record { - const sanitizedRuneName = runeName.replace(/[^A-Za-z]+/g, '').toUpperCase(); + const sanitizedRuneName = sanitizeRuneName(runeName); const utxosToList = Object.entries(listingUtxos) .filter((item) => item[1].selected) .map(([key, item]) => ({ diff --git a/src/app/hooks/queries/runes/useRuneSellPsbtPerMarketplace.ts b/src/app/hooks/queries/runes/useRuneSellPsbtPerMarketplace.ts new file mode 100644 index 000000000..51db2a39e --- /dev/null +++ b/src/app/hooks/queries/runes/useRuneSellPsbtPerMarketplace.ts @@ -0,0 +1,85 @@ +import useXverseApi from '@hooks/apiClients/useXverseApi'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { BitcoinNetworkType } from '@sats-connect/core'; +import type { CreateRuneListingRequest, Marketplace } from '@secretkeylabs/xverse-core'; +import { sanitizeRuneName } from '@utils/helper'; +import type { RuneItem } from '@utils/runes'; +import { useCallback, useState } from 'react'; + +const useRuneSellPsbtPerMarketplace = ( + runeName: string, + listingUtxos: Record, + selectedMarketplaces: Marketplace[], +) => { + const { network } = useWalletSelector(); + const { ordinalsAddress, ordinalsPublicKey, btcAddress } = useSelectedAccount(); + + const [loading, setLoading] = useState(false); + const [signPsbtPayload, setSignPsbtPayload] = useState(null); + const [error, setError] = useState(null); + const xverseApi = useXverseApi(); + + const getRuneSellPsbt = useCallback(async () => { + const sanitizedRuneName = sanitizeRuneName(runeName); + const utxosToList = Object.entries(listingUtxos) + .filter((item) => item[1].selected) + .map(([key, item]) => ({ + location: key.split(':')[0], + index: Number(key.split(':')[1]), + priceSats: item.priceSats, + amount: item.amount, + })); + + const expiresAt = new Date(); + expiresAt.setDate(expiresAt.getDate() + 10); // 10 days from now + + const args: CreateRuneListingRequest = { + rune: sanitizedRuneName, + makerRunesPublicKey: ordinalsPublicKey, + makerRunesAddress: ordinalsAddress, + makerReceiveAddress: btcAddress, + utxos: utxosToList, + expiresAt: expiresAt.toISOString(), + marketplaces: selectedMarketplaces, + }; + + setLoading(true); + setError(null); + + await xverseApi.listings + .getRuneSellOrder(args) + .then((sellOrder) => { + const payload = { + network: { + type: + network.type === 'Mainnet' ? BitcoinNetworkType.Mainnet : BitcoinNetworkType.Testnet, + }, + message: 'Sign Transactions', + psbts: sellOrder.map((order) => ({ + psbtBase64: order.psbt, + inputsToSign: undefined, + marketplaceName: order.marketplace.name, + batchAuctionId: order.batchAuctionId, + })), + }; + + setSignPsbtPayload(payload); + }) + .catch(() => setError('Failed to create listing')) + .finally(() => setLoading(false)); + }, [ + runeName, + listingUtxos, + btcAddress, + network, + ordinalsAddress, + ordinalsPublicKey, + xverseApi, + selectedMarketplaces, + ]); + + return { getRuneSellPsbt, loading, signPsbtPayload, error }; +}; + +export default useRuneSellPsbtPerMarketplace; diff --git a/src/app/hooks/queries/runes/useSubmitRuneSellPsbt.ts b/src/app/hooks/queries/runes/useSubmitRuneSellPsbt.ts index 928a4eee6..b05ba708f 100644 --- a/src/app/hooks/queries/runes/useSubmitRuneSellPsbt.ts +++ b/src/app/hooks/queries/runes/useSubmitRuneSellPsbt.ts @@ -1,6 +1,7 @@ import useRunesApi from '@hooks/apiClients/useRunesApi'; import useSelectedAccount from '@hooks/useSelectedAccount'; import type { SubmitRuneSellRequest } from '@secretkeylabs/xverse-core'; +import { sanitizeRuneName } from '@utils/helper'; import { useCallback } from 'react'; const useSubmitRuneSellPsbt = () => { @@ -10,7 +11,7 @@ const useSubmitRuneSellPsbt = () => { const submitRuneSellPsbt = useCallback( async (signedPsbtBase64: string, runeName: string) => { const expiresAt = new Date(); - const sanitizedRuneName = runeName.replace(/[^A-Za-z]+/g, '').toUpperCase(); + const sanitizedRuneName = sanitizeRuneName(runeName); // setting the expiration date to 10 days from now expiresAt.setDate(expiresAt.getDate() + 10); const args: SubmitRuneSellRequest = { diff --git a/src/app/hooks/useSearchParamsState.ts b/src/app/hooks/useSearchParamsState.ts index 5a88747e4..556ee310f 100644 --- a/src/app/hooks/useSearchParamsState.ts +++ b/src/app/hooks/useSearchParamsState.ts @@ -1,8 +1,12 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; const useSearchParamsState = (key: string, defaultValue: T): [T, (newValue: T) => void] => { - const [searchParams, setSearchParams] = useSearchParams(); + const [searchParams, setSearchParamsInternal] = useSearchParams(); + const setSearchParam = (newValue: T) => { + searchParams.set(key, JSON.stringify(newValue)); + setSearchParamsInternal(searchParams); + }; const paramValue = searchParams.get(key); const initialValue = paramValue !== null ? (JSON.parse(paramValue) as T) : defaultValue; @@ -10,10 +14,13 @@ const useSearchParamsState = (key: string, defaultValue: T): [T, (newValue: T const [value, setValue] = useState(initialValue); const customSetter = (newValue: T) => { setValue(newValue); - searchParams.set(key, JSON.stringify(newValue)); - setSearchParams(searchParams); + setSearchParam(newValue); }; + useEffect(() => { + setSearchParam(initialValue); + }, []); + return [value, customSetter]; }; diff --git a/src/app/hooks/useSignBatchPsbtTx.ts b/src/app/hooks/useSignBatchPsbtTx.ts index f44ba74b6..615461df9 100644 --- a/src/app/hooks/useSignBatchPsbtTx.ts +++ b/src/app/hooks/useSignBatchPsbtTx.ts @@ -9,12 +9,17 @@ import useSeedVault from './useSeedVault'; const useSignBatchPsbtTx = () => { const { accountsList, network } = useWalletSelector(); - const { search } = useLocation(); + const location = useLocation(); + const locationState = useMemo(() => location.state || {}, []); + const { search } = location; const { getSeed } = useSeedVault(); const params = new URLSearchParams(search); + const inApp = params.get('signBatchPsbtsInApp') ?? false; const requestToken = params.get('signBatchPsbtRequest') ?? ''; + const request = useMemo( - () => decodeToken(requestToken) as any as SignMultipleTransactionOptions, + () => + inApp ? locationState : (decodeToken(requestToken) as any as SignMultipleTransactionOptions), [requestToken], ); const tabId = params.get('tabId') ?? '0'; @@ -25,7 +30,7 @@ const useSignBatchPsbtTx = () => { const signingResponse = await signPsbt( seedPhrase, accountsList, - psbt.inputsToSign, + psbt.inputsToSign || [], psbt.psbtBase64, false, network.type, @@ -63,6 +68,8 @@ const useSignBatchPsbtTx = () => { getSigningAddresses, confirmSignPsbt, cancelSignPsbt, + selectedRune: locationState.selectedRune, + minPriceSats: locationState.minPriceSats, }; }; diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index 7afaf70f3..ae1350bb7 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -78,6 +78,7 @@ import Stacking from '@screens/stacking'; import SwapScreen from '@screens/swap'; import TransactionRequest from '@screens/transactionRequest'; import TransactionStatus from '@screens/transactionStatus'; +import MultipleMarketplaceListingResult from '@screens/transactionStatus/multipleMarketplaceListingResult'; import TransferRunesRequest from '@screens/transferRunesRequest'; import UnlistRuneScreen from '@screens/unlistRune'; import WalletExists from '@screens/walletExists'; @@ -400,6 +401,10 @@ const router = createHashRouter([ path: 'tx-status', element: , }, + { + path: 'multiple-marketplace-listing-result', + element: , + }, { path: 'buy/:currency', element: , diff --git a/src/app/screens/listRune/index.tsx b/src/app/screens/listRune/index.tsx index 258a5d06c..ebf1ec96f 100644 --- a/src/app/screens/listRune/index.tsx +++ b/src/app/screens/listRune/index.tsx @@ -1,8 +1,9 @@ +import RequestsRoutes from '@common/utils/route-urls'; import BottomBar from '@components/tabBar'; import TopRow from '@components/topRow'; -import useRuneFloorPriceQuery from '@hooks/queries/runes/useRuneFloorPriceQuery'; +import useRuneFloorPricePerMarketplaceQuery from '@hooks/queries/runes/useRuneFloorPricePerMarketplaceQuery'; import { useVisibleRuneFungibleTokens } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; -import useRuneSellPsbt from '@hooks/queries/runes/useRuneSellPsbt'; +import useRuneSellPsbtPerMarketplace from '@hooks/queries/runes/useRuneSellPsbtPerMarketplace'; import useRuneUtxosQuery from '@hooks/queries/runes/useRuneUtxosQuery'; import useCoinRates from '@hooks/queries/useCoinRates'; import useHasFeature from '@hooks/useHasFeature'; @@ -18,6 +19,8 @@ import { currencySymbolMap, getBtcFiatEquivalent, satsToBtc, + type FungibleToken, + type Marketplace, } from '@secretkeylabs/xverse-core'; import Button, { LinkButton } from '@ui-library/button'; import { StickyButtonContainer, StyledP } from '@ui-library/common.styled'; @@ -25,7 +28,7 @@ import Spinner from '@ui-library/spinner'; import { formatToXDecimalPlaces, ftDecimals } from '@utils/helper'; import { getFullTxId, getTxIdFromFullTxId, getVoutFromFullTxId } from '@utils/runes'; import BigNumber from 'bignumber.js'; -import { useEffect, useReducer } from 'react'; +import { useEffect, useMemo, useReducer, useState } from 'react'; import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; @@ -52,9 +55,15 @@ import { TabButtonsContainer, TabContainer, } from './index.styled'; +import ListMarketplaceItem from './listMarketplaceItem'; import WrapperComponent from './listRuneWrapper'; import SetCustomPriceModal from './setCustomPriceModal'; +const joinedSelectedMarketplaces = (selectedMarketplaces: Marketplace[]) => { + const last = selectedMarketplaces.pop(); + return [selectedMarketplaces.join(', '), last].filter((s) => s).join(' and '); +}; + export default function ListRuneScreen() { const { t } = useTranslation('translation', { keyPrefix: 'LIST_RUNE_SCREEN' }); const navigate = useNavigate(); @@ -82,13 +91,31 @@ export default function ListRuneScreen() { refetch, } = useRuneUtxosQuery(selectedRune?.name ?? '', 'unlisted', false); + const supportedMarketplaces: Marketplace[] = ['Unisat', 'Magic Eden']; const { - data: runeFloorPrice, + data: floorPriceData, isInitialLoading: floorPriceLoading, isRefetching: floorPriceRefetching, - } = useRuneFloorPriceQuery(selectedRune?.name ?? '', false); + } = useRuneFloorPricePerMarketplaceQuery( + { + name: selectedRune?.assetName ?? '', + id: selectedRune?.principal ?? '', + }, + supportedMarketplaces, + false, + ); - const noFloorPrice = runeFloorPrice === 0; + const [selectedMarketplaces, setSelectedMarketplaces] = useState(new Set()); + const runeFloorPrice = useMemo( + () => + Math.min( + ...(floorPriceData + ?.filter((d) => selectedMarketplaces.has(d.marketplace.name)) + .map((d) => d.floorPrice) || [0]), + ), + [floorPriceData, selectedMarketplaces], + ); + const noFloorPrice = useMemo(() => runeFloorPrice === 0, [runeFloorPrice]); const isLoading = listItemsLoading || listItemsRefetching || floorPriceLoading || floorPriceRefetching; @@ -112,7 +139,9 @@ export default function ListRuneScreen() { signPsbtPayload, loading: psbtLoading, error: psbtError, - } = useRuneSellPsbt(selectedRune?.name ?? '', listItemsMap); + } = useRuneSellPsbtPerMarketplace(selectedRune?.name ?? '', listItemsMap, [ + ...selectedMarketplaces, + ]); const selectedListItems = Object.values(listItemsMap).filter((item) => item.selected); @@ -141,7 +170,7 @@ export default function ListRuneScreen() { const curAmount = currentItem.amount; return curAmount > maxAmount ? curAmount : maxAmount; }, 0); - const maxGlobalPriceSats = 1_000_000_000 / highestSelectedRuneAmount; + const maxGlobalPriceSats = 500_000_000 / highestSelectedRuneAmount; // Unisat's maximum const invalidListings: boolean = selectedListItems.some( (item) => item.amount * item.priceSats < 10000 || item.amount * item.priceSats > 1000000000, @@ -157,8 +186,23 @@ export default function ListRuneScreen() { if (section === 'SELECT_RUNES') { navigate(`/coinDashboard/FT?ftKey=${selectedRune?.principal}&protocol=runes`); - } else { + } else if (section === 'SELECT_MARKETPLACES') { dispatch({ type: 'SET_SECTION', payload: 'SELECT_RUNES' }); + } else { + dispatch({ type: 'SET_SECTION', payload: 'SELECT_MARKETPLACES' }); + } + }; + + const getTitle = () => { + switch (section) { + case 'SELECT_RUNES': + return t('SELECT_RUNES'); + case 'SELECT_MARKETPLACES': + return t('SELECT_MARKETPLACES'); + case 'SET_PRICES': + return t('LIST_RUNES'); + default: + return ''; } }; @@ -166,6 +210,8 @@ export default function ListRuneScreen() { switch (section) { case 'SELECT_RUNES': return t('SELECT_RUNES_SECTION'); + case 'SELECT_MARKETPLACES': + return t('SELECT_MARKETPLACES_SECTION'); case 'SET_PRICES': return t('SET_PRICES_SECTION'); default: @@ -241,11 +287,11 @@ export default function ListRuneScreen() { useEffect(() => { if (signPsbtPayload) { - navigate(`/psbt-signing-request?magicEdenPsbt=true&runeId=${runeId}`, { + navigate(`${RequestsRoutes.SignBatchBtcTx}?signBatchPsbtsInApp=true`, { state: { payload: signPsbtPayload, + minPriceSats: Math.min(...Object.values(listItemsMap).map((item) => item.priceSats)), selectedRune, - listRunesState, }, }); } @@ -293,7 +339,7 @@ export default function ListRuneScreen() { -
{t('LIST_RUNES')}
+
{getTitle()}
{getDesc()} @@ -361,8 +407,10 @@ export default function ListRuneScreen() { {isExpanded && ( - {summary?.inputs.map((input) => ( + {extractedTxSummary.inputs.map((input) => ( ))} {t('OUTPUT')} - {summary?.outputs.map((output, index) => ( + {extractedTxSummary.outputs.map((output, index) => ( ))} diff --git a/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx b/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx index c684fb8e6..7d466f7b6 100644 --- a/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx +++ b/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx @@ -8,7 +8,6 @@ import { getTruncatedAddress } from '@utils/helper'; import BigNumber from 'bignumber.js'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -import { isAddressOutput, isPubKeyOutput, isScriptOutput } from '../utils'; const TransferDetailContainer = styled.div((props) => ({ paddingBottom: props.theme.space.m, @@ -37,8 +36,9 @@ type Props = { function TransactionOutput({ output, scriptOutputCount }: Props) { const { btcAddress, ordinalsAddress, btcPublicKey, ordinalsPublicKey } = useSelectedAccount(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const isOutputWithScript = isScriptOutput(output); - const isOutputWithPubKey = isPubKeyOutput(output); + const isOutputWithScript = output.type === 'script'; + const isOutputWithAddress = output.type === 'address'; + const isOutputWithPubKey = !isOutputWithScript && !isOutputWithAddress; const detailViewIcon = isOutputWithScript ? ScriptIcon : OutputIcon; const detailViewHideCopyButton = @@ -93,7 +93,7 @@ function TransactionOutput({ output, scriptOutputCount }: Props) { hideCopyButton={detailViewHideCopyButton} dataTestID="confirm-amount" amount={`${satsToBtc( - new BigNumber(isAddressOutput(output) ? output.amount.toString() : '0'), + new BigNumber(isOutputWithAddress ? output.amount.toString() : '0'), ).toFixed()} BTC`} address={isOutputWithScript || isOutputWithPubKey ? '' : output.address} outputScript={isOutputWithScript ? output.script : undefined} diff --git a/src/app/components/confirmBtcTransaction/utils.ts b/src/app/components/confirmBtcTransaction/utils.ts deleted file mode 100644 index c7ec80165..000000000 --- a/src/app/components/confirmBtcTransaction/utils.ts +++ /dev/null @@ -1,316 +0,0 @@ -import type { - btcTransaction, - BundleSatRange, - FungibleToken, - RuneBase, -} from '@secretkeylabs/xverse-core'; - -// TODO this should all be in core and unit tested - -export type SatRangeTx = { - totalSats: number; - offset: number; - fromAddress: string; - inscriptions: (Omit & { - content_type: string; - inscription_number: number; - })[]; - satributes: btcTransaction.IOSatribute['types']; -}; - -const DUMMY_OFFSET = -1; - -export const isScriptOutput = ( - output: btcTransaction.EnhancedOutput, -): output is btcTransaction.TransactionScriptOutput => - (output as btcTransaction.TransactionScriptOutput).script !== undefined; - -export const isPubKeyOutput = ( - output: btcTransaction.EnhancedOutput, -): output is btcTransaction.TransactionPubKeyOutput => - !!(output as btcTransaction.TransactionPubKeyOutput).pubKeys?.length; - -export const isAddressOutput = ( - output: btcTransaction.EnhancedOutput, -): output is btcTransaction.TransactionOutput => - (output as btcTransaction.TransactionOutput).address !== undefined; - -type CommonInputOutputUtilProps = { - inputs?: btcTransaction.EnhancedInput[]; - outputs?: btcTransaction.EnhancedOutput[]; - btcAddress: string; - ordinalsAddress: string; -}; - -export const getNetAmount = ({ - inputs, - outputs, - btcAddress, - ordinalsAddress, -}: CommonInputOutputUtilProps) => { - if (!inputs || !outputs) { - return 0; - } - - const initialValue = 0; - - const totalUserSpend = inputs.reduce((accumulator: number, input) => { - const isFromUserAddress = [btcAddress, ordinalsAddress].includes(input.extendedUtxo.address); - if (isFromUserAddress) { - return accumulator + input.extendedUtxo.utxo.value; - } - return accumulator; - }, initialValue); - - const totalUserReceive = outputs.reduce((accumulator: number, output) => { - const isToUserAddress = - isAddressOutput(output) && [btcAddress, ordinalsAddress].includes(output.address); - if (isToUserAddress) { - return accumulator + output.amount; - } - return accumulator; - }, initialValue); - - return totalUserReceive - totalUserSpend; -}; - -export const getOutputsWithAssetsFromUserAddress = ({ - btcAddress, - ordinalsAddress, - outputs, -}: Omit): { - outputsFromPayment: (btcTransaction.TransactionOutput | btcTransaction.TransactionPubKeyOutput)[]; - outputsFromOrdinal: (btcTransaction.TransactionOutput | btcTransaction.TransactionPubKeyOutput)[]; -} => { - // we want to discard outputs that are script, are not from user address and do not have inscriptions or satributes - const outputsFromPayment: ( - | btcTransaction.TransactionOutput - | btcTransaction.TransactionPubKeyOutput - )[] = []; - const outputsFromOrdinal: ( - | btcTransaction.TransactionOutput - | btcTransaction.TransactionPubKeyOutput - )[] = []; - outputs?.forEach((output) => { - if (isScriptOutput(output)) { - return; - } - - const itemsFromPayment: (btcTransaction.IOInscription | btcTransaction.IOSatribute)[] = []; - const itemsFromOrdinal: (btcTransaction.IOInscription | btcTransaction.IOSatribute)[] = []; - [...output.inscriptions, ...output.satributes].forEach((item) => { - if (item.fromAddress === btcAddress) { - return itemsFromPayment.push(item); - } - if (item.fromAddress === ordinalsAddress) { - itemsFromOrdinal.push(item); - } - }); - - if (itemsFromOrdinal.length > 0) { - outputsFromOrdinal.push(output); - } - if (itemsFromPayment.length > 0) { - outputsFromPayment.push(output); - } - }); - - return { outputsFromPayment, outputsFromOrdinal }; -}; - -export const getInputsWithAssetsFromUserAddress = ({ - btcAddress, - ordinalsAddress, - inputs, -}: Omit): { - inputsFromPayment: btcTransaction.EnhancedInput[]; - inputsFromOrdinal: btcTransaction.EnhancedInput[]; -} => { - // we want to discard inputs that are not from user address and do not have inscriptions or satributes - const inputsFromPayment: btcTransaction.EnhancedInput[] = []; - const inputsFromOrdinal: btcTransaction.EnhancedInput[] = []; - inputs?.forEach((input) => { - if (!input.inscriptions.length && !input.satributes.length) { - return; - } - - if (input.extendedUtxo.address === btcAddress) { - return inputsFromPayment.push(input); - } - if (input.extendedUtxo.address === ordinalsAddress) { - inputsFromOrdinal.push(input); - } - }); - - return { inputsFromPayment, inputsFromOrdinal }; -}; - -export const getOutputsWithAssetsToUserAddress = ({ - btcAddress, - ordinalsAddress, - outputs, -}: Omit): { - outputsToPayment: btcTransaction.TransactionOutput[]; - outputsToOrdinal: btcTransaction.TransactionOutput[]; -} => { - const outputsToPayment: btcTransaction.TransactionOutput[] = []; - const outputsToOrdinal: btcTransaction.TransactionOutput[] = []; - outputs?.forEach((output) => { - // we want to discard outputs that are not spendable or are not to user address - if ( - isScriptOutput(output) || - isPubKeyOutput(output) || - ![btcAddress, ordinalsAddress].includes(output.address) - ) { - return; - } - - if (output.address === btcAddress) { - return outputsToPayment.push(output); - } - - // we don't want to show amount to ordinals address, because it's not spendable - if ( - output.address === ordinalsAddress && - (output.inscriptions.length > 0 || output.satributes.length > 0) - ) { - outputsToOrdinal.push(output); - } - }); - - return { outputsToPayment, outputsToOrdinal }; -}; - -export const mapTxSatributeInfoToBundleInfo = (item: btcTransaction.IOSatribute | SatRangeTx) => { - const commonProps = { - offset: item.offset, - block: 0, - range: { - start: '0', - end: '0', - }, - yearMined: 0, - }; - - // SatRangeTx - if ('totalSats' in item) { - return { - ...commonProps, - totalSats: item.totalSats, - inscriptions: item.inscriptions, - satributes: item.satributes, - } as BundleSatRange; - } - - // btcTransaction.IOSatribute - return { - ...commonProps, - totalSats: item.amount, - inscriptions: [], - satributes: item.types, - } as BundleSatRange; -}; - -export const getSatRangesWithInscriptions = ({ - satributes, - inscriptions, - amount, -}: { - inscriptions: btcTransaction.IOInscription[]; - satributes: btcTransaction.IOSatribute[]; - amount: number; -}) => { - const satRanges: { - [offset: number]: SatRangeTx; - } = {}; - - satributes.forEach((satribute) => { - const { types, amount: totalSats, ...rest } = satribute; - satRanges[rest.offset] = { ...rest, satributes: types, totalSats, inscriptions: [] }; - }); - - inscriptions.forEach((inscription) => { - const { contentType, number, ...inscriptionRest } = inscription; - const mappedInscription = { - ...inscriptionRest, - content_type: contentType, - inscription_number: number, - }; - if (satRanges[inscription.offset]) { - satRanges[inscription.offset] = { - ...satRanges[inscription.offset], - inscriptions: [...satRanges[inscription.offset].inscriptions, mappedInscription], - }; - return; - } - - satRanges[inscription.offset] = { - totalSats: 1, - offset: inscription.offset, - fromAddress: inscription.fromAddress, - inscriptions: [mappedInscription], - satributes: ['COMMON'], - }; - }); - - const { amountOfExoticsOrInscribedSats, totalExoticSats } = Object.values(satRanges).reduce( - (acc, range) => ({ - amountOfExoticsOrInscribedSats: acc.amountOfExoticsOrInscribedSats + range.totalSats, - totalExoticSats: - acc.totalExoticSats + (!range.satributes.includes('COMMON') ? range.totalSats : 0), - }), - { - amountOfExoticsOrInscribedSats: 0, - totalExoticSats: 0, - }, - ); - - if (amountOfExoticsOrInscribedSats < amount) { - satRanges[DUMMY_OFFSET] = { - totalSats: amount - amountOfExoticsOrInscribedSats, - offset: DUMMY_OFFSET, - fromAddress: '', - inscriptions: [], - satributes: ['COMMON'], - }; - } - - // sort should be: inscribed rare, rare, inscribed common, common - const satRangesArray = Object.values(satRanges).sort((a, b) => { - // Check conditions for each category - const aHasInscriptions = a.inscriptions.length > 0; - const bHasInscriptions = b.inscriptions.length > 0; - const aHasRareSatributes = a.satributes.some((s) => s !== 'COMMON'); - const bHasRareSatributes = b.satributes.some((s) => s !== 'COMMON'); - - // sats not rare and not inscribed at bottom - if (!aHasInscriptions && !aHasRareSatributes) return 1; - - // sats inscribed and rare at top - if (aHasInscriptions && aHasRareSatributes) return -1; - - // sats not inscribed and rare below inscribed and rare - if (bHasInscriptions && bHasRareSatributes) return 1; - - // sats inscribed and not rare above sats not inscribed and not rare - if (aHasRareSatributes) return -1; - if (bHasRareSatributes) return 1; - - // equal ranges - return 0; - }); - - return { satRanges: satRangesArray, totalExoticSats }; -}; - -export const mapRuneBaseToFungibleToken = (rune: RuneBase): FungibleToken => ({ - protocol: 'runes', - name: rune.runeName, - principal: rune.runeId, - assetName: '', - balance: '', - total_received: '', - total_sent: '', - runeSymbol: rune.symbol, - runeInscriptionId: rune.inscriptionId, -}); diff --git a/src/app/components/transferFeeView/index.tsx b/src/app/components/transferFeeView/index.tsx index ed6b775fe..2dffa4b04 100644 --- a/src/app/components/transferFeeView/index.tsx +++ b/src/app/components/transferFeeView/index.tsx @@ -6,6 +6,7 @@ import { btcTransaction, getBtcFiatEquivalent, getFiatEquivalent, + type RareSatsType, } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import BigNumber from 'bignumber.js'; @@ -46,9 +47,16 @@ type Props = { feePerVByte?: BigNumber; fee: BigNumber; currency: string; - inscriptions?: btcTransaction.IOInscription[]; + inscriptions?: + | btcTransaction.IOInscription[] + | (btcTransaction.IOInscription & { satributes: RareSatsType[] })[] + | (Omit & { satributes: RareSatsType[] })[]; satributes?: btcTransaction.IOSatribute[]; - onShowInscription?: (inscription: btcTransaction.IOInscription) => void; + onShowInscription?: ( + inscription: + | (btcTransaction.IOInscription & { satributes: RareSatsType[] }) + | (Omit & { satributes: RareSatsType[] }), + ) => void; }; function TransferFeeView({ diff --git a/src/app/screens/btcSendRequest/index.tsx b/src/app/screens/btcSendRequest/index.tsx index 2fd7caf8b..6efeb832e 100644 --- a/src/app/screens/btcSendRequest/index.tsx +++ b/src/app/screens/btcSendRequest/index.tsx @@ -1,20 +1,12 @@ import { makeRPCError, makeRpcSuccessResponse, sendRpcResponse } from '@common/utils/rpc/helpers'; import ConfirmBtcTransaction from '@components/confirmBtcTransaction'; import useBtcFeeRate from '@hooks/useBtcFeeRate'; -import useHasFeature from '@hooks/useHasFeature'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useTransactionContext from '@hooks/useTransactionContext'; import useWalletSelector from '@hooks/useWalletSelector'; import { RpcErrorCode } from '@sats-connect/core'; import type { TransactionSummary } from '@screens/sendBtc/helpers'; -import { - AnalyticsEvents, - btcTransaction, - FeatureId, - parseSummaryForRunes, - type RuneSummary, - type Transport, -} from '@secretkeylabs/xverse-core'; +import { AnalyticsEvents, btcTransaction, type Transport } from '@secretkeylabs/xverse-core'; import Spinner from '@ui-library/spinner'; import { BITCOIN_DUST_AMOUNT_SATS } from '@utils/constants'; import { trackMixPanel } from '@utils/mixpanel'; @@ -50,8 +42,6 @@ function BtcSendRequest() { const [feeRate, setFeeRate] = useState(''); const [transaction, setTransaction] = useState(); const [summary, setSummary] = useState(); - const [runeSummary, setRuneSummary] = useState(); - const hasRunesSupport = useHasFeature(FeatureId.RUNES_SUPPORT); const [isLoading, setIsLoading] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); @@ -74,7 +64,6 @@ function BtcSendRequest() { useEffect(() => { if (!payload || !transactionContext || !feeRate) { setSummary(undefined); - setRuneSummary(undefined); return; } const generateTxnAndSummary = async () => { @@ -84,13 +73,6 @@ function BtcSendRequest() { const newSummary = await newTransaction.getSummary(); setTransaction(newTransaction); setSummary(newSummary); - if (newSummary && hasRunesSupport) { - setRuneSummary( - await parseSummaryForRunes(transactionContext, newSummary, transactionContext.network, { - separateTransfersOnNoExternalInputs: true, - }), - ); - } } catch (e) { setTransaction(undefined); setSummary(undefined); @@ -231,7 +213,6 @@ function BtcSendRequest() { return ( - input.extendedUtxo.address !== btcAddress && - input.extendedUtxo.address !== ordinalsAddress, - ), - }} + runeEtchDetails={etchRequest} feeRate={+feeRate} confirmText={t('CONFIRM')} cancelText={t('CANCEL')} diff --git a/src/app/screens/listRune/listMarketplaceItem.tsx b/src/app/screens/listRune/listMarketplaceItem.tsx index 261033a01..98250941a 100644 --- a/src/app/screens/listRune/listMarketplaceItem.tsx +++ b/src/app/screens/listRune/listMarketplaceItem.tsx @@ -1,4 +1,3 @@ -import { RowCenter } from '@components/confirmBtcTransaction/runes'; import TokenImage from '@components/tokenImage'; import type { FungibleToken, ListingProvider } from '@secretkeylabs/xverse-core'; import Checkbox from '@ui-library/checkbox'; @@ -24,6 +23,13 @@ const Container = styled.div<{ $selected: boolean }>` transition: background-color 0.1s ease, border-color 0.1s ease; `; +const RowCenter = styled.div({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', +}); + const InfoContainer = styled.div` display: flex; flex: 1; diff --git a/src/app/screens/mintRune/index.tsx b/src/app/screens/mintRune/index.tsx index 134ccb7c8..5d118d152 100644 --- a/src/app/screens/mintRune/index.tsx +++ b/src/app/screens/mintRune/index.tsx @@ -1,6 +1,5 @@ import ConfirmBtcTransaction from '@components/confirmBtcTransaction'; import RequestError from '@components/requests/requestError'; -import useSelectedAccount from '@hooks/useSelectedAccount'; import { RUNE_DISPLAY_DEFAULTS, type Transport } from '@secretkeylabs/xverse-core'; import Spinner from '@ui-library/spinner'; import { useCallback, useEffect } from 'react'; @@ -18,7 +17,6 @@ const LoaderContainer = styled.div(() => ({ function MintRune() { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const { btcAddress, ordinalsAddress } = useSelectedAccount(); const navigate = useNavigate(); const { mintRequest, @@ -70,29 +68,18 @@ function MintRune() { {orderTx && orderTx.summary && runeInfo && !mintError && ( - input.extendedUtxo.address !== btcAddress && - input.extendedUtxo.address !== ordinalsAddress, - ), - receipts: [], - transfers: [], - mint: { - runeName: runeInfo.entry.spaced_rune, - runeId: runeInfo.id, - amount: BigInt(runeInfo.entry.terms.amount?.toNumber() ?? 0), - divisibility: runeInfo.entry.divisibility.toNumber(), - symbol: runeInfo.entry.symbol, - inscriptionId: runeInfo.parent ?? '', - runeIsOpen: runeInfo.mintable, - runeIsMintable: runeInfo.mintable, - destinationAddress: mintRequest.destinationAddress, - repeats: mintRequest.repeats, - runeSize: RUNE_DISPLAY_DEFAULTS.size, - }, + runeMintDetails={{ + runeId: runeInfo.id, + runeName: runeInfo.entry.spaced_rune, + amount: BigInt(runeInfo.entry.terms.amount?.toNumber() ?? 0), + divisibility: runeInfo.entry.divisibility.toNumber(), + symbol: runeInfo.entry.symbol, + inscriptionId: runeInfo.parent ?? '', + runeIsOpen: runeInfo.mintable, + runeIsMintable: runeInfo.mintable, + destinationAddress: mintRequest.destinationAddress, + repeats: mintRequest.repeats, + runeSize: RUNE_DISPLAY_DEFAULTS.size, }} feeRate={+feeRate} confirmText={t('CONFIRM')} diff --git a/src/app/screens/rareSatsBundle/runeAmount.tsx b/src/app/screens/rareSatsBundle/runeAmount.tsx index 2fc483e48..77b119744 100644 --- a/src/app/screens/rareSatsBundle/runeAmount.tsx +++ b/src/app/screens/rareSatsBundle/runeAmount.tsx @@ -1,4 +1,3 @@ -import { mapRuneBaseToFungibleToken } from '@components/confirmBtcTransaction/utils'; import { RightAlignedStyledFiatAmountText } from '@components/fiatAmountText'; import TokenImage from '@components/tokenImage'; import useRuneFiatRateQuery from '@hooks/queries/runes/useRuneFiatRateQuery'; @@ -6,18 +5,19 @@ import useWalletSelector from '@hooks/useWalletSelector'; import type { RuneBase } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import { ftDecimals } from '@utils/helper'; +import { mapRuneBaseToFungibleToken } from '@utils/mappers'; import BigNumber from 'bignumber.js'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; -const Container = styled.div<{ topMargin?: boolean }>((props) => ({ +const Container = styled.div<{ $topMargin?: boolean }>((props) => ({ width: '100%', display: 'flex', flexDirection: 'row', alignItems: 'center', marginBottom: props.theme.space.s, - marginTop: props.topMargin ? props.theme.space.s : 0, + marginTop: props.$topMargin ? props.theme.space.s : 0, backgroundColor: props.theme.colors.elevation1, padding: props.theme.space.m, borderRadius: props.theme.space.xs, @@ -60,7 +60,7 @@ export default function RuneAmount({ const { fiatCurrency } = useWalletSelector(); const { data: runeFiatRate } = useRuneFiatRateQuery(rune); return ( - + (); const [summary, setSummary] = useState(); - const [runeSummary, setRuneSummary] = useState(); - const hasRunesSupport = useHasFeature(FeatureId.RUNES_SUPPORT); useEffect(() => { if (!feeRate && btcFeeRate && !feeRatesLoading) { @@ -76,7 +70,6 @@ function SendBtcScreen() { if (!debouncedRecipient || !feeRate) { setTransaction(undefined); setSummary(undefined); - setRuneSummary(undefined); return; } @@ -94,16 +87,6 @@ function SendBtcScreen() { if (sendMax) { setAmountSats(transactionDetails.summary.outputs[0].amount.toString()); } - if (hasRunesSupport) { - setRuneSummary( - await parseSummaryForRunes( - transactionContext, - transactionDetails.summary, - transactionContext.network, - { separateTransfersOnNoExternalInputs: true }, - ), - ); - } } else { setTransaction(undefined); setSummary(undefined); @@ -202,7 +185,6 @@ function SendBtcScreen() { return ( void; recipientAddress: string; @@ -51,7 +49,6 @@ type Props = { function StepDisplay({ summary, - runeSummary, currentStep, setCurrentStep, recipientAddress, @@ -121,7 +118,6 @@ function StepDisplay({ return ( { const [feeRate, setFeeRate] = useState(''); const [transaction, setTransaction] = useState(); const [summary, setSummary] = useState(); - const [runeSummary, setRuneSummary] = useState(); const [isExecuting, setIsExecuting] = useState(false); const [isLoading, setIsLoading] = useState(false); - const hasRunesSupport = useHasFeature(FeatureId.RUNES_SUPPORT); const { popupPayloadSendInscriptions: { context: { tabId }, @@ -73,13 +67,6 @@ const useSendInscriptions = () => { setFeeRate(desiredFeeRate.toString()); setTransaction(tx); setSummary(txSummary); - if (hasRunesSupport) { - setRuneSummary( - await parseSummaryForRunes(txContext, txSummary, txContext.network, { - separateTransfersOnNoExternalInputs: true, - }), - ); - } } catch (e) { setTransaction(undefined); setSummary(undefined); @@ -148,7 +135,6 @@ const useSendInscriptions = () => { return { transaction, summary, - runeSummary, txError, feeRate, isLoading, diff --git a/src/app/screens/sendOrdinal/index.tsx b/src/app/screens/sendOrdinal/index.tsx index 4725ceb23..57cce0010 100644 --- a/src/app/screens/sendOrdinal/index.tsx +++ b/src/app/screens/sendOrdinal/index.tsx @@ -1,18 +1,10 @@ import useAddressInscription from '@hooks/queries/ordinals/useAddressInscription'; import useBtcFeeRate from '@hooks/useBtcFeeRate'; -import useHasFeature from '@hooks/useHasFeature'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useTransactionContext from '@hooks/useTransactionContext'; import type { TransactionSummary } from '@screens/sendBtc/helpers'; -import { - AnalyticsEvents, - btcTransaction, - FeatureId, - parseSummaryForRunes, - type RuneSummary, - type Transport, -} from '@secretkeylabs/xverse-core'; +import { AnalyticsEvents, btcTransaction, type Transport } from '@secretkeylabs/xverse-core'; import { isInOptions, isLedgerAccount } from '@utils/helper'; import { trackMixPanel } from '@utils/mixpanel'; import RoutePaths from 'app/routes/paths'; @@ -48,9 +40,7 @@ function SendOrdinalScreen() { const [isSubmitting, setIsSubmitting] = useState(false); const [transaction, setTransaction] = useState(); const [summary, setSummary] = useState(); - const [runeSummary, setRuneSummary] = useState(); const [insufficientFundsError, setInsufficientFundsError] = useState(false); - const hasRunesSupport = useHasFeature(FeatureId.RUNES_SUPPORT); useResetUserFlow(RoutePaths.SendOrdinal); @@ -67,7 +57,6 @@ function SendOrdinalScreen() { if (!recipientAddress || !feeRate) { setTransaction(undefined); setSummary(undefined); - setRuneSummary(undefined); setInsufficientFundsError(false); return; } @@ -92,16 +81,6 @@ function SendOrdinalScreen() { if (!transactionDetails) return; setTransaction(transactionDetails); setSummary(await transactionDetails.getSummary()); - if (hasRunesSupport) { - setRuneSummary( - await parseSummaryForRunes( - context, - await transactionDetails.getSummary(), - context.network, - { separateTransfersOnNoExternalInputs: true }, - ), - ); - } } catch (e) { if (e instanceof Error) { // don't log the error if it's just an insufficient funds error @@ -208,7 +187,6 @@ function SendOrdinalScreen() { return ( void; @@ -56,7 +55,6 @@ type Props = { function StepDisplay({ summary, - runeSummary, ordinal, currentStep, setCurrentStep, @@ -130,7 +128,6 @@ function StepDisplay({ return ( ( @@ -51,7 +48,6 @@ function SendRuneScreen() { const transactionContext = useTransactionContext(); const [transaction, setTransaction] = useState(); const [summary, setSummary] = useState(); - const [runeSummary, setRuneSummary] = useState(); const [searchParams] = useSearchParams(); const hasRunesSupport = useHasFeature(FeatureId.RUNES_SUPPORT); @@ -98,7 +94,6 @@ function SendRuneScreen() { if (!recipientAddress || !feeRate || bigAmount.isNaN() || bigAmount.isLessThanOrEqualTo(0)) { setTransaction(undefined); setSummary(undefined); - setRuneSummary(undefined); return; } @@ -119,14 +114,6 @@ function SendRuneScreen() { setTransaction(transactionDetails.transaction); if (transactionDetails.summary) { setSummary(transactionDetails.summary); - setRuneSummary( - await parseSummaryForRunes( - transactionContext, - transactionDetails.summary, - network.type, - { separateTransfersOnNoExternalInputs: true }, - ), - ); } } catch (e) { if (!(e instanceof Error) || !e.message.includes('Insufficient funds')) { @@ -232,7 +219,6 @@ function SendRuneScreen() { return ( void; useTokenValue: boolean; @@ -56,7 +55,6 @@ type Props = { function StepDisplay({ token, summary, - runeSummary, amountToSend, setAmountToSend, useTokenValue, @@ -135,7 +133,6 @@ function StepDisplay({ return ( { const tx = await runesTransaction.recoverRunes(context, desiredFeeRate); const txSummary = await tx.getSummary(); - const txRuneSummary = await parseSummaryForRunes(context, txSummary, context.network, { - separateTransfersOnNoExternalInputs: true, - }); - + const txRuneSummary = await parseSummaryForRunes(context, txSummary, context.network); return { transaction: tx, summary: txSummary, runeSummary: txRuneSummary }; }; @@ -184,7 +181,6 @@ function RecoverRunes() { ) : ( (undefined); - const hasRunesSupport = useHasFeature(FeatureId.RUNES_SUPPORT); - useTrackMixPanelPageViewed(); const [parsedPsbts, setParsedPsbts] = useState([]); - const xverseApi = useXverseApi(); - const individualParsedTxSummaryContext = useMemo( + const individualTxSummaryContext = useMemo( () => ({ - summary: parsedPsbts[currentPsbtIndex]?.summary, - runeSummary: parsedPsbts[currentPsbtIndex]?.runeSummary, + extractedTxSummary: parsedPsbts[currentPsbtIndex]?.extractedSummary, }), [parsedPsbts, currentPsbtIndex], ); - const aggregatedParsedTxSummaryContext: ParsedTxSummaryContextProps = useMemo( - () => ({ - summary: { - inputs: parsedPsbts.map((psbt) => psbt.summary.inputs).flat(), - outputs: parsedPsbts.map((psbt) => psbt.summary.outputs).flat(), - feeOutput: undefined, - isFinal: parsedPsbts.reduce((acc, psbt) => acc && psbt.summary.isFinal, true), - hasSigHashNone: parsedPsbts.reduce( - (acc, psbt) => acc || (psbt.summary as btcTransaction.PsbtSummary)?.hasSigHashNone, - false, - ), - hasSigHashSingle: parsedPsbts.reduce( - (acc, psbt) => acc || (psbt.summary as btcTransaction.PsbtSummary)?.hasSigHashSingle, - false, - ), - } as PsbtSummary, - runeSummary: { - burns: parsedPsbts.map((psbt) => psbt.runeSummary?.burns ?? []).flat(), - transfers: parsedPsbts.map((psbt) => psbt.runeSummary?.transfers ?? []).flat(), - receipts: parsedPsbts.map((psbt) => psbt.runeSummary?.receipts ?? []).flat(), - mint: undefined, - inputsHadRunes: false, - } as RuneSummary, - }), - [parsedPsbts], - ); + useEffect(() => { + if (payload.network.type !== network.type) { + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + error: t('NETWORK_MISMATCH'), + browserTx: true, + }, + }); + } + + const checkAddressMismatch = (input) => { + if ( + input.address !== selectedAccount.btcAddress && + input.address !== selectedAccount.ordinalsAddress + ) { + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + error: t('ADDRESS_MISMATCH'), + browserTx: true, + }, + }); + } + }; - const handlePsbtParsing = useCallback( - async (psbt: SignMultiplePsbtPayload, index: number): Promise => { + payload.psbts?.forEach((psbt) => psbt.inputsToSign?.forEach(checkAddressMismatch)); + }, []); + + useEffect(() => { + const handlePsbtParsing = async (psbt: SignMultiplePsbtPayload): Promise => { try { const parsedPsbt = new btcTransaction.EnhancedPsbt(txnContext, psbt.psbtBase64); const summary = await parsedPsbt.getSummary(); - const runeSummary = hasRunesSupport - ? await parseSummaryForRunes(txnContext, summary, network.type, { - separateTransfersOnNoExternalInputs: true, - }) - : undefined; - return { summary, runeSummary }; + const extractedSummary = await extractViewSummary(txnContext, summary, network.type); + return { extractedSummary, summary }; } catch (err) { + throw new Error('PSBT parsing failed'); + } + }; + + (async () => { + const parsedPsbtsRes = await Promise.allSettled(payload.psbts.map(handlePsbtParsing)); + setIsLoading(false); + + const index = parsedPsbtsRes.findIndex((item) => item.status === 'rejected'); + if (index !== -1) { navigate('/tx-status', { state: { txid: '', @@ -158,61 +155,15 @@ function SignBatchPsbtRequest() { browserTx: true, }, }); - return undefined; - } - }, - [txnContext], - ); - - useEffect(() => { - (async () => { - const parsedPsbtsRes = await Promise.all(payload.psbts.map(handlePsbtParsing)); - if (parsedPsbtsRes.some((item) => item === undefined)) { - setIsLoading(false); return; } - const validParsedPsbts = parsedPsbtsRes.filter( - (item): item is ParsedPsbt => item !== undefined, + + const validParsedPsbts = parsedPsbtsRes.map( + (item) => (item.status === 'fulfilled' && item.value) as ParsedPsbt, ); setParsedPsbts(validParsedPsbts); - setIsLoading(false); })(); - }, [payload.psbts.length, handlePsbtParsing]); - - const checkAddressMismatch = (input) => { - if ( - input.address !== selectedAccount.btcAddress && - input.address !== selectedAccount.ordinalsAddress - ) { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - error: t('ADDRESS_MISMATCH'), - browserTx: true, - }, - }); - } - }; - - const checkIfMismatch = () => { - if (payload.network.type !== network.type) { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - error: t('NETWORK_MISMATCH'), - browserTx: true, - }, - }); - } - - payload.psbts?.forEach((psbt) => psbt.inputsToSign?.forEach(checkAddressMismatch)); - }; - - useEffect(() => { - checkIfMismatch(); - }, []); + }, [payload.psbts, txnContext, network, navigate, t]); const onSignPsbtConfirmed = async () => { try { @@ -360,28 +311,7 @@ function SignBatchPsbtRequest() { window.close(); }; - const hasOutputScript = useMemo( - () => parsedPsbts.some((psbt) => psbt.summary.outputs.some((output) => isScriptOutput(output))), - [parsedPsbts.length], - ); - const signingStatus: ConfirmationStatus = isSigningComplete ? 'SUCCESS' : 'LOADING'; - const runeBurns = parsedPsbts.map((psbt) => psbt.runeSummary?.burns ?? []).flat(); - const distinctRuneDelegations = new Set(); - const runeDelegations = parsedPsbts - .filter((psbt) => !psbt.summary.isFinal) - .map((psbt) => psbt.runeSummary?.receipts ?? []) - .flat() - .filter((delegation) => { - const key = objecthash(delegation); - if (distinctRuneDelegations.has(key)) { - return false; - } - - distinctRuneDelegations.add(key); - return true; - }); - const hasSomeRuneDelegation = runeDelegations.length > 0; if (isSigning || isSigningComplete) { return ( @@ -402,67 +332,56 @@ function SignBatchPsbtRequest() { ); } - return ( - <> - - {isLoading ? ( + const renderBody = () => { + if (isLoading) { + return ( - ) : ( - <> - - {isLedgerAccount(selectedAccount) ? ( - - - + ); + } + + const isLedger = isLedgerAccount(selectedAccount); + + return ( + <> + + + {isLedger ? ( + ) : ( - - - - {t('SIGN_TRANSACTIONS', { count: parsedPsbts.length })} - - setReviewTransaction(true)}> - {t('REVIEW_ALL')} - - - {inscriptionToShow && ( - setInscriptionToShow(undefined)} - inscription={{ - content_type: inscriptionToShow.contentType, - id: inscriptionToShow.id, - inscription_number: inscriptionToShow.number, - }} - /> - )} - {hasSomeRuneDelegation && } - - - {!hasSomeRuneDelegation && } - psbt.runeSummary?.mint)} /> - - {hasOutputScript && - !parsedPsbts.some((psbt) => psbt.runeSummary !== undefined) && ( - - )} - - + <> + + {t('SIGN_TRANSACTIONS', { count: parsedPsbts.length })} + + setReviewTransaction(true)}> + {t('REVIEW_ALL')} + + + + )} - - - - - - {utils - .getClientPermissions(store.permissions, client.id) - .sort((p1, p2) => p1.resourceId.localeCompare(p2.resourceId)) - .map((p) => ( -
- {(() => { - const resource = utils.getResource(store.resources, p.resourceId); - - if (!resource) { - return null; - } - - return ( - - - {resource.name} - - {[...p.actions].map((a) => ( -
{a}
- ))} -
-
-
- ); - })()} -
- ))} - - ))} + {selectedApp ? ( + <> + {selectedApp.name} + + + {t('URL')} + {selectedApp.name} + + + {t('LAST_USED')} + + {selectedApp.lastUsed ? formatDate(new Date(selectedApp.lastUsed)) : '-'} + + + + {t('PERMISSIONS')} + + + + + ) : ( + <> + {t('TITLE')} + {store?.clients.size === 0 ? t('EMPTY_MESSAGE') : t('SUBTITLE')} + {clientsSortedByLastUsed?.map((client) => ( + + ))} + + )} + {(store?.clients?.size ?? 0) > 0 || selectedApp ? ( + + - - {selectedProtocol === 'runes' && !showRunes ? ( - - - - ) : ( - getCoinsList() - )} - + {getCoinsList()}
diff --git a/src/app/screens/sendStx/index.tsx b/src/app/screens/sendStx/index.tsx index b0be2cb2e..0d689ab52 100644 --- a/src/app/screens/sendStx/index.tsx +++ b/src/app/screens/sendStx/index.tsx @@ -60,7 +60,7 @@ function SendStxScreen() { // Will be used in the future when the summary screen is refactored const [, setRecipientDomain] = useState(''); const [memo, setMemo] = useState(stxMemo ?? ''); - const { visible: sip10CoinsList } = useVisibleSip10FungibleTokens(); + const { data: sip10CoinsList } = useVisibleSip10FungibleTokens(); const [searchParams] = useSearchParams(); const principal = searchParams.get('principal'); const fungibleToken = sip10CoinsList?.find((coin) => coin.principal === principal); diff --git a/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts b/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts index e886fc431..7091c0290 100644 --- a/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts +++ b/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts @@ -1,11 +1,11 @@ import useSupportedCoinRates from '@hooks/queries/useSupportedCoinRates'; -import useMasterCoinsList from '@screens/swap/useMasterCoinsList'; +import useVisibleMasterCoinsList from '@screens/swap/useVisibleMasterCoinsList'; import { mapSwapProtocolToFTProtocol } from '@screens/swap/utils'; import { type Token } from '@secretkeylabs/xverse-core'; import { sortFtByFiatBalance } from '@utils/tokens'; const useFromTokens = (toToken?: Token) => { - const tokens = useMasterCoinsList(); + const tokens = useVisibleMasterCoinsList(); const { stxBtcRate, btcFiatRate } = useSupportedCoinRates(); // Sort tokens, keeping BTC as the first element, and STX (if enabled) as the second diff --git a/src/app/screens/swap/components/tokenToBottomSheet/useToTokens.ts b/src/app/screens/swap/components/tokenToBottomSheet/useToTokens.ts index c3085aea6..122796ae3 100644 --- a/src/app/screens/swap/components/tokenToBottomSheet/useToTokens.ts +++ b/src/app/screens/swap/components/tokenToBottomSheet/useToTokens.ts @@ -1,13 +1,18 @@ import useWalletSelector from '@hooks/useWalletSelector'; -import useVisibleMasterCoinsList from '@screens/swap/useVisibleMasterCoinsList'; +import useMasterCoinsList from '@screens/swap/useMasterCoinsList'; import { mapFTProtocolToSwapProtocol } from '@screens/swap/utils'; -import { getXverseApiClient, type Protocol, type TokenBasic } from '@secretkeylabs/xverse-core'; +import { + getXverseApiClient, + type Protocol, + type Token, + type TokenBasic, +} from '@secretkeylabs/xverse-core'; import { useQuery } from '@tanstack/react-query'; import { handleRetries } from '@utils/query'; const useToTokens = (protocol: Protocol, from?: TokenBasic, query?: string) => { - const coinsMasterList = useVisibleMasterCoinsList(); - const { network, spamTokens } = useWalletSelector(); + const coinsMasterList = useMasterCoinsList(); + const { network, spamTokens, runesManageTokens, sip10ManageTokens } = useWalletSelector(); const spamTokenSet = new Set(spamTokens); const userTokens = @@ -41,12 +46,20 @@ const useToTokens = (protocol: Protocol, from?: TokenBasic, query?: string) => { return filteredResponse; }; + const select = (data: Token[]) => + data.filter( + (token) => + !spamTokenSet.has(token.ticker) || + runesManageTokens[token.ticker] === true || // allow user to enable spam tokens + sip10ManageTokens[token.ticker] === true, + ); + return useQuery({ enabled: userTokens.length > 0, retry: handleRetries, queryKey: ['swap-from-tokens', network.type, userTokens, protocol, from, search], queryFn, - select: (data) => data.filter((rune) => !spamTokenSet.has(rune.ticker)), + select, }); }; diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index 120a01708..b045d98c3 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -46,7 +46,7 @@ import trackSwapMixPanel from './mixpanel'; import QuoteSummary from './quoteSummary'; import QuotesModal from './quotesModal'; import type { OrderInfo, Side, StxOrderInfo } from './types'; -import useMasterCoinsList from './useMasterCoinsList'; +import useVisibleMasterCoinsList from './useVisibleMasterCoinsList'; import { getTrackingIdentifier, isStxTx, @@ -138,7 +138,7 @@ export default function SwapScreen() { const defaultFrom = params.get('from'); const { quotes, loading: quotesLoading, error: quotesError, fetchQuotes } = useGetQuotes(); const { data: runeFloorPrice } = useRuneFloorPriceQuery(toToken?.name ?? ''); - const coinsMasterList = useMasterCoinsList(); + const coinsMasterList = useVisibleMasterCoinsList(); const { tokenInfo: sip10FromTokenInfoUSD } = useGetSip10TokenInfo({ principal: toToken?.ticker, fiatCurrency: 'USD', @@ -149,7 +149,7 @@ export default function SwapScreen() { const token = coinsMasterList.find((coin) => coin.principal === defaultFrom); setFromToken(token); } - }, [defaultFrom, coinsMasterList.length]); + }, [defaultFrom, coinsMasterList]); const handleGoBack = () => { navigate('/'); diff --git a/src/app/screens/swap/useMasterCoinsList.tsx b/src/app/screens/swap/useMasterCoinsList.tsx index 2beb12cb8..c0687ff79 100644 --- a/src/app/screens/swap/useMasterCoinsList.tsx +++ b/src/app/screens/swap/useMasterCoinsList.tsx @@ -31,7 +31,7 @@ export const stxFt: FungibleToken = { const useMasterCoinsList = () => { const { data: sip10FtList } = useGetSip10FungibleTokens(); - const { unfilteredData: runesFtList } = useRuneFungibleTokensQuery(); + const { data: runesFtList } = useRuneFungibleTokensQuery(); const { hideStx } = useWalletSelector(); const isStacksSwapsEnabled = useHasFeature(FeatureId.STACKS_SWAPS); diff --git a/src/app/screens/swap/useVisibleMasterCoinsList.tsx b/src/app/screens/swap/useVisibleMasterCoinsList.tsx index d8d0a0267..10f86af07 100644 --- a/src/app/screens/swap/useVisibleMasterCoinsList.tsx +++ b/src/app/screens/swap/useVisibleMasterCoinsList.tsx @@ -7,20 +7,18 @@ import { useMemo } from 'react'; import { btcFt, stxFt } from './useMasterCoinsList'; const useVisibleMasterCoinsList = () => { - const { visible: sip10FtList } = useVisibleSip10FungibleTokens(); - const { visible: runesFtList } = useVisibleRuneFungibleTokens(); - const isStacksSwapsEnabled = useHasFeature(FeatureId.STACKS_SWAPS); - + const { data: sip10FtList } = useVisibleSip10FungibleTokens(); + const { data: runesFtList } = useVisibleRuneFungibleTokens(); const { hideStx } = useWalletSelector(); + const isStacksSwapsEnabled = useHasFeature(FeatureId.STACKS_SWAPS); const coinsMasterList = useMemo( () => [ - ...sip10FtList, - ...runesFtList, + ...(runesFtList || []), btcFt, - ...(!hideStx && isStacksSwapsEnabled ? [stxFt] : []), + ...(!hideStx && isStacksSwapsEnabled ? [stxFt, ...(sip10FtList ?? [])] : []), ], - [sip10FtList, runesFtList, hideStx, isStacksSwapsEnabled], + [runesFtList, hideStx, isStacksSwapsEnabled, sip10FtList], ); return coinsMasterList; diff --git a/src/app/screens/unlistRune/index.tsx b/src/app/screens/unlistRune/index.tsx index 221777024..861c6916a 100644 --- a/src/app/screens/unlistRune/index.tsx +++ b/src/app/screens/unlistRune/index.tsx @@ -27,7 +27,7 @@ export default function UnlistRuneScreen() { const { t } = useTranslation('translation', { keyPrefix: 'LIST_RUNE_SCREEN' }); const navigate = useNavigate(); const { runeId } = useParams(); - const { visible: runesCoinsList } = useVisibleRuneFungibleTokens(); + const { data: runesCoinsList } = useVisibleRuneFungibleTokens(); const selectedRune = runesCoinsList.find((ft) => ft.principal === runeId); const showRunesListing = useHasFeature(FeatureId.RUNES_LISTING) || process.env.NODE_ENV === 'development'; diff --git a/src/app/stores/index.ts b/src/app/stores/index.ts index 61607e500..5eb2b25cc 100644 --- a/src/app/stores/index.ts +++ b/src/app/stores/index.ts @@ -29,8 +29,8 @@ const migrations = { }, 3: ( state: WalletState & { - brcCoinsList: FungibleToken[] | null; // removed in v3 - coinsList: FungibleToken[] | null; // removed in v3 + brcCoinsList: (FungibleToken & { visible?: boolean })[] | null; // removed in v3 + coinsList: (FungibleToken & { visible?: boolean })[] | null; // removed in v3 coins: Coin[]; // removed in v3 }, ) => ({ diff --git a/src/app/utils/helper.ts b/src/app/utils/helper.ts index 2a7671195..0550208d1 100644 --- a/src/app/utils/helper.ts +++ b/src/app/utils/helper.ts @@ -4,7 +4,7 @@ import { microstacksToStx, satsToBtc, type Account, - type FungibleToken, + type FungibleTokenWithStates, type NetworkType, type NftData, type SettingsNetwork, @@ -252,9 +252,9 @@ export const calculateTotalBalance = ({ }: { stxBalance?: string; btcBalance?: string; - sipCoinsList: FungibleToken[]; - brcCoinsList: FungibleToken[]; - runesCoinList: FungibleToken[]; + sipCoinsList: FungibleTokenWithStates[]; + brcCoinsList: FungibleTokenWithStates[]; + runesCoinList: FungibleTokenWithStates[]; stxBtcRate: string; btcFiatRate: string; hideStx: boolean; @@ -277,7 +277,7 @@ export const calculateTotalBalance = ({ if (sipCoinsList) { totalBalance = sipCoinsList.reduce((acc, coin) => { - if (coin.visible && coin.tokenFiatRate && coin.decimals) { + if (coin.isEnabled && coin.tokenFiatRate && coin.decimals) { const tokenUnits = new BigNumber(10).exponentiatedBy(new BigNumber(coin.decimals)); const coinFiatValue = new BigNumber(coin.balance) .dividedBy(tokenUnits) @@ -291,7 +291,7 @@ export const calculateTotalBalance = ({ if (brcCoinsList) { totalBalance = brcCoinsList.reduce((acc, coin) => { - if (coin.visible && coin.tokenFiatRate) { + if (coin.isEnabled && coin.tokenFiatRate) { const coinFiatValue = new BigNumber(coin.balance).multipliedBy( new BigNumber(coin.tokenFiatRate), ); @@ -304,7 +304,7 @@ export const calculateTotalBalance = ({ if (runesCoinList) { totalBalance = runesCoinList.reduce((acc, coin) => { - if (coin.visible && coin.tokenFiatRate) { + if (coin.isEnabled && coin.tokenFiatRate) { const coinFiatValue = new BigNumber(getFtBalance(coin)).multipliedBy( new BigNumber(coin.tokenFiatRate), ); diff --git a/tests/pages/wallet.ts b/tests/pages/wallet.ts index 2fb0bfa9f..2929860c2 100644 --- a/tests/pages/wallet.ts +++ b/tests/pages/wallet.ts @@ -630,18 +630,18 @@ export default class Wallet { await expect(this.labelAccountName).toBeVisible(); await expect(this.buttonMenu).toBeVisible(); - await expect(await this.labelTokenSubtitle.count()).toBeGreaterThanOrEqual(2); + expect(await this.labelTokenSubtitle.count()).toBeGreaterThanOrEqual(2); await expect(this.navigationDashboard).toBeVisible(); await expect(this.navigationNFT).toBeVisible(); await expect(this.navigationStacking).toBeVisible(); await expect(this.navigationExplore).toBeVisible(); await expect(this.navigationSettings).toBeVisible(); - await expect(await this.divTokenRow.count()).toBeGreaterThan(1); + expect(await this.divTokenRow.count()).toBeGreaterThan(1); } async checkVisualsSendSTXPage3() { - await expect(this.page.url()).toContain('confirm-stx-tx'); + expect(this.page.url()).toContain('confirm-stx-tx'); await expect(this.buttonConfirm).toBeVisible(); await expect(this.buttonCancel).toBeVisible(); await expect(this.receiveAddress).toBeVisible(); @@ -655,10 +655,10 @@ export default class Wallet { * Checks the visibility and state of UI elements state on first page in Send Flow * * @param {string} url - The expected URL to validate the correct page navigation. - * @param {boolean} isSTX - Optional flag to apply STX-specific element checks (default: false). + * @param {boolean} moreInputFields - (default: false). */ async checkVisualsSendPage1(url: string, moreInputFields: boolean = false) { - await expect(this.page.url()).toContain(url); + expect(this.page.url()).toContain(url); await expect(this.buttonNext).toBeVisible(); await expect(this.buttonNext).toBeDisabled(); @@ -682,7 +682,7 @@ export default class Wallet { * @param {boolean} isSTX - Indicates if the page is STX-specific; adjusts element checks accordingly (default: false). */ async checkVisualsSendPage2(url: string, isSTX: boolean = false) { - await expect(this.page.url()).toContain(url); + expect(this.page.url()).toContain(url); await expect(this.buttonNext).toBeVisible(); await expect(this.buttonNext).toBeDisabled(); @@ -722,7 +722,7 @@ export default class Wallet { tokenImageShown: boolean = true, ordinalNumber?: string, ) { - await expect(this.page.url()).toContain(url); + expect(this.page.url()).toContain(url); await expect(this.buttonExpand).toBeVisible(); await expect(this.buttonCancel).toBeEnabled(); await expect(this.buttonConfirm).toBeEnabled(); @@ -749,29 +749,27 @@ export default class Wallet { // Execute these checks only if sendAddress is provided if (sendAddress) { await expect(this.sendAddress.first()).toBeVisible(); - await expect(await this.sendAddress.first().innerText()).toContain(sendAddress.slice(-4)); + expect(await this.sendAddress.first().innerText()).toContain(sendAddress.slice(-4)); } // Execute these checks only if recipientAddress is provided if (recipientAddress) { await expect(this.receiveAddress.first()).toBeVisible(); - await expect(await this.receiveAddress.first().innerText()).toContain( - recipientAddress.slice(-4), - ); + expect(await this.receiveAddress.first().innerText()).toContain(recipientAddress.slice(-4)); } // Collection Inscriptions don't have the ordinal number displayed in the Review // Check if the right ordinal number is shown if (ordinalNumber) { const reviewNumberOrdinal = await this.numberInscription.first().innerText(); - await expect(ordinalNumber).toMatch(reviewNumberOrdinal); + expect(ordinalNumber).toMatch(reviewNumberOrdinal); } } // Check Visuals of Rune Dashboard (without List button), return balance amount - async checkVisualsRunesDashboard(runeName) { + async checkVisualsRunesDashboard(runeName: string) { await expect(this.imageToken.first()).toBeVisible(); await expect(this.textCoinTitle).toBeVisible(); - await expect(await this.textCoinTitle).toContainText(runeName); + await expect(this.textCoinTitle).toContainText(runeName); await expect(this.coinBalance).toBeVisible(); await expect(this.buttonReceive).toBeVisible(); await expect(this.buttonSend).toBeVisible(); @@ -786,11 +784,11 @@ export default class Wallet { await expect(this.buttonSetPrice).toBeVisible(); await expect(this.buttonSetPrice).toBeDisabled(); await expect(this.runeItem.first()).toBeVisible(); - await expect(await this.runeItem.count()).toBeGreaterThanOrEqual(1); + expect(await this.runeItem.count()).toBeGreaterThanOrEqual(1); } async checkVisualsSwapPage() { - await expect(this.page.url()).toContain('swap'); + expect(this.page.url()).toContain('swap'); await expect(this.buttonDownArrow.first()).toBeVisible(); await expect(this.buttonGetQuotes.first()).toBeVisible(); await expect(this.buttonGetQuotes.first()).toBeDisabled(); @@ -798,8 +796,8 @@ export default class Wallet { await expect(this.swapTokenBalance).toContainText('--'); await expect(this.buttonBack).toBeVisible(); await expect(this.nameToken.first()).toContainText('Select asset'); - await expect(await this.nameToken).toHaveCount(2); - await expect(await this.buttonDownArrow).toHaveCount(2); + await expect(this.nameToken).toHaveCount(2); + await expect(this.buttonDownArrow).toHaveCount(2); await expect(this.buttonGetQuotes).toBeVisible(); await expect(this.textUSD).toBeVisible(); await expect(this.buttonSwapToken).toBeVisible(); @@ -813,7 +811,7 @@ export default class Wallet { const usdAmount = await this.textUSD.innerText(); const numericUSDValue = parseFloat(usdAmount.replace(/[^0-9.]/g, '')); - await expect(numericUSDValue).toBeGreaterThan(0); + expect(numericUSDValue).toBeGreaterThan(0); return numericUSDValue; } @@ -823,8 +821,8 @@ export default class Wallet { async checkVisualsQuotePage( tokenName: string, slippage: boolean, - numericQuoteValue, - numericUSDValue, + numericQuoteValue: number, + numericUSDValue: number, ) { await expect(this.buttonSwap).toBeVisible(); await expect(this.buttonEditFee).toBeVisible(); @@ -832,7 +830,7 @@ export default class Wallet { await expect(this.buttonSlippage).toBeVisible(); } // Only 2 token should be visible - await expect(await this.buttonSwapPlace.count()).toBe(2); + expect(await this.buttonSwapPlace.count()).toBe(2); // await expect(await this.imageToken.count()).toBe(2); // Check Rune token name @@ -841,18 +839,18 @@ export default class Wallet { // Check if USD amount from quote page is the same as from th swap start flow page const usdAmountQuote = await this.textUSD.first().innerText(); const numericUSDQuote = parseFloat(usdAmountQuote.replace(/[^0-9.]/g, '')); - await expect(numericUSDQuote).toEqual(numericUSDValue); + expect(numericUSDQuote).toEqual(numericUSDValue); // min-received-amount value should be the same as quoteAmount const minReceivedAmount = await this.minReceivedAmount.innerText(); const numericMinReceivedAmount = parseFloat(minReceivedAmount.replace(/[^0-9.]/g, '')); const formattedNumericMinReceivedAmount = parseFloat(numericMinReceivedAmount.toFixed(3)); - await expect(formattedNumericMinReceivedAmount).toEqual(numericQuoteValue); + expect(formattedNumericMinReceivedAmount).toEqual(numericQuoteValue); // check if quoteAmount is the same from the page before const quoteAmount2Page = await this.quoteAmount.last().innerText(); const numericQuote2Page = parseFloat(quoteAmount2Page.replace(/[^0-9.]/g, '')); - await expect(numericQuote2Page).toEqual(numericQuoteValue); + expect(numericQuote2Page).toEqual(numericQuoteValue); } async checkVisualsListOnMEPage() { @@ -879,7 +877,7 @@ export default class Wallet { // Save the current fee amount for comparison const originalFee = await this.feeAmount.innerText(); const numericOriginalFee = parseFloat(originalFee.replace(/[^0-9.]/g, '')); - await expect(numericOriginalFee).toBeGreaterThan(0); + expect(numericOriginalFee).toBeGreaterThan(0); let feePriority = 'Medium'; if (feePriorityShown) { feePriority = await this.labelFeePriority.innerText(); @@ -896,7 +894,7 @@ export default class Wallet { .locator(this.labelTotalFee) .innerText(); const numericFee = parseFloat(fee.replace(/[^0-9.]/g, '')); - await expect(numericFee).toBe(numericOriginalFee); + expect(numericFee).toBe(numericOriginalFee); // Save high fee rate for comparison const highFee = await this.labelTotalFee.first().innerText(); @@ -907,12 +905,12 @@ export default class Wallet { const newFee = await this.feeAmount.innerText(); const numericNewFee = parseFloat(newFee.replace(/[^0-9.]/g, '')); - await expect(numericNewFee).toBe(numericHighFee); + expect(numericNewFee).toBe(numericHighFee); } async navigateToCollectibles() { await this.navigationNFT.click(); - await expect(this.page.url()).toContain('nft-dashboard'); + expect(this.page.url()).toContain('nft-dashboard'); // If 'enable' rare sats pop up is appearing if (await this.buttonEnable.isVisible()) { await this.buttonEnable.click(); @@ -923,32 +921,30 @@ export default class Wallet { } // had to disable this rule as my first assertion was always changed to a wrong assertion - /* eslint-disable playwright/prefer-web-first-assertions */ async checkAmountsSendingSTX(amountSTXSend, STXTest, sendFee) { - await expect(await this.receiveAddress.first().innerText()).toContain(STXTest.slice(-4)); + expect(await this.receiveAddress.first().innerText()).toContain(STXTest.slice(-4)); // Sending amount without Fee const sendAmount = await this.confirmAmount.first().innerText(); const numericValueSendAmount = parseFloat(sendAmount.replace(/[^0-9.]/g, '')); - await expect(numericValueSendAmount).toEqual(amountSTXSend); + expect(numericValueSendAmount).toEqual(amountSTXSend); // Fees const fee = await this.feeAmount.innerText(); const numericValueFee = parseFloat(fee.replace(/[^0-9.]/g, '')); - await expect(numericValueFee).toEqual(sendFee); + expect(numericValueFee).toEqual(sendFee); } - /* eslint-disable playwright/prefer-web-first-assertions */ async checkAmountsSendingBTC(selfBTCTest, BTCTest, amountBTCSend) { // Sending amount without Fee const amountText = await this.confirmAmount.first().innerText(); const numericValueAmountText = parseFloat(amountText.replace(/[^0-9.]/g, '')); - await expect(numericValueAmountText).toEqual(amountBTCSend); + expect(numericValueAmountText).toEqual(amountBTCSend); // Address check sending and receiving - await expect(await this.sendAddress.innerText()).toContain(selfBTCTest.slice(-4)); - await expect(await this.receiveAddress.first().innerText()).toContain(BTCTest.slice(-4)); + expect(await this.sendAddress.innerText()).toContain(selfBTCTest.slice(-4)); + expect(await this.receiveAddress.first().innerText()).toContain(BTCTest.slice(-4)); const confirmAmountAfter = await this.confirmAmount.last().innerText(); const originalFee = await this.feeAmount.innerText(); @@ -963,7 +959,7 @@ export default class Wallet { // Balance - fees - sending amount const roundedResult = Number((num3 - num2 - amountBTCSend).toFixed(9)); // Check if Balance value after the transaction is the same as the calculated value - await expect(num1).toEqual(roundedResult); + expect(num1).toEqual(roundedResult); } async confirmSendTransaction(transactionIDShown: boolean = true) { @@ -976,7 +972,7 @@ export default class Wallet { await this.buttonClose.click(); } - async getAddress(whichAddress): Promise { + async getAddress(whichAddress: string): Promise { // click on 'Receive' button await this.allUpperButtons.nth(1).click(); @@ -994,7 +990,7 @@ export default class Wallet { return address; } - async getTokenBalance(tokenname) { + async getTokenBalance(tokenname: string) { const locator = this.page .getByRole('button') .filter({ has: this.labelTokenSubtitle.getByText(tokenname, { exact: true }) }) @@ -1005,14 +1001,14 @@ export default class Wallet { return numericValue; } - async clickOnSpecificToken(tokenname) { + async clickOnSpecificToken(tokenname: string) { const specificToken = this.page .getByRole('button') .filter({ has: this.labelTokenSubtitle.getByText(tokenname, { exact: true }) }); await specificToken.last().click(); } - async clickOnSpecificInscription(inscriptionName) { + async clickOnSpecificInscription(inscriptionName: string) { const specificToken = this.containersCollectibleItem .filter({ has: this.nameInscription.getByText(inscriptionName, { exact: true }), @@ -1022,7 +1018,7 @@ export default class Wallet { } // This function tries to click on a specific rune, if the rune is not enabled it will enable the test rune and then click on it - async checkAndClickOnSpecificRune(tokenname) { + async checkAndClickOnSpecificRune(tokenname: string) { // Check if test rune is enabled and if not enabled the test rune try { // click on the test rune @@ -1066,7 +1062,7 @@ export default class Wallet { await expect(this.inputFallbackBTCURL).toBeVisible(); } - async checkTestnetUrls(shouldContainTestnet) { + async checkTestnetUrls(shouldContainTestnet: boolean) { const inputsURL = [this.inputStacksURL, this.inputBTCURL, this.inputFallbackBTCURL]; const checks = inputsURL.map(async (input) => { const inputValue = await input.inputValue(); @@ -1097,6 +1093,7 @@ export default class Wallet { await this.checkTestnetUrls(true); + // TODO think of a better way to do this // Wait for the network to be switched so that API doesn't fail because of the rate limiting await this.page.waitForTimeout(15000); @@ -1119,6 +1116,7 @@ export default class Wallet { await this.checkTestnetUrls(false); + // TODO think of a better way to do this // Wait for the network to be switched so that API doesn't fail because of the rate limiting await this.page.waitForTimeout(15000); @@ -1156,30 +1154,21 @@ export default class Wallet { const balanceText = await this.labelCoinBalanceCurrency.innerText(); totalBalance = parseFloat(balanceText.replace(/[^\d.-]/g, '')); } - // Check if total balance of all tokens is the same as total wallet balance - const totalBalanceText = await this.balance.innerText(); - const totalBalanceWallet = parseFloat(totalBalanceText.replace(/[^\d.-]/g, '')); - await expect(totalBalanceWallet).toBe(totalBalance); return totalBalance; } - // The enableRandomToken function takes a parameter tokenType which can either be ‘BRC20’ or ‘SIP10’. This parameter determines additional actions specific to BRC20 tokens. - async enableRandomToken(tokenType: 'BRC20' | 'SIP10'): Promise { + async selectLastToken(tokenType: 'BRC20' | 'SIP10'): Promise { await this.manageTokenButton.click(); - await expect(this.page.url()).toContain('manage-tokens'); + expect(this.page.url()).toContain('manage-tokens'); // Click on the specific token type button if BRC20 is selected if (tokenType === 'BRC20') { await this.buttonBRC20.click(); } - // Enable a random token - const tokenName = await this.toggleRandomToken(true); - - // Navigate back and verify the token is visible + const chosenToken = this.divTokenRow.last(); + const tokenName = (await chosenToken.getAttribute('data-testid')) || 'default-value'; await this.buttonBack.click(); - await expect(this.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeVisible(); - return tokenName; } diff --git a/tests/specs/managementAccount.spec.ts b/tests/specs/managementAccount.spec.ts index 03afbfded..20d8a5286 100644 --- a/tests/specs/managementAccount.spec.ts +++ b/tests/specs/managementAccount.spec.ts @@ -12,7 +12,7 @@ test.describe('Account Management', () => { await page.goto(`chrome-extension://${extensionId}/popup.html`); await wallet.checkVisualsStartpage(); await wallet.labelAccountName.click(); - await expect(page.url()).toContain('account-list'); + expect(page.url()).toContain('account-list'); await expect(wallet.labelAccountName).toHaveCount(1); await expect(wallet.buttonGenerateAccount).toBeVisible(); await expect(wallet.buttonConnectHardwareWallet).toBeVisible(); @@ -32,7 +32,7 @@ test.describe('Account Management', () => { await page.goto(`chrome-extension://${extensionId}/popup.html`); await wallet.checkVisualsStartpage(); await wallet.labelAccountName.click(); - await expect(page.url()).toContain('account-list'); + expect(page.url()).toContain('account-list'); await expect(wallet.labelAccountName).toHaveCount(1); await wallet.buttonAccountOptions.click(); await expect(wallet.buttonRenameAccount).toBeVisible(); @@ -65,7 +65,7 @@ test.describe('Account Management', () => { await page.goto(`chrome-extension://${extensionId}/popup.html`); await wallet.checkVisualsStartpage(); await wallet.labelAccountName.click(); - await expect(page.url()).toContain('account-list'); + expect(page.url()).toContain('account-list'); await expect(wallet.labelAccountName).toHaveCount(1); await wallet.buttonAccountOptions.click(); await expect(wallet.buttonRenameAccount).toBeVisible(); @@ -93,14 +93,14 @@ test.describe('Account Management', () => { await page.goto(`chrome-extension://${extensionId}/popup.html`); await wallet.checkVisualsStartpage(); await wallet.labelAccountName.click(); - await expect(page.url()).toContain('account-list'); + expect(page.url()).toContain('account-list'); await expect(wallet.labelAccountName).toHaveCount(1); await wallet.buttonGenerateAccount.click(); await expect(wallet.labelAccountName).toHaveCount(2); await expect(wallet.buttonAccountOptions).toHaveCount(2); await expect(wallet.accountBalance).toHaveCount(2); const balanceText = await wallet.getBalanceOfAllAccounts(); - await expect(balanceText).toBe(0); + expect(balanceText).toBe(0); }); test('Switch to another account and switch back', async ({ page, extensionId }) => { @@ -111,12 +111,12 @@ test.describe('Account Management', () => { await wallet.checkVisualsStartpage(); await expect(wallet.labelAccountName).toHaveText('Account 1'); await wallet.labelAccountName.click(); - await expect(page.url()).toContain('account-list'); + expect(page.url()).toContain('account-list'); await expect(wallet.labelAccountName).toHaveCount(1); await wallet.buttonGenerateAccount.click(); await expect(wallet.labelAccountName).toHaveCount(2); const balanceText = await wallet.getBalanceOfAllAccounts(); - await expect(balanceText).toBe(0); + expect(balanceText).toBe(0); await wallet.labelAccountName.last().click(); await wallet.checkVisualsStartpage(); await expect(wallet.labelAccountName).toHaveText('Account 2'); diff --git a/tests/specs/managementToken.spec.ts b/tests/specs/managementToken.spec.ts index 0b559955f..69ded0ed7 100644 --- a/tests/specs/managementToken.spec.ts +++ b/tests/specs/managementToken.spec.ts @@ -13,278 +13,143 @@ test.describe('Token Management', () => { await wallet.checkVisualsStartpage(); await expect(wallet.balance).toHaveText('$0.00'); await wallet.manageTokenButton.click(); - await expect(page.url()).toContain('manage-tokens'); + expect(page.url()).toContain('manage-tokens'); await expect(wallet.buttonBack).toBeVisible(); await expect(wallet.buttonSip10).toBeVisible(); await expect(wallet.buttonBRC20).toBeVisible(); await expect(wallet.buttonRunes).toBeVisible(); await expect(wallet.headingTokens).toBeVisible(); - // Check SIP10 token - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenSIP = await wallet.labelCoinTitle.count(); - - await expect(amounttokenSIP).toBeGreaterThanOrEqual(15); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenSIP - 1); + // Check SIP10 token tab - only Stacks should be showing when user has no sip10 balances + await wallet.buttonSip10.click(); + await expect(wallet.labelCoinTitle).toHaveCount(1); + await expect(wallet.checkboxToken).toHaveCount(1); await expect(wallet.checkboxTokenActive).toHaveCount(1); - await expect(wallet.checkboxToken).toHaveCount(amounttokenSIP); + await expect(wallet.checkboxTokenInactive).toHaveCount(0); - // Check BRC20 token + // Check BRC20 token tab - nothing shows when user has no brc20 balances await wallet.buttonBRC20.click(); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenBRC20 = await wallet.labelCoinTitle.count(); - await expect(amounttokenBRC20).toBeGreaterThanOrEqual(8); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenBRC20); + await expect(wallet.labelCoinTitle).toHaveCount(0); + await expect(wallet.checkboxToken).toHaveCount(0); + await expect(wallet.checkboxTokenInactive).toHaveCount(0); await expect(wallet.checkboxTokenActive).toHaveCount(0); - await expect(wallet.checkboxToken).toHaveCount(amounttokenBRC20); - // Check rune token + // Check rune token tab - nothing shows when user has no runes balances await wallet.buttonRunes.click(); await expect(wallet.labelCoinTitle).toHaveCount(0); + await expect(wallet.checkboxToken).toHaveCount(0); await expect(wallet.checkboxTokenInactive).toHaveCount(0); await expect(wallet.checkboxTokenActive).toHaveCount(0); - await expect(wallet.checkboxToken).toHaveCount(0); }); - test('Enable and disable some BRC-20 token', async ({ page, extensionId }) => { - const onboardingPage = new Onboarding(page); + test('Toggle a BRC-20 token', async ({ page, extensionId }) => { const wallet = new Wallet(page); - await onboardingPage.createWalletSkipBackup(strongPW); + await wallet.setupTest(extensionId, 'SEED_WORDS1', false); - await test.step('Enable a random token', async () => { - await page.goto(`chrome-extension://${extensionId}/popup.html`); - await wallet.checkVisualsStartpage(); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - let balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); + await test.step('Toggle a random token', async () => { await wallet.manageTokenButton.click(); - await expect(page.url()).toContain('manage-tokens'); await wallet.buttonBRC20.click(); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenBRC20 = await wallet.labelCoinTitle.count(); - // Enable random token - const tokenName = await wallet.toggleRandomToken(true); - // Check that amount of checkboxes changed - await expect(wallet.checkboxTokenActive).toHaveCount(1); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenBRC20 - 1); - await wallet.buttonBack.click(); - // new enabled token should be visible on dashboard - await expect(wallet.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeVisible(); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); - }); - await test.step('Enable some more token', async () => { - await wallet.manageTokenButton.click(); - await wallet.buttonBRC20.click(); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenBRC20 = await wallet.labelCoinTitle.count(); - const tokenName1 = await wallet.toggleRandomToken(true); - const tokenName2 = await wallet.toggleRandomToken(true); - const tokenName3 = await wallet.toggleRandomToken(true); - await expect(wallet.checkboxTokenActive).toHaveCount(4); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenBRC20 - 4); + // NOTE: requires an account with at least 1 brc20 token with balance + await expect(wallet.checkboxTokenActive.first()).toBeVisible(); + + // disable a random token + const tokenName = await wallet.toggleRandomToken(false); + + // expect token to be hidden on dashboard + const fetchTokens = page.waitForResponse((response) => + response.url().includes('/brc20/tokens'), + ); await wallet.buttonBack.click(); - // new enabled tokens should be visible on dashboard - await expect(wallet.labelTokenSubtitle.getByText(tokenName1, { exact: true })).toBeVisible(); - await expect(wallet.labelTokenSubtitle.getByText(tokenName2, { exact: true })).toBeVisible(); - await expect(wallet.labelTokenSubtitle.getByText(tokenName3, { exact: true })).toBeVisible(); - }); + await fetchTokens; + await expect(wallet.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeHidden(); - await test.step('Disable a random token', async () => { - await page.goto(`chrome-extension://${extensionId}/popup.html`); - await wallet.checkVisualsStartpage(); + // enable the token again await wallet.manageTokenButton.click(); - await expect(page.url()).toContain('manage-tokens'); await wallet.buttonBRC20.click(); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenBRC20 = await wallet.labelCoinTitle.count(); - const tokenName = await wallet.toggleRandomToken(false); - await expect(wallet.checkboxTokenActive).toHaveCount(3); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenBRC20 - 3); + await page.getByTestId(tokenName).locator('label').click(); + + // expect to be visible again on dashboard + const fetchTokensAgain = page.waitForResponse((response) => + response.url().includes('/brc20/tokens'), + ); await wallet.buttonBack.click(); - // new enabled token should be visible on dashboard - await expect(wallet.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeHidden(); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - const balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); + await fetchTokensAgain; + await expect(wallet.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeVisible(); }); }); - test('Enable and disable some SIP-10 token', async ({ page, extensionId }) => { - const onboardingPage = new Onboarding(page); + test('Toggle a SIP-10 token', async ({ page, extensionId }) => { const wallet = new Wallet(page); - await onboardingPage.createWalletSkipBackup(strongPW); + await wallet.setupTest(extensionId, 'SEED_WORDS1', false); - await test.step('Enable a random token', async () => { - await page.goto(`chrome-extension://${extensionId}/popup.html`); - await wallet.checkVisualsStartpage(); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - let balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); + await test.step('Toggle a random token', async () => { await wallet.manageTokenButton.click(); - await expect(page.url()).toContain('manage-tokens'); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenSIP = await wallet.labelCoinTitle.count(); - // Enable random token - const tokenName = await wallet.toggleRandomToken(true); - // Check that amount of checkboxes changed - await expect(wallet.checkboxTokenActive).toHaveCount(2); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenSIP - 2); - await wallet.buttonBack.click(); - // new enabled token should be visible on dashboard - await expect(wallet.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeVisible(); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); - }); + await wallet.buttonSip10.click(); - await test.step('Enable some more token', async () => { - await wallet.manageTokenButton.click(); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenSIP = await wallet.labelCoinTitle.count(); - const tokenName1 = await wallet.toggleRandomToken(true); - const tokenName2 = await wallet.toggleRandomToken(true); - const tokenName3 = await wallet.toggleRandomToken(true); - await expect(wallet.checkboxTokenActive).toHaveCount(5); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenSIP - 5); - await wallet.buttonBack.click(); - // new enabled tokens should be visible on dashboard - await expect(wallet.labelTokenSubtitle.getByText(tokenName1, { exact: true })).toBeVisible(); - await expect(wallet.labelTokenSubtitle.getByText(tokenName2, { exact: true })).toBeVisible(); - await expect(wallet.labelTokenSubtitle.getByText(tokenName3, { exact: true })).toBeVisible(); - }); + // NOTE: requires an account with at least 1 sip10 token with balance + await expect(wallet.checkboxTokenActive.first()).toBeVisible(); - await test.step('Disable a random token', async () => { - await page.goto(`chrome-extension://${extensionId}/popup.html`); - await wallet.checkVisualsStartpage(); - await wallet.manageTokenButton.click(); - await expect(page.url()).toContain('manage-tokens'); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenSIP = await wallet.labelCoinTitle.count(); + // disable a random token const tokenName = await wallet.toggleRandomToken(false); - await expect(wallet.checkboxTokenActive).toHaveCount(4); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenSIP - 4); + + // expect token to be hidden on dashboard + const fetchTokens = page.waitForResponse((response) => + response.url().includes('/sip10/tokens'), + ); await wallet.buttonBack.click(); - // new enabled token should be visible on dashboard + await fetchTokens; await expect(wallet.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeHidden(); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - const balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); - }); - }); - test('Enable and disable all SIP-10 token', async ({ page, extensionId }) => { - const onboardingPage = new Onboarding(page); - const wallet = new Wallet(page); - await onboardingPage.createWalletSkipBackup(strongPW); - - await test.step('Enable a all tokens', async () => { - await page.goto(`chrome-extension://${extensionId}/popup.html`); - await wallet.checkVisualsStartpage(); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - let balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); - await expect(wallet.labelTokenSubtitle).toHaveCount(2); + // enable the token again await wallet.manageTokenButton.click(); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenSIP = await wallet.labelCoinTitle.count(); - await expect(page.url()).toContain('manage-tokens'); - await expect(wallet.checkboxToken).toHaveCount(amounttokenSIP); - await wallet.toggleAllTokens(true); - await expect(wallet.checkboxTokenActive).toHaveCount(amounttokenSIP); - await expect(wallet.checkboxTokenInactive).toHaveCount(0); - await wallet.buttonBack.click(); - await expect(wallet.labelTokenSubtitle).toHaveCount(amounttokenSIP + 1); - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); - }); + await wallet.buttonSip10.click(); + await page.getByTestId(tokenName).locator('label').click(); - await test.step('Disable all tokens', async () => { - await page.goto(`chrome-extension://${extensionId}/popup.html`); - await wallet.checkVisualsStartpage(); - await wallet.manageTokenButton.click(); - await expect(page.url()).toContain('manage-tokens'); - await wallet.toggleAllTokens(false); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenSIP = await wallet.labelCoinTitle.count(); - await expect(wallet.checkboxTokenActive).toHaveCount(0); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenSIP); + // expect to be visible again on dashboard + const fetchTokensAgain = page.waitForResponse((response) => + response.url().includes('/sip10/tokens'), + ); await wallet.buttonBack.click(); - await expect(wallet.labelTokenSubtitle).toHaveCount(1); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - const balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); + await fetchTokensAgain; + await expect(wallet.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeVisible(); }); }); - test('Enable and disable all BRC-20 token', async ({ page, extensionId }) => { - const onboardingPage = new Onboarding(page); + + test('Toggle a Runes token', async ({ page, extensionId }) => { const wallet = new Wallet(page); - await onboardingPage.createWalletSkipBackup(strongPW); + await wallet.setupTest(extensionId, 'SEED_WORDS1', false); - await test.step('Enable a all tokens', async () => { - await page.goto(`chrome-extension://${extensionId}/popup.html`); - await wallet.checkVisualsStartpage(); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - let balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); - await expect(wallet.labelTokenSubtitle).toHaveCount(2); + await test.step('Toggle a random token', async () => { await wallet.manageTokenButton.click(); - await expect(page.url()).toContain('manage-tokens'); - await wallet.buttonBRC20.click(); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenBRC20 = await wallet.labelCoinTitle.count(); - await expect(wallet.checkboxToken).toHaveCount(amounttokenBRC20); - await wallet.toggleAllTokens(true); - await expect(wallet.checkboxTokenActive).toHaveCount(amounttokenBRC20); - await expect(wallet.checkboxTokenInactive).toHaveCount(0); + await wallet.buttonRunes.click(); + + // NOTE: requires an account with at least 1 runes token with balance + await expect(wallet.checkboxTokenActive.first()).toBeVisible(); + + // disable a random token + const tokenName = await wallet.toggleRandomToken(false); + + // expect token to be hidden on dashboard + const fetchTokens = page.waitForResponse((response) => + response.url().includes('/runes/fiat-rates'), + ); await wallet.buttonBack.click(); - await expect(wallet.labelTokenSubtitle).toHaveCount(amounttokenBRC20 + 2); - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); - }); + await fetchTokens; + await expect(wallet.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeHidden(); - await test.step('Disable all tokens', async () => { - await page.goto(`chrome-extension://${extensionId}/popup.html`); - await wallet.checkVisualsStartpage(); + // enable the token again await wallet.manageTokenButton.click(); - await expect(page.url()).toContain('manage-tokens'); - await wallet.buttonBRC20.click(); - await expect(wallet.checkboxTokenActive.first()).toBeVisible(); - const amounttokenBRC20 = await wallet.labelCoinTitle.count(); - await wallet.toggleAllTokens(false); - await expect(wallet.checkboxTokenActive).toHaveCount(0); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenBRC20); + await wallet.buttonRunes.click(); + await page.getByTestId(tokenName).locator('label').click(); + + // expect to be visible again on dashboard + const fetchTokensAgain = page.waitForResponse((response) => + response.url().includes('/runes/fiat-rates'), + ); await wallet.buttonBack.click(); - await expect(wallet.labelTokenSubtitle).toHaveCount(2); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - const balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); + await fetchTokensAgain; + await expect(wallet.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeVisible(); }); }); }); diff --git a/tests/specs/transactionHistory.spec.ts b/tests/specs/transactionHistory.spec.ts index 51b4918c2..3acaccbeb 100644 --- a/tests/specs/transactionHistory.spec.ts +++ b/tests/specs/transactionHistory.spec.ts @@ -5,12 +5,11 @@ test.describe('Transaction', () => { test('Visual Check SIP 10 Token Transaction history mainnet', async ({ page, extensionId }) => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', false); - - const tokenName = await wallet.enableRandomToken('SIP10'); + const tokenName = await wallet.selectLastToken('SIP10'); await wallet.clickOnSpecificToken(tokenName); - await expect(page.url()).toContain('coinDashboard'); + expect(page.url()).toContain('coinDashboard'); // Check token detail page for token image and coin title await expect(wallet.imageToken).toBeVisible(); await expect(wallet.textCoinTitle).toBeVisible(); @@ -26,10 +25,10 @@ test.describe('Transaction', () => { test('Visual Check BRC 20 Token Transaction history mainnet', async ({ page, extensionId }) => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', false); - const tokenName = await wallet.enableRandomToken('BRC20'); + const tokenName = await wallet.selectLastToken('BRC20'); await wallet.clickOnSpecificToken(tokenName); - await expect(page.url()).toContain('coinDashboard'); + expect(page.url()).toContain('coinDashboard'); // Check token detail page for coin title await expect(wallet.textCoinTitle).toBeVisible(); await expect(wallet.textCoinTitle).toContainText(tokenName); From c6cb8cf354358d6c55865e1204bd7f8c4332b7e4 Mon Sep 17 00:00:00 2001 From: Victor Kirov Date: Wed, 16 Oct 2024 10:52:46 +0300 Subject: [PATCH 176/227] Add native segwit support (#608) * Prep for native segwit * Update ledger confirmation screen to be stx specific * bump core * Migrate to new Core structure for accounts * Prep for native segwit * Remove unused type lib * Fix address type selector styling * Fix styling * Fix get address hook to use react query * Add bitcoin address balance breakdown * Fix label * Update core * Add preferred btc address changer in balance screen * Add address type switcher to receive component * Hide address type selectors on ledger * Fix some issues around ledger address type * Fix some ledger issues * Fix rerender issue * Fix xverse api call issue * Fix issue with xverse api call * Ensure address type selector is only shown if multiple addresses exist * Add address type to selectedAddress compiler * Bump core * Address PR review comments * Rename filterLedgerAccountsByNetwork * Remove extra function definition * Remove custom check circle SVG * [ENG-5261, ENG-5290, ENG-5291] Add change account sats connect event on selected account type switch + [ENG-4984,ENG- 5361,ENG-4983,ENG-5259] (#611) * Add change account sats connect event on selected account type switch * [ENG-5259]Default to native segwit and disable nested segwit for new wallets + [ENG-4984,ENG- 5361,ENG-4983] (#615) * Disable nested segwit for new wallets * Add switchPaymentAddressTypeEnabled to store migrations * rename variable as per PR review * [ENG-4983] Add address selector on btc send + [ENG-4984, ENG- 5361] (#618) * Add address selector on btc send * [ENG-4984] Add native segwit flow to onboarding and refactor account regeneration (#623) * Add native segwit flow to onboarding and refactor account regeneration * Clean up * Up the account checks to 20 * Add both addresses to txn history * Removed stale time * refactored click prop * Use styledP * [ENG-5361] Add native segwit callouts and tooltips (#666) * Start on native segwit modal * Add useStorage * Add alert tracker * Add type safe migrations and enable native segwit alert shower in migration * Extract receive sheet into its own component * Add callout to receive sheet * Add bitcoin address is native callout * Add callout tooltip * Make casing consistent * Extract crossbutton to own component * knip fixes * Sip 10 token list is no longer prepopulated * Merge issue * Fix merge issue * Fix banner carousel button propagation --- package-lock.json | 92 ++--- package.json | 16 +- src/app/App.tsx | 111 +++--- .../components/accountRow/accountAvatar.tsx | 4 +- src/app/components/accountRow/index.tsx | 23 +- .../components/accountRow/lazyAccountRow.tsx | 15 +- .../components/btcAddressTypeLabel/index.tsx | 59 +++ .../txInOutput/transactionInput.tsx | 11 +- .../txInOutput/transactionOutput.tsx | 17 +- src/app/components/guards/auth.tsx | 4 +- .../addressTypeSelector.tsx | 32 ++ .../components/preferredBtcAddress/index.tsx | 126 +++++++ .../preferredBtcAddressItem/index.tsx | 82 ++++ .../components/receiveCardComponent/index.tsx | 106 ++++-- src/app/components/seedPhraseInput/index.tsx | 2 +- src/app/components/tokenTile/index.tsx | 4 +- src/app/components/tooltip/context.ts | 8 + src/app/components/tooltip/index.tsx | 93 +++++ src/app/components/tooltip/provider.tsx | 29 ++ src/app/components/topRow/index.tsx | 12 +- src/app/hooks/queries/useAccountBalance.ts | 36 +- .../queries/useSelectedAccountBtcBalance.tsx | 39 ++ src/app/hooks/queries/useTransactions.ts | 20 +- src/app/hooks/useAvatarCleanup.ts | 36 +- src/app/hooks/useBtcAddressBalance.ts | 29 ++ src/app/hooks/useCanUserSwitchPaymentType.ts | 13 + src/app/hooks/useSanityCheck.ts | 10 +- src/app/hooks/useSelectedAccount.ts | 46 ++- src/app/hooks/useStorage.ts | 45 +++ src/app/hooks/useTransactionContext.ts | 7 +- src/app/hooks/useWalletReducer.ts | 320 ++++++++++------ src/app/hooks/useWalletSelector.ts | 4 +- src/app/routes/index.tsx | 9 + src/app/routes/paths.ts | 1 + src/app/screens/accountList/index.tsx | 10 +- src/app/screens/coinDashboard/coinHeader.tsx | 8 +- .../coins/btc/addressBalance.tsx | 117 ++++++ .../coins/btc/balanceBreakdown.tsx | 53 +++ .../screens/coinDashboard/coins/btc/index.tsx | 77 ++++ src/app/screens/coinDashboard/coins/other.tsx | 248 ++++++++++++ src/app/screens/coinDashboard/index.styled.ts | 120 ++++++ src/app/screens/coinDashboard/index.tsx | 353 +----------------- .../screens/confirmNftTransaction/index.tsx | 10 +- src/app/screens/connect/selectAccount.tsx | 2 +- .../screens/home/announcementModal/index.tsx | 43 +++ .../home/announcementModal/nativeSegWit.tsx | 66 ++++ src/app/screens/home/balanceCard/index.tsx | 16 +- src/app/screens/home/bannerCarousel.tsx | 12 +- src/app/screens/home/index.styled.ts | 8 +- src/app/screens/home/index.tsx | 168 +-------- src/app/screens/home/receiveSheet.tsx | 204 ++++++++++ src/app/screens/landing/index.tsx | 51 ++- .../screens/ledger/addStxAddress/index.tsx | 10 +- .../ledger/importLedgerAccount/index.tsx | 28 +- src/app/screens/nftDetail/index.tsx | 27 +- src/app/screens/ordinalDetail/index.tsx | 28 +- src/app/screens/ordinalsCollection/index.tsx | 12 +- src/app/screens/receive/index.tsx | 96 ++++- .../screens/restoreWallet/enterSeedphrase.tsx | 14 +- src/app/screens/restoreWallet/index.tsx | 62 +-- .../paymentAddressTypeSelector.tsx | 148 ++++++++ src/app/screens/sendBtc/accountSelector.tsx | 95 +++++ src/app/screens/sendBtc/amountSelector.tsx | 20 +- src/app/screens/sendBtc/index.tsx | 33 +- src/app/screens/sendBtc/stepDisplay.tsx | 68 ++-- src/app/screens/sendBtc/steps.tsx | 15 +- src/app/screens/sendBtc/title.tsx | 29 ++ src/app/screens/sendOrdinal/index.tsx | 10 +- src/app/screens/settings/advanced/index.tsx | 32 +- .../paymentAddressTypeSelector/index.tsx | 63 ++++ .../screens/settings/changeNetwork/index.tsx | 4 + .../settings/changeNetwork/networkRow.tsx | 4 +- .../settings/changeNetwork/nodeInput.tsx | 10 +- .../useSignMessageRequest.ts | 22 +- src/app/stores/index.ts | 138 +++++-- .../stores/wallet/actions/actionCreators.ts | 20 +- .../stores/wallet/actions/migrationTypes.ts | 162 ++++++++ src/app/stores/wallet/actions/types.ts | 20 +- src/app/stores/wallet/reducer.ts | 23 +- src/app/ui-components/btcAmountSelector.tsx | 11 +- src/app/ui-library/callout.tsx | 9 +- src/app/ui-library/crossButton.tsx | 44 +++ src/app/ui-library/sheet.tsx | 47 +-- src/app/utils/alertTracker.ts | 47 +++ src/app/utils/gradient.ts | 5 +- src/app/utils/helper.ts | 12 +- src/assets/img/btcFlashy.svg | 41 ++ src/common/utils/getSelectedAccount.ts | 40 +- src/common/utils/ledger.ts | 6 +- .../dispatchEvent/index.ts | 4 +- .../extensionToContentScript/utils/index.ts | 12 +- .../rpc/btc/getAddresses/getAddresses.ts | 6 +- .../utils/rpc/btc/getAddresses/utils.ts | 6 +- src/common/utils/rpc/btc/getBalance.ts | 7 +- .../utils/rpc/ordinals/getInscriptions.ts | 2 +- src/common/utils/rpc/runes/getBalance.ts | 4 +- .../utils/rpc/wallet/requestPermissions.ts | 18 +- src/locales/en.json | 60 ++- src/theme/index.ts | 3 + 99 files changed, 3432 insertions(+), 1172 deletions(-) create mode 100644 src/app/components/btcAddressTypeLabel/index.tsx create mode 100644 src/app/components/preferredBtcAddress/addressTypeSelector.tsx create mode 100644 src/app/components/preferredBtcAddress/index.tsx create mode 100644 src/app/components/preferredBtcAddressItem/index.tsx create mode 100644 src/app/components/tooltip/context.ts create mode 100644 src/app/components/tooltip/index.tsx create mode 100644 src/app/components/tooltip/provider.tsx create mode 100644 src/app/hooks/queries/useSelectedAccountBtcBalance.tsx create mode 100644 src/app/hooks/useBtcAddressBalance.ts create mode 100644 src/app/hooks/useCanUserSwitchPaymentType.ts create mode 100644 src/app/hooks/useStorage.ts create mode 100644 src/app/screens/coinDashboard/coins/btc/addressBalance.tsx create mode 100644 src/app/screens/coinDashboard/coins/btc/balanceBreakdown.tsx create mode 100644 src/app/screens/coinDashboard/coins/btc/index.tsx create mode 100644 src/app/screens/coinDashboard/coins/other.tsx create mode 100644 src/app/screens/coinDashboard/index.styled.ts create mode 100644 src/app/screens/home/announcementModal/index.tsx create mode 100644 src/app/screens/home/announcementModal/nativeSegWit.tsx create mode 100644 src/app/screens/home/receiveSheet.tsx create mode 100644 src/app/screens/restoreWallet/paymentAddressTypeSelector.tsx create mode 100644 src/app/screens/sendBtc/accountSelector.tsx create mode 100644 src/app/screens/sendBtc/title.tsx create mode 100644 src/app/screens/settings/advanced/paymentAddressTypeSelector/index.tsx create mode 100644 src/app/stores/wallet/actions/migrationTypes.ts create mode 100644 src/app/ui-library/crossButton.tsx create mode 100644 src/app/utils/alertTracker.ts create mode 100644 src/assets/img/btcFlashy.svg diff --git a/package-lock.json b/package-lock.json index 7d7c77fb0..0d6b48d78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,13 @@ "dependencies": { "@aryzing/superqs": "0.0.6", "@ledgerhq/hw-transport-webusb": "^6.27.13", + "@noble/hashes": "^1.5.0", "@phosphor-icons/react": "^2.1.0", "@playwright/test": "1.46.1", "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.3.0", - "@secretkeylabs/xverse-core": "24.2.0", + "@scure/btc-signer": "1.2.1", + "@secretkeylabs/xverse-core": "26.0.0", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.16.1", @@ -938,7 +940,7 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@noble/hashes": { + "node_modules/@noble/curves/node_modules/@noble/hashes": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", @@ -949,6 +951,17 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/hashes": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/secp256k1": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", @@ -1216,17 +1229,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@scure/bip32/node_modules/@noble/hashes": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", - "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@scure/bip39": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz", @@ -1269,10 +1271,21 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/btc-signer/node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@secretkeylabs/xverse-core": { - "version": "24.2.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/24.2.0/67ae914442d4276dcfd0d1e3b8bdf623133557bb", - "integrity": "sha512-mBxIPL6jmGhRZLpUBL0ztTvBporCL/1CzTsz42/XZo7WFZIFfb1Arj2oQkeT+AngV+bf1CH60cV2vXnqH0mYkQ==", + "version": "26.0.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/26.0.0/1ae56e20c7fc38d32cae5c38d51b708a88f17957", + "integrity": "sha512-PCcreXqcLCTMtgwCyVZT9EYKZgEWtPk3EAYBCw9dpIcqoFDywxvRyl70Sg1eiVs0YJPinEgW2/JJgmlZcDf/eA==", "license": "ISC", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", @@ -1326,17 +1339,6 @@ "react-dom": ">18.0.0" } }, - "node_modules/@secretkeylabs/xverse-core/node_modules/@noble/hashes": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", - "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@secretkeylabs/xverse-core/node_modules/@scure/bip39": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.4.0.tgz", @@ -13425,12 +13427,19 @@ "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", "requires": { "@noble/hashes": "1.3.3" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==" + } } }, "@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==" }, "@noble/secp256k1": { "version": "1.7.1", @@ -13599,11 +13608,6 @@ "requires": { "@noble/hashes": "1.5.0" } - }, - "@noble/hashes": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", - "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==" } } }, @@ -13632,12 +13636,19 @@ "@noble/hashes": "~1.3.3", "@scure/base": "~1.1.5", "micro-packed": "~0.5.1" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==" + } } }, "@secretkeylabs/xverse-core": { - "version": "24.2.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/24.2.0/67ae914442d4276dcfd0d1e3b8bdf623133557bb", - "integrity": "sha512-mBxIPL6jmGhRZLpUBL0ztTvBporCL/1CzTsz42/XZo7WFZIFfb1Arj2oQkeT+AngV+bf1CH60cV2vXnqH0mYkQ==", + "version": "26.0.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/26.0.0/1ae56e20c7fc38d32cae5c38d51b708a88f17957", + "integrity": "sha512-PCcreXqcLCTMtgwCyVZT9EYKZgEWtPk3EAYBCw9dpIcqoFDywxvRyl70Sg1eiVs0YJPinEgW2/JJgmlZcDf/eA==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", @@ -13681,11 +13692,6 @@ "varuint-bitcoin": "^1.1.2" }, "dependencies": { - "@noble/hashes": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", - "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==" - }, "@scure/bip39": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.4.0.tgz", diff --git a/package.json b/package.json index a7ad4f339..7a308ca62 100644 --- a/package.json +++ b/package.json @@ -7,16 +7,16 @@ "node": "^18.18.2" }, "scripts": { - "build": "npm run clean && NODE_ENV=production npx node webpack/utils/build.js", + "build": "npm run clean && NODE_ENV=production node webpack/utils/build.js", "build-named": "npm run clean && NODE_ENV=production node scripts/build-named.js", - "start": "npx node webpack/utils/devServer.js", + "start": "node webpack/utils/devServer.js", "clean": "rimraf build", "style": "prettier --write \"src/**/*.{ts,tsx}\"", "prepare": "husky install", - "e2etest": "npx playwright test -g \"\" --grep-invert \"#localexecution\"", - "e2etest:ui": "npx playwright test --ui", - "e2etest:smoketest": "npx playwright test --grep \"#smoketest\"", - "e2etest:skipped": "npx playwright test --grep \"#localexecution\" --workers=1", + "e2etest": "playwright test -g \"\" --grep-invert \"#localexecution\"", + "e2etest:ui": "playwright test --ui", + "e2etest:smoketest": "playwright test --grep \"#smoketest\"", + "e2etest:skipped": "playwright test --grep \"#localexecution\" --workers=1", "e2etest:report": "playwright show-report", "knip": "knip", "ts-check": "tsc --noEmit", @@ -38,11 +38,13 @@ "dependencies": { "@aryzing/superqs": "0.0.6", "@ledgerhq/hw-transport-webusb": "^6.27.13", + "@noble/hashes": "^1.5.0", "@phosphor-icons/react": "^2.1.0", "@playwright/test": "1.46.1", "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.3.0", - "@secretkeylabs/xverse-core": "24.2.0", + "@scure/btc-signer": "1.2.1", + "@secretkeylabs/xverse-core": "26.0.0", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.16.1", diff --git a/src/app/App.tsx b/src/app/App.tsx index 97cd485a2..a323fc46c 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,6 +1,7 @@ import { PermissionsProvider } from '@components/permissionsManager'; import StartupLoadingScreen from '@components/startupLoadingScreen'; import Toaster from '@components/toaster'; +import TooltipProvider from '@components/tooltip/provider'; import { CheckCircle, XCircle } from '@phosphor-icons/react'; import { setXClientVersion } from '@secretkeylabs/xverse-core'; import rootStore from '@stores/index'; @@ -34,63 +35,65 @@ function App(): React.ReactNode { <> - - - - }> - - - - - - - ), - style: { - ...Theme.typography.body_medium_m, - backgroundColor: Theme.colors.success_medium, - borderRadius: Theme.radius(2), - padding: Theme.space.s, - color: Theme.colors.elevation0, + + + + + }> + + + + + + + ), + style: { + ...Theme.typography.body_medium_m, + backgroundColor: Theme.colors.success_medium, + borderRadius: Theme.radius(2), + padding: Theme.space.s, + color: Theme.colors.elevation0, + }, }, - }, - error: { - icon: ( - - - - ), - style: { - ...Theme.typography.body_medium_m, - backgroundColor: Theme.colors.danger_dark, - borderRadius: Theme.radius(2), - padding: Theme.space.s, - color: Theme.colors.white_0, + error: { + icon: ( + + + + ), + style: { + ...Theme.typography.body_medium_m, + backgroundColor: Theme.colors.danger_dark, + borderRadius: Theme.radius(2), + padding: Theme.space.s, + color: Theme.colors.white_0, + }, }, - }, - blank: { - style: { - ...Theme.typography.body_medium_m, - backgroundColor: Theme.colors.white_0, - borderRadius: Theme.radius(2), - padding: Theme.space.s, - color: Theme.colors.elevation0, + blank: { + style: { + ...Theme.typography.body_medium_m, + backgroundColor: Theme.colors.white_0, + borderRadius: Theme.radius(2), + padding: Theme.space.s, + color: Theme.colors.elevation0, + }, }, - }, - }} - /> - - - - - + }} + /> + + + + + + ); diff --git a/src/app/components/accountRow/accountAvatar.tsx b/src/app/components/accountRow/accountAvatar.tsx index c8e28463c..5c678cbbd 100644 --- a/src/app/components/accountRow/accountAvatar.tsx +++ b/src/app/components/accountRow/accountAvatar.tsx @@ -10,7 +10,7 @@ type Props = { account: Account | null; isSelected: boolean; isAccountListView?: boolean; - avatar?: AvatarInfo | null; + avatar?: AvatarInfo; }; function AccountAvatar({ account, avatar, isSelected, isAccountListView = false }: Props) { @@ -30,7 +30,7 @@ function AccountAvatar({ account, avatar, isSelected, isAccountListView = false ); } - const gradient = getAccountGradient(account?.stxAddress || account?.btcAddress!); + const gradient = getAccountGradient(account); return ( (); const stxCopiedTooltipTimeoutRef = useRef(); const [showRemoveAccountModal, setShowRemoveAccountModal] = useState(false); @@ -71,7 +73,8 @@ function AccountRow({ const [accountName, setAccountName] = useState(''); const [accountNameError, setAccountNameError] = useState(null); const [isAccountNameChangeLoading, setIsAccountNameChangeLoading] = useState(false); - const { removeLedgerAccount, renameAccount, updateLedgerAccounts } = useWalletReducer(); + + const { removeLedgerAccount, renameSoftwareAccount, updateLedgerAccounts } = useWalletReducer(); useEffect( () => () => { @@ -116,7 +119,9 @@ function AccountRow({ }; const handleRemoveAvatar = () => { - dispatch(removeAccountAvatarAction({ address: account?.btcAddress ?? '' })); + if (!account) return; + + dispatch(removeAccountAvatarAction({ address: account?.btcAddresses.taproot.address })); toast.custom( , ); @@ -157,7 +162,7 @@ function AccountRow({ if (isLedgerAccount(account)) { await updateLedgerAccounts({ ...account, accountName }); } else { - await renameAccount({ ...account, accountName }); + await renameSoftwareAccount({ ...account, accountName }); } handleRenameAccountModalClose(); } catch (err) { @@ -177,7 +182,7 @@ function AccountRow({ if (isLedgerAccount(account)) { updateLedgerAccounts({ ...account, accountName: undefined }); } else { - renameAccount({ ...account, accountName: undefined }); + renameSoftwareAccount({ ...account, accountName: undefined }); } setAccountName(''); handleRenameAccountModalClose(); @@ -193,7 +198,7 @@ function AccountRow({ @@ -253,7 +258,7 @@ function AccountRow({ {optionsDialogTranslation('RENAME_ACCOUNT')} - {accountAvatar?.type && ( + {selectedAvatar?.type && ( {optionsDialogTranslation('NFT_AVATAR.REMOVE_ACTION')} diff --git a/src/app/components/accountRow/lazyAccountRow.tsx b/src/app/components/accountRow/lazyAccountRow.tsx index 486b71d5f..b79353cf0 100644 --- a/src/app/components/accountRow/lazyAccountRow.tsx +++ b/src/app/components/accountRow/lazyAccountRow.tsx @@ -1,6 +1,7 @@ import useIntersectionObserver from '@hooks/useIntersectionObserver'; import useWalletSelector from '@hooks/useWalletSelector'; import type { Account } from '@secretkeylabs/xverse-core'; +import { getAccountBalanceKey } from '@utils/helper'; import { useEffect, useRef, useState } from 'react'; import AccountRow from '.'; @@ -16,17 +17,23 @@ type Props = { function LazyAccountRow(props: Props) { const { fetchBalance, account } = props; const { accountBalances } = useWalletSelector(); - const totalBalance = accountBalances[account?.btcAddress ?? '']; const [shouldFetch, setShouldFetch] = useState(false); const ref = useRef(null); useIntersectionObserver(ref, () => setShouldFetch(true), {}); useEffect(() => { - if (fetchBalance && shouldFetch && !totalBalance) { - fetchBalance(account); + if ( + !shouldFetch || + !fetchBalance || + !account || + getAccountBalanceKey(account) in accountBalances + ) { + return; } - }, [shouldFetch, totalBalance]); + + fetchBalance(account); + }, [account, shouldFetch]); return (
diff --git a/src/app/components/btcAddressTypeLabel/index.tsx b/src/app/components/btcAddressTypeLabel/index.tsx new file mode 100644 index 000000000..8e5d59b74 --- /dev/null +++ b/src/app/components/btcAddressTypeLabel/index.tsx @@ -0,0 +1,59 @@ +import * as btc from '@scure/btc-signer'; +import type { BtcAddressType, NetworkType } from '@secretkeylabs/xverse-core'; +import styled from 'styled-components'; + +const LabelContainer = styled.div((props) => ({ + backgroundColor: props.theme.colors.white_900, + borderRadius: '6px', + padding: '3px 8px', +})); + +const labelMap: Record = { + native: 'Native SegWit', + nested: 'Nested SegWit', + taproot: 'Taproot', +}; + +type BtcAddressTypeLabelProps = { + addressType: BtcAddressType; +}; + +export function BtcAddressTypeLabel({ addressType }: BtcAddressTypeLabelProps) { + return {labelMap[addressType]}; +} + +type BtcAddressTypeForAddressLabelProps = { + address: string; + network: NetworkType; +}; + +export function BtcAddressTypeForAddressLabel({ + address, + network, +}: BtcAddressTypeForAddressLabelProps) { + try { + const addressMetadata = btc + .Address(network === 'Mainnet' ? btc.NETWORK : btc.TEST_NETWORK) + .decode(address); + + let addressType: BtcAddressType; + + switch (addressMetadata.type) { + case 'sh': + addressType = 'nested'; + break; + case 'wpkh': + addressType = 'native'; + break; + case 'tr': + addressType = 'taproot'; + break; + default: + return null; + } + + return ; + } catch (e) { + return null; + } +} diff --git a/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx b/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx index 438843d94..a89af0725 100644 --- a/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx +++ b/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx @@ -28,19 +28,18 @@ type Props = { }; function TransactionInput({ input }: Props) { - const { btcAddress, ordinalsAddress } = useSelectedAccount(); + const { btcAddresses } = useSelectedAccount(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const isPaymentsAddress = input.extendedUtxo.address === btcAddress; - const isOrdinalsAddress = input.extendedUtxo.address === ordinalsAddress; - const isExternalInput = !isPaymentsAddress && !isOrdinalsAddress; + const userAddresses = Object.values(btcAddresses).map((address) => address.address); + const isExternalInput = userAddresses.every((address) => address !== input.extendedUtxo.address); // TODO: show this in the UI? // const insecureInput = // input.sigHash === btc.SigHash.NONE || input.sigHash === btc.SigHash.NONE_ANYONECANPAY; const renderAddress = (addressToBeDisplayed: string) => - addressToBeDisplayed === btcAddress || addressToBeDisplayed === ordinalsAddress ? ( + userAddresses.some((address) => address === addressToBeDisplayed) ? ( {getTruncatedAddress(addressToBeDisplayed)} @@ -59,7 +58,7 @@ function TransactionInput({ input }: Props) { icon={InputIcon} hideAddress dataTestID="confirm-balance" - hideCopyButton={isPaymentsAddress || isOrdinalsAddress} + hideCopyButton={!isExternalInput} amount={`${satsToBtc( new BigNumber(input.extendedUtxo.utxo.value.toString()), ).toFixed()} BTC`} diff --git a/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx b/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx index 7d466f7b6..ec18a8f8f 100644 --- a/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx +++ b/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx @@ -34,17 +34,24 @@ type Props = { }; function TransactionOutput({ output, scriptOutputCount }: Props) { - const { btcAddress, ordinalsAddress, btcPublicKey, ordinalsPublicKey } = useSelectedAccount(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + + const { btcAddresses, btcPublicKey, ordinalsPublicKey } = useSelectedAccount(); + + const userAddresses = Object.values(btcAddresses).map((address) => address.address); + const isOutputWithScript = output.type === 'script'; const isOutputWithAddress = output.type === 'address'; const isOutputWithPubKey = !isOutputWithScript && !isOutputWithAddress; + const isOutputToOwnAddress = + isOutputWithScript || isOutputWithPubKey + ? false + : userAddresses.some((address) => address === output.address); + const detailViewIcon = isOutputWithScript ? ScriptIcon : OutputIcon; const detailViewHideCopyButton = - isOutputWithScript || isOutputWithPubKey - ? true - : btcAddress === output.address || ordinalsAddress === output.address; + isOutputWithScript || isOutputWithPubKey ? true : isOutputToOwnAddress; const detailView = () => { if (isOutputWithScript) { @@ -67,7 +74,7 @@ function TransactionOutput({ output, scriptOutputCount }: Props) { ); } - if (output.address === btcAddress || output.address === ordinalsAddress) { + if (isOutputToOwnAddress) { return ( diff --git a/src/app/components/guards/auth.tsx b/src/app/components/guards/auth.tsx index 9a8eac8c5..ec3251d96 100644 --- a/src/app/components/guards/auth.tsx +++ b/src/app/components/guards/auth.tsx @@ -66,7 +66,9 @@ function AuthGuard({ children }: PropsWithChildren) { navigate('/login'); } - await loadWallet(); + await loadWallet(() => { + isInitialised.current = true; + }); }; useEffect(() => { diff --git a/src/app/components/preferredBtcAddress/addressTypeSelector.tsx b/src/app/components/preferredBtcAddress/addressTypeSelector.tsx new file mode 100644 index 000000000..a40b09a29 --- /dev/null +++ b/src/app/components/preferredBtcAddress/addressTypeSelector.tsx @@ -0,0 +1,32 @@ +import PreferredBtcAddressItem from '@components/preferredBtcAddressItem'; +import useBtcAddressBalance from '@hooks/useBtcAddressBalance'; +import { satsToBtc } from '@secretkeylabs/xverse-core'; +import BigNumber from 'bignumber.js'; + +type Props = { + title: string; + address?: string; + onClick: () => void; + isSelected: boolean; +}; + +function AddressTypeSelector({ title, onClick, address, isSelected }: Props) { + const { data } = useBtcAddressBalance(address ?? ''); + + const balance = + data?.confirmedBalance !== undefined + ? `${satsToBtc(BigNumber(data.confirmedBalance))} BTC` + : ''; + + return ( + + ); +} + +export default AddressTypeSelector; diff --git a/src/app/components/preferredBtcAddress/index.tsx b/src/app/components/preferredBtcAddress/index.tsx new file mode 100644 index 000000000..14e75662d --- /dev/null +++ b/src/app/components/preferredBtcAddress/index.tsx @@ -0,0 +1,126 @@ +import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletReducer from '@hooks/useWalletReducer'; +import useWalletSelector from '@hooks/useWalletSelector'; +import type { BtcPaymentType } from '@secretkeylabs/xverse-core'; +import Button from '@ui-library/button'; +import { StyledP } from '@ui-library/common.styled'; +import Sheet from '@ui-library/sheet'; +import { useState } from 'react'; +import toast from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import AddressTypeSelector from './addressTypeSelector'; + +const AddressTypeContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + gap: props.theme.space.xxs, + marginTop: props.theme.space.s, + marginBottom: props.theme.space.s, +})); + +type AddressTypeSelectorProps = { + selectedType: BtcPaymentType; + setSelectedType: (type: BtcPaymentType) => void; +}; + +export function AddressTypeSelectors({ selectedType, setSelectedType }: AddressTypeSelectorProps) { + const { t } = useTranslation('translation', { + keyPrefix: 'SETTING_SCREEN.PREFERRED_BTC_ADDRESS', + }); + const { btcAddresses } = useSelectedAccount(); + + return ( + <> + setSelectedType('native')} + isSelected={selectedType === 'native'} + /> + setSelectedType('nested')} + isSelected={selectedType === 'nested'} + /> + + ); +} + +type PreferredAddressSheetProps = { + visible: boolean; + onSave: (newType: BtcPaymentType) => void; + onCancel: () => void; + currentType: BtcPaymentType; +}; + +function PreferredBtcAddressSheet({ + visible, + onSave, + onCancel, + currentType, +}: PreferredAddressSheetProps) { + const { t } = useTranslation('translation', { + keyPrefix: 'SETTING_SCREEN.PREFERRED_BTC_ADDRESS', + }); + const { t: tSettings } = useTranslation('translation', { + keyPrefix: 'SETTING_SCREEN', + }); + const [selectedType, setSelectedType] = useState(currentType); + + const onSaveHandler = () => { + onSave(selectedType); + }; + + return ( + + {t('DESCRIPTION')} + + + + + ); +} diff --git a/src/app/components/receiveCardComponent/index.tsx b/src/app/components/receiveCardComponent/index.tsx index 5cf14d3e0..099e25b18 100644 --- a/src/app/components/receiveCardComponent/index.tsx +++ b/src/app/components/receiveCardComponent/index.tsx @@ -1,7 +1,9 @@ import Copy from '@assets/img/nftDashboard/Copy.svg'; import QrCode from '@assets/img/nftDashboard/QrCode.svg'; import Tick from '@assets/img/tick.svg'; +import { BtcAddressTypeForAddressLabel } from '@components/btcAddressTypeLabel'; import ActionButton from '@components/button'; +import useWalletSelector from '@hooks/useWalletSelector'; import { getShortTruncatedAddress } from '@utils/helper'; import { useEffect, useState, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,13 +14,18 @@ import styled from 'styled-components'; const ReceiveCard = styled.div((props) => ({ background: props.theme.colors.elevation6_600, borderRadius: props.theme.radius(2), - width: 328, - height: 104, + width: '100%', + minHeight: 105, padding: props.theme.space.m, + display: 'flex', + flexDirection: 'column', +})); + +const Container = styled.div({ display: 'flex', flexDirection: 'row', alignItems: 'center', -})); +}); const Button = styled.button((props) => ({ background: props.theme.colors.elevation6, @@ -52,6 +59,10 @@ const TitleText = styled.h1((props) => ({ ...props.theme.typography.body_bold_m, marginTop: props.theme.spacing(3), color: props.theme.colors.white_0, + display: 'flex', + flexDirection: 'row', + gap: props.theme.space.xs, + alignItems: 'center', })); const AddressText = styled.h1((props) => ({ @@ -76,10 +87,11 @@ type Props = { title: string; address: string; onQrAddressClick: () => void; - children: ReactNode; + children?: ReactNode; onCopyAddressClick?: () => void; showVerifyButton?: boolean; currency?: string; + icon?: React.ReactNode; }; function ReceiveCardComponent({ @@ -91,9 +103,13 @@ function ReceiveCardComponent({ onCopyAddressClick, showVerifyButton, currency, + icon, }: Props) { const [isCopied, setIsCopied] = useState(false); const { t } = useTranslation('translation', { keyPrefix: 'NFT_DASHBOARD_SCREEN' }); + + const { network } = useWalletSelector(); + let addressText = 'Receive Ordinals, Runes & BRC20 tokens'; if (currency === 'BTC') addressText = 'Receive payments in BTC'; @@ -115,43 +131,51 @@ function ReceiveCardComponent({ return ( - - {children} - {title} - - {showVerifyButton ? addressText : getShortTruncatedAddress(address)} - - - {showVerifyButton ? ( - - { - await chrome.tabs.create({ - url: chrome.runtime.getURL(`options.html#/verify-ledger?currency=${currency}`), - }); - }} - /> - - ) : ( - - - - )} + + + {icon} + + {title} + {currency === 'BTC' && ( + + )} + + + {showVerifyButton ? addressText : getShortTruncatedAddress(address)} + + + {showVerifyButton ? ( + + { + await chrome.tabs.create({ + url: chrome.runtime.getURL(`options.html#/verify-ledger?currency=${currency}`), + }); + }} + /> + + ) : ( + + + + )} + + {children} ); } diff --git a/src/app/components/seedPhraseInput/index.tsx b/src/app/components/seedPhraseInput/index.tsx index fdcc03800..15938f3d2 100644 --- a/src/app/components/seedPhraseInput/index.tsx +++ b/src/app/components/seedPhraseInput/index.tsx @@ -245,7 +245,7 @@ export default function SeedPhraseInput({ {seedError} - {t('HAVE_A_24_WORDS_SEEDPHRASE?', { number: show24Words ? '12' : '24' })} + {t('HAVE_A_COUNT_WORDS_SEED_PHRASE', { number: show24Words ? '12' : '24' })} ); diff --git a/src/app/components/tokenTile/index.tsx b/src/app/components/tokenTile/index.tsx index 3e4b452e6..5181dc011 100644 --- a/src/app/components/tokenTile/index.tsx +++ b/src/app/components/tokenTile/index.tsx @@ -1,7 +1,7 @@ import { BetterBarLoader } from '@components/barLoader'; import { StyledFiatAmountText } from '@components/fiatAmountText'; import TokenImage from '@components/tokenImage'; -import useBtcWalletData from '@hooks/queries/useBtcWalletData'; +import useSelectedAccountBtcBalance from '@hooks/queries/useSelectedAccountBtcBalance'; import useStxWalletData from '@hooks/queries/useStxWalletData'; import useSupportedCoinRates from '@hooks/queries/useSupportedCoinRates'; import useWalletSelector from '@hooks/useWalletSelector'; @@ -121,7 +121,7 @@ function TokenTile({ const { fiatCurrency } = useWalletSelector(); const { btcFiatRate, stxBtcRate } = useSupportedCoinRates(); const { data: stxData } = useStxWalletData(); - const { data: btcBalance } = useBtcWalletData(); + const { confirmedBalance: btcBalance } = useSelectedAccountBtcBalance(); const getTickerTitle = () => { if (currency === 'STX' || currency === 'BTC') return `${currency}`; diff --git a/src/app/components/tooltip/context.ts b/src/app/components/tooltip/context.ts new file mode 100644 index 000000000..7688a3453 --- /dev/null +++ b/src/app/components/tooltip/context.ts @@ -0,0 +1,8 @@ +import { createContext } from 'react'; + +export type TooltipContext = { + targetDiv?: HTMLDivElement; + initialised: boolean; +}; + +export const tooltipContext = createContext({ initialised: false }); diff --git a/src/app/components/tooltip/index.tsx b/src/app/components/tooltip/index.tsx new file mode 100644 index 000000000..ec11c6def --- /dev/null +++ b/src/app/components/tooltip/index.tsx @@ -0,0 +1,93 @@ +import Callout, { type CalloutProps } from '@ui-library/callout'; +import { useContext, useEffect, useState, type MutableRefObject } from 'react'; +import { createPortal } from 'react-dom'; +import styled from 'styled-components'; +import { tooltipContext } from './context'; + +type Position = { + $left?: number; + $right?: number; + $top?: number; + $bottom?: number; +}; + +const TooltipContainer = styled.div` + position: absolute; + ${(props) => (props.$left !== undefined ? `left: ${props.$left}px` : '')}; + ${(props) => (props.$right !== undefined ? `right: ${props.$right}px` : '')}; + ${(props) => (props.$top !== undefined ? `top: ${props.$top}px` : '')}; + ${(props) => (props.$bottom !== undefined ? `bottom: ${props.$bottom}px` : '')}; + + &::after { + content: ''; + position: absolute; + ${(props) => (props.$left !== undefined ? `left: 10px` : '')}; + ${(props) => (props.$right !== undefined ? `right: 10px` : '')}; + ${(props) => (props.$top !== undefined ? `top: -5px` : '')}; + ${(props) => (props.$bottom !== undefined ? `bottom: -5px` : '')}; + border-width: 5px; + border-style: solid; + border-color: #2d2f34; + transform: rotate(45deg); + } +`; + +const StyledCallout = styled(Callout)(() => ({ + backgroundColor: '#2d2f34', +})); + +type Props = CalloutProps & { + target: MutableRefObject; + positionVertical?: 'top' | 'bottom'; + positionHorizontal?: 'left' | 'right'; +}; + +export default function TooltipCallout(props: Props) { + const tooltip = useContext(tooltipContext); + + const [position, setPosition] = useState({}); + + const { + target, + className, + positionHorizontal = 'left', + positionVertical = 'bottom', + ...calloutProps + } = props; + + const tooltipBody = ( + + + + ); + + useEffect(() => { + if (!target.current) { + console.warn('Tooltip set without a target'); + return; + } + + const targetRect = target.current.getBoundingClientRect(); + const newPosition: Position = {}; + + const spacer = 5; + + if (positionVertical === 'bottom') { + newPosition.$top = targetRect.y + targetRect.height + spacer; + } else { + newPosition.$bottom = window.innerHeight - targetRect.y + spacer; + } + + if (positionHorizontal === 'left') { + newPosition.$right = window.innerWidth - targetRect.x - targetRect.width; + } else { + newPosition.$left = targetRect.x; + } + + setPosition(newPosition); + }, [positionHorizontal, positionVertical, target]); + + if (tooltip.initialised && tooltip.targetDiv) { + return createPortal(tooltipBody, tooltip.targetDiv); + } +} diff --git a/src/app/components/tooltip/provider.tsx b/src/app/components/tooltip/provider.tsx new file mode 100644 index 000000000..abfba93af --- /dev/null +++ b/src/app/components/tooltip/provider.tsx @@ -0,0 +1,29 @@ +import { useEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; +import { tooltipContext, type TooltipContext } from './context'; + +const TooltipContainer = styled.div(() => ({ + width: 0, + height: 0, + overflow: 'hidden', +})); + +type Props = { + children: React.ReactNode; +}; + +export default function TooltipProvider({ children }: Props) { + const tooltipParentRef = useRef(null); + const [contextValue, setContextValue] = useState({ initialised: false }); + + useEffect(() => { + setContextValue({ initialised: true, targetDiv: tooltipParentRef.current ?? undefined }); + }, []); + + return ( + + {children} + + + ); +} diff --git a/src/app/components/topRow/index.tsx b/src/app/components/topRow/index.tsx index f7cf1fec8..310d2d376 100644 --- a/src/app/components/topRow/index.tsx +++ b/src/app/components/topRow/index.tsx @@ -1,4 +1,5 @@ -import { ArrowLeft, DotsThreeVertical, Star } from '@phosphor-icons/react'; +import { ArrowLeft, DotsThreeVertical, FadersHorizontal, Star } from '@phosphor-icons/react'; +import type { MutableRefObject } from 'react'; import styled from 'styled-components'; import Theme from 'theme'; @@ -56,6 +57,8 @@ type Props = { showBackButton?: boolean; className?: string; onMenuClick?: (e: React.MouseEvent) => void; + onSettingsClick?: (e: React.MouseEvent) => void; + settingsRef?: MutableRefObject; onStarClick?: (e: React.MouseEvent) => void; isStarred?: boolean; }; @@ -66,6 +69,8 @@ function TopRow({ showBackButton = true, className, onMenuClick, + onSettingsClick, + settingsRef, onStarClick, isStarred, }: Props) { @@ -88,6 +93,11 @@ function TopRow({ )} + {onSettingsClick && ( + + + + )} ); } diff --git a/src/app/hooks/queries/useAccountBalance.ts b/src/app/hooks/queries/useAccountBalance.ts index 627bae3f7..e5790cc18 100644 --- a/src/app/hooks/queries/useAccountBalance.ts +++ b/src/app/hooks/queries/useAccountBalance.ts @@ -13,7 +13,7 @@ import { type TokensResponse, } from '@secretkeylabs/xverse-core'; import { setAccountBalanceAction } from '@stores/wallet/actions/actionCreators'; -import { calculateTotalBalance } from '@utils/helper'; +import { calculateTotalBalance, getAccountBalanceKey } from '@utils/helper'; import axios from 'axios'; import BigNumber from 'bignumber.js'; import { useEffect, useRef, useState } from 'react'; @@ -65,21 +65,35 @@ const useAccountBalance = () => { let finalRunesCoinsList: FungibleTokenWithStates[] = []; try { - if (account.btcAddress) { - const btcData: BtcAddressData = await btcClient.getBalance(account.btcAddress); - btcBalance = btcData.finalBalance.toString(); - } - - if (account.ordinalsAddress) { + const getBtcBalance = async (address?: string) => { + if (!address) { + return '0'; + } + const btcData: BtcAddressData = await btcClient.getBalance(address); + return btcData.finalBalance.toString(); + }; + + const [nativeBalance, nestedBalance, taprootBalance] = await Promise.all([ + getBtcBalance(account.btcAddresses.native?.address), + getBtcBalance(account.btcAddresses.nested?.address), + getBtcBalance(account.btcAddresses.taproot.address), + ]); + btcBalance = BigNumber(nativeBalance).plus(nestedBalance).plus(taprootBalance).toString(); + + if (account.btcAddresses.taproot.address) { const fetchBrc20Balances = fetchBrc20FungibleTokens( - account.ordinalsAddress, + account.btcAddresses.taproot.address, fiatCurrency, network, ); finalBrcCoinsList = (await fetchBrc20Balances()) .map(withDerivedState) .filter((ft) => ft.isEnabled); - const runeBalances = fetchRuneBalances(runesApi, account.ordinalsAddress, fiatCurrency); + const runeBalances = fetchRuneBalances( + runesApi, + account.btcAddresses.taproot.address, + fiatCurrency, + ); finalRunesCoinsList = (await runeBalances()) .map(withDerivedState) .filter((ft) => ft.isEnabled); @@ -119,7 +133,7 @@ const useAccountBalance = () => { btcFiatRate, hideStx, }); - dispatch(setAccountBalanceAction(account.btcAddress, totalBalance)); + dispatch(setAccountBalanceAction(getAccountBalanceKey(account), totalBalance)); return totalBalance; }; @@ -158,7 +172,7 @@ const useAccountBalance = () => { const setAccountBalance = (account: Account | null, balance: string) => { if (account) { - dispatch(setAccountBalanceAction(account.btcAddress, balance)); + dispatch(setAccountBalanceAction(getAccountBalanceKey(account), balance)); } }; diff --git a/src/app/hooks/queries/useSelectedAccountBtcBalance.tsx b/src/app/hooks/queries/useSelectedAccountBtcBalance.tsx new file mode 100644 index 000000000..8e059cefc --- /dev/null +++ b/src/app/hooks/queries/useSelectedAccountBtcBalance.tsx @@ -0,0 +1,39 @@ +import useBtcAddressBalance from '@hooks/useBtcAddressBalance'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import BigNumber from 'bignumber.js'; + +export default function useSelectedAccountBtcBalance() { + const selectedAccount = useSelectedAccount(); + const { data: nativeBalance, isLoading: nativeLoading } = useBtcAddressBalance( + selectedAccount.btcAddresses.native?.address ?? '', + ); + const { data: nestedBalance, isLoading: nestedLoading } = useBtcAddressBalance( + selectedAccount.btcAddresses.nested?.address ?? '', + ); + const { data: taprootBalance, isLoading: taprootLoading } = useBtcAddressBalance( + selectedAccount.btcAddresses.taproot.address ?? '', + ); + + if (nativeLoading || nestedLoading || taprootLoading) { + return { isLoading: true }; + } + + const confirmedBalance = BigNumber(nativeBalance?.confirmedBalance ?? 0) + .plus(nestedBalance?.confirmedBalance ?? 0) + .plus(taprootBalance?.confirmedBalance ?? 0) + .toNumber(); + + const unconfirmedBalance = BigNumber(nativeBalance?.unconfirmedBalance ?? 0) + .plus(nestedBalance?.unconfirmedBalance ?? 0) + .plus(taprootBalance?.unconfirmedBalance ?? 0) + .toNumber(); + + return { + confirmedBalance, + unconfirmedBalance, + nativeBalance, + nestedBalance, + taprootBalance, + isLoading: false, + }; +} diff --git a/src/app/hooks/queries/useTransactions.ts b/src/app/hooks/queries/useTransactions.ts index 087653ff8..3410105a8 100644 --- a/src/app/hooks/queries/useTransactions.ts +++ b/src/app/hooks/queries/useTransactions.ts @@ -24,8 +24,9 @@ export default function useTransactions( brc20Token: string | null, runeToken: string | null, ) { - const { stxAddress, btcAddress, ordinalsAddress } = useSelectedAccount(); - const { network, hasActivatedOrdinalsKey } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const { network, hasActivatedOrdinalsKey, btcPaymentAddressType, allowNestedSegWitAddress } = + useWalletSelector(); const selectedNetwork = useNetworkSelector(); const btcClient = useBtcClient(); const fetchTransactions = async (): Promise< @@ -35,25 +36,30 @@ export default function useTransactions( | APIGetRunesActivityForAddressResponse > => { if (coinType === 'FT' && brc20Token) { - return getBrc20History(network.type, ordinalsAddress, brc20Token); + return getBrc20History(network.type, selectedAccount.ordinalsAddress, brc20Token); } if (coinType === 'FT' && runeToken) { return getXverseApiClient(network.type).getRuneTxHistory( - ordinalsAddress, + selectedAccount.ordinalsAddress, runeToken, 0, PAGINATION_LIMIT, ); } if (coinType === 'STX' || coinType === 'FT' || coinType === 'NFT') { - return getStxAddressTransactions(stxAddress, selectedNetwork, 0, PAGINATION_LIMIT); + return getStxAddressTransactions( + selectedAccount.stxAddress, + selectedNetwork, + 0, + PAGINATION_LIMIT, + ); } if (coinType === 'BTC') { return fetchBtcTransactionsData( - btcAddress, - ordinalsAddress, + selectedAccount, btcClient, hasActivatedOrdinalsKey as boolean, + allowNestedSegWitAddress ? undefined : btcPaymentAddressType, ); } return []; diff --git a/src/app/hooks/useAvatarCleanup.ts b/src/app/hooks/useAvatarCleanup.ts index e9703c9c4..e166e1f29 100644 --- a/src/app/hooks/useAvatarCleanup.ts +++ b/src/app/hooks/useAvatarCleanup.ts @@ -1,6 +1,7 @@ import { - getNftCollections, + getStacksApiClient, getXverseApiClient, + isNftOwnedByAccount, isOrdinalOwnedByAccount, type Account, } from '@secretkeylabs/xverse-core'; @@ -8,48 +9,49 @@ import { removeAccountAvatarAction } from '@stores/wallet/actions/actionCreators import type { AvatarInfo } from '@stores/wallet/actions/types'; import { useCallback, useEffect } from 'react'; import { useDispatch } from 'react-redux'; -import useNetworkSelector from './useNetwork'; import useWalletSelector from './useWalletSelector'; /* Check and clear NFT Avatar that being used, but no-longer belongs to this Wallet */ export default function useAvatarCleanup() { const dispatch = useDispatch(); const { avatarIds, network, accountsList } = useWalletSelector(); - const selectedNetwork = useNetworkSelector(); const xverseApi = getXverseApiClient(network.type); + const stacksApiClient = getStacksApiClient(network.type); const checkAndRemoveInvalidAvatar = useCallback( - async (account: Account, avatar: AvatarInfo | null) => { + async (account: Account, avatar: AvatarInfo) => { + const { address } = account.btcAddresses.taproot; + if (avatar?.type === 'inscription') { const inscription = await xverseApi.getInscription( - account.ordinalsAddress, + account.btcAddresses.taproot.address, avatar.inscription.id, ); if (!isOrdinalOwnedByAccount(inscription, account)) { - dispatch(removeAccountAvatarAction({ address: account.btcAddress })); + dispatch(removeAccountAvatarAction({ address })); } } else if (avatar?.type === 'stacks') { - const nftList = await getNftCollections(account.stxAddress, selectedNetwork); - const isNftOwnedByAccount = nftList.results.some((collection) => - collection.all_nfts.some((nft) => { - const fullyQualifiedTokenId = `${nft.asset_identifier}:${nft.identifier.tokenId}`; - return fullyQualifiedTokenId === avatar.nft.fully_qualified_token_id; - }), + const isStacksOwnedByAccount = await isNftOwnedByAccount( + avatar.nft, + account, + stacksApiClient, ); - if (!isNftOwnedByAccount) { - dispatch(removeAccountAvatarAction({ address: account.btcAddress })); + if (!isStacksOwnedByAccount) { + dispatch(removeAccountAvatarAction({ address })); } } }, - [dispatch, selectedNetwork, xverseApi], + [dispatch, stacksApiClient, xverseApi], ); useEffect(() => { if (!avatarIds) return; accountsList - .filter((account) => !!avatarIds[account.btcAddress]) - .forEach((account) => checkAndRemoveInvalidAvatar(account, avatarIds[account.btcAddress])); + .filter((account) => !!avatarIds[account.btcAddresses.taproot.address]) + .forEach((account) => + checkAndRemoveInvalidAvatar(account, avatarIds[account.btcAddresses.taproot.address]), + ); }, [checkAndRemoveInvalidAvatar, avatarIds, accountsList]); } diff --git a/src/app/hooks/useBtcAddressBalance.ts b/src/app/hooks/useBtcAddressBalance.ts new file mode 100644 index 000000000..b77c1dff0 --- /dev/null +++ b/src/app/hooks/useBtcAddressBalance.ts @@ -0,0 +1,29 @@ +import { useQuery } from '@tanstack/react-query'; +import BigNumber from 'bignumber.js'; +import useBtcClient from './apiClients/useBtcClient'; + +export default function useBtcAddressBalance(address: string) { + const btcClient = useBtcClient(); + + const fetchBalance = async () => { + if (!address) { + return null; + } + + const addressData = await btcClient.getAddressData(address); + const confirmedBalance = BigNumber(addressData.chain_stats.funded_txo_sum) + .minus(addressData.chain_stats.spent_txo_sum) + .toNumber(); + const unconfirmedBalance = BigNumber(addressData.mempool_stats.funded_txo_sum) + .minus(addressData.mempool_stats.spent_txo_sum) + .toNumber(); + + return { address, confirmedBalance, unconfirmedBalance }; + }; + + return useQuery({ + queryKey: ['btc-address-balance', address], + queryFn: fetchBalance, + staleTime: 10 * 1000, // 10 secs + }); +} diff --git a/src/app/hooks/useCanUserSwitchPaymentType.ts b/src/app/hooks/useCanUserSwitchPaymentType.ts new file mode 100644 index 000000000..0eed33143 --- /dev/null +++ b/src/app/hooks/useCanUserSwitchPaymentType.ts @@ -0,0 +1,13 @@ +import useSelectedAccount from './useSelectedAccount'; +import useWalletSelector from './useWalletSelector'; + +export default function useCanUserSwitchPaymentType() { + const selectedAccount = useSelectedAccount(); + const { allowNestedSegWitAddress, selectedAccountType } = useWalletSelector(); + return !!( + allowNestedSegWitAddress && + selectedAccountType !== 'ledger' && + selectedAccount.btcAddresses.native && + selectedAccount.btcAddresses.nested + ); +} diff --git a/src/app/hooks/useSanityCheck.ts b/src/app/hooks/useSanityCheck.ts index ee0c7f955..6b2f25b9e 100644 --- a/src/app/hooks/useSanityCheck.ts +++ b/src/app/hooks/useSanityCheck.ts @@ -1,4 +1,4 @@ -import { getXverseApiClient, walletFromSeedPhrase } from '@secretkeylabs/xverse-core'; +import { getAccountFromSeedPhrase, getXverseApiClient } from '@secretkeylabs/xverse-core'; import { useCallback } from 'react'; import useSeedVault from './useSeedVault'; import useWalletSelector from './useWalletSelector'; @@ -13,12 +13,14 @@ const useSanityCheck = () => { async (origin: string) => { const mnemonic = await getSeed(); - const wallet = await walletFromSeedPhrase({ mnemonic, index: 0n, network: network.type }); + const wallet = await getAccountFromSeedPhrase({ mnemonic, index: 0n, network: network.type }); if (wallet.masterPubKey !== accountsList[0].masterPubKey) { await getXverseApiClient(network.type).getAppFeatures( { - ordinalsAddress: accountsList[0].ordinalsAddress, - paymentAddress: accountsList[0].btcAddress, + ordinalsAddress: accountsList[0].btcAddresses.taproot.address, + paymentAddress: + accountsList[0].btcAddresses.native?.address || + accountsList[0].btcAddresses.nested?.address, }, { [origin]: VERSION, diff --git a/src/app/hooks/useSelectedAccount.ts b/src/app/hooks/useSelectedAccount.ts index 8ba5f7d43..7af390a7e 100644 --- a/src/app/hooks/useSelectedAccount.ts +++ b/src/app/hooks/useSelectedAccount.ts @@ -1,39 +1,65 @@ -import getSelectedAccount from '@common/utils/getSelectedAccount'; +import getSelectedAccount, { + embellishAccountWithDetails, + type AccountWithDetails, +} from '@common/utils/getSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; -import type { Account } from '@secretkeylabs/xverse-core'; +import type { BtcPaymentType } from '@secretkeylabs/xverse-core'; import { useMemo } from 'react'; import useWalletReducer from './useWalletReducer'; -const useSelectedAccount = (): Account => { +const useSelectedAccount = (overridePayAddressType?: BtcPaymentType): AccountWithDetails => { const { switchAccount } = useWalletReducer(); const { selectedAccountIndex, selectedAccountType, accountsList: softwareAccountsList, ledgerAccountsList, + btcPaymentAddressType, } = useWalletSelector(); return useMemo(() => { - const existingAccount = getSelectedAccount({ + let account = getSelectedAccount({ selectedAccountIndex, selectedAccountType, softwareAccountsList, ledgerAccountsList, }); - if (existingAccount) { - return existingAccount; + + if (!account) { + [account] = softwareAccountsList; + if (account) { + switchAccount(account); + } } - const fallbackAccount = softwareAccountsList[0]; - if (fallbackAccount) { - switchAccount(fallbackAccount); + + let accountType = btcPaymentAddressType; + + if (overridePayAddressType) { + switch (overridePayAddressType) { + case 'nested': + if (account.btcAddresses.nested) { + accountType = overridePayAddressType; + } + break; + case 'native': + if (account.btcAddresses.native) { + accountType = overridePayAddressType; + } + break; + default: + break; + } } - return fallbackAccount; + + return embellishAccountWithDetails(account, accountType); }, [ selectedAccountIndex, selectedAccountType, softwareAccountsList, ledgerAccountsList, switchAccount, + btcPaymentAddressType, + overridePayAddressType, ]); }; diff --git a/src/app/hooks/useStorage.ts b/src/app/hooks/useStorage.ts new file mode 100644 index 000000000..8b8a792e4 --- /dev/null +++ b/src/app/hooks/useStorage.ts @@ -0,0 +1,45 @@ +import { useCallback, useMemo, useState } from 'react'; + +const tryParse = (val: string | null | undefined, defaultValue?: T) => { + if (!val) return defaultValue; + try { + return JSON.parse(val) as T; + } catch (e) { + return defaultValue; + } +}; + +const useStorage = (storageType: 'local' | 'session', key: string, defaultValue?: T) => { + const lookupKey = `useStorage:${key}`; + + const storage = useMemo(() => { + if (storageType === 'local') return localStorage; + return sessionStorage; + }, [storageType]); + + const [value, setValue] = useState(tryParse(storage.getItem(lookupKey), defaultValue)); + + const set = useCallback( + (newValue: T | undefined) => { + if (newValue === undefined) { + storage.removeItem(lookupKey); + setValue(undefined); + } else { + storage.setItem(lookupKey, JSON.stringify(newValue)); + setValue(newValue); + } + }, + [lookupKey, storage], + ); + + return [value, set] as const; +}; + +// !NOTE: add localStorage if needed. Knip fails if it's added and unused. +export function useSessionStorage( + key: string, +): [T | undefined, (newValue: T | undefined) => void]; +export function useSessionStorage(key: string, defaultValue: T): [T, (newValue: T) => void]; +export function useSessionStorage(key: string, defaultValue?: T) { + return useStorage('session', key, defaultValue); +} diff --git a/src/app/hooks/useTransactionContext.ts b/src/app/hooks/useTransactionContext.ts index 1ef680760..85903626c 100644 --- a/src/app/hooks/useTransactionContext.ts +++ b/src/app/hooks/useTransactionContext.ts @@ -1,12 +1,12 @@ -import { btcTransaction, UtxoCache } from '@secretkeylabs/xverse-core'; +import { btcTransaction, UtxoCache, type BtcPaymentType } from '@secretkeylabs/xverse-core'; import { useMemo } from 'react'; import useBtcClient from './apiClients/useBtcClient'; import useSeedVault from './useSeedVault'; import useSelectedAccount from './useSelectedAccount'; import useWalletSelector from './useWalletSelector'; -const useTransactionContext = () => { - const selectedAccount = useSelectedAccount(); +const useTransactionContext = (overridePayAddressType?: BtcPaymentType) => { + const selectedAccount = useSelectedAccount(overridePayAddressType); const { network } = useWalletSelector(); const seedVault = useSeedVault(); const btcClient = useBtcClient(); @@ -43,6 +43,7 @@ const useTransactionContext = () => { utxoCache, network: network.type, esploraApiProvider: btcClient, + btcPaymentAddressType: selectedAccount.btcAddressType, }); }, [utxoCache, selectedAccount, network, seedVault, btcClient]); diff --git a/src/app/hooks/useWalletReducer.ts b/src/app/hooks/useWalletReducer.ts index 6dd40d5a4..011b31a28 100644 --- a/src/app/hooks/useWalletReducer.ts +++ b/src/app/hooks/useWalletReducer.ts @@ -1,25 +1,28 @@ +import getSelectedAccount from '@common/utils/getSelectedAccount'; import { getDeviceAccountIndex } from '@common/utils/ledger'; import { dispatchEventAuthorizedConnectedClients } from '@common/utils/messages/extensionToContentScript/dispatchEvent'; import { makeAccountResourceId } from '@components/permissionsManager/resources'; +import type { Permission } from '@components/permissionsManager/schemas'; import useNetworkSelector from '@hooks/useNetwork'; import useWalletSelector from '@hooks/useWalletSelector'; import { AnalyticsEvents, - StacksMainnet, - StacksNetwork, - StacksTestnet, createWalletAccount, decryptSeedPhraseCBC, - getBnsName, + getAccountFromSeedPhrase, restoreWalletWithAccounts, - walletFromSeedPhrase, + StacksMainnet, + StacksNetwork, + StacksTestnet, type Account, type NetworkType, type SettingsNetwork, } from '@secretkeylabs/xverse-core'; import { + ChangeBtcPaymentAddressType, ChangeNetworkAction, changeShowDataCollectionAlertAction, + EnableNestedSegWitAddress, resetWalletAction, selectAccount, setWalletHideStxAction, @@ -39,45 +42,22 @@ import { } from '@utils/mixpanel'; import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; +import useBtcClient from './apiClients/useBtcClient'; import useSeedVault from './useSeedVault'; import useWalletSession from './useWalletSession'; -// TODO: move this to core as the primary way to create an account const createSingleAccount = async ( seedPhrase: string, accountIndex: number, btcNetwork: NetworkType, - stacksNetwork: StacksNetwork, savedNames: { id: number; name?: string }[] = [], ) => { - const { - stxAddress, - btcAddress, - ordinalsAddress, - masterPubKey, - stxPublicKey, - btcPublicKey, - ordinalsPublicKey, - } = await walletFromSeedPhrase({ + const account = await getAccountFromSeedPhrase({ mnemonic: seedPhrase, index: BigInt(accountIndex), network: btcNetwork, }); - const bnsName = await getBnsName(stxAddress, stacksNetwork); - const customName = savedNames.find((name) => name.id === accountIndex)?.name; - const account: Account = { - id: accountIndex, - stxAddress, - btcAddress, - ordinalsAddress, - masterPubKey, - stxPublicKey, - btcPublicKey, - ordinalsPublicKey, - bnsName, - accountName: customName, - accountType: 'software', - }; + account.accountName = savedNames.find((name) => name.id === accountIndex)?.name; return account; }; @@ -88,7 +68,7 @@ const useWalletReducer = () => { encryptedSeed, selectedAccountIndex, selectedAccountType, - accountsList: accounts, + accountsList: softwareAccountsList, savedNames, ledgerAccountsList, showDataCollectionAlert, @@ -96,6 +76,13 @@ const useWalletReducer = () => { } = useWalletSelector(); const seedVault = useSeedVault(); const selectedNetwork = useNetworkSelector(); + const currentlySelectedAccount = getSelectedAccount({ + selectedAccountIndex, + selectedAccountType, + softwareAccountsList, + ledgerAccountsList, + }); + const btcClient = useBtcClient(); const dispatch = useDispatch(); const { setSessionStartTime, clearSessionTime, setSessionStartTimeAndMigrate } = @@ -106,6 +93,7 @@ const useWalletReducer = () => { async ( selectedType = selectedAccountType, selectedIndex = selectedAccountIndex, + accountsList = softwareAccountsList, ): Promise => { if (selectedType === 'ledger') { // these accounts are created by Ledger, so we cannot regenerate them @@ -117,11 +105,10 @@ const useWalletReducer = () => { seedPhrase, selectedIndex, network.type, - selectedNetwork, savedNames[network.type], ); - const selectedAccount = accounts.find((account) => account.id === selectedIndex); + const selectedAccount = accountsList.find((account) => account.id === selectedIndex); if (!selectedAccount) { // if the selected account index does not exist, we cannot update it @@ -130,11 +117,11 @@ const useWalletReducer = () => { } const accountsMatch = Object.keys(recreatedAccount).every( - (key) => selectedAccount[key] === recreatedAccount[key], + (key) => JSON.stringify(selectedAccount[key]) === JSON.stringify(recreatedAccount[key]), ); if (!accountsMatch) { - const newAccountsList = accounts.map((account) => + const newAccountsList = accountsList.map((account) => account.id === selectedIndex ? recreatedAccount : account, ); @@ -147,8 +134,7 @@ const useWalletReducer = () => { seedVault, selectedAccountIndex, selectedAccountType, - selectedNetwork, - accounts, + softwareAccountsList, savedNames, ], ); @@ -156,28 +142,55 @@ const useWalletReducer = () => { const loadActiveAccounts = async ( secretKey: string, currentNetwork: SettingsNetwork, - currentNetworkObject: StacksNetwork, + currentStacksNetwork: StacksNetwork, currentAccounts: Account[], - resetIndex?: boolean, + options?: { + resetIndex?: boolean; + checkForNewAccounts?: boolean; + accountLoadCallback?: (loadedAccounts: Account[]) => void; + }, ) => { - const walletAccounts = await restoreWalletWithAccounts( + const newSoftwareAccountList: Account[] = []; + + // Load custom account names for the new network + const savedCustomAccountNames = savedNames[currentNetwork.type]; + + const walletAccountsGenerator = restoreWalletWithAccounts( + btcClient, secretKey, currentNetwork, - currentNetworkObject, + currentStacksNetwork, currentAccounts, + options?.checkForNewAccounts ? 1 : 0, + true, ); - // Load custom account names for the new network - const savedCustomAccountNames = savedNames[currentNetwork.type]; - if (savedCustomAccountNames?.length) { - walletAccounts.forEach((account) => { - const savedAccount = savedCustomAccountNames.find((acc) => acc.id === account.id); + let newAccountResponse = await walletAccountsGenerator.next(); + + while (!newAccountResponse.done) { + const newAccount = newAccountResponse.value; + if (savedCustomAccountNames?.length) { + const savedAccount = savedCustomAccountNames.find((acc) => acc.id === newAccount.id); if (savedAccount) { - account.accountName = savedAccount.name; + newAccount.accountName = savedAccount.name; } - }); + } + newSoftwareAccountList.push(newAccount); + + if (newSoftwareAccountList.length >= currentAccounts.length) { + dispatch(updateSoftwareAccountsAction([...newSoftwareAccountList])); + + if (options?.accountLoadCallback) { + await options.accountLoadCallback([...newSoftwareAccountList]); + } + } + + newAccountResponse = await walletAccountsGenerator.next(); } + const finalNewAccountsList = newAccountResponse.value; + dispatch(updateSoftwareAccountsAction(finalNewAccountsList)); + // ledger accounts initially didn't have a deviceAccountIndex // this is a migration to add the deviceAccountIndex to the ledger accounts without them // it should only fire once if ever @@ -193,13 +206,9 @@ const useWalletReducer = () => { dispatch(updateLedgerAccountsAction(newLedgerAccountsList)); } - dispatch(updateSoftwareAccountsAction(walletAccounts)); - - if (resetIndex) { - dispatch(selectAccount(walletAccounts[0])); + if (options?.resetIndex) { + dispatch(selectAccount(finalNewAccountsList[0])); } - - return walletAccounts; }; const migrateLegacySeedStorage = async (password: string) => { @@ -212,18 +221,21 @@ const useWalletReducer = () => { }; const loadAccountNames = () => { - const updatedSavedNames = accounts.reduce<{ id: number; name: string }[]>((acc, account) => { - if (account.accountName) { - acc.push({ id: account.id, name: account.accountName }); - } - return acc; - }, []); + const updatedSavedNames = softwareAccountsList.reduce<{ id: number; name: string }[]>( + (acc, account) => { + if (account.accountName) { + acc.push({ id: account.id, name: account.accountName }); + } + return acc; + }, + [], + ); dispatch(updateSavedNamesAction(network.type, updatedSavedNames)); }; - const loadWallet = async () => { + const loadWallet = async (onReady?: () => void) => { const seedPhrase = await seedVault.getSeed(); - const currentAccounts = accounts || []; + let currentAccounts = softwareAccountsList || []; if (currentAccounts.length === 0) { // This will happen on first load after the wallet is created. We create the accounts here to ensure @@ -234,25 +246,68 @@ const useWalletReducer = () => { seedPhrase, 0, network.type, - selectedNetwork, savedNames[network.type], ); - currentAccounts.push(account); - await loadActiveAccounts(seedPhrase, network, selectedNetwork, currentAccounts); + currentAccounts = [account]; } - await ensureSelectedAccountValid(); + // we need to generate the nested and native segwit address for each account if not yet generated + // this would happen on the first load after native segwit was added + const accountsHaveFullAddressData = currentAccounts.every( + (account) => account.btcAddresses.native && account.btcAddresses.nested, + ); + + if (!accountsHaveFullAddressData) { + currentAccounts = await Promise.all( + currentAccounts.map(async (account) => { + if (!account.btcAddresses.native || !account.btcAddresses.nested) { + const updatedAccount = await createSingleAccount( + seedPhrase, + account.id, + network.type, + savedNames[network.type], + ); + + return updatedAccount; + } + + return account; + }), + ); + } - if (!savedNames[network.type]?.length && accounts.some((account) => !!account.accountName)) { + if ( + !savedNames[network.type]?.length && + softwareAccountsList.some((account) => !!account.accountName) + ) { // there was no savedNames store object initially // this is a migration to add the savedNames if there are custom account names that are not saved // it should only fire once if ever loadAccountNames(); } - dispatch(setWalletUnlockedAction(true)); - setSessionStartTimeAndMigrate(); + let initialised = false; + const initialise = () => { + if (initialised) return; + + onReady?.(); + dispatch(setWalletUnlockedAction(true)); + setSessionStartTimeAndMigrate(); + initialised = true; + }; + + await loadActiveAccounts(seedPhrase, network, selectedNetwork, currentAccounts, { + checkForNewAccounts: network.type === 'Mainnet', + accountLoadCallback: async (loadedAccounts) => { + if (loadedAccounts.length === currentAccounts.length) { + await ensureSelectedAccountValid(undefined, undefined, loadedAccounts); + initialise(); + } + }, + }); + + initialise(); }; const unlockWallet = async (password: string) => { @@ -299,13 +354,7 @@ const useWalletReducer = () => { // We create an account to ensure that the seed phrase is valid, but we don't store it // The actual account creation is done on startup of the wallet // If the seed phrase is invalid, then this will throw an error - await createSingleAccount( - seedPhrase, - 0, - network.type, - selectedNetwork, - savedNames[network.type], - ); + await createSingleAccount(seedPhrase, 0, network.type, savedNames[network.type]); await chrome.storage.local.clear(); await chrome.storage.session.clear(); @@ -322,12 +371,12 @@ const useWalletReducer = () => { // reinitialise with masterpubkey hash now that we have it if (hasOptedInMixPanelTracking()) { const seed = await seedVault.getSeed(); - const wallet = await walletFromSeedPhrase({ + const account = await getAccountFromSeedPhrase({ mnemonic: seed, index: 0n, network: 'Mainnet', }); - optInMixPanel(wallet.masterPubKey); + optInMixPanel(account.masterPubKey); } } localStorage.setItem('migrated', 'true'); @@ -348,7 +397,12 @@ const useWalletReducer = () => { const createAccount = async () => { const seedPhrase = await seedVault.getSeed(); - const newAccounts = await createWalletAccount(seedPhrase, network, selectedNetwork, accounts); + const newAccounts = await createWalletAccount( + seedPhrase, + network, + selectedNetwork, + softwareAccountsList, + ); dispatch(updateSoftwareAccountsAction(newAccounts)); }; @@ -362,32 +416,36 @@ const useWalletReducer = () => { dispatch(selectAccount(nextAccount)); - dispatchEventAuthorizedConnectedClients( + const changeEventPermissions: Omit[] = [ { resourceId: makeAccountResourceId({ - accountId: accounts[selectedAccountIndex].id, - masterPubKey: accounts[selectedAccountIndex].masterPubKey, + accountId: nextAccount.id, + masterPubKey: nextAccount.masterPubKey, networkType: network.type, }), actions: new Set(['read']), }, - { type: 'accountChange' }, - ); + ]; + if (currentlySelectedAccount) { + changeEventPermissions.push({ + resourceId: makeAccountResourceId({ + accountId: currentlySelectedAccount.id, + masterPubKey: currentlySelectedAccount.masterPubKey, + networkType: network.type, + }), + actions: new Set(['read']), + }); + } + + dispatchEventAuthorizedConnectedClients(changeEventPermissions, { type: 'accountChange' }); }, - [ - accounts, - dispatch, - ensureSelectedAccountValid, - network.type, - queryClient, - selectedAccountIndex, - ], + [dispatch, ensureSelectedAccountValid, network.type, queryClient, currentlySelectedAccount], ); const changeNetwork = async (changedNetwork: SettingsNetwork) => { // Save current custom account names const currentNetworkType = network.type; - const customAccountNames = accounts.map((account) => ({ + const customAccountNames = softwareAccountsList.map((account) => ({ id: account.id, name: account.accountName, })); @@ -399,17 +457,29 @@ const useWalletReducer = () => { dispatch(ChangeNetworkAction(changedNetwork)); - dispatchEventAuthorizedConnectedClients( - { - resourceId: makeAccountResourceId({ - accountId: accounts[selectedAccountIndex].id, - masterPubKey: accounts[selectedAccountIndex].masterPubKey, - networkType: network.type, - }), - actions: new Set(['read']), - }, - { type: 'networkChange' }, - ); + if (currentlySelectedAccount) { + dispatchEventAuthorizedConnectedClients( + [ + { + resourceId: makeAccountResourceId({ + accountId: currentlySelectedAccount.id, + masterPubKey: currentlySelectedAccount.masterPubKey, + networkType: network.type, + }), + actions: new Set(['read']), + }, + { + resourceId: makeAccountResourceId({ + accountId: currentlySelectedAccount.id, + masterPubKey: currentlySelectedAccount.masterPubKey, + networkType: changedNetwork.type, + }), + actions: new Set(['read']), + }, + ], + { type: 'networkChange' }, + ); + } const seedPhrase = await seedVault.getSeed(); const changedStacksNetwork = @@ -420,18 +490,19 @@ const useWalletReducer = () => { const nextAccounts: Account[] = []; // we recreate the same number of accounts on the new network - for (let i = 0; i < accounts.length; i++) { + for (let i = 0; i < softwareAccountsList.length; i++) { const account = await createSingleAccount( seedPhrase, i, changedNetwork.type, - changedStacksNetwork, savedNames[changedNetwork.type], ); nextAccounts.push(account); } - await loadActiveAccounts(seedPhrase, changedNetwork, changedStacksNetwork, nextAccounts, true); + await loadActiveAccounts(seedPhrase, changedNetwork, changedStacksNetwork, nextAccounts, { + resetIndex: true, + }); }; const addLedgerAccount = async (ledgerAccount: Account) => { @@ -458,12 +529,11 @@ const useWalletReducer = () => { dispatch(updateLedgerAccountsAction(newLedgerAccountsList)); }; - // TODO: refactor this to be more specific to renaming software accounts - const renameAccount = async (updatedAccount: Account) => { + const renameSoftwareAccount = async (updatedAccount: Account) => { if (updatedAccount.accountType !== 'software') { throw new Error('Expected software account. Renaming cancelled.'); } - const newAccountsList = accounts.map((account) => + const newAccountsList = softwareAccountsList.map((account) => account.id === updatedAccount.id ? updatedAccount : account, ); @@ -481,6 +551,30 @@ const useWalletReducer = () => { dispatch(updateSavedNamesAction(network.type, updatedSavedNames)); }; + const enableNestedSegWitAddress = () => { + dispatch(EnableNestedSegWitAddress()); + }; + + const changeBtcPaymentAddressType = async (btcPaymentAddressType: 'native' | 'nested') => { + dispatch(ChangeBtcPaymentAddressType(btcPaymentAddressType)); + + if (currentlySelectedAccount) { + dispatchEventAuthorizedConnectedClients( + [ + { + resourceId: makeAccountResourceId({ + accountId: currentlySelectedAccount.id, + masterPubKey: currentlySelectedAccount.masterPubKey, + networkType: network.type, + }), + actions: new Set(['read']), + }, + ], + { type: 'accountChange' }, + ); + } + }; + return { unlockWallet, loadWallet, @@ -494,9 +588,11 @@ const useWalletReducer = () => { addLedgerAccount, removeLedgerAccount, updateLedgerAccounts, - renameAccount, + renameSoftwareAccount, toggleStxVisibility, changeShowDataCollectionAlert, + enableNestedSegWitAddress, + changeBtcPaymentAddressType, }; }; diff --git a/src/app/hooks/useWalletSelector.ts b/src/app/hooks/useWalletSelector.ts index 69cb74569..489d00ef9 100644 --- a/src/app/hooks/useWalletSelector.ts +++ b/src/app/hooks/useWalletSelector.ts @@ -2,9 +2,7 @@ import type { StoreState } from '@stores/index'; import { useSelector } from 'react-redux'; const useWalletSelector = () => { - const walletState = useSelector((state: StoreState) => ({ - ...state.walletState, - })); + const walletState = useSelector((state: StoreState) => state.walletState); return { ...walletState, diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index 5c4d20d74..a6cbdee70 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -58,6 +58,7 @@ import SendStxScreen from '@screens/sendStx'; import Setting from '@screens/settings'; import About from '@screens/settings/about'; import AdvancedSettings from '@screens/settings/advanced'; +import PaymentAddressTypeSelector from '@screens/settings/advanced/paymentAddressTypeSelector'; import RestoreFunds from '@screens/settings/advanced/restoreFunds'; import RecoverRunes from '@screens/settings/advanced/restoreFunds/recoverRunes'; import RestoreOrdinals from '@screens/settings/advanced/restoreFunds/restoreOrdinals'; @@ -372,6 +373,14 @@ const router = createHashRouter([ ), }, + { + path: RoutePaths.PreferredAddress, + element: ( + + + + ), + }, { path: 'restore-funds', element: , diff --git a/src/app/routes/paths.ts b/src/app/routes/paths.ts index fc938663d..786801b18 100644 --- a/src/app/routes/paths.ts +++ b/src/app/routes/paths.ts @@ -8,6 +8,7 @@ enum RoutePaths { Preferences = '/preferences', Security = '/security', AdvancedSettings = '/advanced-settings', + PreferredAddress = '/preferred-address', TransferRunesRequest = '/transfer-runes-request', AccountList = '/account-list', } diff --git a/src/app/screens/accountList/index.tsx b/src/app/screens/accountList/index.tsx index 1fb8a50ff..5688219a9 100644 --- a/src/app/screens/accountList/index.tsx +++ b/src/app/screens/accountList/index.tsx @@ -1,5 +1,5 @@ import ConnectLedger from '@assets/img/dashboard/connect_ledger.svg'; -import { filterLedgerAccounts } from '@common/utils/ledger'; +import { filterLedgerAccountsByNetwork } from '@common/utils/ledger'; import LazyAccountRow from '@components/accountRow/lazyAccountRow'; import Separator from '@components/separator'; import TopRow from '@components/topRow'; @@ -67,7 +67,7 @@ function AccountList(): JSX.Element { const hideListActions = Boolean(params.get('hideListActions')) || false; const displayedAccountsList = useMemo(() => { - const networkLedgerAccounts = filterLedgerAccounts(ledgerAccountsList, network.type); + const networkLedgerAccounts = filterLedgerAccountsByNetwork(ledgerAccountsList, network.type); return [...networkLedgerAccounts, ...accountsList]; }, [accountsList, ledgerAccountsList, network]); @@ -84,8 +84,8 @@ function AccountList(): JSX.Element { }; const isAccountSelected = (account: Account) => - account.btcAddress === selectedAccount?.btcAddress && - account.stxAddress === selectedAccount?.stxAddress; + account.btcAddresses.taproot.address === selectedAccount.ordinalsAddress && + account.stxAddress === selectedAccount.stxAddress; const onCreateAccount = async () => { await createAccount(); @@ -105,7 +105,7 @@ function AccountList(): JSX.Element { {t('TITLE')} {displayedAccountsList.map((account) => ( -
+
({ + display: 'flex', + flexDirection: 'row', + width: '100%', + marginBottom: props.theme.space.m, +})); + +const IconOuterContainer = styled.div((_) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +})); + +const IconContainer = styled.div((_) => ({ + position: 'relative', +})); + +const TitleContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + margin: `${props.theme.space.s} ${props.theme.space.m}`, +})); + +const AddressTypeContainer = styled.div((_) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: '4px', +})); + +const BalanceContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-end', + margin: `${props.theme.space.s} ${props.theme.space.m}`, + flexGrow: 1, +})); + +const StarIconContainer = styled.div((props) => ({ + position: 'absolute', + right: '-8px', + bottom: '4px', + backgroundColor: props.theme.colors.elevation0, + borderRadius: '50%', + padding: '2.2px', + display: 'flex', + alignItems: 'center', +})); + +type AddressBalanceProps = { + balance: number | undefined; + addressType: BtcAddressType | undefined; + totalBalance: number | undefined; +}; + +export default function AddressBalance({ + balance, + addressType, + totalBalance, +}: AddressBalanceProps) { + const { fiatCurrency, btcPaymentAddressType } = useWalletSelector(); + const { btcFiatRate } = useSupportedCoinRates(); + + if (balance === undefined) { + return null; + } + + const balancePercentage = + balance && totalBalance + ? BigNumber(balance).div(totalBalance).multipliedBy(100).toFixed(2) + : '0.00'; + + const isCurrentPaymentAddress = addressType === btcPaymentAddressType; + + return ( + + + + + {isCurrentPaymentAddress && ( + + + + )} + + + + + BTC + {addressType && } + + {`${balancePercentage}%`} + + + {satsToBtc(BigNumber(balance)).toString()} + + + + + + ); +} diff --git a/src/app/screens/coinDashboard/coins/btc/balanceBreakdown.tsx b/src/app/screens/coinDashboard/coins/btc/balanceBreakdown.tsx new file mode 100644 index 000000000..b13c4cc77 --- /dev/null +++ b/src/app/screens/coinDashboard/coins/btc/balanceBreakdown.tsx @@ -0,0 +1,53 @@ +import useSelectedAccountBtcBalance from '@hooks/queries/useSelectedAccountBtcBalance'; +import useWalletSelector from '@hooks/useWalletSelector'; +import Spinner from '@ui-library/spinner'; +import styled from 'styled-components'; +import { SecondaryContainer } from '../../index.styled'; +import AddressBalance from './addressBalance'; + +const SpinnerContainer = styled.div` + width: 100%; + display: flex; + justify-content: center; + align-items: center; +`; + +export default function BalanceBreakdown() { + const { btcPaymentAddressType } = useWalletSelector(); + const { confirmedBalance, nativeBalance, nestedBalance, taprootBalance, isLoading } = + useSelectedAccountBtcBalance(); + + if (isLoading) { + return ( + + + + + + ); + } + + const showNested = btcPaymentAddressType === 'nested' || nestedBalance?.confirmedBalance !== 0; + + return ( + + + {showNested && ( + + )} + + + ); +} diff --git a/src/app/screens/coinDashboard/coins/btc/index.tsx b/src/app/screens/coinDashboard/coins/btc/index.tsx new file mode 100644 index 000000000..489dc71f3 --- /dev/null +++ b/src/app/screens/coinDashboard/coins/btc/index.tsx @@ -0,0 +1,77 @@ +import { GlobalPreferredBtcAddressSheet } from '@components/preferredBtcAddress'; +import BottomBar from '@components/tabBar'; +import TopRow from '@components/topRow'; +import useCanUserSwitchPaymentType from '@hooks/useCanUserSwitchPaymentType'; +import { broadcastResetUserFlow, useResetUserFlow } from '@hooks/useResetUserFlow'; +import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSearchParams } from 'react-router-dom'; +import CoinHeader from '../../coinHeader'; +import { Button, Container, FtInfoContainer } from '../../index.styled'; +import TransactionsHistoryList from '../../transactionsHistoryList'; +import BalanceBreakdown from './balanceBreakdown'; + +export default function CoinDashboard() { + const [searchParams] = useSearchParams(); + const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' }); + const fromSecondaryTab = searchParams.get('secondaryTab') === 'true' ? 'secondary' : 'primary'; + + const [currentTab, setCurrentTab] = useState<'primary' | 'secondary'>(fromSecondaryTab); + const [showPreferredBtcAddressSheet, setShowPreferredBtcAddressSheet] = useState(false); + + useResetUserFlow('/coinDashboard'); + + const showBtcAddressTypeSelector = useCanUserSwitchPaymentType(); + + const handleGoBack = () => broadcastResetUserFlow(); + const handleChangeAddressTypeClick = showBtcAddressTypeSelector + ? () => setShowPreferredBtcAddressSheet(true) + : undefined; + + useTrackMixPanelPageViewed({}); + + const onCancelAddressType = () => setShowPreferredBtcAddressSheet(false); + + return ( + <> + + + + {/* TODO: import { Tabs } from ui-library/tabs.tsx */} + + + + + {currentTab === 'primary' && ( + + )} + {currentTab === 'secondary' && } + + + + + ); +} diff --git a/src/app/screens/coinDashboard/coins/other.tsx b/src/app/screens/coinDashboard/coins/other.tsx new file mode 100644 index 000000000..b6d5c09ad --- /dev/null +++ b/src/app/screens/coinDashboard/coins/other.tsx @@ -0,0 +1,248 @@ +import linkIcon from '@assets/img/linkIcon.svg'; +import CopyButton from '@components/copyButton'; +import OptionsDialog from '@components/optionsDialog/optionsDialog'; +import BottomBar from '@components/tabBar'; +import TopRow from '@components/topRow'; +import { useVisibleBrc20FungibleTokens } from '@hooks/queries/ordinals/useGetBrc20FungibleTokens'; +import { useVisibleRuneFungibleTokens } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; +import useRuneUtxosQuery from '@hooks/queries/runes/useRuneUtxosQuery'; +import { useVisibleSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens'; +import useSpamTokens from '@hooks/queries/useSpamTokens'; +import { broadcastResetUserFlow, useResetUserFlow } from '@hooks/useResetUserFlow'; +import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed'; +import { Flag } from '@phosphor-icons/react'; +import RuneBundleRow from '@screens/coinDashboard/runes/bundleRow'; +import type { FungibleToken } from '@secretkeylabs/xverse-core'; +import { mapRareSatsAPIResponseToBundle } from '@secretkeylabs/xverse-core'; +import { + setBrc20ManageTokensAction, + setRunesManageTokensAction, + setSip10ManageTokensAction, + setSpamTokenAction, +} from '@stores/wallet/actions/actionCreators'; +import { SPAM_OPTIONS_WIDTH, type CurrencyTypes } from '@utils/constants'; +import { ftDecimals, getExplorerUrl, getTruncatedAddress } from '@utils/helper'; +import { getFullTxId, getTxIdFromFullTxId, getVoutFromFullTxId } from '@utils/runes'; +import BigNumber from 'bignumber.js'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; +import { useParams, useSearchParams } from 'react-router-dom'; +import Theme from 'theme'; +import CoinHeader from '../coinHeader'; +import { + Button, + ButtonRow, + Container, + ContractAddressCopyButton, + ContractDeploymentButton, + CopyButtonContainer, + FtInfoContainer, + RuneBundlesContainer, + SecondaryContainer, + ShareIcon, + TokenContractAddress, + TokenText, +} from '../index.styled'; +import TransactionsHistoryList from '../transactionsHistoryList'; + +// TODO: This should be refactored into separate components for each protocol/coin as needed +// TODO: with a shared component for the common parts + +export default function CoinDashboard() { + const [searchParams] = useSearchParams(); + const ftKey = searchParams.get('ftKey') ?? ''; + const protocol = searchParams.get('protocol') ?? ''; + const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' }); + const fromSecondaryTab = searchParams.get('secondaryTab') === 'true' ? 'secondary' : 'primary'; + const [currentTab, setCurrentTab] = useState<'primary' | 'secondary'>(fromSecondaryTab); + const [showOptionsDialog, setShowOptionsDialog] = useState(false); + const [optionsDialogIndents, setOptionsDialogIndents] = useState< + { top: string; left: string } | undefined + >(); + const { addToSpamTokens } = useSpamTokens(); + const dispatch = useDispatch(); + const { currency } = useParams(); + const { data: runesCoinsList } = useVisibleRuneFungibleTokens(); + const { data: sip10CoinsList } = useVisibleSip10FungibleTokens(); + const { data: brc20CoinsList } = useVisibleBrc20FungibleTokens(); + + let selectedFt: FungibleToken | undefined; + + if (ftKey && protocol) { + switch (protocol) { + case 'stacks': + selectedFt = sip10CoinsList.find((ft) => ft.principal === ftKey); + break; + case 'brc-20': + selectedFt = brc20CoinsList.find((ft) => ft.principal === ftKey); + break; + case 'runes': + selectedFt = runesCoinsList.find((ft) => ft.principal === ftKey); + break; + default: + selectedFt = undefined; + } + } + const { data: runeUtxos } = useRuneUtxosQuery( + selectedFt?.protocol === 'runes' ? selectedFt?.name : '', + ); + + const showTxHistory = currentTab === 'primary'; + const displayTabs = ['stacks', 'runes'].includes(protocol); + const showStxContract = currentTab === 'secondary' && selectedFt && protocol === 'stacks'; + const showRuneBundles = currentTab === 'secondary' && selectedFt && protocol === 'runes'; + + useResetUserFlow('/coinDashboard'); + + const handleGoBack = () => broadcastResetUserFlow(); + + useTrackMixPanelPageViewed( + protocol + ? { + protocol, + } + : {}, + ); + + const openOptionsDialog = (event: React.MouseEvent) => { + setShowOptionsDialog(true); + setOptionsDialogIndents({ + top: `${(event.target as HTMLElement).parentElement?.getBoundingClientRect().top}px`, + left: `calc(100% - ${SPAM_OPTIONS_WIDTH}px)`, + }); + }; + + const closeOptionsDialog = () => setShowOptionsDialog(false); + + return ( + <> + + {showOptionsDialog && ( + + { + if (!selectedFt) { + handleGoBack(); + return; + } + // set the visibility to false + const payload = { + principal: selectedFt.principal, + isEnabled: false, + }; + if (protocol === 'runes') { + dispatch(setRunesManageTokensAction(payload)); + } else if (protocol === 'stacks') { + dispatch(setSip10ManageTokensAction(payload)); + } else if (protocol === 'brc-20') { + dispatch(setBrc20ManageTokensAction(payload)); + } + + addToSpamTokens(selectedFt.principal); + dispatch(setSpamTokenAction(selectedFt)); + + handleGoBack(); + }} + > + + + {t('HIDE_AND_REPORT')} + + + + )} + + + {/* TODO: import { Tabs } from ui-library/tabs.tsx */} + {displayTabs && ( + + + + + )} + {showTxHistory && ( + + )} + {showStxContract && ( + +

{t('FT_CONTRACT_PREFIX')}

+ navigator.clipboard.writeText(selectedFt?.principal as string)} + > + + {getTruncatedAddress(selectedFt?.principal as string, 20)} + + + + + + window.open(getExplorerUrl(selectedFt?.principal as string), '_blank')} + > + {t('OPEN_FT_CONTRACT_DEPLOYMENT')} + {t('STACKS_EXPLORER')} + + +
+ )} + {showRuneBundles && ( + + + {runeUtxos?.map((utxo) => { + const fullTxId = getFullTxId(utxo); + const runeAmount = utxo.runes?.filter((rune) => rune[0] === selectedFt?.name)[0][1] + .amount; + return ( + + ); + })} + + + )} +
+ + + ); +} diff --git a/src/app/screens/coinDashboard/index.styled.ts b/src/app/screens/coinDashboard/index.styled.ts new file mode 100644 index 000000000..f38341537 --- /dev/null +++ b/src/app/screens/coinDashboard/index.styled.ts @@ -0,0 +1,120 @@ +import { StyledP } from '@ui-library/common.styled'; +import styled from 'styled-components'; + +export const Container = styled.div((props) => ({ + display: 'flex', + flex: 1, + marginTop: props.theme.space.xs, + flexDirection: 'column', + overflowY: 'auto', + '&::-webkit-scrollbar': { + display: 'none', + }, +})); + +export const SecondaryContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + paddingLeft: props.theme.space.m, + paddingRight: props.theme.space.m, + marginTop: props.theme.space.l, + marginBottom: props.theme.space.xl, + h1: { + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_400, + }, +})); + +export const ContractAddressCopyButton = styled.button((props) => ({ + display: 'flex', + marginTop: props.theme.space.xxs, + background: 'transparent', +})); + +export const TokenContractAddress = styled.p((props) => ({ + ...props.theme.typography.body_medium_l, + color: props.theme.colors.white_0, + textAlign: 'left', + overflowWrap: 'break-word', + width: 300, +})); + +export const FtInfoContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + borderTop: `1px solid ${props.theme.colors.elevation2}`, + paddingTop: props.theme.space.l, + marginTop: props.theme.space.xl, + paddingLeft: props.theme.space.m, +})); + +export const ShareIcon = styled.img({ + width: 18, + height: 18, +}); + +export const CopyButtonContainer = styled.div((props) => ({ + marginRight: props.theme.space.xxs, +})); + +export const ContractDeploymentButton = styled.button((props) => ({ + ...props.theme.typography.body_m, + display: 'flex', + alignItems: 'center', + marginTop: props.theme.space.l, + background: 'none', + color: props.theme.colors.white_400, + span: { + color: props.theme.colors.white_0, + marginLeft: props.theme.space.xs, + }, + img: { + marginLeft: props.theme.space.xs, + }, +})); + +export const Button = styled.button<{ + isSelected: boolean; +}>((props) => ({ + ...props.theme.typography.body_bold_l, + fontSize: 11, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: 31, + paddingLeft: props.theme.space.s, + paddingRight: props.theme.space.s, + marginRight: props.theme.space.xxs, + borderRadius: 44, + background: props.isSelected ? props.theme.colors.elevation2 : 'transparent', + color: props.theme.colors.white_0, + opacity: props.isSelected ? 1 : 0.6, +})); + +export const ButtonRow = styled.button` + display: flex; + align-items: center; + background-color: transparent; + flex-direction: row; + padding-left: ${(props) => props.theme.space.m}; + padding-right: ${(props) => props.theme.space.m}; + padding-top: ${(props) => props.theme.space.s}; + padding-bottom: ${(props) => props.theme.space.s}; + transition: background-color 0.2s ease; + :hover { + background-color: ${(props) => props.theme.colors.elevation3}; + } + :active { + background-color: ${(props) => props.theme.colors.elevation3}; + } +`; + +export const TokenText = styled(StyledP)` + margin-left: ${(props) => props.theme.space.m}; +`; + +export const RuneBundlesContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${(props) => props.theme.space.s}; +`; diff --git a/src/app/screens/coinDashboard/index.tsx b/src/app/screens/coinDashboard/index.tsx index 490ba015b..a89395893 100644 --- a/src/app/screens/coinDashboard/index.tsx +++ b/src/app/screens/coinDashboard/index.tsx @@ -1,350 +1,15 @@ -import linkIcon from '@assets/img/linkIcon.svg'; -import CopyButton from '@components/copyButton'; -import OptionsDialog from '@components/optionsDialog/optionsDialog'; -import BottomBar from '@components/tabBar'; -import TopRow from '@components/topRow'; -import { useVisibleBrc20FungibleTokens } from '@hooks/queries/ordinals/useGetBrc20FungibleTokens'; -import { useVisibleRuneFungibleTokens } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; -import useRuneUtxosQuery from '@hooks/queries/runes/useRuneUtxosQuery'; -import { useVisibleSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens'; -import useSpamTokens from '@hooks/queries/useSpamTokens'; -import { broadcastResetUserFlow, useResetUserFlow } from '@hooks/useResetUserFlow'; -import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed'; -import { Flag } from '@phosphor-icons/react'; -import RuneBundleRow from '@screens/coinDashboard/runes/bundleRow'; -import type { FungibleToken } from '@secretkeylabs/xverse-core'; -import { mapRareSatsAPIResponseToBundle } from '@secretkeylabs/xverse-core'; -import { - setBrc20ManageTokensAction, - setRunesManageTokensAction, - setSip10ManageTokensAction, - setSpamTokenAction, -} from '@stores/wallet/actions/actionCreators'; -import { StyledP } from '@ui-library/common.styled'; -import { SPAM_OPTIONS_WIDTH, type CurrencyTypes } from '@utils/constants'; -import { ftDecimals, getExplorerUrl, getTruncatedAddress } from '@utils/helper'; -import { getFullTxId, getTxIdFromFullTxId, getVoutFromFullTxId } from '@utils/runes'; -import BigNumber from 'bignumber.js'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useDispatch } from 'react-redux'; -import { useParams, useSearchParams } from 'react-router-dom'; -import styled from 'styled-components'; -import Theme from 'theme'; -import CoinHeader from './coinHeader'; -import TransactionsHistoryList from './transactionsHistoryList'; - -const Container = styled.div((props) => ({ - display: 'flex', - flex: 1, - marginTop: props.theme.space.xs, - flexDirection: 'column', - overflowY: 'auto', - '&::-webkit-scrollbar': { - display: 'none', - }, -})); - -const SecondaryContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - paddingLeft: props.theme.space.m, - paddingRight: props.theme.space.m, - marginTop: props.theme.space.l, - marginBottom: props.theme.space.xl, - h1: { - ...props.theme.typography.body_medium_m, - color: props.theme.colors.white_400, - }, -})); - -const ContractAddressCopyButton = styled.button((props) => ({ - display: 'flex', - marginTop: props.theme.space.xxs, - background: 'transparent', -})); - -const TokenContractAddress = styled.p((props) => ({ - ...props.theme.typography.body_medium_l, - color: props.theme.colors.white_0, - textAlign: 'left', - overflowWrap: 'break-word', - width: 300, -})); - -const FtInfoContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - borderTop: `1px solid ${props.theme.colors.elevation2}`, - paddingTop: props.theme.space.l, - marginTop: props.theme.space.xl, - paddingLeft: props.theme.space.m, -})); - -const ShareIcon = styled.img({ - width: 18, - height: 18, -}); - -const CopyButtonContainer = styled.div((props) => ({ - marginRight: props.theme.space.xxs, -})); - -const ContractDeploymentButton = styled.button((props) => ({ - ...props.theme.typography.body_m, - display: 'flex', - alignItems: 'center', - marginTop: props.theme.space.l, - background: 'none', - color: props.theme.colors.white_400, - span: { - color: props.theme.colors.white_0, - marginLeft: props.theme.space.xs, - }, - img: { - marginLeft: props.theme.space.xs, - }, -})); - -const Button = styled.button<{ - isSelected: boolean; -}>((props) => ({ - ...props.theme.typography.body_bold_l, - fontSize: 11, - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: 31, - paddingLeft: props.theme.space.s, - paddingRight: props.theme.space.s, - marginRight: props.theme.space.xxs, - borderRadius: 44, - background: props.isSelected ? props.theme.colors.elevation2 : 'transparent', - color: props.theme.colors.white_0, - opacity: props.isSelected ? 1 : 0.6, -})); - -const ButtonRow = styled.button` - display: flex; - align-items: center; - background-color: transparent; - flex-direction: row; - padding-left: ${(props) => props.theme.space.m}; - padding-right: ${(props) => props.theme.space.m}; - padding-top: ${(props) => props.theme.space.s}; - padding-bottom: ${(props) => props.theme.space.s}; - transition: background-color 0.2s ease; - :hover { - background-color: ${(props) => props.theme.colors.elevation3}; - } - :active { - background-color: ${(props) => props.theme.colors.elevation3}; - } -`; - -const TokenText = styled(StyledP)` - margin-left: ${(props) => props.theme.space.m}; -`; - -const RuneBundlesContainer = styled.div` - display: flex; - flex-direction: column; - gap: ${(props) => props.theme.space.s}; -`; +import { useParams } from 'react-router-dom'; +import Btc from './coins/btc'; +import Other from './coins/other'; export default function CoinDashboard() { - const [searchParams] = useSearchParams(); - const ftKey = searchParams.get('ftKey') ?? ''; - const protocol = searchParams.get('protocol') ?? ''; - const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' }); - const fromSecondaryTab = searchParams.get('secondaryTab') === 'true' ? 'secondary' : 'primary'; - const [currentTab, setCurrentTab] = useState<'primary' | 'secondary'>(fromSecondaryTab); - const [showOptionsDialog, setShowOptionsDialog] = useState(false); - const [optionsDialogIndents, setOptionsDialogIndents] = useState< - { top: string; left: string } | undefined - >(); - const { addToSpamTokens } = useSpamTokens(); - const dispatch = useDispatch(); const { currency } = useParams(); - const { data: runesCoinsList } = useVisibleRuneFungibleTokens(); - const { data: sip10CoinsList } = useVisibleSip10FungibleTokens(); - const { data: brc20CoinsList } = useVisibleBrc20FungibleTokens(); - - let selectedFt: FungibleToken | undefined; - if (ftKey && protocol) { - switch (protocol) { - case 'stacks': - selectedFt = sip10CoinsList.find((ft) => ft.principal === ftKey); - break; - case 'brc-20': - selectedFt = brc20CoinsList.find((ft) => ft.principal === ftKey); - break; - case 'runes': - selectedFt = runesCoinsList.find((ft) => ft.principal === ftKey); - break; - default: - selectedFt = undefined; - } + switch (currency) { + case 'BTC': + return ; + default: + // TODO: split this more. Other is currently doing too much + return ; } - const { data: runeUtxos } = useRuneUtxosQuery( - selectedFt?.protocol === 'runes' ? selectedFt?.name : '', - ); - - const showTxHistory = currentTab === 'primary'; - const displayTabs = ['stacks', 'runes'].includes(protocol); - const showStxContract = currentTab === 'secondary' && selectedFt && protocol === 'stacks'; - const showRuneBundles = currentTab === 'secondary' && selectedFt && protocol === 'runes'; - - useResetUserFlow('/coinDashboard'); - - const handleGoBack = () => broadcastResetUserFlow(); - - useTrackMixPanelPageViewed( - protocol - ? { - protocol, - } - : {}, - ); - - const openOptionsDialog = (event: React.MouseEvent) => { - setShowOptionsDialog(true); - setOptionsDialogIndents({ - top: `${(event.target as HTMLElement).parentElement?.getBoundingClientRect().top}px`, - left: `calc(100% - ${SPAM_OPTIONS_WIDTH}px)`, - }); - }; - - const closeOptionsDialog = () => setShowOptionsDialog(false); - - return ( - <> - - {showOptionsDialog && ( - - { - if (!selectedFt) { - handleGoBack(); - return; - } - // set the visibility to false - const payload = { - principal: selectedFt.principal, - isEnabled: false, - }; - if (protocol === 'runes') { - dispatch(setRunesManageTokensAction(payload)); - } else if (protocol === 'stacks') { - dispatch(setSip10ManageTokensAction(payload)); - } else if (protocol === 'brc-20') { - dispatch(setBrc20ManageTokensAction(payload)); - } - - addToSpamTokens(selectedFt.principal); - dispatch(setSpamTokenAction(selectedFt)); - - handleGoBack(); - }} - > - - - {t('HIDE_AND_REPORT')} - - - - )} - - - {displayTabs && ( - - - - - )} - {showTxHistory && ( - - )} - {showStxContract && ( - -

{t('FT_CONTRACT_PREFIX')}

- navigator.clipboard.writeText(selectedFt?.principal as string)} - > - - {getTruncatedAddress(selectedFt?.principal as string, 20)} - - - - - - window.open(getExplorerUrl(selectedFt?.principal as string), '_blank')} - > - {t('OPEN_FT_CONTRACT_DEPLOYMENT')} - {t('STACKS_EXPLORER')} - - -
- )} - {showRuneBundles && ( - - - {runeUtxos?.map((utxo) => { - const fullTxId = getFullTxId(utxo); - const runeAmount = utxo.runes?.filter((rune) => rune[0] === selectedFt?.name)[0][1] - .amount; - return ( - - ); - })} - - - )} -
- - - ); } diff --git a/src/app/screens/confirmNftTransaction/index.tsx b/src/app/screens/confirmNftTransaction/index.tsx index f6638aeb6..37d3357fc 100644 --- a/src/app/screens/confirmNftTransaction/index.tsx +++ b/src/app/screens/confirmNftTransaction/index.tsx @@ -78,7 +78,7 @@ function ConfirmNftTransaction() { const isGalleryOpen: boolean = document.documentElement.clientWidth > 360; const selectedAccount = useSelectedAccount(); const { avatarIds } = useWalletSelector(); - const currentAvatar = avatarIds[selectedAccount.btcAddress]; + const selectedAvatar = avatarIds[selectedAccount.ordinalsAddress]; const [fee, setFee] = useState(); const navigate = useNavigate(); const location = useLocation(); @@ -117,8 +117,8 @@ function ConfirmNftTransaction() { refetch(); }, 1000); - if (currentAvatar?.type === 'stacks' && currentAvatar.nft?.token_id === nft?.token_id) { - dispatch(removeAccountAvatarAction({ address: selectedAccount.btcAddress })); + if (selectedAvatar?.type === 'stacks' && selectedAvatar.nft?.token_id === nft?.token_id) { + dispatch(removeAccountAvatarAction({ address: selectedAccount.ordinalsAddress })); } } }, [ @@ -126,9 +126,9 @@ function ConfirmNftTransaction() { navigate, refetch, stxTxBroadcastData, - selectedAccount.btcAddress, + selectedAccount.ordinalsAddress, nft, - currentAvatar, + selectedAvatar, ]); useEffect(() => { diff --git a/src/app/screens/connect/selectAccount.tsx b/src/app/screens/connect/selectAccount.tsx index ba98ddab3..dcdb969e3 100644 --- a/src/app/screens/connect/selectAccount.tsx +++ b/src/app/screens/connect/selectAccount.tsx @@ -74,7 +74,7 @@ type Props = { }; function SelectAccount({ account, handlePressAccount }: Props) { - const gradient = getAccountGradient(account?.stxAddress || account?.btcAddress!); + const gradient = getAccountGradient(account); const { t } = useTranslation('translation', { keyPrefix: 'SELECT_BTC_ADDRESS_SCREEN' }); const theme = useTheme(); diff --git a/src/app/screens/home/announcementModal/index.tsx b/src/app/screens/home/announcementModal/index.tsx new file mode 100644 index 000000000..47d2fa1b2 --- /dev/null +++ b/src/app/screens/home/announcementModal/index.tsx @@ -0,0 +1,43 @@ +import { useSessionStorage } from '@hooks/useStorage'; +import { markAlertSeen, shouldShowAlert, type AnnouncementKey } from '@utils/alertTracker'; +import { useEffect, useRef, useState } from 'react'; +import NativeSegWit from './nativeSegWit'; + +const getAnnouncementToShow = () => { + if (shouldShowAlert('native_segwit_intro')) { + return 'native_segwit_intro'; + } + return undefined; +}; + +export default function AnnouncementModal() { + const [hasShown, setHasShown] = useSessionStorage('announceShown', false); + const stickyShown = useRef(hasShown); + const [announcementToShow, setAnnouncementToShow] = useState(); + const [isVisible, setIsVisible] = useState(true); + + useEffect(() => { + setHasShown(true); + + if (stickyShown.current) { + // we've already shown the announcement modal on this load, so don't show it again + return; + } + + const announcement = getAnnouncementToShow(); + setAnnouncementToShow(announcement); + // eslint-disable-next-line react-hooks/exhaustive-deps -- we only want to run this once + }, []); + + const onClose = (announcementKey: AnnouncementKey) => () => { + setIsVisible(false); + markAlertSeen(announcementKey); + }; + + switch (announcementToShow) { + case 'native_segwit_intro': + return ; + default: + return null; + } +} diff --git a/src/app/screens/home/announcementModal/nativeSegWit.tsx b/src/app/screens/home/announcementModal/nativeSegWit.tsx new file mode 100644 index 000000000..e04d1f10d --- /dev/null +++ b/src/app/screens/home/announcementModal/nativeSegWit.tsx @@ -0,0 +1,66 @@ +import BtcLogo from '@assets/img/btcFlashy.svg'; +import Button from '@ui-library/button'; +import Sheet from '@ui-library/sheet'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; + +const LogoContainer = styled.div(() => ({ + display: 'flex', + justifyContent: 'center', +})); + +const LogoImg = styled.img(() => ({ + height: '135px', +})); + +const DescriptionItem = styled.div<{ $bottomSpacer?: boolean }>((props) => ({ + ...props.theme.typography.body_m, + marginBottom: props.$bottomSpacer ? props.theme.space.m : 0, +})); + +const ButtonContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + gap: props.theme.space.s, + marginTop: props.theme.space.xl, +})); + +type Props = { + isVisible: boolean; + onClose: () => void; +}; + +export default function NativeSegWit({ isVisible, onClose }: Props) { + const { t } = useTranslation('translation', { + keyPrefix: 'DASHBOARD_SCREEN.ANNOUNCEMENTS.NATIVE_SEGWIT', + }); + const navigate = useNavigate(); + + const onNavigate = () => { + navigate('/preferred-address'); + onClose(); + }; + + return ( + + + + } + onClose={onClose} + visible={isVisible} + > + {t('DESCRIPTION_1')} + {t('DESCRIPTION_2')} + {t('DESCRIPTION_3')} + + )} diff --git a/src/app/screens/signMessageRequest/useSignMessageRequest.ts b/src/app/screens/signMessageRequest/useSignMessageRequest.ts index 6bbfec52e..06376c521 100644 --- a/src/app/screens/signMessageRequest/useSignMessageRequest.ts +++ b/src/app/screens/signMessageRequest/useSignMessageRequest.ts @@ -57,19 +57,15 @@ export const useSignMessageValidation = (requestPayload: SignMessagePayload | un const { t } = useTranslation('translation', { keyPrefix: 'REQUEST_ERRORS' }); const selectedAccount = useSelectedAccount(); const { accountsList, network } = useWalletSelector(); - const { btcAddress } = useSelectedAccount(); const { switchAccount } = useWalletReducer(); const checkAddressAvailability = () => { - const account = accountsList.filter((acc) => { - if (acc.btcAddress === requestPayload?.address) { - return true; - } - if (acc.ordinalsAddress === requestPayload?.address) { - return true; - } - return false; - }); + const account = accountsList.filter( + (acc) => + selectedAccount.btcAddress === acc.btcAddresses.native?.address || + selectedAccount.btcAddress === acc.btcAddresses.nested?.address || + selectedAccount.btcAddress === acc.btcAddresses.taproot.address, + ); return isHardwareAccount(selectedAccount) ? account[0] || selectedAccount : account[0]; }; @@ -90,7 +86,11 @@ export const useSignMessageValidation = (requestPayload: SignMessagePayload | un return; } - if (btcAddress === account.btcAddress) return; + if ( + selectedAccount.btcAddress === account.btcAddresses.native?.address || + selectedAccount.btcAddress === account.btcAddresses.nested?.address + ) + return; switchAccount(account); }; diff --git a/src/app/stores/index.ts b/src/app/stores/index.ts index 5eb2b25cc..6e5a77af2 100644 --- a/src/app/stores/index.ts +++ b/src/app/stores/index.ts @@ -1,10 +1,21 @@ -import type { Account, AccountType, Coin, FungibleToken } from '@secretkeylabs/xverse-core'; +/* eslint-disable no-underscore-dangle */ +import { markAlertsForShow } from '@utils/alertTracker'; import chromeStorage from '@utils/chromeStorage'; import { applyMiddleware, combineReducers, createStore } from 'redux'; import { createMigrate, persistReducer, persistStore, type PersistConfig } from 'redux-persist'; import { createStateSyncMiddleware, initMessageListener } from 'redux-state-sync'; import NftDataStateReducer from './nftData/reducer'; -import type { WalletState } from './wallet/actions/types'; +import type { + AccountBtcAddressesV5, + AccountV1, + AccountV5, + WalletStateV1, + WalletStateV2, + WalletStateV3, + WalletStateV4, + WalletStateV5, +} from './wallet/actions/migrationTypes'; +import { type AvatarInfo, type WalletState } from './wallet/actions/types'; import walletReducer, { initialWalletState, rehydrateError } from './wallet/reducer'; const rootPersistConfig = { @@ -15,9 +26,9 @@ const rootPersistConfig = { }; const migrations = { - 2: (state: WalletState) => { + 2: (state: WalletStateV1): WalletStateV2 => { if (state.network.type !== 'Mainnet') { - return state; + return state as WalletStateV2; } return { ...state, @@ -28,12 +39,8 @@ const migrations = { }; }, 3: ( - state: WalletState & { - brcCoinsList: (FungibleToken & { visible?: boolean })[] | null; // removed in v3 - coinsList: (FungibleToken & { visible?: boolean })[] | null; // removed in v3 - coins: Coin[]; // removed in v3 - }, - ) => ({ + state: WalletStateV2, + ): WalletStateV3 & { coins: undefined; coinsList: undefined; brcCoinsList: undefined } => ({ ...state, brc20ManageTokens: state.brcCoinsList?.reduce((acc, coin) => { @@ -49,33 +56,114 @@ const migrations = { } return acc; }, {}) ?? {}, + runesManageTokens: {}, coins: undefined, coinsList: undefined, brcCoinsList: undefined, }), - 4: ( - state: WalletState & { - stxAddress: string; - btcAddress: string; - ordinalsAddress: string; - masterPubKey: string; - stxPublicKey: string; - btcPublicKey: string; - ordinalsPublicKey: string; - selectedAccount: Account | null; - accountType: AccountType | undefined; - accountName: string | undefined; - }, - ) => ({ + 4: (state: WalletStateV3): WalletStateV4 => ({ ...state, selectedAccountIndex: state.selectedAccount?.deviceAccountIndex ?? state.selectedAccount?.id ?? 0, selectedAccountType: state.selectedAccount?.accountType ?? 'software', }), + 5: (state: WalletStateV4): WalletState => { + const migrateAccount = + (accountType: 'software' | 'ledger') => + (account: AccountV1 & { btcAddresses?: AccountBtcAddressesV5 }): AccountV5 => { + const { + btcAddress, + btcPublicKey, + ordinalsAddress, + ordinalsPublicKey, + btcAddresses, + ...rest + } = account; + + if (account.btcAddresses?.taproot.address) { + return { ...account, accountType, btcAddresses: account.btcAddresses }; + } + + const paymentAddress = { address: btcAddress, publicKey: btcPublicKey }; + + if (accountType === 'ledger') { + return { + ...rest, + accountType, + btcAddresses: { + native: paymentAddress, + taproot: { address: ordinalsAddress, publicKey: ordinalsPublicKey }, + }, + }; + } + + return { + ...rest, + accountType, + btcAddresses: { + nested: paymentAddress, + taproot: { address: ordinalsAddress, publicKey: ordinalsPublicKey }, + }, + }; + }; + + const migrateAvatarIds = (existingIds: Record) => { + const migratedIds: Record = {}; + const { accountsList } = state; + + Object.keys(existingIds).forEach((btcAddress) => { + if (!existingIds[btcAddress]) return; + + const legacyAccount = accountsList.find((account) => account.btcAddress === btcAddress); + + if (legacyAccount?.ordinalsAddress) { + migratedIds[legacyAccount.ordinalsAddress] = existingIds[legacyAccount.btcAddress]; + } else { + migratedIds[btcAddress] = existingIds[btcAddress]; + } + }); + + return migratedIds; + }; + + markAlertsForShow( + 'native_segwit_intro', + 'co:panel:address_changed_to_native', + 'co:receive:address_change_button', + 'co:receive:address_changed_to_native', + ); + + return { + ...state, + btcPaymentAddressType: 'nested', + accountsList: state.accountsList.map(migrateAccount('software')), + ledgerAccountsList: state.ledgerAccountsList.map(migrateAccount('ledger')), + allowNestedSegWitAddress: true, + + // we cast state to v5 as the below went live without a migration + hiddenCollectibleIds: (state as unknown as WalletStateV5).hiddenCollectibleIds || {}, + starredCollectibleIds: (state as unknown as WalletStateV5).starredCollectibleIds || {}, + avatarIds: migrateAvatarIds((state as unknown as WalletStateV5).avatarIds), + }; + }, + /* * + * When adding a new migration, add the new wallet state type to the migrationTypes file + * and add the migration here. Update the previous head migration's output type to be the versioned wallet state. + * The last migration should be a function that takes the previous state and returns the current WalletState type. + * + * e.g. if the current head is this: + * 6: (state: WalletStateV5): WalletState => ({ + * + * Then update it to this: + * 6: (state: WalletStateV5): WalletStateV6 => ({ + * + * And add this: + * 7: (state: WalletStateV6): WalletState => ({ + * */ }; const WalletPersistConfig: PersistConfig = { - version: 4, + version: 5, key: 'walletState', storage: chromeStorage.local, migrate: createMigrate(migrations as any, { debug: false }), diff --git a/src/app/stores/wallet/actions/actionCreators.ts b/src/app/stores/wallet/actions/actionCreators.ts index bcc9a2c29..930e65e7f 100644 --- a/src/app/stores/wallet/actions/actionCreators.ts +++ b/src/app/stores/wallet/actions/actionCreators.ts @@ -3,6 +3,7 @@ import type { Account, AccountType, AppInfo, + BtcPaymentType, FungibleToken, NetworkType, SettingsNetwork, @@ -90,6 +91,21 @@ export function ChangeNetworkAction(network: SettingsNetwork): actions.ChangeNet }; } +export function EnableNestedSegWitAddress(): actions.EnableNestedSegWitAddress { + return { + type: actions.EnableNestedSegWitAddressKey, + }; +} + +export function ChangeBtcPaymentAddressType( + newType: BtcPaymentType, +): actions.ChangeBtcPaymentAddressType { + return { + type: actions.ChangeBtcPaymentAddressTypeKey, + btcPaymentType: newType, + }; +} + export function ChangeActivateOrdinalsAction( hasActivatedOrdinalsKey: boolean, ): actions.ChangeActivateOrdinals { @@ -200,12 +216,12 @@ export function setWalletUnlockedAction(isUnlocked: boolean): actions.SetWalletU } export function setAccountBalanceAction( - btcAddress: string, + accountKey: string, totalBalance: string, ): actions.SetAccountBalance { return { type: actions.SetAccountBalanceKey, - btcAddress, + accountKey, totalBalance, }; } diff --git a/src/app/stores/wallet/actions/migrationTypes.ts b/src/app/stores/wallet/actions/migrationTypes.ts new file mode 100644 index 000000000..253954a90 --- /dev/null +++ b/src/app/stores/wallet/actions/migrationTypes.ts @@ -0,0 +1,162 @@ +import type { + AccountType, + AppInfo, + BtcPaymentType, + Coin, + FungibleToken, + NetworkType, + SettingsNetwork, + SupportedCurrency, +} from '@secretkeylabs/xverse-core'; +import type { AvatarInfo, WalletSessionPeriods } from './types'; + +/** + * Initial wallet state (V1) + */ +type SettingsNetworkV1 = { + type: NetworkType; + address: string; + btcApiUrl: string; +}; + +export type AccountV1 = { + id: number; + stxAddress: string; + btcAddress: string; + ordinalsAddress: string; + masterPubKey: string; + stxPublicKey: string; + btcPublicKey: string; + ordinalsPublicKey: string; + bnsName?: string; + accountType?: AccountType; + accountName?: string; + deviceAccountIndex?: number; +}; + +export type WalletStateV1 = { + stxAddress: string; + btcAddress: string; + ordinalsAddress: string; + masterPubKey: string; + stxPublicKey: string; + btcPublicKey: string; + ordinalsPublicKey: string; + accountsList: AccountV1[]; + ledgerAccountsList: AccountV1[]; + selectedAccount: AccountV1 | null; + network: SettingsNetworkV1; + savedNetworks: SettingsNetwork[]; + encryptedSeed: string; + fiatCurrency: SupportedCurrency; + coinsList: FungibleToken[] | null; + coins: Coin[]; + brcCoinsList: FungibleToken[] | null; + notificationBanners: Record; + feeMultipliers: AppInfo | null; + hasActivatedOrdinalsKey: boolean | undefined; + hasActivatedRareSatsKey: boolean | undefined; + hasActivatedRBFKey: boolean | undefined; + rareSatsNoticeDismissed: boolean | undefined; + showBtcReceiveAlert: boolean | null; + showOrdinalReceiveAlert: boolean | null; + showDataCollectionAlert: boolean | null; + accountType: AccountType | undefined; + accountName: string | undefined; + walletLockPeriod: WalletSessionPeriods; + isUnlocked: boolean; + accountBalances: { + [key: string]: string; + }; + hideStx: boolean; + showSpamTokens: boolean; + spamToken: FungibleToken | null; + spamTokens: string[]; + savedNames: { + [key in NetworkType]?: { id: number; name?: string }[]; + }; +}; + +/** + * =========V2========= + */ +type SettingsNetworkV2 = { + type: NetworkType; + address: string; + btcApiUrl: string; + fallbackBtcApiUrl: string; +}; +export type WalletStateV2 = Omit & { network: SettingsNetworkV2 }; + +/** + * =========V3========= + */ +export type WalletStateV3 = Omit & { + sip10ManageTokens: Record; + brc20ManageTokens: Record; + runesManageTokens: Record; +}; + +/** + * =========V4========= + */ +export type WalletStateV4 = Omit< + WalletStateV3, + | 'selectedAccount' + | 'stxAddress' + | 'btcAddress' + | 'ordinalsAddress' + | 'masterPubKey' + | 'stxPublicKey' + | 'btcPublicKey' + | 'ordinalsPublicKey' + | 'bnsName' + | 'accountType' + | 'accountName' +> & { + selectedAccountIndex: number; + selectedAccountType: AccountType; +}; + +/** + * =========V5========= + */ +type BtcAddressV5 = { + address: string; + publicKey: string; +}; +export type AccountBtcAddressesV5 = { + nested?: BtcAddressV5; + native?: BtcAddressV5; + taproot: BtcAddressV5; +}; +export type AccountV5 = { + id: number; + deviceAccountIndex?: number; + masterPubKey: string; + accountType: AccountType; + accountName?: string; + stxAddress: string; + stxPublicKey: string; + bnsName?: string; + btcAddresses: AccountBtcAddressesV5; +}; + +export type WalletStateV5 = Omit< + WalletStateV4, + | 'btcAddress' + | 'btcPublicKey' + | 'ordinalsAddress' + | 'ordinalsPublicKey' + | 'accountsList' + | 'ledgerAccountsList' +> & { + accountsList: AccountV5[]; + ledgerAccountsList: AccountV5[]; + btcPaymentAddressType: BtcPaymentType; + switchPaymentAddressTypeEnabled: boolean; + + hiddenCollectibleIds: Record>; + starredCollectibleIds: Record>; + avatarIds: Record; +}; diff --git a/src/app/stores/wallet/actions/types.ts b/src/app/stores/wallet/actions/types.ts index 280c7b2ae..ced01601a 100644 --- a/src/app/stores/wallet/actions/types.ts +++ b/src/app/stores/wallet/actions/types.ts @@ -2,6 +2,7 @@ import type { Account, AccountType, AppInfo, + BtcPaymentType, FungibleToken, Inscription, NetworkType, @@ -17,6 +18,8 @@ export const UpdateSoftwareAccountsKey = 'UpdateSoftwareAccountsKey'; export const SetFeeMultiplierKey = 'SetFeeMultiplierKey'; export const ChangeFiatCurrencyKey = 'ChangeFiatCurrency'; export const ChangeNetworkKey = 'ChangeNetwork'; +export const ChangeBtcPaymentAddressTypeKey = 'ChangeBtcPaymentAddressTypeKey'; +export const EnableNestedSegWitAddressKey = 'EnableNestedSegWitAddressKey'; export const ChangeHasActivatedOrdinalsKey = 'ChangeHasActivatedOrdinalsKey'; export const RareSatsNoticeDismissedKey = 'RareSatsNoticeDismissedKey'; export const ChangeHasActivatedRareSatsKey = 'ChangeHasActivatedRareSatsKey'; @@ -57,6 +60,8 @@ export interface WalletState { ledgerAccountsList: Account[]; selectedAccountIndex: number; selectedAccountType: AccountType; + btcPaymentAddressType: BtcPaymentType; + allowNestedSegWitAddress: boolean; network: SettingsNetwork; // currently selected network urls and type savedNetworks: SettingsNetwork[]; // previously set network urls for type encryptedSeed: string; @@ -87,7 +92,7 @@ export interface WalletState { }; hiddenCollectibleIds: Record>; starredCollectibleIds: Record>; - avatarIds: Record; + avatarIds: Record; } export interface StoreEncryptedSeed { @@ -126,6 +131,15 @@ export interface ChangeNetwork { network: SettingsNetwork; } +export interface EnableNestedSegWitAddress { + type: typeof EnableNestedSegWitAddressKey; +} + +export interface ChangeBtcPaymentAddressType { + type: typeof ChangeBtcPaymentAddressTypeKey; + btcPaymentType: BtcPaymentType; +} + export interface ChangeActivateOrdinals { type: typeof ChangeHasActivatedOrdinalsKey; hasActivatedOrdinalsKey: boolean; @@ -197,7 +211,7 @@ export interface SetWalletUnlocked { export interface SetAccountBalance { type: typeof SetAccountBalanceKey; - btcAddress: string; + accountKey: string; totalBalance: string; } @@ -288,6 +302,8 @@ export type WalletActions = | SetFeeMultiplier | ChangeFiatCurrency | ChangeNetwork + | EnableNestedSegWitAddress + | ChangeBtcPaymentAddressType | ChangeActivateOrdinals | ChangeActivateRareSats | ChangeActivateRBF diff --git a/src/app/stores/wallet/reducer.ts b/src/app/stores/wallet/reducer.ts index 64883909e..19b532b84 100644 --- a/src/app/stores/wallet/reducer.ts +++ b/src/app/stores/wallet/reducer.ts @@ -3,6 +3,7 @@ import { REHYDRATE } from 'redux-persist'; import { AddToHideCollectiblesKey, AddToStarCollectiblesKey, + ChangeBtcPaymentAddressTypeKey, ChangeFiatCurrencyKey, ChangeHasActivatedOrdinalsKey, ChangeHasActivatedRareSatsKey, @@ -11,6 +12,7 @@ import { ChangeShowBtcReceiveAlertKey, ChangeShowDataCollectionAlertKey, ChangeShowOrdinalReceiveAlertKey, + EnableNestedSegWitAddressKey, RareSatsNoticeDismissedKey, RemoveAccountAvatarKey, RemoveAllFromHideCollectiblesKey, @@ -70,6 +72,8 @@ export const initialWalletState: WalletState = { ledgerAccountsList: [], selectedAccountIndex: 0, selectedAccountType: 'software', + btcPaymentAddressType: 'native', + allowNestedSegWitAddress: false, encryptedSeed: '', fiatCurrency: 'USD', sip10ManageTokens: {}, @@ -169,6 +173,16 @@ const walletReducer = ( accountsList: [], accountBalances: {}, }; + case EnableNestedSegWitAddressKey: + return { + ...state, + allowNestedSegWitAddress: true, + }; + case ChangeBtcPaymentAddressTypeKey: + return { + ...state, + btcPaymentAddressType: action.btcPaymentType, + }; case ChangeHasActivatedOrdinalsKey: return { ...state, @@ -251,7 +265,7 @@ const walletReducer = ( ...state, accountBalances: { ...state.accountBalances, - [action.btcAddress]: action.totalBalance, + [action.accountKey]: action.totalBalance, }, }; case SetWalletHideStxKey: @@ -355,12 +369,11 @@ const walletReducer = ( }, }; case RemoveAccountAvatarKey: { + const clonedAvatarIds = { ...state.avatarIds }; + delete clonedAvatarIds[action.address]; return { ...state, - avatarIds: { - ...state.avatarIds, - [action.address]: null, - }, + avatarIds: clonedAvatarIds, }; } default: diff --git a/src/app/ui-components/btcAmountSelector.tsx b/src/app/ui-components/btcAmountSelector.tsx index 9c2438801..fb795b7a3 100644 --- a/src/app/ui-components/btcAmountSelector.tsx +++ b/src/app/ui-components/btcAmountSelector.tsx @@ -1,5 +1,6 @@ -import useBtcWalletData from '@hooks/queries/useBtcWalletData'; import useSupportedCoinRates from '@hooks/queries/useSupportedCoinRates'; +import useBtcAddressBalance from '@hooks/useBtcAddressBalance'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { ArrowsDownUp } from '@phosphor-icons/react'; import { @@ -7,6 +8,7 @@ import { currencySymbolMap, getBtcFiatEquivalent, getFiatBtcEquivalent, + type BtcPaymentType, } from '@secretkeylabs/xverse-core'; import Input, { ConvertComplication, MaxButton, VertRule } from '@ui-library/input'; import { satsToBtcString } from '@utils/helper'; @@ -45,6 +47,7 @@ type Props = { sendMax: boolean; setSendMax: (sendMax: boolean) => void; disabled?: boolean; + overridePaymentType?: BtcPaymentType; }; function AmountSelector({ @@ -52,12 +55,14 @@ function AmountSelector({ setAmountSats, sendMax, setSendMax, + overridePaymentType, disabled = false, }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'SEND' }); const { fiatCurrency } = useWalletSelector(); - const { data: btcBalanceSats } = useBtcWalletData(); const { btcFiatRate } = useSupportedCoinRates(); + const selectedAccount = useSelectedAccount(overridePaymentType); + const { data: addressBtcBalance } = useBtcAddressBalance(selectedAccount.btcAddress); const [amountDisplay, setAmountDisplay] = useState( amountSats && satsToBtcString(new BigNumber(amountSats)), @@ -79,7 +84,7 @@ function AmountSelector({ // eslint-disable-next-line react-hooks/exhaustive-deps -- We specifically only want to run this on these 2 deps }, [sendMax, amountSats]); - const btcBalance = new BigNumber(btcBalanceSats ?? 0); + const btcBalance = new BigNumber(addressBtcBalance?.confirmedBalance ?? 0); const balance = useBtcValue ? satsToBtcString(btcBalance) : getBtcFiatEquivalent(btcBalance, new BigNumber(btcFiatRate)).toFixed(2); diff --git a/src/app/ui-library/callout.tsx b/src/app/ui-library/callout.tsx index 1c1e84238..25e0abdf8 100644 --- a/src/app/ui-library/callout.tsx +++ b/src/app/ui-library/callout.tsx @@ -9,6 +9,7 @@ import { import styled from 'styled-components'; import Theme from 'theme'; import { StyledHeading, StyledP } from './common.styled'; +import CrossButton from './crossButton'; type CalloutVariant = 'info' | 'warning' | 'danger' | 'success'; @@ -33,6 +34,7 @@ const icons = { }; const Container = styled.div<{ variant: CalloutVariant }>` + position: relative; display: flex; flex-direction: row; border-radius: ${(props) => props.theme.radius(2)}px; @@ -82,7 +84,9 @@ export type CalloutProps = { variant?: CalloutVariant; redirectText?: string; onClickRedirect?: () => void; + onClose?: () => void; anchorRedirect?: string; + hideIcon?: boolean; }; function Callout({ @@ -93,12 +97,14 @@ function Callout({ variant = 'info', redirectText, onClickRedirect, + onClose, anchorRedirect, + hideIcon = false, }: CalloutProps) { const StyledIcon = icons[variant]; return ( - + {!hideIcon && } {titleText && ( @@ -127,6 +133,7 @@ function Callout({ )} + {onClose && } ); } diff --git a/src/app/ui-library/crossButton.tsx b/src/app/ui-library/crossButton.tsx new file mode 100644 index 000000000..7454d04f5 --- /dev/null +++ b/src/app/ui-library/crossButton.tsx @@ -0,0 +1,44 @@ +import { XCircle } from '@phosphor-icons/react'; +import styled, { useTheme } from 'styled-components'; +import Theme from 'theme'; + +const Button = styled.button` + position: absolute; + top: 12px; + right: 12px; + background-color: transparent; + cursor: pointer; + display: flex; + transition: opacity 0.1s ease; + + &:hover { + opacity: 0.8; + } + + &:active { + opacity: 0.6; + } +`; + +type Props = { + onClick: () => void; + size?: keyof typeof Theme.space; + className?: string; +}; + +function CrossButton({ onClick, size = 'l', className }: Props) { + const theme = useTheme(); + + const internalOnClick = (e) => { + e.stopPropagation(); + onClick(); + }; + + return ( + + ); +} + +export default CrossButton; diff --git a/src/app/ui-library/sheet.tsx b/src/app/ui-library/sheet.tsx index 45f89f1bf..ba90e26c0 100644 --- a/src/app/ui-library/sheet.tsx +++ b/src/app/ui-library/sheet.tsx @@ -1,51 +1,42 @@ -import { XCircle } from '@phosphor-icons/react'; import { isInOptions } from '@utils/helper'; import Modal from 'react-modal'; import styled, { useTheme } from 'styled-components'; - -export const CrossButton = styled.button` - background-color: transparent; - cursor: pointer; - display: flex; - transition: opacity 0.1s ease; - - &:hover { - opacity: 0.8; - } - - &:active { - opacity: 0.6; - } -`; +import CrossButton from './crossButton'; const Title = styled.h1((props) => ({ ...props.theme.typography.body_bold_l, flex: 1, + width: '100%', })); -const RowContainer = styled.div((props) => ({ +const HeaderContainer = styled.div((props) => ({ display: 'flex', - flexDirection: 'row', + flexDirection: 'column', alignItems: 'center', justifyContent: 'space-between', margin: props.theme.space.m, + gap: props.theme.space.m, })); const CustomisedModal = styled(Modal)` - overflow-y: auto; position: absolute; - &::-webkit-scrollbar { - display: none; - } + max-height: 100%; + display: flex; + flex-direction: column; `; const BodyContainer = styled.div` + position: relative; + flex: 1; + overflow-y: auto; margin: ${(props) => props.theme.space.m}; + margin-top: 0; `; type Props = { title?: string; visible: boolean; + logo?: React.ReactNode; children: React.ReactNode; onClose?: () => void; overlayStylesOverriding?: {}; @@ -56,6 +47,7 @@ type Props = { function Sheet({ title, + logo, children, visible, onClose, @@ -104,14 +96,11 @@ function Sheet({ shouldCloseOnOverlayClick={shouldCloseOnOverlayClick} onRequestClose={onClose} > - + {onClose && } + + {logo} {title} - {onClose && ( - - - - )} - + {children} ); diff --git a/src/app/utils/alertTracker.ts b/src/app/utils/alertTracker.ts new file mode 100644 index 000000000..d8829b2f3 --- /dev/null +++ b/src/app/utils/alertTracker.ts @@ -0,0 +1,47 @@ +export type AnnouncementKey = 'native_segwit_intro'; +type CalloutKey = + | 'co:panel:address_changed_to_native' + | 'co:receive:address_changed_to_native' + | 'co:receive:address_change_button'; + +const alertTrackerStorageKey = 'alertTracker:alertsToShow'; + +const getAlertData = () => { + try { + const alertsToShow = localStorage.getItem(alertTrackerStorageKey); + const alertsToShowParsed = alertsToShow && JSON.parse(alertsToShow); + return Array.isArray(alertsToShowParsed) ? alertsToShowParsed : []; + } catch (e) { + return []; + } +}; + +export const shouldShowAlert = (callout: AnnouncementKey | CalloutKey): boolean => { + const alertsToShow = getAlertData(); + return alertsToShow.includes(callout); +}; + +export const markAlertSeen = (alertType: AnnouncementKey | CalloutKey): void => { + const alertsToShow = getAlertData(); + + if (alertsToShow.includes(alertType)) { + const newAlertsToShow = alertsToShow.filter((alert) => alert !== alertType); + localStorage.setItem(alertTrackerStorageKey, JSON.stringify(newAlertsToShow)); + } +}; + +export const markAlertsForShow = (...alertTypes: (AnnouncementKey | CalloutKey)[]): void => { + const alertsToShow = getAlertData(); + + let changed = false; + alertTypes.forEach((alertType) => { + if (!alertsToShow.includes(alertType)) { + alertsToShow.push(alertType); + changed = true; + } + }); + + if (changed) { + localStorage.setItem(alertTrackerStorageKey, JSON.stringify(alertsToShow)); + } +}; diff --git a/src/app/utils/gradient.ts b/src/app/utils/gradient.ts index 802f2269e..9b0f988c5 100644 --- a/src/app/utils/gradient.ts +++ b/src/app/utils/gradient.ts @@ -1,3 +1,5 @@ +import type { Account } from '@secretkeylabs/xverse-core'; + /* eslint-disable import/prefer-default-export */ function toHex(str: string) { let result = ''; @@ -35,7 +37,8 @@ function stringToHslColor(str: string, saturation: number, lightness: number): s return `hsl(${hue}, ${saturation}%, ${lightness}%)`; } -export function getAccountGradient(text: string): string[] { +export function getAccountGradient(account: Account | null | undefined): string[] { + const text = account?.stxAddress || account?.btcAddresses.taproot.address || ''; const pubKeyLikeString = toHex(text); const bg = stringToHslColor(text, 50, 60); diff --git a/src/app/utils/helper.ts b/src/app/utils/helper.ts index 0550208d1..3934e3c62 100644 --- a/src/app/utils/helper.ts +++ b/src/app/utils/helper.ts @@ -82,9 +82,12 @@ export const getTruncatedAddress = (address: string, lengthToShow = 4) => address.length, )}`; -export const getShortTruncatedAddress = (address: string) => { +export const getShortTruncatedAddress = (address: string, charCount = 8) => { if (address) { - return `${address.substring(0, 8)}...${address.substring(address.length - 8, address.length)}`; + return `${address.substring(0, charCount)}...${address.substring( + address.length - charCount, + address.length, + )}`; } }; @@ -240,6 +243,11 @@ export const validateAccountName = ( return null; }; +export const getAccountBalanceKey = (account: Account | null) => { + if (!account) return ''; + return `${account.accountType}-${account.deviceAccountIndex || account.id}`; +}; + export const calculateTotalBalance = ({ stxBalance, btcBalance, diff --git a/src/assets/img/btcFlashy.svg b/src/assets/img/btcFlashy.svg new file mode 100644 index 000000000..0a60887b6 --- /dev/null +++ b/src/assets/img/btcFlashy.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/common/utils/getSelectedAccount.ts b/src/common/utils/getSelectedAccount.ts index 6304c8853..7149ec3ae 100644 --- a/src/common/utils/getSelectedAccount.ts +++ b/src/common/utils/getSelectedAccount.ts @@ -1,4 +1,5 @@ -import type { Account, AccountType } from '@secretkeylabs/xverse-core'; +import type { Account, AccountType, BtcPaymentType } from '@secretkeylabs/xverse-core'; +import { getAccountAddressDetails } from '@secretkeylabs/xverse-core'; type GetSelectedAccountProps = { selectedAccountType: AccountType; @@ -7,7 +8,42 @@ type GetSelectedAccountProps = { softwareAccountsList: Account[]; }; -const getSelectedAccount = (props: GetSelectedAccountProps) => { +export type AccountWithDetails = Account & { + btcAddress: string; + btcPublicKey: string; + btcAddressType: BtcPaymentType; + ordinalsAddress: string; + ordinalsPublicKey: string; +}; + +export function embellishAccountWithDetails( + account: Account, + btcPaymentType: BtcPaymentType, +): AccountWithDetails; +export function embellishAccountWithDetails( + account: undefined, + btcPaymentType: BtcPaymentType, +): undefined; +export function embellishAccountWithDetails( + account: Account | undefined, + btcPaymentType: BtcPaymentType, +): AccountWithDetails | undefined { + if (!account) { + return undefined; + } + + if (account.accountType === 'ledger') { + return { ...account, ...getAccountAddressDetails(account, 'native'), btcAddressType: 'native' }; + } + + return { + ...account, + ...getAccountAddressDetails(account, btcPaymentType), + btcAddressType: btcPaymentType, + }; +} + +const getSelectedAccount = (props: GetSelectedAccountProps): Account | undefined => { const { selectedAccountType, selectedAccountIndex, ledgerAccountsList, softwareAccountsList } = props; diff --git a/src/common/utils/ledger.ts b/src/common/utils/ledger.ts index 4aa8de115..88d0c38e8 100644 --- a/src/common/utils/ledger.ts +++ b/src/common/utils/ledger.ts @@ -5,9 +5,9 @@ export const delay = (ms: number) => setTimeout(res, ms); }); -export const filterLedgerAccounts = (accounts: Account[], network: NetworkType) => +export const filterLedgerAccountsByNetwork = (accounts: Account[], network: NetworkType) => accounts.filter((account) => - account.ordinalsAddress?.startsWith(network === 'Mainnet' ? 'bc1' : 'tb1'), + account.btcAddresses.taproot.address.startsWith(network === 'Mainnet' ? 'bc1' : 'tb1'), ); // this is used for migrating the old ledger accounts to the new format @@ -43,7 +43,7 @@ export const getDeviceNewAccountIndex = ( network: NetworkType, masterKey?: string, ) => { - const networkLedgerAccounts = filterLedgerAccounts(ledgerAccountsList, network); + const networkLedgerAccounts = filterLedgerAccountsByNetwork(ledgerAccountsList, network); const ledgerAccountsIndexList = networkLedgerAccounts .filter((account) => masterKey === account.masterPubKey) diff --git a/src/common/utils/messages/extensionToContentScript/dispatchEvent/index.ts b/src/common/utils/messages/extensionToContentScript/dispatchEvent/index.ts index 94bc504ec..609508629 100644 --- a/src/common/utils/messages/extensionToContentScript/dispatchEvent/index.ts +++ b/src/common/utils/messages/extensionToContentScript/dispatchEvent/index.ts @@ -39,10 +39,10 @@ export async function dispatchEventConnectedClients(data: WalletEvent) { * @public */ export async function dispatchEventAuthorizedConnectedClients( - permission: Omit, + permissions: Omit[], data: WalletEvent, ) { - sendMessageAuthorizedConnectedClients(permission, { + sendMessageAuthorizedConnectedClients(permissions, { type: contentScriptWalletEventMessageName, data, }); diff --git a/src/common/utils/messages/extensionToContentScript/utils/index.ts b/src/common/utils/messages/extensionToContentScript/utils/index.ts index e233c6bd2..01ea31764 100644 --- a/src/common/utils/messages/extensionToContentScript/utils/index.ts +++ b/src/common/utils/messages/extensionToContentScript/utils/index.ts @@ -108,7 +108,7 @@ export async function sendMessageConnectedClients(message: ContentScriptMessage) * @public */ export async function sendMessageAuthorizedConnectedClients( - permission: Omit, + permissions: Omit[], message: ContentScriptMessage, ) { const [error, store] = await getPermissionsStore(); @@ -125,10 +125,12 @@ export async function sendMessageAuthorizedConnectedClients( } const authorizedClientIds = [...store.permissions] - .filter( - (p) => - p.resourceId === permission.resourceId && - [...permission.actions].every((action) => p.actions.has(action)), + .filter((p) => + permissions.some( + (permission) => + p.resourceId === permission.resourceId && + [...permission.actions].every((action) => p.actions.has(action)), + ), ) .map((p) => p.clientId); diff --git a/src/common/utils/rpc/btc/getAddresses/getAddresses.ts b/src/common/utils/rpc/btc/getAddresses/getAddresses.ts index 7483c78a6..3c67c7643 100644 --- a/src/common/utils/rpc/btc/getAddresses/getAddresses.ts +++ b/src/common/utils/rpc/btc/getAddresses/getAddresses.ts @@ -1,6 +1,6 @@ /* eslint-disable import/prefer-default-export */ import { getTabIdFromPort } from '@common/utils'; -import getSelectedAccount from '@common/utils/getSelectedAccount'; +import getSelectedAccount, { embellishAccountWithDetails } from '@common/utils/getSelectedAccount'; import { makeContext, openPopup } from '@common/utils/popup'; import * as utils from '@components/permissionsManager/utils'; import { getAddressesRequestMessageSchema, type RpcRequestMessage } from '@sats-connect/core'; @@ -50,6 +50,7 @@ export const handleGetAddresses = async (message: RpcRequestMessage, port: chrom selectedAccountType, accountsList: softwareAccountsList, ledgerAccountsList, + btcPaymentAddressType, } = rootStore.store.getState().walletState; const account = getSelectedAccount({ @@ -64,7 +65,8 @@ export const handleGetAddresses = async (message: RpcRequestMessage, port: chrom return; } - const addresses = accountPurposeAddresses(account, parseResult.output.params.purposes); + const embellishedAccount = embellishAccountWithDetails(account, btcPaymentAddressType); + const addresses = accountPurposeAddresses(embellishedAccount, parseResult.output.params.purposes); sendGetAddressesSuccessResponseMessage({ tabId, messageId: message.id, diff --git a/src/common/utils/rpc/btc/getAddresses/utils.ts b/src/common/utils/rpc/btc/getAddresses/utils.ts index 9793e41ca..32d61b73c 100644 --- a/src/common/utils/rpc/btc/getAddresses/utils.ts +++ b/src/common/utils/rpc/btc/getAddresses/utils.ts @@ -1,8 +1,8 @@ /* eslint-disable import/prefer-default-export */ +import type { AccountWithDetails } from '@common/utils/getSelectedAccount'; import { AddressPurpose, AddressType } from '@sats-connect/core'; -import type { Account } from '@secretkeylabs/xverse-core'; -export function accountPurposeAddresses(account: Account, purposes: AddressPurpose[]) { +export function accountPurposeAddresses(account: AccountWithDetails, purposes: AddressPurpose[]) { return purposes.map((purpose) => { if (purpose === AddressPurpose.Ordinals) { return { @@ -24,7 +24,7 @@ export function accountPurposeAddresses(account: Account, purposes: AddressPurpo address: account.btcAddress, publicKey: account.btcPublicKey, purpose: AddressPurpose.Payment, - addressType: account.accountType === 'ledger' ? AddressType.p2wpkh : AddressType.p2sh, + addressType: account.btcAddressType === 'native' ? AddressType.p2wpkh : AddressType.p2sh, }; }); } diff --git a/src/common/utils/rpc/btc/getBalance.ts b/src/common/utils/rpc/btc/getBalance.ts index ce1271791..c497fac89 100644 --- a/src/common/utils/rpc/btc/getBalance.ts +++ b/src/common/utils/rpc/btc/getBalance.ts @@ -1,5 +1,5 @@ import { getTabIdFromPort } from '@common/utils'; -import getSelectedAccount from '@common/utils/getSelectedAccount'; +import getSelectedAccount, { embellishAccountWithDetails } from '@common/utils/getSelectedAccount'; import { makeContext } from '@common/utils/popup'; import { safePromise, type Result } from '@common/utils/safe'; import * as utils from '@components/permissionsManager/utils'; @@ -81,6 +81,7 @@ const handleGetBalance = async (message: RpcRequestMessage, port: chrome.runtime accountsList: softwareAccountsList, ledgerAccountsList, network, + btcPaymentAddressType, } = rootStore.store.getState().walletState; const account = getSelectedAccount({ @@ -95,7 +96,9 @@ const handleGetBalance = async (message: RpcRequestMessage, port: chrome.runtime return; } - const address = account.btcAddress; + const detailedAccount = embellishAccountWithDetails(account, btcPaymentAddressType); + + const address = detailedAccount.btcAddress; const [getBalanceError, balances] = await getBalance(address, network.type); if (getBalanceError) { sendInternalErrorMessage({ diff --git a/src/common/utils/rpc/ordinals/getInscriptions.ts b/src/common/utils/rpc/ordinals/getInscriptions.ts index c3dd42a2f..05688f367 100644 --- a/src/common/utils/rpc/ordinals/getInscriptions.ts +++ b/src/common/utils/rpc/ordinals/getInscriptions.ts @@ -94,7 +94,7 @@ const handleGetInscriptions = async (message: RpcRequestMessage, port: chrome.ru try { const inscriptionsList = await ordinalsApi.getInscriptions( - existingAccount.ordinalsAddress, + existingAccount.btcAddresses.taproot.address, parseResult.output.params.offset, parseResult.output.params.limit, ); diff --git a/src/common/utils/rpc/runes/getBalance.ts b/src/common/utils/rpc/runes/getBalance.ts index 876dfbde1..b45c8f6fd 100644 --- a/src/common/utils/rpc/runes/getBalance.ts +++ b/src/common/utils/rpc/runes/getBalance.ts @@ -97,7 +97,9 @@ const handleGetRunesBalance = async (message: RpcRequestMessage, port: chrome.ru const runesApi = getRunesClient(network.type); try { - const runesBalances = await runesApi.getRuneBalances(existingAccount.ordinalsAddress); + const runesBalances = await runesApi.getRuneBalances( + existingAccount.btcAddresses.taproot.address, + ); sendRpcResponse( tabId, makeRpcSuccessResponse<'runes_getBalance'>(message.id, { diff --git a/src/common/utils/rpc/wallet/requestPermissions.ts b/src/common/utils/rpc/wallet/requestPermissions.ts index bb38eed8b..d59eb1292 100644 --- a/src/common/utils/rpc/wallet/requestPermissions.ts +++ b/src/common/utils/rpc/wallet/requestPermissions.ts @@ -1,11 +1,13 @@ /* eslint-disable import/prefer-default-export */ import { getTabIdFromPort } from '@common/utils'; import { makeContext, openPopup } from '@common/utils/popup'; +import * as utils from '@components/permissionsManager/utils'; import { requestPermissionsRequestMessageSchema, type RpcRequestMessage } from '@sats-connect/core'; import * as v from 'valibot'; import RequestsRoutes from '../../route-urls'; import { handleInvalidMessage } from '../handle-invalid-message'; -import { makeSendPopupClosedUserRejectionMessage } from '../helpers'; +import { hasAccountReadPermissions, makeSendPopupClosedUserRejectionMessage } from '../helpers'; +import { sendRequestPermissionsSuccessResponseMessage } from '../responseMessages/wallet'; export const handleRequestPermissions = async ( message: RpcRequestMessage, @@ -18,10 +20,22 @@ export const handleRequestPermissions = async ( return; } + const requestContext = makeContext(port); + + const [error, store] = await utils.getPermissionsStore(); + if (!error && hasAccountReadPermissions(requestContext.origin, store)) { + sendRequestPermissionsSuccessResponseMessage({ + messageId: parseResult.output.id, + tabId: requestContext.tabId, + result: true, + }); + return; + } + await openPopup({ path: RequestsRoutes.ConnectionRequest, data: parseResult.output, - context: makeContext(port), + context: requestContext, onClose: makeSendPopupClosedUserRejectionMessage({ tabId: getTabIdFromPort(port), messageId: parseResult.output.id, diff --git a/src/locales/en.json b/src/locales/en.json index 470eec381..a203ba8b8 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -229,7 +229,23 @@ "ALLOW": "Allow" }, "TOKEN_HIDDEN": "Token hidden and reported", - "UNDO": "Undo" + "UNDO": "Undo", + "ANNOUNCEMENTS": { + "NATIVE_SEGWIT": { + "TITLE": "Native SegWit is here!", + "DESCRIPTION_1": "Xverse now supports Native SegWit for lower fees and better performance!", + "DESCRIPTION_2": "You can select your preferred Bitcoin payment address (native or nested) used for sending, receiving, and connecting with apps.", + "DESCRIPTION_3": "Your Bitcoin balance includes funds from both addresses, and you can switch back anytime.", + "SELECT": "Select my Preferred Bitcoin Address", + "LATER": "Maybe later" + } + }, + "CALLOUTS": { + "NATIVE_SEGWIT": { + "TITLE": "Now using Native SegWit!", + "DESCRIPTION": "Your new Native SegWit address is set as preferred. Switch back to legacy (Nested SegWit) in Settings." + } + } }, "TOKEN_SCREEN": { "ADD_COINS": "Manage tokens", @@ -254,7 +270,17 @@ "ORDINAL_ADDRESS": "Ordinal, Runes & BRC-20 address", "ORDINALS_RECEIVE_MESSAGE": "Only use this address to receive Ordinals, Runes, and BRC-20 tokens.", "STX_RECEIVE_MESSAGE": "Only use this address to receive SIP-10 tokens.", - "BTC_RECEIVE_MESSAGE": "Only use this address to receive Bitcoin." + "BTC_RECEIVE_MESSAGE": "Only use this address to receive Bitcoin.", + "CALLOUTS": { + "NATIVE_SEGWIT": { + "TITLE": "Now using Native SegWit!", + "DESCRIPTION": "Your new Native SegWit address is set as preferred address." + }, + "ADDRESS_CHANGE_TOOLTIP": { + "TITLE": "Switch Preferred Bitcoin Address", + "DESCRIPTION": "You can switch between Native SegWit and legacy (Nested SegWit)" + } + } }, "SEND": { "BTC": { @@ -263,7 +289,9 @@ "MAX_IGNORING_DUST_UTXO_MSG": "Small amounts may remain in your wallet, as sending them costs more in fees than their value.", "NO_FUNDS_TITLE": "Your balance is empty", "NO_FUNDS": "You don't have any funds to send. You can purchase crypto quickly and deposit directly into your Xverse wallet.", - "BUY_BTC": "Buy Bitcoin" + "BUY_BTC": "Buy Bitcoin", + "SELECT_ADDRESS_TYPE": "Select address type", + "SELECT_ADDRESS_TYPE_DESCRIPTION": "Select your preferred address to send BTC. Using Native SegWit is recommended for reduced fees and better performance." }, "STX": { "BUY_STX": "Buy Stacks" @@ -644,17 +672,23 @@ }, "FORGOT_PASSWORD_SCREEN": { "TITLE": "Forgot Password", - "PARAGRAPH1": "Xverse does not keep a copy of your password. If you're unable to access your account, you will need to reset your wallet and input the seedphrase you used when you generated your wallet.", + "PARAGRAPH1": "Xverse does not keep a copy of your password. If you're unable to access your account, you will need to reset your wallet and input the seed phrase you used when you generated your wallet.", "PARAGRAPH2": "This will reset your wallet from this browser. Make sure you have your seed phrase backed up.", - "BACKUP_CHECKBOX_LABEL": "I backed up my seedphrase", + "BACKUP_CHECKBOX_LABEL": "I backed up my seed phrase", "CANCEL": "Cancel", "RESET": "Reset" }, "RESTORE_WALLET_SCREEN": { - "ENTER_SEED_HEADER": "Enter your seedphrase to restore your wallet.", + "ENTER_SEED_HEADER": "Enter your seed phrase to restore your wallet.", "SEED_INPUT_ERROR": "Invalid seed phrase, please try again", "CONTINUE_BUTTON": "Continue", - "HAVE_A_24_WORDS_SEEDPHRASE?": "Have a {{number}} words seedphrase?" + "HAVE_A_COUNT_WORDS_SEED_PHRASE": "Have a {{number}} words seed phrase?", + "SELECT_ADDRESS_TYPE": { + "TITLE": "Default address type selection", + "DESCRIPTION": "Select the address type to set as your default; Native SegWit (more efficient) or Nested SegWit (legacy). You can switch back any time from settings.", + "ACCOUNT_COUNT": "Accounts: {{accounts}}", + "ACCOUNT_SUMMARY_DESCRIPTION": "The balances shown above are cumulated BTC holdings for each type of address across the detected accounts." + } }, "STACKING_SCREEN": { "STACK_AND_EARN": "Stack STX, earn BTC", @@ -705,6 +739,7 @@ "MINT": "Mint", "TRANSFER": "Transfer", "DEPLOY": "Deploy", + "VERIFY": "Verify", "VERIFY_ADDRESS_ON_LEDGER": "Verify address on Ledger", "VIEW_ADDRESS": "View address", "ADD_STACKS_ADDRESS": "Add a Stacks address", @@ -897,6 +932,13 @@ "ENABLE_RARE_SATS_DETAIL": "Automatically scan your ordinal address for rare sats and display them in your collectibles and transaction review screens", "ENABLE_SPEED_UP_TRANSACTIONS": "Enable speed up transactions", "ENABLE_SPEED_UP_TRANSACTIONS_DETAIL": "Allows you to speed up unconfirmed transactions by paying a higher fee.", + "PREFERRED_BTC_ADDRESS": { + "TITLE": "Preferred Bitcoin Address", + "DESCRIPTION": "You can switch between Native SegWit (more efficient) and Nested SegWit (legacy) for your transactions. The selected address will be used for sending, receiving, and connecting with dapps. Your total balance includes funds from both addresses, and you can switch back anytime.", + "NESTED_SEGWIT": "Nested SegWit", + "NATIVE_SEGWIT": "Native SegWit", + "SUCCESS": "Preferred Bitcoin address changed" + }, "ADVANCED": "Advanced", "XVERSE_DEFAULT": "Use Xverse as default wallet", "XVERSE_DEFAULT_DESCRIPTION": "Allow apps to prioritize Xverse when looking for a wallet with which to connect." @@ -938,7 +980,8 @@ "SET_ACTION": "Set as avatar", "SET_TOAST": "Item set as avatar", "REMOVE_ACTION": "Remove avatar", - "REMOVE_TOAST": "Avatar removed" + "REMOVE_TOAST": "Avatar removed", + "UNDO": "Avatar reverted" } }, "BUY_SCREEN": { @@ -1043,6 +1086,7 @@ "TRANSACTIONS": "TRANSACTIONS", "CONTRACT": "CONTRACT", "BUNDLES": "BUNDLES", + "BREAKDOWN": "BREAKDOWN", "COMING_SOON": "Coming soon", "VERIFY_ADDRESS_ON_LEDGER": "Verify address on Ledger", "VIEW_ADDRESS": "View address", diff --git a/src/theme/index.ts b/src/theme/index.ts index 3e01d0118..c02cb0907 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -95,6 +95,9 @@ const Theme = { lilac: '#5E41C5', lilac_dark: '#4F34BA', + // utility + transparent: 'rgba(0, 0, 0, 0)', + action: { /** * @deprecated From ab896c80569e7ea0d9fee46a788fbd36cf0bfe90 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Wed, 16 Oct 2024 08:24:29 +0000 Subject: [PATCH 177/227] release: v0.43.3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16ff10803..d6a786dc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "xverse-web-extension", - "version": "0.43.2", + "version": "0.43.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "xverse-web-extension", - "version": "0.43.2", + "version": "0.43.3", "dependencies": { "@aryzing/superqs": "0.0.6", "@ledgerhq/hw-transport-webusb": "^6.27.13", diff --git a/package.json b/package.json index ce7f76e91..3d0767e07 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xverse-web-extension", "description": "A Bitcoin wallet for Web3", - "version": "0.43.2", + "version": "0.43.3", "private": true, "engines": { "node": "^18.18.2" From 935d695eea68924ff58bde68faf731f5c93ba13b Mon Sep 17 00:00:00 2001 From: Den <36603049+dhriaznov@users.noreply.github.com> Date: Wed, 16 Oct 2024 10:55:32 +0200 Subject: [PATCH 178/227] [ENG-3578][ENG-3638] Star/hide/unhide Stacks NFT/collection (#595) * feat: star/unstar collections, standalone inscriptions, and inscriptions in a collection (#569) * commit initial draft * use latest core * use latest core and support inscription in collection * complete UI work * fix final touchups * fix standalone inscription not handled * touchup namings * touchup namings * typo * commit lock * use latest UI * touchup * fix keys * fix conditionals * use latest core * use release core * code review * [ENG-3634][ENG-3635] Inscriptions - hide a collection / manage hidden collections (#567) * [ENG-3634][ENG-3635] Inscriptions - hide a collection / manage hidden collections * Add a todo * commit initial draft * use latest core * use latest core and support inscription in collection * Update the three dots menu button to match Figma and be consistent on different screens * complete UI work * Add HiddenCollectibles screen, upgrade `@phosphor-icons/react` package * fix final touchups * fix standalone inscription not handled * touchup namings * touchup namings * typo * commit lock * use latest UI * touchup * fix errors * touchup (incomplete) * update naming * fix keys * fix conditionals * use latest core * fix logics * chore: add dedicated route for hidden items and fix logic and ui issues founded * chore: update core version and fix small issue and copy improvements * chore: update core to v19.2.1 * fix: toast message right margin when no action is present * chore: change starredCollectibleIds back to array to keep the order in which id were added and fix some ui issues found during testing * chore: remove unused comments, use dynamic tab params in hidden dashboard and remove unnecessary check * chore: DRY --------- Co-authored-by: Terence Ng Co-authored-by: fede erbes * fix bug (#593) * feat: star/unstar collections, standalone inscriptions, and inscriptions in a collection (#569) * commit initial draft * use latest core * use latest core and support inscription in collection * complete UI work * fix final touchups * fix standalone inscription not handled * touchup namings * touchup namings * typo * commit lock * use latest UI * touchup * fix keys * fix conditionals * use latest core * use release core * code review * [ENG-3634][ENG-3635] Inscriptions - hide a collection / manage hidden collections (#567) * [ENG-3634][ENG-3635] Inscriptions - hide a collection / manage hidden collections * Add a todo * commit initial draft * use latest core * use latest core and support inscription in collection * Update the three dots menu button to match Figma and be consistent on different screens * complete UI work * Add HiddenCollectibles screen, upgrade `@phosphor-icons/react` package * fix final touchups * fix standalone inscription not handled * touchup namings * touchup namings * typo * commit lock * use latest UI * touchup * fix errors * touchup (incomplete) * update naming * fix keys * fix conditionals * use latest core * fix logics * chore: add dedicated route for hidden items and fix logic and ui issues founded * chore: update core version and fix small issue and copy improvements * chore: update core to v19.2.1 * fix: toast message right margin when no action is present * chore: change starredCollectibleIds back to array to keep the order in which id were added and fix some ui issues found during testing * chore: remove unused comments, use dynamic tab params in hidden dashboard and remove unnecessary check * chore: DRY --------- Co-authored-by: Terence Ng Co-authored-by: fede erbes * fix bug --------- Co-authored-by: Den <36603049+dhriaznov@users.noreply.github.com> Co-authored-by: fede erbes * [ENG-3578][ENG-3638] Star/hide Stacks NFT/collection * remove collectibleId from hiddenCollectible (not needed) * add new lock * Move ordinal details loaders to a separate file * Add options dialog for the nft collection screen * Fix back button redirect for ordinals * Add NFTs tab to the Hidden collectibles screen, integrate new logic for filtering hidden/starred items * Make some code tweaks, fix redirect to the hidden nft collectible * Fix the useStacksCollectibles hook caching * Add unhide all for stacks nfts * upgrade the xverse-core version * feat: nft as avatar feature (#597) feat: accept NFT as Avatar source Co-authored-by: Cloud Le * Add stacks NFT starring functionality * Move NftDetailScreen loaders to a separate file * Rearrange NftDetailScreen imports * Fix Stacks NFT starring logic * Add the $ sign for the isGalleryOpen style property on the ordinalsCollecion screen * Add star and three dots menu on the Stacks NFT collection gallery screen * Show the Unhide all button on the Hidden collectibles gallery page * Redirect to the hidden collectibles from the Stacks NFT hidden collection screen * Fix stacks nft go back function & button copy * Make some code tweaks, fix the button indents for Stacks nft screen * Remove the Stacks NFT avatar when hiding its collection * update new lock * feat: clean-up invalid Avatar on launch (#607) * feat: clean-up invalid Avatar on launch * feat: conditionally remove avatar when hide collection * feat: disable SetAvatar action for hidden collection * Update src/app/screens/ordinalsCollection/index.tsx * Make some code tweaks * chore: better naming for conditional that not click instantly * chore: use stacks instead of stack for avatar related feature * feat: optimize avatarCleanup hook, better network performance * chore: optimize accountList loop --------- Co-authored-by: Cloud Le Co-authored-by: Den <36603049+dhriaznov@users.noreply.github.com> Co-authored-by: Denys Hriaznov * chore: fix style check warning * Update stacks nft check logic * Update the nft check * Update the NftDashboardHidden screen * Add snackbar for the unhide all, with Undo button * Remove the redundant dismissToast prop * Improve the check for hiding stacks collection * Fix the nft starred logic * Remove the old TODO comment * Update xverse core version, fix the horizontal scroll on the Collectible details --------- Co-authored-by: Terence Ng Co-authored-by: fede erbes Co-authored-by: Kayaba Akihiko Co-authored-by: Cloud Le --- .../collectibleCollectionGridItem/index.tsx | 9 +- .../hooks/queries/useStacksCollectibles.ts | 14 +- src/app/routes/index.tsx | 6 +- .../screens/confirmNftTransaction/index.tsx | 3 +- src/app/screens/nftCollection/index.styled.ts | 99 +++++ src/app/screens/nftCollection/index.tsx | 353 ++++++++++++------ .../screens/nftCollection/useNftCollection.ts | 23 +- .../collectiblesTabs/index.styled.ts | 12 + .../nftDashboard/collectiblesTabs/index.tsx | 48 +-- src/app/screens/nftDashboard/hidden/index.tsx | 222 ++++++++--- .../nftDashboard/inscriptionsTabGridItem.tsx | 10 +- src/app/screens/nftDashboard/nft.tsx | 23 +- .../screens/nftDashboard/nftTabGridItem.tsx | 46 ++- .../screens/nftDashboard/useNftDashboard.tsx | 45 ++- src/app/screens/nftDetail/index.styled.ts | 124 ++---- src/app/screens/nftDetail/index.tsx | 221 ++++++----- src/app/screens/nftDetail/loaders.tsx | 113 ++++++ src/app/screens/nftDetail/useNftDetail.ts | 10 +- src/app/screens/ordinalDetail/index.styled.ts | 44 +-- src/app/screens/ordinalDetail/index.tsx | 87 +---- src/app/screens/ordinalDetail/loaders.tsx | 101 +++++ .../ordinalsCollection/index.styled.ts | 13 +- src/app/screens/ordinalsCollection/index.tsx | 50 +-- .../stores/wallet/actions/actionCreators.ts | 9 + src/app/stores/wallet/actions/types.ts | 7 + src/app/stores/wallet/reducer.ts | 9 + src/app/utils/constants.ts | 1 + src/locales/en.json | 4 +- 28 files changed, 1139 insertions(+), 567 deletions(-) create mode 100644 src/app/screens/nftCollection/index.styled.ts create mode 100644 src/app/screens/nftDetail/loaders.tsx create mode 100644 src/app/screens/ordinalDetail/loaders.tsx diff --git a/src/app/components/collectibleCollectionGridItem/index.tsx b/src/app/components/collectibleCollectionGridItem/index.tsx index b788ad74c..b6bafc120 100644 --- a/src/app/components/collectibleCollectionGridItem/index.tsx +++ b/src/app/components/collectibleCollectionGridItem/index.tsx @@ -89,17 +89,22 @@ function CollectibleCollectionGridItem({ } : undefined; - const { ordinalsAddress } = useSelectedAccount(); + const { ordinalsAddress, stxAddress } = useSelectedAccount(); const { starredCollectibleIds } = useWalletSelector(); const inscriptionStarred = isInscription(item) && starredCollectibleIds[ordinalsAddress]?.some(({ id }) => id === item.id); + const nftStarred = + !isInscription(item) && + starredCollectibleIds[stxAddress]?.some( + ({ id }) => id === `${item?.asset_identifier}:${item?.identifier.tokenId}`, + ); return ( {children} - {inscriptionStarred && ( + {(inscriptionStarred || nftStarred) && ( )} diff --git a/src/app/hooks/queries/useStacksCollectibles.ts b/src/app/hooks/queries/useStacksCollectibles.ts index b8f21ae35..da1a70d77 100644 --- a/src/app/hooks/queries/useStacksCollectibles.ts +++ b/src/app/hooks/queries/useStacksCollectibles.ts @@ -1,20 +1,28 @@ import useNetworkSelector from '@hooks/useNetwork'; import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; import { getNftCollections, type StacksCollectionList } from '@secretkeylabs/xverse-core'; import { useQuery } from '@tanstack/react-query'; import { handleRetries } from '@utils/query'; -const useStacksCollectibles = () => { +const useStacksCollectibles = (showHiddenOnly?: boolean) => { const { stxAddress } = useSelectedAccount(); const selectedNetwork = useNetworkSelector(); + const { hiddenCollectibleIds, starredCollectibleIds } = useWalletSelector(); + const hiddenIds = Object.keys(hiddenCollectibleIds[stxAddress] ?? {}); + const starredIds = starredCollectibleIds[stxAddress]?.map(({ id }) => id); const fetchNftCollections = (): Promise => - getNftCollections(stxAddress, selectedNetwork); + getNftCollections(stxAddress, selectedNetwork, { + hiddenCollectibleIds: hiddenIds, + starredCollectibleIds: starredIds, + showHiddenOnly, + }); return useQuery({ enabled: !!stxAddress, retry: handleRetries, - queryKey: ['nft-collection-data', stxAddress], + queryKey: ['nft-collection-data', stxAddress, hiddenIds, starredIds, showHiddenOnly], queryFn: fetchNftCollections, staleTime: 5 * 60 * 1000, // 5mins }); diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index a6cbdee70..10072848e 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -38,7 +38,7 @@ import ManageTokens from '@screens/manageTokens'; import MintRune from '@screens/mintRune'; import NftCollection from '@screens/nftCollection'; import NftDashboard from '@screens/nftDashboard'; -import Index from '@screens/nftDashboard/hidden'; +import NftDashboardHidden from '@screens/nftDashboard/hidden'; import SupportedRarities from '@screens/nftDashboard/supportedRarities'; import NftDetailScreen from '@screens/nftDetail'; import OrdinalDetailScreen from '@screens/ordinalDetail'; @@ -574,7 +574,7 @@ const router = createHashRouter([ path: 'nft-dashboard/hidden', element: ( - + ), }, @@ -611,7 +611,7 @@ const router = createHashRouter([ element: , }, { - path: 'nft-dashboard/nft-collection/:id', + path: 'nft-dashboard/nft-collection/:id/:from?', element: ( diff --git a/src/app/screens/confirmNftTransaction/index.tsx b/src/app/screens/confirmNftTransaction/index.tsx index 37d3357fc..3c39993f8 100644 --- a/src/app/screens/confirmNftTransaction/index.tsx +++ b/src/app/screens/confirmNftTransaction/index.tsx @@ -77,7 +77,7 @@ function ConfirmNftTransaction() { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const isGalleryOpen: boolean = document.documentElement.clientWidth > 360; const selectedAccount = useSelectedAccount(); - const { avatarIds } = useWalletSelector(); + const { avatarIds, network } = useWalletSelector(); const selectedAvatar = avatarIds[selectedAccount.ordinalsAddress]; const [fee, setFee] = useState(); const navigate = useNavigate(); @@ -89,7 +89,6 @@ function ConfirmNftTransaction() { const { unsignedTx: unsignedTxHex, recipientAddress } = location.state; const unsignedTx = useMemo(() => deserializeTransaction(unsignedTxHex), [unsignedTxHex]); - const { network } = useWalletSelector(); const { refetch } = useStxWalletData(); const selectedNetwork = useNetworkSelector(); const { diff --git a/src/app/screens/nftCollection/index.styled.ts b/src/app/screens/nftCollection/index.styled.ts new file mode 100644 index 000000000..552e0c923 --- /dev/null +++ b/src/app/screens/nftCollection/index.styled.ts @@ -0,0 +1,99 @@ +import { GridContainer } from '@screens/nftDashboard/collectiblesTabs/index.styled'; +import styled from 'styled-components'; + +interface Props { + $isGalleryOpen?: boolean; +} + +export const Container = styled.div((props) => ({ + ...props.theme.scrollbar, + display: 'flex', + flexDirection: 'column', + flex: 1, +})); + +export const NoCollectiblesText = styled.p((props) => ({ + ...props.theme.typography.body_bold_m, + color: props.theme.colors.white_200, + marginTop: props.theme.space.xl, + marginBottom: 'auto', + textAlign: 'center', +})); + +export const HeadingText = styled.p((props) => ({ + ...props.theme.typography.body_bold_m, + color: props.theme.colors.white_400, +})); + +export const CollectionText = styled.p((props) => ({ + ...props.theme.typography.headline_s, + color: props.theme.colors.white_0, + marginTop: props.theme.space.xxxs, + marginBottom: props.theme.space.xs, + wordBreak: 'break-word', +})); + +export const BottomBarContainer = styled.div({ + marginTop: 'auto', +}); + +export const PageHeader = styled.div` + padding: ${(props) => props.theme.space.xs}; + padding-top: 0; + max-width: 1224px; + margin-top: ${(props) => (props.$isGalleryOpen ? props.theme.space.xxl : props.theme.space.l)}; + margin-left: auto; + margin-right: auto; + width: 100%; +`; + +export const PageHeaderContent = styled.div` + display: flex; + flex-direction: ${(props) => (props.$isGalleryOpen ? 'row' : 'column')}; + justify-content: ${(props) => (props.$isGalleryOpen ? 'space-between' : 'initial')}; + row-gap: ${(props) => props.theme.space.xl}; +`; + +export const NftContainer = styled.div` + display: flex; + flex-direction: ${(props) => (props.$isGalleryOpen ? 'column' : 'row')}; + justify-content: ${(props) => (props.$isGalleryOpen ? 'space-between' : 'initial')}; + column-gap: ${(props) => props.theme.space.m}; +`; + +export const BackButtonContainer = styled.div((props) => ({ + display: 'flex', + marginBottom: props.theme.space.xxl, +})); + +export const BackButton = styled.button((props) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + background: 'transparent', + marginBottom: props.theme.space.l, +})); + +export const AssetDetailButtonText = styled.div((props) => ({ + ...props.theme.typography.body_m, + marginLeft: props.theme.space.xxxs, + color: props.theme.colors.white_0, + textAlign: 'center', +})); + +export const StyledGridContainer = styled(GridContainer)` + margin-top: ${(props) => props.theme.space.s}; + padding: 0 ${(props) => props.theme.space.xs}; + padding-bottom: ${(props) => props.theme.space.xl}; + max-width: 1224px; + margin-left: auto; + margin-right: auto; + width: 100%; +`; + +export const CollectionNameDiv = styled.div` + display: flex; + align-items: center; + gap: ${(props) => props.theme.space.s}; +`; diff --git a/src/app/screens/nftCollection/index.tsx b/src/app/screens/nftCollection/index.tsx index 38ff17e39..5eb7d96f0 100644 --- a/src/app/screens/nftCollection/index.tsx +++ b/src/app/screens/nftCollection/index.tsx @@ -1,121 +1,55 @@ import AccountHeaderComponent from '@components/accountHeader'; import CollectibleCollectionGridItem from '@components/collectibleCollectionGridItem'; import CollectibleDetailTile from '@components/collectibleDetailTile'; +import SquareButton from '@components/squareButton'; import BottomTabBar from '@components/tabBar'; import { StyledBarLoader, TilesSkeletonLoader } from '@components/tilesSkeletonLoader'; import TopRow from '@components/topRow'; import WebGalleryButton from '@components/webGalleryButton'; import WrenchErrorMessage from '@components/wrenchErrorMessage'; import useNftDetail from '@hooks/queries/useNftDetail'; -import { ArrowLeft } from '@phosphor-icons/react'; -import { GridContainer } from '@screens/nftDashboard/collectiblesTabs/index.styled'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { ArchiveTray, ArrowLeft, DotsThreeVertical, Star } from '@phosphor-icons/react'; import Nft from '@screens/nftDashboard/nft'; import NftImage from '@screens/nftDashboard/nftImage'; +import { StyledButton } from '@screens/ordinalsCollection/index.styled'; import type { NonFungibleToken, StacksCollectionData } from '@secretkeylabs/xverse-core'; -import { EMPTY_LABEL } from '@utils/constants'; +import { + addToHideCollectiblesAction, + addToStarCollectiblesAction, + removeAccountAvatarAction, + removeFromHideCollectiblesAction, + removeFromStarCollectiblesAction, +} from '@stores/wallet/actions/actionCreators'; +import Sheet from '@ui-library/sheet'; +import SnackBar from '@ui-library/snackBar'; +import { EMPTY_LABEL, LONG_TOAST_DURATION } from '@utils/constants'; import { getFullyQualifiedKey, getNftCollectionsGridItemId, isBnsCollection } from '@utils/nfts'; -import { useRef, type PropsWithChildren } from 'react'; +import { useRef, useState, type PropsWithChildren } from 'react'; import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { useIsVisible } from 'react-is-visible'; +import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; +import Theme from 'theme'; +import { + AssetDetailButtonText, + BackButton, + BackButtonContainer, + BottomBarContainer, + CollectionNameDiv, + CollectionText, + Container, + HeadingText, + NftContainer, + NoCollectiblesText, + PageHeader, + PageHeaderContent, + StyledGridContainer, +} from './index.styled'; import useNftCollection from './useNftCollection'; -interface Props { - isGalleryOpen?: boolean; -} -const Container = styled.div((props) => ({ - ...props.theme.scrollbar, - display: 'flex', - flexDirection: 'column', - flex: 1, -})); - -const NoCollectiblesText = styled.p((props) => ({ - ...props.theme.typography.body_bold_m, - color: props.theme.colors.white_200, - marginTop: props.theme.spacing(16), - marginBottom: 'auto', - textAlign: 'center', -})); - -const HeadingText = styled.p((props) => ({ - ...props.theme.typography.body_bold_m, - color: props.theme.colors.white_400, -})); - -const CollectionText = styled.p((props) => ({ - ...props.theme.typography.headline_s, - color: props.theme.colors.white_0, - marginTop: props.theme.spacing(1), - marginBottom: props.theme.spacing(4), - wordBreak: 'break-word', -})); - -const BottomBarContainer = styled.div({ - marginTop: 'auto', -}); - -const PageHeader = styled.div` - padding: ${(props) => props.theme.space.xs}; - padding-top: 0; - max-width: 1224px; - margin-top: ${(props) => (props.isGalleryOpen ? props.theme.space.xxl : props.theme.space.l)}; - margin-left: auto; - margin-right: auto; - width: 100%; -`; - -const PageHeaderContent = styled.div` - display: flex; - flex-direction: ${(props) => (props.isGalleryOpen ? 'row' : 'column')}; - justify-content: ${(props) => (props.isGalleryOpen ? 'space-between' : 'initial')}; - row-gap: ${(props) => props.theme.space.xl}; -`; - -const NftContainer = styled.div` - display: flex; - flex-direction: ${(props) => (props.isGalleryOpen ? 'column' : 'row')}; - justify-content: ${(props) => (props.isGalleryOpen ? 'space-between' : 'initial')}; - column-gap: ${(props) => props.theme.space.m}; -`; - -const BackButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - width: 800, - marginTop: props.theme.spacing(40), -})); - -const BackButton = styled.button((props) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'flex-start', - alignItems: 'center', - background: 'transparent', - marginBottom: props.theme.spacing(12), -})); - -const AssetDetailButtonText = styled.div((props) => ({ - ...props.theme.typography.body_m, - fontWeight: 400, - fontSize: 14, - marginLeft: 2, - color: props.theme.colors.white_0, - textAlign: 'center', -})); - -const StyledGridContainer = styled(GridContainer)` - margin-top: ${(props) => props.theme.space.s}; - padding: 0 ${(props) => props.theme.space.xs}; - padding-bottom: ${(props) => props.theme.space.xl}; - max-width: 1224px; - margin-left: auto; - margin-right: auto; - width: 100%; -`; - /* * component to virtualise the grid item if not in window * placeholder is required to match grid item size, in order to negate scroll jank @@ -176,6 +110,12 @@ function CollectionGridItemWithData({ function NftCollection() { const { t } = useTranslation('translation', { keyPrefix: 'COLLECTIBLE_COLLECTION_SCREEN' }); + const { t: commonT } = useTranslation('translation', { keyPrefix: 'COMMON' }); + const selectedAccount = useSelectedAccount(); + const { starredCollectibleIds, hiddenCollectibleIds, avatarIds } = useWalletSelector(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [isOptionsModalVisible, setIsOptionsModalVisible] = useState(false); const { collectionData, portfolioValue, @@ -186,37 +126,193 @@ function NftCollection() { handleBackButtonClick, openInGalleryView, } = useNftCollection(); + const currentAvatar = avatarIds[selectedAccount.btcAddress]; + + const openOptionsDialog = () => { + setIsOptionsModalVisible(true); + }; + + const closeOptionsDialog = () => { + setIsOptionsModalVisible(false); + }; + + const collectionStarred = starredCollectibleIds[selectedAccount.stxAddress]?.some( + ({ id }) => id === collectionData?.collection_id, + ); + const collectionHidden = Object.keys(hiddenCollectibleIds[selectedAccount.stxAddress] ?? {}).some( + (id) => id === collectionData?.collection_id, + ); + + const handleUnHideCollection = () => { + const isLastHiddenItem = + Object.keys(hiddenCollectibleIds[selectedAccount.stxAddress] ?? {}).length === 1; + dispatch( + removeFromHideCollectiblesAction({ + address: selectedAccount.stxAddress, + id: collectionData?.collection_id ?? '', + }), + ); + closeOptionsDialog(); + toast.custom(); + navigate(`/nft-dashboard/${isLastHiddenItem ? '' : 'hidden'}?tab=nfts`); + }; + + const handleClickUndoHiding = (toastId: string) => { + dispatch( + removeFromHideCollectiblesAction({ + address: selectedAccount.stxAddress, + id: collectionData?.collection_id ?? '', + }), + ); + toast.remove(toastId); + toast.custom(, { + duration: LONG_TOAST_DURATION, + }); + }; + + const handleHideCollection = () => { + dispatch( + addToHideCollectiblesAction({ + address: selectedAccount.stxAddress, + id: collectionData?.collection_id ?? '', + }), + ); + + if (currentAvatar?.type === 'stacks') { + const isHidingUsedAvatar = collectionData?.all_nfts.some( + (nft) => + `${nft.asset_identifier}:${nft.identifier.tokenId}` === + currentAvatar.nft.fully_qualified_token_id, + ); + + if (isHidingUsedAvatar) { + dispatch(removeAccountAvatarAction({ address: selectedAccount.btcAddress })); + } + } + + closeOptionsDialog(); + navigate('/nft-dashboard?tab=nfts'); + const toastId = toast.custom( + handleClickUndoHiding(toastId), + }} + />, + { duration: LONG_TOAST_DURATION }, + ); + }; + + const handleClickUndoStarring = (toastId: string) => { + dispatch( + removeFromStarCollectiblesAction({ + address: selectedAccount.stxAddress, + id: collectionData?.collection_id ?? '', + }), + ); + toast.remove(toastId); + toast.custom(); + }; + + const handleStarClick = () => { + if (collectionStarred) { + dispatch( + removeFromStarCollectiblesAction({ + address: selectedAccount.stxAddress, + id: collectionData?.collection_id ?? '', + }), + ); + toast.custom(, { + duration: LONG_TOAST_DURATION, + }); + } else { + dispatch( + addToStarCollectiblesAction({ + address: selectedAccount.stxAddress, + id: collectionData?.collection_id ?? '', + }), + ); + const toastId = toast.custom( + handleClickUndoStarring(toastId), + }} + />, + { duration: LONG_TOAST_DURATION }, + ); + } + }; return ( <> {isGalleryOpen ? ( ) : ( - + )} - + {isGalleryOpen && ( <> - {t('BACK_TO_GALLERY')} + {t(collectionHidden ? 'BACK_TO_HIDDEN_COLLECTIBLES' : 'BACK_TO_GALLERY')} )} - +
{t('COLLECTION')} - - {collectionData?.collection_name || } - + + + {collectionData?.collection_name || } + + {isGalleryOpen && ( + <> + {collectionHidden ? null : ( + + ) : ( + + ) + } + onPress={handleStarClick} + isTransparent + size={44} + radiusSize={12} + /> + )} + + } + onPress={openOptionsDialog} + isTransparent + size={44} + radiusSize={12} + /> + + )} + {!isGalleryOpen && }
- + ) : ( - collectionData?.all_nfts - .sort((a, b) => (a.value.repr > b.value.repr ? 1 : -1)) - .map((nft) => ( - - - - )) + collectionData?.all_nfts.map((nft) => ( + + + + )) )} @@ -266,6 +360,29 @@ function NftCollection() { )} + {isOptionsModalVisible && ( + + {collectionHidden ? ( + } + title={t('UNHIDE_COLLECTION')} + onClick={handleUnHideCollection} + /> + ) : ( + } + title={t('HIDE_COLLECTION')} + onClick={handleHideCollection} + /> + )} + + )} ); } diff --git a/src/app/screens/nftCollection/useNftCollection.ts b/src/app/screens/nftCollection/useNftCollection.ts index 37e64bc07..cf74fa48b 100644 --- a/src/app/screens/nftCollection/useNftCollection.ts +++ b/src/app/screens/nftCollection/useNftCollection.ts @@ -1,5 +1,7 @@ import useStacksCollectibles from '@hooks/queries/useStacksCollectibles'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; import { useMemo } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; @@ -7,12 +9,18 @@ export default function useNftCollection() { const navigate = useNavigate(); useResetUserFlow('/nft-collection'); - const { id: collectionId } = useParams(); - const { data, isLoading, error } = useStacksCollectibles(); + const { id: collectionId, from } = useParams(); + const comesFromHidden = from === 'hidden'; + const { data, isLoading, error } = useStacksCollectibles(comesFromHidden); + const { hiddenCollectibleIds } = useWalletSelector(); + const { stxAddress } = useSelectedAccount(); const collectionData = data?.results.find( (collection) => collection.collection_id === collectionId, ); + const collectionHidden = Object.keys(hiddenCollectibleIds[stxAddress] ?? {}).some( + (id) => id === collectionId, + ); const portfolioValue = collectionData?.floor_price && !Number.isNaN(collectionData?.all_nfts?.length) @@ -21,13 +29,16 @@ export default function useNftCollection() { const isGalleryOpen: boolean = useMemo(() => document.documentElement.clientWidth > 360, []); - const handleBackButtonClick = () => { - navigate('/nft-dashboard?tab=nfts'); - }; + const handleBackButtonClick = () => + navigate(`/nft-dashboard${comesFromHidden || collectionHidden ? '/hidden' : ''}?tab=nfts`); const openInGalleryView = async () => { await chrome.tabs.create({ - url: chrome.runtime.getURL(`options.html#/nft-dashboard/nft-collection/${collectionId}`), + url: chrome.runtime.getURL( + `options.html#/nft-dashboard/nft-collection/${collectionId}${ + comesFromHidden || collectionHidden ? '/hidden' : '' + }`, + ), }); }; diff --git a/src/app/screens/nftDashboard/collectiblesTabs/index.styled.ts b/src/app/screens/nftDashboard/collectiblesTabs/index.styled.ts index 50af867c7..ec0bd8425 100644 --- a/src/app/screens/nftDashboard/collectiblesTabs/index.styled.ts +++ b/src/app/screens/nftDashboard/collectiblesTabs/index.styled.ts @@ -83,6 +83,18 @@ export const StyledButton = styled(Button)` } `; +export const StyledSheetButton = styled(Button)` + &.tertiary { + justify-content: flex-start; + color: ${(props) => props.theme.colors.white_200}; + padding-left: 0; + + &:hover:enabled { + opacity: 0.8; + } + } +`; + export const TopBarContainer = styled.div((props) => ({ display: 'flex', justifyContent: 'space-between', diff --git a/src/app/screens/nftDashboard/collectiblesTabs/index.tsx b/src/app/screens/nftDashboard/collectiblesTabs/index.tsx index 412e37ddf..4a8eec173 100644 --- a/src/app/screens/nftDashboard/collectiblesTabs/index.tsx +++ b/src/app/screens/nftDashboard/collectiblesTabs/index.tsx @@ -1,5 +1,5 @@ import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed'; -import { TrayArrowDown } from '@phosphor-icons/react'; +import { ArchiveTray } from '@phosphor-icons/react'; import { mapRareSatsAPIResponseToBundle, type Bundle } from '@secretkeylabs/xverse-core'; import Button from '@ui-library/button'; import { StyledP } from '@ui-library/common.styled'; @@ -134,25 +134,23 @@ export default function CollectiblesTabs({ ) : ( <> - - {totalInscriptions > 0 ? ( + {totalInscriptions > 0 && ( + {totalInscriptions === 1 ? t('TOTAL_ITEMS_ONE') : t('TOTAL_ITEMS', { count: totalInscriptions })} - ) : ( -
- )} - } - title={t('HIDDEN_COLLECTIBLES')} - onClick={() => { - navigate(`/nft-dashboard/hidden?tab=${tabs[tabIndex]?.key}`); - }} - /> - + } + title={t('HIDDEN_COLLECTIBLES')} + onClick={() => { + navigate(`/nft-dashboard/hidden?tab=${tabs[tabIndex]?.key}`); + }} + /> + + )} {inscriptionListView} )} @@ -165,13 +163,19 @@ export default function CollectiblesTabs({ ) : ( <> {totalNfts > 0 && ( - - {totalNfts === 1 ? t('TOTAL_ITEMS_ONE') : t('TOTAL_ITEMS', { count: totalNfts })} - + + + {totalNfts === 1 ? t('TOTAL_ITEMS_ONE') : t('TOTAL_ITEMS', { count: totalNfts })} + + } + title={t('HIDDEN_COLLECTIBLES')} + onClick={() => { + navigate('/nft-dashboard/hidden?tab=nfts'); + }} + /> + )} {nftListView} diff --git a/src/app/screens/nftDashboard/hidden/index.tsx b/src/app/screens/nftDashboard/hidden/index.tsx index 32b3a0dd6..abc8942ab 100644 --- a/src/app/screens/nftDashboard/hidden/index.tsx +++ b/src/app/screens/nftDashboard/hidden/index.tsx @@ -2,18 +2,30 @@ import AccountHeaderComponent from '@components/accountHeader'; import BottomTabBar from '@components/tabBar'; import TopRow from '@components/topRow'; import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; import { ArrowLeft, TrayArrowUp } from '@phosphor-icons/react'; -import { removeAllFromHideCollectiblesAction } from '@stores/wallet/actions/actionCreators'; +import { + removeAllFromHideCollectiblesAction, + setHiddenCollectiblesAction, +} from '@stores/wallet/actions/actionCreators'; import { StyledHeading, StyledP } from '@ui-library/common.styled'; import Sheet from '@ui-library/sheet'; +import SnackBar from '@ui-library/snackBar'; +import { TabItem } from '@ui-library/tabs'; +import { LONG_TOAST_DURATION } from '@utils/constants'; import { useState } from 'react'; +import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { TabPanel, Tabs } from 'react-tabs'; import styled from 'styled-components'; -import Theme from '../../../../theme'; -import { StyledButton } from '../collectiblesTabs/index.styled'; +import Theme from 'theme'; +import { + StickyStyledTabList, + StyledButton, + StyledSheetButton, +} from '../collectiblesTabs/index.styled'; import SkeletonLoader from '../collectiblesTabs/skeletonLoader'; import useNftDashboard from '../useNftDashboard'; @@ -62,11 +74,32 @@ const BackButton = styled.button` color: ${(props) => props.theme.colors.white_0}; `; -const ItemCountContainer = styled.div<{ isGalleryOpen: boolean }>` - margin-top: ${(props) => (props.isGalleryOpen ? props.theme.space.l : props.theme.space.m)}; +const ItemCountContainer = styled.div<{ $isGalleryOpen: boolean }>` + margin-top: ${(props) => (props.$isGalleryOpen ? props.theme.space.l : props.theme.space.m)}; `; -function Index() { +type TabButton = { + key: string; + label: string; +}; + +const tabs: TabButton[] = [ + { + key: 'inscriptions', + label: 'INSCRIPTIONS', + }, + { + key: 'nfts', + label: 'NFTS', + }, +]; + +const tabKeyToIndex = (key?: string | null) => { + if (!key) return 0; + return tabs.findIndex((tab) => tab.key === key); +}; + +function NftDashboardHidden() { const { t } = useTranslation('translation', { keyPrefix: 'NFT_DASHBOARD_SCREEN' }); const { t: tCommon } = useTranslation('translation', { keyPrefix: 'COMMON' }); const { t: tCollectibles } = useTranslation('translation', { @@ -74,28 +107,89 @@ function Index() { }); const [searchParams] = useSearchParams(); const tab = searchParams?.get('tab'); - const nftDashboard = useNftDashboard(); - const { ordinalsAddress } = useSelectedAccount(); - const navigate = useNavigate(); - const dispatch = useDispatch(); const { isGalleryOpen, hiddenInscriptionsQuery, totalHiddenInscriptions, HiddenInscriptionListView, - } = nftDashboard; - + hiddenStacksNftsQuery, + totalHiddenNfts, + HiddenNftListView, + hasActivatedOrdinalsKey, + } = useNftDashboard(); + const { ordinalsAddress, stxAddress } = useSelectedAccount(); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const { hiddenCollectibleIds } = useWalletSelector(); const [isOptionsModalVisible, setIsOptionsModalVisible] = useState(false); + const [tabIndex, setTabIndex] = useState(tabKeyToIndex(tab)); + + const visibleTabButtons = tabs.filter((tabItem: TabButton) => { + if (tabItem.key === 'inscriptions' && !hasActivatedOrdinalsKey) { + return false; + } + return true; + }); const handleBackButtonClick = () => { navigate(`/nft-dashboard?tab=${tab}`); }; + const handleClickUndoHidingAll = ({ + toastId, + currentInscriptionsHidden, + currentStacksNftsHidden, + }: { + toastId: string; + currentInscriptionsHidden: Record; + currentStacksNftsHidden: Record; + }) => { + dispatch( + setHiddenCollectiblesAction({ + collectibleIds: { + [ordinalsAddress]: currentInscriptionsHidden, + [stxAddress]: currentStacksNftsHidden, + }, + }), + ); + toast.remove(toastId); + toast.custom(, { + duration: LONG_TOAST_DURATION, + }); + }; + const handleUnHideAll = () => { + const currentInscriptionsHidden = { + ...(hiddenCollectibleIds[ordinalsAddress] ?? {}), + }; + const currentStacksNftsHidden = { + ...(hiddenCollectibleIds[stxAddress] ?? {}), + }; + dispatch(removeAllFromHideCollectiblesAction({ address: ordinalsAddress })); + dispatch(removeAllFromHideCollectiblesAction({ address: stxAddress })); handleBackButtonClick(); + + const toastId = toast.custom( + + handleClickUndoHidingAll({ + toastId, + currentInscriptionsHidden, + currentStacksNftsHidden, + }), + }} + />, + { duration: LONG_TOAST_DURATION }, + ); }; + const handleSelectTab = (index: number) => setTabIndex(index); + const openOptionsDialog = () => { setIsOptionsModalVisible(true); }; @@ -111,7 +205,9 @@ function Index() { ) : ( 0 ? openOptionsDialog : undefined} + onMenuClick={ + totalHiddenInscriptions > 0 || totalHiddenNfts > 0 ? openOptionsDialog : undefined + } /> )} @@ -128,7 +224,7 @@ function Index() { {t('HIDDEN_COLLECTIBLES')} - {isGalleryOpen && totalHiddenInscriptions > 0 ? ( + {isGalleryOpen && (totalHiddenInscriptions > 0 || totalHiddenNfts > 0) ? ( } @@ -139,35 +235,74 @@ function Index() { - - {/* TODO: add tab selector once we support nft */} - -
+ {/* TODO: replace with Tabs component from `src/app/ui-library/tabs.tsx` */} + + {visibleTabButtons.length > 1 && ( + + {visibleTabButtons.map(({ key, label }) => ( + handleSelectTab(tabKeyToIndex(key))} + > + {t(label)} + + ))} + + )} + {hasActivatedOrdinalsKey && ( +
- {hiddenInscriptionsQuery.isInitialLoading ? ( - - ) : ( - <> - - {totalHiddenInscriptions > 0 ? ( - - {totalHiddenInscriptions === 1 - ? t('TOTAL_ITEMS_ONE') - : t('TOTAL_ITEMS', { count: totalHiddenInscriptions })} - - ) : ( -
- )} - - - - )} +
+ {hiddenInscriptionsQuery.isInitialLoading ? ( + + ) : ( + <> + + {totalHiddenInscriptions > 0 ? ( + + {totalHiddenInscriptions === 1 + ? t('TOTAL_ITEMS_ONE') + : t('TOTAL_ITEMS', { count: totalHiddenInscriptions })} + + ) : ( +
+ )} + + + + )} +
-
+ + )} + + {hiddenStacksNftsQuery.isInitialLoading ? ( + + ) : ( + <> + + {totalHiddenNfts > 0 ? ( + + {totalHiddenNfts === 1 + ? t('TOTAL_ITEMS_ONE') + : t('TOTAL_ITEMS', { count: totalHiddenNfts })} + + ) : ( +
+ )} + + + + )} @@ -179,7 +314,7 @@ function Index() { visible={isOptionsModalVisible} onClose={closeOptionsDialog} > - } title={t('UNHIDE_ALL')} @@ -190,4 +325,5 @@ function Index() { ); } -export default Index; + +export default NftDashboardHidden; diff --git a/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx b/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx index 2d9701bb1..74d384f0b 100644 --- a/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx +++ b/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx @@ -34,12 +34,12 @@ const InfoContainer = styled.div` const StyledItemIdContainer = styled.div` display: flex; - gap: 4px; + gap: ${(props) => props.theme.space.xxs}; width: 100%; `; const StyledStar = styled(Star)` - margin-top: 2px; + margin-top: ${(props) => props.theme.space.xxxs}; `; const StyledItemId = styled(StyledP)` @@ -76,12 +76,12 @@ function InscriptionsTabGridItem({ item: collection }: { item: InscriptionCollec (id) => id === getCollectionKey(collection), ); - const handleClickCollectionId = (e: React.MouseEvent) => { + const handleClickCollection = (e: React.MouseEvent) => { const collectionId = e.currentTarget.value; navigate(`/nft-dashboard/ordinals-collection/${collectionId}/${isItemHidden ? 'hidden' : ''}`); }; - const handleClickInscriptionId = (e: React.MouseEvent) => { + const handleClickInscription = (e: React.MouseEvent) => { const inscriptionId = e.currentTarget.value; navigate(`/nft-dashboard/ordinal-detail/${inscriptionId}/${isItemHidden ? 'hidden' : ''}`); }; @@ -95,7 +95,7 @@ function InscriptionsTabGridItem({ item: collection }: { item: InscriptionCollec data-testid="inscription-container" type="button" value={getCollectionKey(collection)} - onClick={isCollection(collection) ? handleClickCollectionId : handleClickInscriptionId} + onClick={isCollection(collection) ? handleClickCollection : handleClickInscription} > {!collection.thumbnail_inscriptions ? ( // eslint-disable-line no-nested-ternary diff --git a/src/app/screens/nftDashboard/nft.tsx b/src/app/screens/nftDashboard/nft.tsx index 31ba8f8f2..95cb4c251 100644 --- a/src/app/screens/nftDashboard/nft.tsx +++ b/src/app/screens/nftDashboard/nft.tsx @@ -5,15 +5,9 @@ import { isBnsContract } from '@utils/nfts'; import styled from 'styled-components'; import NftImage from './nftImage'; -interface Props { - asset: NonFungibleToken; - isGalleryOpen: boolean; -} -interface ContainerProps { - isGalleryView: boolean; -} - -const NftImageContainer = styled.div((props) => ({ +const NftImageContainer = styled.div<{ + $isGalleryView: boolean; +}>((props) => ({ display: 'flex', justifyContent: 'center', alignItems: 'flex-start', @@ -25,7 +19,7 @@ const NftImageContainer = styled.div((props) => ({ '> img': { width: '100%', }, - flexGrow: props.isGalleryView ? 1 : 'initial', + flexGrow: props.$isGalleryView ? 1 : 'initial', })); const GridItemContainer = styled.div((props) => ({ @@ -43,11 +37,17 @@ const BnsImage = styled.img({ height: '100%', }); +type Props = { + asset: NonFungibleToken; + isGalleryOpen: boolean; +}; + function Nft({ asset, isGalleryOpen }: Props) { const { data } = useNftDetail(asset.identifier); + return ( - + {isBnsContract(asset?.asset_identifier) ? ( ) : ( @@ -57,4 +57,5 @@ function Nft({ asset, isGalleryOpen }: Props) { ); } + export default Nft; diff --git a/src/app/screens/nftDashboard/nftTabGridItem.tsx b/src/app/screens/nftDashboard/nftTabGridItem.tsx index ee3ee663f..5fd1c8c15 100644 --- a/src/app/screens/nftDashboard/nftTabGridItem.tsx +++ b/src/app/screens/nftDashboard/nftTabGridItem.tsx @@ -1,9 +1,13 @@ import CollectibleCollage from '@components/collectibleCollage/collectibleCollage'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { Star } from '@phosphor-icons/react'; import type { StacksCollectionData } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import { getNftsTabGridItemSubText } from '@utils/nfts'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; +import Theme from 'theme'; import Nft from './nft'; import NftImage from './nftImage'; @@ -24,6 +28,12 @@ const InfoContainer = styled.div` width: 100%; `; +const TitleContainer = styled.div` + display: flex; + gap: 4px; + width: 100%; +`; + const StyledItemId = styled(StyledP)` text-align: left; text-wrap: nowrap; @@ -40,17 +50,32 @@ const StyledItemSub = styled(StyledP)` width: 100%; `; -function NftTabGridItem({ - item: collection, - isLoading = false, -}: { +const StyledStar = styled(Star)` + margin-top: ${(props) => props.theme.space.xxxs}; +`; + +type Props = { item: StacksCollectionData; isLoading?: boolean; -}) { +}; + +function NftTabGridItem({ item: collection, isLoading = false }: Props) { const navigate = useNavigate(); + const { stxAddress } = useSelectedAccount(); + const { hiddenCollectibleIds, starredCollectibleIds } = useWalletSelector(); + + const isItemHidden = Object.keys(hiddenCollectibleIds[stxAddress] ?? {}).some( + (id) => id === collection.collection_id, + ); + + const collectionStarred = starredCollectibleIds[stxAddress]?.some( + ({ id }) => id === collection.collection_id, + ); const handleClickCollection = () => { - navigate(`nft-collection/${collection.collection_id}`); + navigate( + `/nft-dashboard/nft-collection/${collection.collection_id}/${isItemHidden ? 'hidden' : ''}`, + ); }; const itemId = collection.collection_name; @@ -68,9 +93,12 @@ function NftTabGridItem({ )} - - {itemId} - + + {collectionStarred && } + + {itemId} + + {itemSubText} diff --git a/src/app/screens/nftDashboard/useNftDashboard.tsx b/src/app/screens/nftDashboard/useNftDashboard.tsx index 5b43a8f04..67427cb19 100644 --- a/src/app/screens/nftDashboard/useNftDashboard.tsx +++ b/src/app/screens/nftDashboard/useNftDashboard.tsx @@ -24,20 +24,20 @@ import NftTabGridItem from './nftTabGridItem'; const NoCollectiblesText = styled.h1((props) => ({ ...props.theme.typography.body_bold_m, color: props.theme.colors.white_200, - marginTop: props.theme.spacing(16), + marginTop: props.theme.space.xl, marginBottom: 'auto', textAlign: 'center', })); const ErrorContainer = styled.div((props) => ({ - marginTop: props.theme.spacing(20), + marginTop: props.theme.space.xxl, display: 'flex', flexDirection: 'column', alignItems: 'center', })); const ErrorTextContainer = styled.div((props) => ({ - marginTop: props.theme.spacing(8), + marginTop: props.theme.space.m, display: 'flex', flexDirection: 'column', alignItems: 'center', @@ -77,6 +77,7 @@ export type NftDashboardState = { stacksNftsQuery: ReturnType; inscriptionsQuery: ReturnType; hiddenInscriptionsQuery: ReturnType; + hiddenStacksNftsQuery: ReturnType; rareSatsQuery: ReturnType; openInGalleryView: () => void; onReceiveModalOpen: () => void; @@ -86,6 +87,7 @@ export type NftDashboardState = { InscriptionListView: () => JSX.Element; HiddenInscriptionListView: () => JSX.Element; NftListView: () => JSX.Element; + HiddenNftListView: () => JSX.Element; onActivateRareSatsAlertCrossPress: () => void; onActivateRareSatsAlertDenyPress: () => void; onActivateRareSatsAlertEnablePress: () => void; @@ -95,6 +97,7 @@ export type NftDashboardState = { hasActivatedRareSatsKey?: boolean; showNoticeAlert?: boolean; totalNfts: number; + totalHiddenNfts: number; totalInscriptions: number; totalHiddenInscriptions: number; }; @@ -109,6 +112,7 @@ const useNftDashboard = (): NftDashboardState => { const [showNoticeAlert, setShowNoticeAlert] = useState(false); const [isOrdinalReceiveAlertVisible, setIsOrdinalReceiveAlertVisible] = useState(false); const stacksNftsQuery = useStacksCollectibles(); + const hiddenStacksNftsQuery = useStacksCollectibles(true); const inscriptionsQuery = useAddressInscriptions(); const hiddenInscriptionsQuery = useAddressInscriptions(true); const rareSatsQuery = useAddressRareSats(); @@ -117,6 +121,7 @@ const useNftDashboard = (): NftDashboardState => { const totalHiddenInscriptions = hiddenInscriptionsQuery.data?.pages?.[0]?.total_inscriptions ?? 0; const totalNfts = stacksNftsQuery.data?.total_nfts ?? 0; + const totalHiddenNfts = hiddenStacksNftsQuery.data?.total_nfts ?? 0; const isGalleryOpen: boolean = useMemo(() => document.documentElement.clientWidth > 360, []); @@ -270,6 +275,37 @@ const useNftDashboard = (): NftDashboardState => { ); }, [stacksNftsQuery, isGalleryOpen, totalNfts, t]); + const HiddenNftListView = useCallback(() => { + if ( + hiddenStacksNftsQuery.error && + !(hiddenStacksNftsQuery.error instanceof InvalidParamsError) + ) { + return ( + + + + {t('ERROR_RETRIEVING')} + {t('TRY_AGAIN')} + + + ); + } + + if (totalHiddenNfts === 0) { + return {t('NO_COLLECTIBLES')}; + } + + return ( + + {hiddenStacksNftsQuery.data?.results.map((collection: StacksCollectionData) => ( + + + + ))} + + ); + }, [hiddenStacksNftsQuery, isGalleryOpen, totalHiddenNfts, t]); + const onActivateRareSatsAlertCrossPress = () => { setShowNewFeatureAlert(false); }; @@ -296,6 +332,7 @@ const useNftDashboard = (): NftDashboardState => { showNewFeatureAlert, isOrdinalReceiveAlertVisible, stacksNftsQuery, + hiddenStacksNftsQuery, inscriptionsQuery, hiddenInscriptionsQuery, openInGalleryView, @@ -306,6 +343,7 @@ const useNftDashboard = (): NftDashboardState => { InscriptionListView, HiddenInscriptionListView, NftListView, + HiddenNftListView, onActivateRareSatsAlertCrossPress, onActivateRareSatsAlertDenyPress, onActivateRareSatsAlertEnablePress, @@ -316,6 +354,7 @@ const useNftDashboard = (): NftDashboardState => { showNoticeAlert, rareSatsQuery, totalNfts, + totalHiddenNfts, totalInscriptions, totalHiddenInscriptions, }; diff --git a/src/app/screens/nftDetail/index.styled.ts b/src/app/screens/nftDetail/index.styled.ts index f9b546b94..49865ab83 100644 --- a/src/app/screens/nftDetail/index.styled.ts +++ b/src/app/screens/nftDetail/index.styled.ts @@ -1,22 +1,19 @@ -import { BetterBarLoader } from '@components/barLoader'; -import Separator from '@components/separator'; import { Tooltip } from 'react-tooltip'; import styled from 'styled-components'; +interface DetailSectionProps { + $isGallery: boolean; +} + export const ExtensionContainer = styled.div((props) => ({ ...props.theme.scrollbar, display: 'flex', flexDirection: 'column', - marginTop: props.theme.spacing(4), + marginTop: props.theme.space.xs, alignItems: 'center', flex: 1, - paddingLeft: props.theme.spacing(4), - paddingRight: props.theme.spacing(4), -})); - -export const GalleryReceiveButtonContainer = styled.div((props) => ({ - marginRight: props.theme.spacing(6), - width: '100%', + paddingLeft: props.theme.space.xs, + paddingRight: props.theme.space.xs, })); export const BackButtonContainer = styled.div((props) => ({ @@ -32,8 +29,8 @@ export const ButtonContainer = styled.div((props) => ({ justifyContent: 'center', columnGap: props.theme.spacing(11), paddingBottom: props.theme.spacing(16), - marginBottom: props.theme.spacing(4), - marginTop: props.theme.spacing(4), + marginBottom: props.theme.space.xs, + marginTop: props.theme.space.xs, width: '100%', borderBottom: `1px solid ${props.theme.colors.elevation3}`, })); @@ -52,7 +49,7 @@ export const NftContainer = styled.div((props) => ({ justifyContent: 'flex-start', alignItems: 'flex-start', borderRadius: 8, - marginBottom: props.theme.spacing(12), + marginBottom: props.theme.space.l, })); export const ExtensionNftContainer = styled.div((props) => ({ @@ -63,13 +60,14 @@ export const ExtensionNftContainer = styled.div((props) => ({ justifyContent: 'center', alignItems: 'center', borderRadius: 8, - marginBottom: props.theme.spacing(12), + marginBottom: props.theme.space.l, })); export const NftTitleText = styled.h1((props) => ({ ...props.theme.typography.headline_m, color: props.theme.colors.white_0, textAlign: 'center', + wordBreak: 'break-word', })); export const CollectibleText = styled.p((props) => ({ @@ -81,7 +79,7 @@ export const CollectibleText = styled.p((props) => ({ export const NftGalleryTitleText = styled.h1((props) => ({ ...props.theme.typography.headline_l, color: props.theme.colors.white_0, - marginBottom: props.theme.spacing(4), + marginBottom: props.theme.space.xs, })); export const NftOwnedByText = styled.p((props) => ({ @@ -109,20 +107,15 @@ export const GridContainer = styled.div((props) => ({ display: 'grid', width: '100%', marginTop: props.theme.spacing(6), - columnGap: props.theme.spacing(4), - rowGap: props.theme.spacing(4), + columnGap: props.theme.space.xs, + rowGap: props.theme.space.xs, gridTemplateColumns: 'repeat(auto-fit,minmax(150px,1fr))', - marginBottom: props.theme.spacing(8), -})); - -export const ShareButtonContainer = styled.div((props) => ({ - marginRight: props.theme.spacing(6), - width: '100%', + marginBottom: props.theme.space.m, })); export const DescriptionContainer = styled.div((props) => ({ display: 'flex', - marginLeft: props.theme.spacing(20), + marginLeft: props.theme.space.xxl, flexDirection: 'column', marginBottom: props.theme.spacing(30), })); @@ -140,8 +133,8 @@ export const WebGalleryButton = styled.button((props) => ({ borderRadius: props.theme.radius(1), backgroundColor: 'transparent', width: '100%', - marginTop: props.theme.spacing(4), - marginBottom: props.theme.spacing(12), + marginTop: props.theme.space.xs, + marginBottom: props.theme.space.l, })); export const WebGalleryButtonText = styled.div((props) => ({ @@ -162,20 +155,12 @@ export const BackButton = styled.button((props) => ({ justifyContent: 'flex-start', alignItems: 'center', background: 'transparent', - marginBottom: props.theme.spacing(12), + marginBottom: props.theme.space.l, })); -export const ExtensionLoaderContainer = styled.div({ - height: '100%', - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-around', - alignItems: 'center', -}); - export const SeeDetailsButtonContainer = styled.div((props) => ({ marginBottom: props.theme.spacing(27), - marginTop: props.theme.spacing(4), + marginTop: props.theme.space.xs, })); export const Button = styled.button((props) => ({ @@ -184,7 +169,7 @@ export const Button = styled.button((props) => ({ justifyContent: 'center', alignItems: 'center', backgroundColor: 'transparent', - width: props.isGallery ? 400 : 328, + width: props.$isGallery ? 400 : 328, height: 44, padding: 12, borderRadius: 12, @@ -220,19 +205,19 @@ export const GalleryScrollContainer = styled.div((props) => ({ export const ButtonHiglightedText = styled.p((props) => ({ ...props.theme.typography.body_m, color: props.theme.colors.white_0, - marginLeft: props.theme.spacing(2), - marginRight: props.theme.spacing(2), + marginLeft: props.theme.space.xxs, + marginRight: props.theme.space.xxs, })); export const GalleryRowContainer = styled.div<{ - withGap?: boolean; + $withGap?: boolean; }>((props) => ({ display: 'flex', alignItems: 'flex-start', - marginTop: props.theme.spacing(8), - marginBottom: props.theme.spacing(12), + marginTop: props.theme.space.m, + marginBottom: props.theme.space.l, flexDirection: 'row', - columnGap: props.withGap ? props.theme.spacing(20) : 0, + columnGap: props.$withGap ? props.theme.space.m : 0, })); export const StyledTooltip = styled(Tooltip)` @@ -245,14 +230,6 @@ export const StyledTooltip = styled(Tooltip)` } `; -export const StyledBarLoader = styled(BetterBarLoader)<{ - withMarginBottom?: boolean; -}>((props) => ({ - padding: 0, - borderRadius: props.theme.radius(1), - marginBottom: props.withMarginBottom ? props.theme.spacing(6) : 0, -})); - export const GalleryContainer = styled.div({ marginLeft: 'auto', marginRight: 'auto', @@ -264,57 +241,20 @@ export const GalleryContainer = styled.div({ maxWidth: 1224, }); -export const ActionButtonLoader = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - rowGap: props.theme.spacing(4), -})); - -export const ActionButtonsLoader = styled.div((props) => ({ - display: 'flex', - justifyContent: 'center', - columnGap: props.theme.spacing(11), -})); - -export const GalleryLoaderContainer = styled.div({ - display: 'flex', - flexDirection: 'column', -}); - -export const StyledSeparator = styled(Separator)` - width: 100%; -`; - -export const TitleLoader = styled.div` - display: flex; - flex-direction: column; -`; -interface DetailSectionProps { - isGallery: boolean; -} - export const NftDetailsContainer = styled.div((props) => ({ display: 'flex', alignItems: 'flex-start', flexDirection: 'column', wordBreak: 'break-all', whiteSpace: 'pre-wrap', - width: props.isGallery ? 400 : '100%', - marginTop: props.theme.spacing(8), + width: props.$isGallery ? 400 : '100%', + marginTop: props.theme.space.m, })); export const DetailSection = styled.div((props) => ({ display: 'flex', - flexDirection: !props.isGallery ? 'row' : 'column', + flexDirection: !props.$isGallery ? 'row' : 'column', justifyContent: 'center', width: '100%', - columnGap: props.theme.spacing(8), -})); - -export const InfoContainer = styled.div((props) => ({ - width: '100%', - display: 'flex', - justifyContent: 'space-between', - padding: `0 ${props.theme.spacing(8)}px`, + columnGap: props.theme.space.m, })); diff --git a/src/app/screens/nftDetail/index.tsx b/src/app/screens/nftDetail/index.tsx index b9398fb79..9b19f7d60 100644 --- a/src/app/screens/nftDetail/index.tsx +++ b/src/app/screens/nftDetail/index.tsx @@ -12,6 +12,7 @@ import { ArrowUp, DotsThreeVertical, Share, + Star, UserCircleCheck, UserCircleMinus, } from '@phosphor-icons/react'; @@ -19,21 +20,25 @@ import NftImage from '@screens/nftDashboard/nftImage'; import { StyledButton } from '@screens/ordinalsCollection/index.styled'; import type { Attribute } from '@secretkeylabs/xverse-core'; import { + addToStarCollectiblesAction, removeAccountAvatarAction, + removeFromStarCollectiblesAction, setAccountAvatarAction, } from '@stores/wallet/actions/actionCreators'; import ActionButton from '@ui-library/button'; import Sheet from '@ui-library/sheet'; import SnackBar from '@ui-library/snackBar'; -import { EMPTY_LABEL } from '@utils/constants'; +import { EMPTY_LABEL, LONG_TOAST_DURATION } from '@utils/constants'; import { useCallback } from 'react'; import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import Theme from '../../../theme'; +import { ExtensionLoader, GalleryLoader } from './loaders'; +import NftAttribute from './nftAttribute'; +import useNftDetailScreen from './useNftDetail'; + import { - ActionButtonLoader, - ActionButtonsLoader, AssetDeatilButtonText, AttributeText, BackButton, @@ -49,16 +54,12 @@ import { DescriptionContainer, DetailSection, ExtensionContainer, - ExtensionLoaderContainer, ExtensionNftContainer, GalleryCollectibleText, GalleryContainer, - GalleryLoaderContainer, - GalleryReceiveButtonContainer, GalleryRowContainer, GalleryScrollContainer, GridContainer, - InfoContainer, NftContainer, NftDetailsContainer, NftGalleryTitleText, @@ -67,16 +68,10 @@ import { OwnerAddressText, RowContainer, SeeDetailsButtonContainer, - ShareButtonContainer, - StyledBarLoader, - StyledSeparator, StyledTooltip, - TitleLoader, WebGalleryButton, WebGalleryButtonText, } from './index.styled'; -import NftAttribute from './nftAttribute'; -import useNftDetailScreen from './useNftDetail'; function NftDetailScreen() { const dispatch = useDispatch(); @@ -100,7 +95,7 @@ function NftDetailScreen() { galleryTitle, } = useNftDetailScreen(); const { ordinalsAddress } = useSelectedAccount(); - const { avatarIds } = useWalletSelector(); + const { avatarIds, hiddenCollectibleIds, starredCollectibleIds } = useWalletSelector(); const selectedAvatar = avatarIds[ordinalsAddress]; const isNftSelectedAsAvatar = selectedAvatar?.type === 'stacks' && selectedAvatar.nft.token_id === nftData?.token_id; @@ -146,6 +141,53 @@ function NftDetailScreen() { optionsSheet.close(); }, [dispatch, optionsDialogT, ordinalsAddress, optionsSheet]); + const isNftCollectionHidden = Object.keys(hiddenCollectibleIds[stxAddress] ?? {}).some( + (id) => id === collection?.collection_id, + ); + const nftId = nftData?.fully_qualified_token_id ?? ''; + const nftStarred = starredCollectibleIds[stxAddress]?.some(({ id }) => id === nftId); + + const handleClickUndoStarring = (toastId: string) => { + dispatch( + removeFromStarCollectiblesAction({ + address: stxAddress, + id: nftId, + }), + ); + toast.remove(toastId); + toast.custom(); + }; + + const handleStarClick = () => { + if (nftStarred) { + dispatch( + removeFromStarCollectiblesAction({ + address: stxAddress, + id: nftId, + }), + ); + toast.custom(); + } else { + dispatch( + addToStarCollectiblesAction({ + address: stxAddress, + id: nftId, + }), + ); + const toastId = toast.custom( + handleClickUndoStarring(toastId), + }} + />, + { duration: LONG_TOAST_DURATION }, + ); + } + }; + const nftAttributes = nftData?.nft_token_attributes?.length !== 0 && ( <> {t('ATTRIBUTES')} @@ -161,9 +203,9 @@ function NftDetailScreen() { ); const nftDetails = ( - + {collection?.collection_name && ( - + )} {!isGalleryOpen && nftAttributes} - + {nftData?.rarity_score && ( @@ -188,35 +230,7 @@ function NftDetailScreen() { ); const extensionView = isLoading ? ( - - - - - - - - - - - - - - - - - - - -
- - -
-
- - -
-
-
+ ) : ( {t('COLLECTIBLE')} @@ -253,11 +267,11 @@ function NftDetailScreen() { {nftDetails} - - @@ -272,31 +286,12 @@ function NftDetailScreen() { <> - {t('MOVE_TO_ASSET_DETAIL')} + {t('BACK_TO_COLLECTION')} - - - - - - - - - - - - - - - - - - - - + ) : ( @@ -305,7 +300,7 @@ function NftDetailScreen() { - {t('MOVE_TO_ASSET_DETAIL')} + {t('BACK_TO_COLLECTION')} @@ -325,44 +320,59 @@ function NftDetailScreen() { )}`} - - - } - title={t('SEND')} - onClick={handleOnSendClick} - /> - - - } - title={t('SHARE')} - onClick={onSharePress} - variant="secondary" - /> - - - } - onPress={optionsSheet.open} - isTransparent - size={44} - radiusSize={12} + + } + title={t('SEND')} + onClick={handleOnSendClick} /> + } + title={t('SHARE')} + onClick={onSharePress} + variant="secondary" + /> + + {isNftCollectionHidden ? null : ( + <> + + ) : ( + + ) + } + onPress={handleStarClick} + isTransparent + size={44} + radiusSize={12} + /> + + } + onPress={optionsSheet.open} + isTransparent + size={44} + radiusSize={12} + /> + + )} {nftDetails} - - @@ -377,7 +387,12 @@ function NftDetailScreen() { {isGalleryOpen ? ( ) : ( - + )} {isGalleryOpen ? galleryView : extensionView} {!isGalleryOpen && ( diff --git a/src/app/screens/nftDetail/loaders.tsx b/src/app/screens/nftDetail/loaders.tsx new file mode 100644 index 000000000..f816cfd89 --- /dev/null +++ b/src/app/screens/nftDetail/loaders.tsx @@ -0,0 +1,113 @@ +import { BetterBarLoader } from '@components/barLoader'; +import Separator from '@components/separator'; +import styled from 'styled-components'; +import { ButtonContainer, GalleryRowContainer } from './index.styled'; + +const ActionButtonLoader = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + rowGap: props.theme.space.xs, +})); + +const ActionButtonsLoader = styled.div((props) => ({ + display: 'flex', + justifyContent: 'center', + columnGap: props.theme.spacing(11), +})); + +const ExtensionLoaderContainer = styled.div({ + height: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-around', + alignItems: 'center', +}); + +const GalleryLoaderContainer = styled.div({ + display: 'flex', + flexDirection: 'column', +}); + +const InfoContainer = styled.div((props) => ({ + width: '100%', + display: 'flex', + justifyContent: 'space-between', + padding: `0 ${props.theme.space.m}`, +})); + +const StyledBarLoader = styled(BetterBarLoader)<{ + withMarginBottom?: boolean; +}>((props) => ({ + padding: 0, + borderRadius: props.theme.radius(1), + marginBottom: props.withMarginBottom ? props.theme.space.s : 0, +})); + +const StyledSeparator = styled(Separator)` + width: 100%; +`; + +const TitleLoader = styled.div` + display: flex; + flex-direction: column; +`; + +export function GalleryLoader() { + return ( + + + + + + + + + + + + + + + + + + + + + ); +} + +export function ExtensionLoader() { + return ( + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+
+
+ ); +} diff --git a/src/app/screens/nftDetail/useNftDetail.ts b/src/app/screens/nftDetail/useNftDetail.ts index d8817b04f..c211ad327 100644 --- a/src/app/screens/nftDetail/useNftDetail.ts +++ b/src/app/screens/nftDetail/useNftDetail.ts @@ -2,6 +2,7 @@ import useNftDetail from '@hooks/queries/useNftDetail'; import useStacksCollectibles from '@hooks/queries/useStacksCollectibles'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; import { GAMMA_URL } from '@utils/constants'; import { getExplorerUrl, isInOptions, isLedgerAccount } from '@utils/helper'; import { useMemo } from 'react'; @@ -11,10 +12,13 @@ export default function useNftDetailScreen() { const navigate = useNavigate(); const selectedAccount = useSelectedAccount(); const { id } = useParams(); - + const { hiddenCollectibleIds } = useWalletSelector(); const nftDetailQuery = useNftDetail(id!); - const nftCollectionsQuery = useStacksCollectibles(); const collectionId = nftDetailQuery.data?.data.collection_contract_id; + const collectionHidden = Object.keys(hiddenCollectibleIds[selectedAccount.stxAddress] ?? {}).some( + (itemId) => itemId === collectionId, + ); + const nftCollectionsQuery = useStacksCollectibles(collectionHidden); const collection = nftCollectionsQuery.data?.results.find( (c) => c.collection_id === collectionId, ); @@ -32,7 +36,7 @@ export default function useNftDetailScreen() { }; const handleBackButtonClick = () => { - navigate(`/nft-dashboard/nft-collection/${collectionId}`); + navigate(`/nft-dashboard/nft-collection/${collectionId}${collectionHidden ? '/hidden' : ''}`); }; const onGammaPress = () => { diff --git a/src/app/screens/ordinalDetail/index.styled.ts b/src/app/screens/ordinalDetail/index.styled.ts index e691812eb..fea6707a7 100644 --- a/src/app/screens/ordinalDetail/index.styled.ts +++ b/src/app/screens/ordinalDetail/index.styled.ts @@ -227,54 +227,12 @@ export const DetailSection = styled.div((props) => ({ width: '100%', })); -export const ExtensionLoaderContainer = styled.div({ - height: '100%', - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-around', - alignItems: 'center', -}); - -export const GalleryLoaderContainer = styled.div({ - display: 'flex', - flexDirection: 'column', -}); - export const StyledBarLoader = styled(BetterBarLoader)<{ withMarginBottom?: boolean; }>((props) => ({ padding: 0, borderRadius: props.theme.radius(1), - marginBottom: props.withMarginBottom ? props.theme.spacing(6) : 0, -})); - -export const StyledSeparator = styled(Separator)` - width: 100%; -`; - -export const TitleLoader = styled.div` - display: flex; - flex-direction: column; -`; - -export const ActionButtonsLoader = styled.div((props) => ({ - display: 'flex', - justifyContent: 'center', - columnGap: props.theme.spacing(11), -})); - -export const ActionButtonLoader = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - rowGap: props.theme.space.xs, -})); - -export const InfoContainer = styled.div((props) => ({ - width: '100%', - display: 'flex', - justifyContent: 'space-between', - padding: `0 ${props.theme.space.m}`, + marginBottom: props.withMarginBottom ? props.theme.space.s : 0, })); export const RareSatsBundleCallout = styled(Callout)((props) => ({ diff --git a/src/app/screens/ordinalDetail/index.tsx b/src/app/screens/ordinalDetail/index.tsx index 9491bfd05..8780cb8bc 100644 --- a/src/app/screens/ordinalDetail/index.tsx +++ b/src/app/screens/ordinalDetail/index.tsx @@ -11,12 +11,11 @@ import { useResetUserFlow } from '@hooks/useResetUserFlow'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { + ArchiveTray, ArrowUp, DotsThreeVertical, Share, Star, - TrayArrowDown, - TrayArrowUp, UserCircleCheck, UserCircleMinus, } from '@phosphor-icons/react'; @@ -34,7 +33,7 @@ import Button from '@ui-library/button'; import { StyledP } from '@ui-library/common.styled'; import Sheet from '@ui-library/sheet'; import SnackBar from '@ui-library/snackBar'; -import { EMPTY_LABEL } from '@utils/constants'; +import { EMPTY_LABEL, LONG_TOAST_DURATION } from '@utils/constants'; import { getRareSatsColorsByRareSatsType, getRareSatsLabelByType } from '@utils/rareSats'; import { useCallback } from 'react'; import toast from 'react-hot-toast'; @@ -43,8 +42,6 @@ import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import Theme from '../../../theme'; import { - ActionButtonLoader, - ActionButtonsLoader, BackButtonContainer, Badge, BottomBarContainer, @@ -59,14 +56,11 @@ import { DetailSection, Divider, ExtensionContainer, - ExtensionLoaderContainer, ExtensionOrdinalsContainer, GalleryButtonContainer, GalleryCollectibleText, GalleryContainer, - GalleryLoaderContainer, GalleryScrollContainer, - InfoContainer, OrdinalDetailsContainer, OrdinalGalleryTitleText, OrdinalsContainer, @@ -80,12 +74,11 @@ import { SatributesBadges, SatributesIconsContainer, StyledBarLoader, - StyledSeparator, StyledTooltip, StyledWebGalleryButton, - TitleLoader, ViewInExplorerButton, } from './index.styled'; +import { ExtensionLoader, GalleryLoader } from './loaders'; import OrdinalAttributeComponent from './ordinalAttributeComponent'; import useOrdinalDetail from './useOrdinalDetail'; @@ -139,25 +132,29 @@ function OrdinalDetailScreen() { useResetUserFlow('/ordinal-detail'); + const handleUnstarClick = (toastId: string) => { + dispatch(removeFromStarCollectiblesAction({ address: ordinalsAddress, id: ordinal?.id ?? '' })); + toast.remove(toastId); + toast.custom(); + }; + const handleStarClick = () => { if (inscriptionStarred) { - const toastId = toast.custom( - toast.remove(toastId)} - />, - ); dispatch( removeFromStarCollectiblesAction({ address: ordinalsAddress, id: ordinal?.id ?? '' }), ); + toast.custom(); } else { const toastId = toast.custom( toast.remove(toastId)} + action={{ + text: commonT('UNDO'), + onClick: () => handleUnstarClick(toastId), + }} />, + { duration: LONG_TOAST_DURATION }, ); dispatch( addToStarCollectiblesAction({ @@ -193,7 +190,7 @@ function OrdinalDetailScreen() { onClick: () => handleClickUndoHiding(toastId), }} />, - { duration: 4000 }, + { duration: LONG_TOAST_DURATION }, ); }; @@ -447,35 +444,7 @@ function OrdinalDetailScreen() { ); const extensionView = isLoading ? ( - - - - - - - - - - - - - - - - - - - -
- - -
-
- - -
-
-
+ ) : ( @@ -534,23 +503,7 @@ function OrdinalDetailScreen() { - - - - - - - - - - - - - - - - - + @@ -712,14 +665,14 @@ function OrdinalDetailScreen() { (isInscriptionHidden ? ( } + icon={} title={t('UNHIDE_INSCRIPTION')} onClick={handleUnHideStandaloneInscription} /> ) : ( } + icon={} title={t('HIDE_INSCRIPTION')} onClick={handleHideStandaloneInscription} /> diff --git a/src/app/screens/ordinalDetail/loaders.tsx b/src/app/screens/ordinalDetail/loaders.tsx new file mode 100644 index 000000000..5d1e247f2 --- /dev/null +++ b/src/app/screens/ordinalDetail/loaders.tsx @@ -0,0 +1,101 @@ +import Separator from '@components/separator'; +import styled from 'styled-components'; +import { ButtonContainer, StyledBarLoader } from './index.styled'; + +const ActionButtonsLoader = styled.div((props) => ({ + display: 'flex', + justifyContent: 'center', + columnGap: props.theme.spacing(11), +})); + +const ActionButtonLoader = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + rowGap: props.theme.space.xs, +})); + +const ExtensionLoaderContainer = styled.div({ + height: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-around', + alignItems: 'center', +}); + +const InfoContainer = styled.div((props) => ({ + width: '100%', + display: 'flex', + justifyContent: 'space-between', + padding: `0 ${props.theme.space.m}`, +})); + +const StyledSeparator = styled(Separator)` + width: 100%; +`; + +const TitleLoader = styled.div` + display: flex; + flex-direction: column; +`; + +const GalleryLoaderContainer = styled.div({ + display: 'flex', + flexDirection: 'column', +}); + +export function ExtensionLoader() { + return ( + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+
+
+ ); +} + +export function GalleryLoader() { + return ( + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/app/screens/ordinalsCollection/index.styled.ts b/src/app/screens/ordinalsCollection/index.styled.ts index 8fe290c2c..4f78ee844 100644 --- a/src/app/screens/ordinalsCollection/index.styled.ts +++ b/src/app/screens/ordinalsCollection/index.styled.ts @@ -7,7 +7,7 @@ import Button from '@ui-library/button'; import styled from 'styled-components'; interface DetailSectionProps { - isGalleryOpen?: boolean; + $isGalleryOpen?: boolean; } /* layout */ @@ -23,7 +23,7 @@ export const PageHeader = styled.div` padding: ${(props) => props.theme.space.xs}; padding-top: 0; max-width: 1224px; - margin-top: ${(props) => (props.isGalleryOpen ? props.theme.space.xxl : props.theme.space.l)}; + margin-top: ${(props) => (props.$isGalleryOpen ? props.theme.space.xxl : props.theme.space.l)}; margin-left: auto; margin-right: auto; width: 100%; @@ -32,15 +32,15 @@ export const PageHeader = styled.div` // TODO tim: use media queries for this once we get responsive layouts with breakpoints export const PageHeaderContent = styled.div` display: flex; - flex-direction: ${(props) => (props.isGalleryOpen ? 'row' : 'column')}; - justify-content: ${(props) => (props.isGalleryOpen ? 'space-between' : 'initial')}; + flex-direction: ${(props) => (props.$isGalleryOpen ? 'row' : 'column')}; + justify-content: ${(props) => (props.$isGalleryOpen ? 'space-between' : 'initial')}; row-gap: ${(props) => props.theme.space.xl}; `; export const AttributesContainer = styled.div` display: flex; - flex-direction: ${(props) => (props.isGalleryOpen ? 'column' : 'row')}; - justify-content: ${(props) => (props.isGalleryOpen ? 'space-between' : 'initial')}; + flex-direction: ${(props) => (props.$isGalleryOpen ? 'column' : 'row')}; + justify-content: ${(props) => (props.$isGalleryOpen ? 'space-between' : 'initial')}; column-gap: ${(props) => props.theme.space.m}; `; @@ -74,7 +74,6 @@ export const StyledWrenchErrorMessage = styled(WrenchErrorMessage)` export const BackButtonContainer = styled.div` display: flex; - flex-direction: row; margin-bottom: ${(props) => props.theme.space.xxl}; `; diff --git a/src/app/screens/ordinalsCollection/index.tsx b/src/app/screens/ordinalsCollection/index.tsx index db4d7c193..841c32cff 100644 --- a/src/app/screens/ordinalsCollection/index.tsx +++ b/src/app/screens/ordinalsCollection/index.tsx @@ -11,13 +11,7 @@ import useOptionsSheet from '@hooks/useOptionsSheet'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; -import { - ArrowLeft, - DotsThreeVertical, - Star, - TrayArrowDown, - TrayArrowUp, -} from '@phosphor-icons/react'; +import { ArchiveTray, ArrowLeft, DotsThreeVertical, Star } from '@phosphor-icons/react'; import OrdinalImage from '@screens/ordinals/ordinalImage'; import { addToHideCollectiblesAction, @@ -30,7 +24,7 @@ import Button from '@ui-library/button'; import { StyledHeading, StyledP } from '@ui-library/common.styled'; import Sheet from '@ui-library/sheet'; import SnackBar from '@ui-library/snackBar'; -import { EMPTY_LABEL } from '@utils/constants'; +import { EMPTY_LABEL, LONG_TOAST_DURATION } from '@utils/constants'; import { getInscriptionsCollectionGridItemId, getInscriptionsCollectionGridItemSubText, @@ -104,7 +98,9 @@ function OrdinalsCollection() { removeFromHideCollectiblesAction({ address: ordinalsAddress, id: collectionId ?? '' }), ); toast.remove(toastId); - toast.custom(, { duration: 4000 }); + toast.custom(, { + duration: LONG_TOAST_DURATION, + }); }; const handleHideCollection = () => { @@ -132,7 +128,7 @@ function OrdinalsCollection() { onClick: () => handleClickUndoHiding(toastId), }} />, - { duration: 4000 }, + { duration: LONG_TOAST_DURATION }, ); }; @@ -156,27 +152,33 @@ function OrdinalsCollection() { ? `${collectionMarketData?.floor_price?.toFixed(8)} BTC` : EMPTY_LABEL; + const handleClickUndoStarring = (toastId: string) => { + dispatch( + removeFromStarCollectiblesAction({ address: ordinalsAddress, id: collectionId ?? '' }), + ); + toast.remove(toastId); + toast.custom(); + }; + const handleStarClick = () => { if (collectionStarred) { - const toastId = toast.custom( - toast.remove(toastId)} - />, - ); dispatch( removeFromStarCollectiblesAction({ address: ordinalsAddress, id: collectionId ?? '' }), ); + toast.custom(); } else { + dispatch(addToStarCollectiblesAction({ address: ordinalsAddress, id: collectionId ?? '' })); const toastId = toast.custom( toast.remove(toastId)} + action={{ + text: tCommon('UNDO'), + onClick: () => handleClickUndoStarring(toastId), + }} />, + { duration: LONG_TOAST_DURATION }, ); - dispatch(addToStarCollectiblesAction({ address: ordinalsAddress, id: collectionId ?? '' })); } }; @@ -193,7 +195,7 @@ function OrdinalsCollection() { /> )} - + {isGalleryOpen && ( @@ -204,7 +206,7 @@ function OrdinalsCollection() { )} - +
{t('COLLECTION')} @@ -244,7 +246,7 @@ function OrdinalsCollection() { {!isGalleryOpen && }
- + } + icon={} title={t('UNHIDE_COLLECTION')} onClick={handleUnHideCollection} /> ) : ( } + icon={} title={t('HIDE_COLLECTION')} onClick={handleHideCollection} /> diff --git a/src/app/stores/wallet/actions/actionCreators.ts b/src/app/stores/wallet/actions/actionCreators.ts index 930e65e7f..da18c5f96 100644 --- a/src/app/stores/wallet/actions/actionCreators.ts +++ b/src/app/stores/wallet/actions/actionCreators.ts @@ -313,6 +313,15 @@ export function removeAllFromHideCollectiblesAction(params: { }; } +export function setHiddenCollectiblesAction(params: { + collectibleIds: Record>; +}): actions.SetHiddenCollectibles { + return { + type: actions.SetHiddenCollectiblesKey, + ...params, + }; +} + export function setAccountAvatarAction(params: { address: string; avatar: actions.AvatarInfo; diff --git a/src/app/stores/wallet/actions/types.ts b/src/app/stores/wallet/actions/types.ts index ced01601a..b1a388efe 100644 --- a/src/app/stores/wallet/actions/types.ts +++ b/src/app/stores/wallet/actions/types.ts @@ -43,6 +43,7 @@ export const UpdateSavedNamesKey = 'UpdateSavedNamesKey'; export const AddToStarCollectiblesKey = 'AddToStarCollectiblesKey'; export const RemoveFromStarCollectiblesKey = 'RemoveFromStarCollectiblesKey'; export const AddToHideCollectiblesKey = 'AddToHideCollectiblesKey'; +export const SetHiddenCollectiblesKey = 'SetHiddenCollectiblesKey'; export const RemoveFromHideCollectiblesKey = 'RemoveFromHideCollectiblesKey'; export const RemoveAllFromHideCollectiblesKey = 'RemoveAllFromHideCollectiblesKey'; export const SetAccountAvatarKey = 'SetAccountAvatarKey'; @@ -272,6 +273,11 @@ export interface RemoveAllFromHideCollectibles { address: string; } +export interface SetHiddenCollectibles { + type: typeof SetHiddenCollectiblesKey; + collectibleIds: Record>; +} + export interface SetAccountAvatar { type: typeof SetAccountAvatarKey; address: string; @@ -328,5 +334,6 @@ export type WalletActions = | AddToHideCollectibles | RemoveFromHideCollectibles | RemoveAllFromHideCollectibles + | SetHiddenCollectibles | SetAccountAvatar | RemoveAccountAvatar; diff --git a/src/app/stores/wallet/reducer.ts b/src/app/stores/wallet/reducer.ts index 19b532b84..b1f390c73 100644 --- a/src/app/stores/wallet/reducer.ts +++ b/src/app/stores/wallet/reducer.ts @@ -24,6 +24,7 @@ import { SetAccountBalanceKey, SetBrc20ManageTokensKey, SetFeeMultiplierKey, + SetHiddenCollectiblesKey, SetNotificationBannersKey, SetRunesManageTokensKey, SetShowSpamTokensKey, @@ -360,6 +361,14 @@ const walletReducer = ( [action.address]: {}, }, }; + case SetHiddenCollectiblesKey: + return { + ...state, + hiddenCollectibleIds: { + ...state.hiddenCollectibleIds, + ...action.collectibleIds, + }, + }; case SetAccountAvatarKey: return { ...state, diff --git a/src/app/utils/constants.ts b/src/app/utils/constants.ts index 971e2eff7..2350dddb7 100644 --- a/src/app/utils/constants.ts +++ b/src/app/utils/constants.ts @@ -70,6 +70,7 @@ export const MAX_ACC_NAME_LENGTH = 20; export const EMPTY_LABEL = '--'; export const OPTIONS_DIALOG_WIDTH = 179; export const SPAM_OPTIONS_WIDTH = 244; +export const LONG_TOAST_DURATION = 4000; export const XverseProviderInfo: Provider = { id: 'XverseProviders.BitcoinProvider', diff --git a/src/locales/en.json b/src/locales/en.json index a203ba8b8..0224d45db 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -757,7 +757,9 @@ "FROM_RARE_SAT_BUNDLE": "This inscription belongs to the same bundle as other assets. Transferring it will involve transferring the full bundle.", "HOLDS_RARE_SAT": "This inscription holds a rare sat.", "HIDDEN_COLLECTIBLES": "Hidden collectibles", - "UNHIDE_ALL": "Unhide all" + "UNHIDE_ALL": "Unhide all", + "HIDDEN_ITEMS_RESTORED": "Hidden items restored", + "ITEMS_RETURNED_TO_HIDDEN": "Items returned to hidden" }, "RESTORE_FUND_SCREEN": { "TITLE": "Recover assets", From 22d4658cdf62bd1b80ab1f0116240f308c3e7ea3 Mon Sep 17 00:00:00 2001 From: Terence Ng Date: Wed, 16 Oct 2024 10:56:59 +0200 Subject: [PATCH 179/227] Terence/fix back nav (#673) * remove duplicate * fixup back flows from send --- src/app/screens/sendBrc20OneStep/index.tsx | 4 +--- src/app/screens/sendBtc/index.tsx | 2 +- src/app/screens/sendRune/index.tsx | 5 +++-- src/app/screens/sendStx/index.tsx | 12 +++++++----- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/app/screens/sendBrc20OneStep/index.tsx b/src/app/screens/sendBrc20OneStep/index.tsx index 327df29ef..fe679b2ee 100644 --- a/src/app/screens/sendBrc20OneStep/index.tsx +++ b/src/app/screens/sendBrc20OneStep/index.tsx @@ -37,8 +37,6 @@ function SendBrc20Screen() { const [processing, setProcessing] = useState(false); const transactionContext = useTransactionContext(); - const isFullScreen = isInOptions(); - useResetUserFlow('/send-brc20-one-step'); const principal = searchParams.get('principal'); @@ -165,7 +163,7 @@ function SendBrc20Screen() { return ( <> - + => { diff --git a/src/app/screens/sendRune/index.tsx b/src/app/screens/sendRune/index.tsx index 8c67e4cdd..5ce01e29e 100644 --- a/src/app/screens/sendRune/index.tsx +++ b/src/app/screens/sendRune/index.tsx @@ -4,7 +4,6 @@ import useHasFeature from '@hooks/useHasFeature'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useTransactionContext from '@hooks/useTransactionContext'; -import useWalletSelector from '@hooks/useWalletSelector'; import { AnalyticsEvents, FeatureId, @@ -144,7 +143,9 @@ function SendRuneScreen() { window.close(); return; } - navigate('/'); + navigate( + `/coinDashboard/FT?ftKey=${fungibleToken.principal}&protocol=${fungibleToken.protocol}`, + ); }; const handleBackButtonClick = () => { diff --git a/src/app/screens/sendStx/index.tsx b/src/app/screens/sendStx/index.tsx index 0d689ab52..e91d18c2d 100644 --- a/src/app/screens/sendStx/index.tsx +++ b/src/app/screens/sendStx/index.tsx @@ -2,7 +2,7 @@ import TokenImage from '@components/tokenImage'; import { useVisibleSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; import { microstacksToStx, stxToMicrostacks } from '@secretkeylabs/xverse-core'; -import { isInOptions } from '@utils/helper'; +import { isInOptions, isLedgerAccount } from '@utils/helper'; import SendLayout from 'app/layouts/sendLayout'; import BigNumber from 'bignumber.js'; import { useState } from 'react'; @@ -78,11 +78,13 @@ function SendStxScreen() { const [unsignedSendStxTx, setUnsignedSendStxTx] = useState(''); const handleCancel = () => { - if (isInOption) { - window.close(); - return; + if (fungibleToken) { + navigate( + `/coinDashboard/FT?ftKey=${fungibleToken.principal}&protocol=${fungibleToken.protocol}`, + ); + } else { + navigate(`/coinDashboard/STX`); } - navigate('/'); }; const handleBackButtonClick = () => { From f275b4050ea0a4cad59533e6f543ffa2d5fa6eca Mon Sep 17 00:00:00 2001 From: Terence Ng Date: Wed, 16 Oct 2024 14:43:52 +0200 Subject: [PATCH 180/227] Terence/fix pw screen (#676) * remove duplicate * remove error message * remove unnecessary props * fix boolean logic * remove e2e logic check * restore message (opps) * fixup button jumping * Update src/app/components/passwordInput/index.styled.ts Co-authored-by: Den <36603049+dhriaznov@users.noreply.github.com> * remove --------- Co-authored-by: Den <36603049+dhriaznov@users.noreply.github.com> --- .../components/passwordInput/index.styled.ts | 5 +- src/app/components/passwordInput/index.tsx | 140 ++++++------------ src/app/screens/createPassword/index.tsx | 1 - src/app/screens/restoreWallet/index.tsx | 1 - .../security/changePassword/index.tsx | 1 - src/locales/en.json | 1 - tests/pages/onboarding.ts | 13 -- 7 files changed, 50 insertions(+), 112 deletions(-) diff --git a/src/app/components/passwordInput/index.styled.ts b/src/app/components/passwordInput/index.styled.ts index dfdca4914..d65daf440 100644 --- a/src/app/components/passwordInput/index.styled.ts +++ b/src/app/components/passwordInput/index.styled.ts @@ -34,7 +34,7 @@ export const ButtonsContainer = styled.div<{ flexDirection: props.$stackButtonAlignment ? 'column-reverse' : 'row', alignItems: props.$stackButtonAlignment ? 'center' : 'flex-end', flex: 1, - marginTop: props.$ifError ? props.theme.spacing(30) : props.theme.spacing(40), + marginTop: props.$ifError ? props.theme.space.xxxl : props.theme.space.xxxxl, marginBottom: props.theme.space.m, })); @@ -47,8 +47,9 @@ export const StyledButton = styled.button({ }, }); -export const PasswordStrengthContainer = styled.div((props) => ({ +export const PasswordStrengthContainer = styled.div<{ $visibility: boolean }>((props) => ({ ...props.theme.typography.body_medium_m, + visibility: props.$visibility ? 'visible' : 'hidden', display: 'flex', alignItems: 'center', width: '100%', diff --git a/src/app/components/passwordInput/index.tsx b/src/app/components/passwordInput/index.tsx index dfbeab673..22cdd640b 100644 --- a/src/app/components/passwordInput/index.tsx +++ b/src/app/components/passwordInput/index.tsx @@ -20,8 +20,6 @@ import { StyledButton, } from './index.styled'; -const REQUIRED_PASSWORD_LENGTH = 5; - enum PasswordStrength { NoScore, PoorScore, @@ -43,7 +41,6 @@ type Props = { checkPasswordStrength?: boolean; stackButtonAlignment?: boolean; loading?: boolean; - createPasswordFlow?: boolean; autoFocus?: boolean; }; @@ -59,18 +56,16 @@ function PasswordInput({ checkPasswordStrength, stackButtonAlignment = false, loading, - createPasswordFlow, autoFocus = false, }: Props): JSX.Element { - const { t } = useTranslation('translation', { keyPrefix: 'CREATE_PASSWORD_SCREEN' }); const theme = useTheme(); + const { t } = useTranslation('translation', { keyPrefix: 'CREATE_PASSWORD_SCREEN' }); const [isPasswordVisible, setIsPasswordVisible] = useState(false); const [passwordStrength, setPasswordStrength] = useState( PasswordStrength.NoScore, ); + const { score } = zxcvbn(enteredPassword); - const enteredPasswordLength = enteredPassword.length; - const [error, setError] = useState(passwordError ?? ''); const transition = useTransition(passwordStrength, { from: { opacity: 0, @@ -79,6 +74,33 @@ function PasswordInput({ opacity: 1, }, }); + const enteredPasswordLength = enteredPassword.length; + let passwordStrengthLabel; + if (!enteredPassword) { + passwordStrengthLabel = { + color: theme.colors.white_600, + width: '0', + message: '', + }; + } else if (score <= PasswordStrength.WeakScore) { + passwordStrengthLabel = { + color: theme.colors.feedback.error, + width: '20%', + message:

{t('PASSWORD_STRENGTH_WEAK')}

, + }; + } else if (score <= PasswordStrength.AverageScore) { + passwordStrengthLabel = { + color: theme.colors.feedback.caution, + width: '50%', + message:

{t('PASSWORD_STRENGTH_MEDIUM')}

, + }; + } else { + passwordStrengthLabel = { + color: theme.colors.feedback.success, + width: '100%', + message:

{t('PASSWORD_STRENGTH_STRONG')}

, + }; + } useEffect(() => { const keyDownHandler = (event) => { @@ -86,7 +108,6 @@ function PasswordInput({ event.key === 'Enter' && document.activeElement?.id === 'password-input' && !!enteredPassword && - enteredPasswordLength >= REQUIRED_PASSWORD_LENGTH && (checkPasswordStrength ? score >= PasswordStrength.AverageScore : true) ) { event.preventDefault(); @@ -97,94 +118,16 @@ function PasswordInput({ return () => { document.removeEventListener('keydown', keyDownHandler); }; - }, [enteredPassword]); - - useEffect(() => { - if (passwordError) { - setError(passwordError); - return; - } - if (enteredPassword && !!createPasswordFlow && score <= PasswordStrength.WeakScore) { - setError(t('PASSWORD_STRENGTH_ERROR')); - return; - } - setError(''); - }, [passwordError, enteredPassword]); + }, [checkPasswordStrength, enteredPassword, enteredPasswordLength, handleContinue, score]); useEffect(() => { if (enteredPassword !== '') { setPasswordStrength(score); } - return () => { setPasswordStrength(PasswordStrength.NoScore); }; - }, [enteredPassword, setPasswordStrength]); - - const handleTogglePasswordView = () => { - setIsPasswordVisible(!isPasswordVisible); - }; - - const handlePasswordChange = (event: React.FormEvent) => { - setEnteredPassword(event.currentTarget.value); - }; - - const renderStrengthBar = () => { - if (enteredPassword !== '') { - if ( - enteredPasswordLength <= REQUIRED_PASSWORD_LENGTH || - score <= PasswordStrength.WeakScore - ) { - return ( - - {t('PASSWORD_STRENGTH_LABEL')} - - {transition((style) => ( - - ))} - -

{t('PASSWORD_STRENGTH_WEAK')}

-
- ); - } - - if (score <= PasswordStrength.AverageScore) { - return ( - - {t('PASSWORD_STRENGTH_LABEL')} - {transition((style) => ( - - - - ))} -

{t('PASSWORD_STRENGTH_MEDIUM')}

-
- ); - } - - return ( - - {t('PASSWORD_STRENGTH_LABEL')} - {transition((style) => ( - - - - ))} -

{t('PASSWORD_STRENGTH_STRONG')}

-
- ); - } - return ( - - {t('PASSWORD_STRENGTH_LABEL')} - - {transition((style) => ( - - ))} - - - ); - }; + }, [enteredPassword, score, setPasswordStrength]); return ( @@ -198,10 +141,10 @@ function PasswordInput({ id="password-input" type={isPasswordVisible ? 'text' : 'password'} value={enteredPassword} - onChange={handlePasswordChange} + onChange={(e) => setEnteredPassword(e.currentTarget.value)} autoFocus={autoFocus} complications={ - + setIsPasswordVisible(!isPasswordVisible)}> toggle password visibility } - feedback={error !== '' ? [{ message: error, variant: 'danger' }] : undefined} + feedback={passwordError ? [{ message: passwordError, variant: 'danger' }] : undefined} hideClear /> - {checkPasswordStrength ? renderStrengthBar() : null} - + + {t('PASSWORD_STRENGTH_LABEL')} + + {transition((style) => ( + + ))} + + {passwordStrengthLabel.message} + + - + setCurrentTab(tabClicked)} + /> {currentTab === 'primary' && ( ({ flexDirection: 'column', paddingLeft: props.theme.space.m, paddingRight: props.theme.space.m, - marginTop: props.theme.space.l, + marginTop: props.theme.space.m, marginBottom: props.theme.space.xl, h1: { ...props.theme.typography.body_medium_m, diff --git a/src/app/ui-library/tabs.tsx b/src/app/ui-library/tabs.tsx index 28a8cb7b6..002eb9202 100644 --- a/src/app/ui-library/tabs.tsx +++ b/src/app/ui-library/tabs.tsx @@ -30,14 +30,16 @@ export const TabItem = styled.button<{ $active?: boolean }>` `} `; -type TabProps = { - tabs: { label: string; value: T }[]; +export type TabProp = { label: string; value: T }; + +export type TabsProps = { + tabs: TabProp[]; activeTab: T; onTabClick: (value: T) => void; className?: string; }; -function Tabs({ tabs, activeTab, onTabClick, className }: TabProps) { +export function Tabs({ tabs, activeTab, onTabClick, className }: TabsProps) { return ( {tabs.map((tab) => ( From 6b06fc52c49ad710f910415312d8e2df1b83ed49 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Wed, 23 Oct 2024 17:28:39 +0800 Subject: [PATCH 199/227] fix: style the no runes to list message --- src/app/screens/listRune/index.styled.ts | 5 +++-- src/app/screens/listRune/index.tsx | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/screens/listRune/index.styled.ts b/src/app/screens/listRune/index.styled.ts index 3e7e79b35..c9454d423 100644 --- a/src/app/screens/listRune/index.styled.ts +++ b/src/app/screens/listRune/index.styled.ts @@ -134,6 +134,7 @@ export const RightAlignStyledP = styled(StyledP)` text-align: right; `; -export const NoItemsContainer = styled.div((props) => ({ - padding: `${props.theme.space.m} ${props.theme.space.s}`, +export const NoItemsContainer = styled(StyledP)((props) => ({ + marginTop: props.theme.space.xxxl, + textAlign: 'center', })); diff --git a/src/app/screens/listRune/index.tsx b/src/app/screens/listRune/index.tsx index 910e3b3cb..a405011cb 100644 --- a/src/app/screens/listRune/index.tsx +++ b/src/app/screens/listRune/index.tsx @@ -327,7 +327,9 @@ export default function ListRuneScreen() { selectedRuneId={selectedRune?.principal ?? ''} getDesc={getDesc} > - {t('NO_UNLISTED_ITEMS')} + + {t('NO_UNLISTED_ITEMS')} + ); } From fe59428f206fddce16efdeb0dfbd6673c75b9270 Mon Sep 17 00:00:00 2001 From: Victor Kirov Date: Wed, 23 Oct 2024 12:56:02 +0300 Subject: [PATCH 200/227] Fix copy nit --- src/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/en.json b/src/locales/en.json index c47cb4e9e..d62eb2e41 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -690,7 +690,7 @@ "DESCRIPTION": "Select the default address type you would like to use for all accounts. You can switch at any time from settings.", "LEARN_MORE": "Learn more about address types.", "ACCOUNT_COUNT": "Accounts: {{accounts}}", - "ACCOUNT_SUMMARY_DESCRIPTION": "The balances shown are cumulative across all accounts for each address type.", + "ACCOUNT_SUMMARY_DESCRIPTION": "The balances shown are cumulative across all found accounts for each address type.", "ACCOUNT_SUMMARY_DESCRIPTION_NO_FUNDS": "No balances found for this seed phrase." } }, From 755deeb0df85cfa7addcb921d789b7c76346631d Mon Sep 17 00:00:00 2001 From: Tim Man Date: Wed, 23 Oct 2024 17:52:44 +0800 Subject: [PATCH 201/227] fix: preferred btc address sheet description color and selector spacing --- .../components/preferredBtcAddress/index.tsx | 21 ++++++++++++------- .../preferredBtcAddressItem/index.tsx | 1 - .../paymentAddressTypeSelector.tsx | 5 +++-- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/app/components/preferredBtcAddress/index.tsx b/src/app/components/preferredBtcAddress/index.tsx index 14e75662d..2889b8136 100644 --- a/src/app/components/preferredBtcAddress/index.tsx +++ b/src/app/components/preferredBtcAddress/index.tsx @@ -12,11 +12,8 @@ import styled from 'styled-components'; import AddressTypeSelector from './addressTypeSelector'; const AddressTypeContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - gap: props.theme.space.xxs, - marginTop: props.theme.space.s, - marginBottom: props.theme.space.s, + marginTop: props.theme.space.l, + marginBottom: props.theme.space.l, })); type AddressTypeSelectorProps = { @@ -24,6 +21,12 @@ type AddressTypeSelectorProps = { setSelectedType: (type: BtcPaymentType) => void; }; +const AddressTypeSelectorsContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${(props) => props.theme.space.s}; +`; + export function AddressTypeSelectors({ selectedType, setSelectedType }: AddressTypeSelectorProps) { const { t } = useTranslation('translation', { keyPrefix: 'SETTING_SCREEN.PREFERRED_BTC_ADDRESS', @@ -31,7 +34,7 @@ export function AddressTypeSelectors({ selectedType, setSelectedType }: AddressT const { btcAddresses } = useSelectedAccount(); return ( - <> + setSelectedType('nested')} isSelected={selectedType === 'nested'} /> - + ); } @@ -75,7 +78,9 @@ function PreferredBtcAddressSheet({ return ( - {t('DESCRIPTION')} + + {t('DESCRIPTION')} + diff --git a/src/app/components/preferredBtcAddressItem/index.tsx b/src/app/components/preferredBtcAddressItem/index.tsx index 7011ffa2d..175afc307 100644 --- a/src/app/components/preferredBtcAddressItem/index.tsx +++ b/src/app/components/preferredBtcAddressItem/index.tsx @@ -12,7 +12,6 @@ const Button = styled.button<{ $isSelected: boolean }>((props) => ({ ? props.theme.colors.white_900 : props.theme.colors.transparent, padding: props.theme.space.m, - margin: `${props.theme.space.xxs} 0`, borderRadius: props.theme.space.s, border: `solid 1px ${props.theme.colors.white_800}`, display: 'flex', diff --git a/src/app/screens/restoreWallet/paymentAddressTypeSelector.tsx b/src/app/screens/restoreWallet/paymentAddressTypeSelector.tsx index 8fbf4a2e1..3b721bfca 100644 --- a/src/app/screens/restoreWallet/paymentAddressTypeSelector.tsx +++ b/src/app/screens/restoreWallet/paymentAddressTypeSelector.tsx @@ -50,11 +50,12 @@ const SummaryContainer = styled.div({ margin: '18px 0', }); -const TypesContainer = styled.div({ +const TypesContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'column', margin: '4px 0', -}); + gap: props.theme.space.s, +})); const ButtonContainer = styled.div((props) => ({ width: '100%', From ab10a381e3959b81f62ba2110ee72f7ca28a5686 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Wed, 23 Oct 2024 18:11:57 +0800 Subject: [PATCH 202/227] fix: ledger learn more link --- src/app/screens/ledger/importLedgerAccount/steps/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/screens/ledger/importLedgerAccount/steps/index.tsx b/src/app/screens/ledger/importLedgerAccount/steps/index.tsx index 52a7e30b1..4a22e8ba0 100644 --- a/src/app/screens/ledger/importLedgerAccount/steps/index.tsx +++ b/src/app/screens/ledger/importLedgerAccount/steps/index.tsx @@ -52,7 +52,7 @@ import { } from './index.styled'; const LINK_TO_LEDGER_ACCOUNT_ISSUE_GUIDE = - 'https://support.xverse.app/hc/en-us/articles/17901278165773'; + 'https://support.xverse.app/hc/en-us/articles/17898446492557'; const LINK_TO_LEDGER_PASSPHRASE_GUIDE = 'https://support.xverse.app/hc/en-us/articles/17901278165773'; From b5eaaff7db4b525afda0c31df4cb450930ae036c Mon Sep 17 00:00:00 2001 From: Tim Man Date: Wed, 23 Oct 2024 18:43:02 +0800 Subject: [PATCH 203/227] fix: font styling on announcement modal --- .../screens/home/announcementModal/nativeSegWit.tsx | 12 +++++++++++- src/locales/en.json | 4 +++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/app/screens/home/announcementModal/nativeSegWit.tsx b/src/app/screens/home/announcementModal/nativeSegWit.tsx index e04d1f10d..9cb60f664 100644 --- a/src/app/screens/home/announcementModal/nativeSegWit.tsx +++ b/src/app/screens/home/announcementModal/nativeSegWit.tsx @@ -16,9 +16,15 @@ const LogoImg = styled.img(() => ({ const DescriptionItem = styled.div<{ $bottomSpacer?: boolean }>((props) => ({ ...props.theme.typography.body_m, + color: props.theme.colors.white_200, marginBottom: props.$bottomSpacer ? props.theme.space.m : 0, })); +const HighlightSpan = styled.span` + ${(props) => props.theme.typography.body_medium_m}; + color: ${(props) => props.theme.colors.white_0}; +`; + const ButtonContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'column', @@ -54,7 +60,11 @@ export default function NativeSegWit({ isVisible, onClose }: Props) { onClose={onClose} visible={isVisible} > - {t('DESCRIPTION_1')} + + {t('DESCRIPTION_1a')} + {t('DESCRIPTION_1b')} + {t('DESCRIPTION_1c')} + {t('DESCRIPTION_2')} {t('DESCRIPTION_3')} diff --git a/src/locales/en.json b/src/locales/en.json index c47cb4e9e..c43862b98 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -238,7 +238,9 @@ "ANNOUNCEMENTS": { "NATIVE_SEGWIT": { "TITLE": "Native SegWit is here!", - "DESCRIPTION_1": "Xverse now supports Native SegWit addresses for lower fees and better performance!", + "DESCRIPTION_1a": "Xverse now supports ", + "DESCRIPTION_1b": "Native SegWit", + "DESCRIPTION_1c": " addresses for lower fees and better performance!", "DESCRIPTION_2": "You can select your preferred Bitcoin payment address (native or nested) used for sending, receiving, and connecting with apps.", "DESCRIPTION_3": "Your Bitcoin balance includes funds from both addresses, and you can switch back anytime.", "SELECT": "Select my Preferred Bitcoin Address", From c421a00a56ebd9038ad02eb3cb0225dd85557dc8 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Wed, 23 Oct 2024 18:45:16 +0800 Subject: [PATCH 204/227] fix: add min height back to sheet after cross button was changed to position absolute --- src/app/ui-library/sheet.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/ui-library/sheet.tsx b/src/app/ui-library/sheet.tsx index e4855c6e6..856ac8c12 100644 --- a/src/app/ui-library/sheet.tsx +++ b/src/app/ui-library/sheet.tsx @@ -16,6 +16,7 @@ const HeaderContainer = styled.div((props) => ({ justifyContent: 'space-between', margin: props.theme.space.m, gap: props.theme.space.m, + minHeight: props.theme.space.m, })); const CustomisedModal = styled(Modal)` From 1d5cad98dbca3fe5c743cea83324915521082571 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Wed, 23 Oct 2024 18:50:49 +0800 Subject: [PATCH 205/227] fix: larger min height on Sheet --- src/app/ui-library/sheet.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/ui-library/sheet.tsx b/src/app/ui-library/sheet.tsx index 856ac8c12..6ba1efa7c 100644 --- a/src/app/ui-library/sheet.tsx +++ b/src/app/ui-library/sheet.tsx @@ -16,7 +16,7 @@ const HeaderContainer = styled.div((props) => ({ justifyContent: 'space-between', margin: props.theme.space.m, gap: props.theme.space.m, - minHeight: props.theme.space.m, + minHeight: props.theme.space.l, })); const CustomisedModal = styled(Modal)` From 155d27e410ce88a809dfcdb842e864b928758ec1 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Wed, 23 Oct 2024 18:59:35 +0800 Subject: [PATCH 206/227] fix: tx status screen needs a min height after margintop was removed --- src/app/screens/transactionStatus/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/screens/transactionStatus/index.tsx b/src/app/screens/transactionStatus/index.tsx index 1af2a7453..b8560a634 100644 --- a/src/app/screens/transactionStatus/index.tsx +++ b/src/app/screens/transactionStatus/index.tsx @@ -24,7 +24,7 @@ const TxStatusContainer = styled.div((props) => ({ background: props.theme.colors.elevation0, display: 'flex', flexDirection: 'column', - height: '100%', + minHeight: 600, })); const OuterContainer = styled.div((_props) => ({ From c1b361898c3dcb4da7bcfa6f3b9afcecfa682e5e Mon Sep 17 00:00:00 2001 From: Victor Kirov Date: Wed, 23 Oct 2024 14:31:40 +0300 Subject: [PATCH 207/227] Fix knip issues (#708) --- src/app/screens/explore/index.tsx | 2 +- src/app/ui-library/tabs.tsx | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/app/screens/explore/index.tsx b/src/app/screens/explore/index.tsx index 924b9dabb..35d78932c 100644 --- a/src/app/screens/explore/index.tsx +++ b/src/app/screens/explore/index.tsx @@ -6,7 +6,7 @@ import useFeaturedDapps from '@hooks/useFeaturedDapps'; import { ArrowsOut } from '@phosphor-icons/react'; import { StyledHeading } from '@ui-library/common.styled'; import Spinner from '@ui-library/spinner'; -import Tabs from '@ui-library/tabs'; +import { Tabs } from '@ui-library/tabs'; import { XVERSE_EXPLORE_URL } from '@utils/constants'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/src/app/ui-library/tabs.tsx b/src/app/ui-library/tabs.tsx index 002eb9202..41a148b82 100644 --- a/src/app/ui-library/tabs.tsx +++ b/src/app/ui-library/tabs.tsx @@ -32,7 +32,7 @@ export const TabItem = styled.button<{ $active?: boolean }>` export type TabProp = { label: string; value: T }; -export type TabsProps = { +type TabsProps = { tabs: TabProp[]; activeTab: T; onTabClick: (value: T) => void; @@ -54,5 +54,3 @@ export function Tabs({ tabs, activeTab, onTabClick, className ); } - -export default Tabs; From 0715a704f321ee10e1e6867655c242ea4af79ed4 Mon Sep 17 00:00:00 2001 From: Victor Kirov Date: Thu, 24 Oct 2024 09:31:41 +0300 Subject: [PATCH 208/227] Add errors specific to address and address type mismatch --- src/app/screens/btcSendRequest/index.tsx | 33 ++++++++++++------- src/app/screens/mintRune/useMintRequest.ts | 24 +++++++++++--- .../screens/signBatchPsbtRequest/index.tsx | 17 +++++++++- .../useSignMessageRequest.ts | 30 ++++++++++++----- .../useSignPsbtValidationGate.ts | 22 ++++++++++--- src/app/screens/transactionStatus/index.tsx | 4 ++- src/app/stores/index.ts | 16 +++++---- src/locales/en.json | 5 ++- 8 files changed, 111 insertions(+), 40 deletions(-) diff --git a/src/app/screens/btcSendRequest/index.tsx b/src/app/screens/btcSendRequest/index.tsx index d3e72ce32..b01680774 100644 --- a/src/app/screens/btcSendRequest/index.tsx +++ b/src/app/screens/btcSendRequest/index.tsx @@ -105,24 +105,35 @@ function BtcSendRequest() { useEffect(() => { const checkIfMismatch = () => { + let errorTitle = ''; + let error = ''; + let textAlignment = 'center'; + if (payload.senderAddress !== selectedAccount.btcAddress) { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - error: t('REQUEST_ERRORS.ADDRESS_MISMATCH'), - browserTx: true, - textAlignment: 'left', - }, - }); + if ( + selectedAccount.btcAddresses.native?.address === payload.senderAddress || + selectedAccount.btcAddresses.nested?.address === payload.senderAddress + ) { + errorTitle = t('REQUEST_ERRORS.ADDRESS_TYPE_MISMATCH_TITLE'); + error = t('REQUEST_ERRORS.ADDRESS_TYPE_MISMATCH'); + } else { + errorTitle = t('REQUEST_ERRORS.ADDRESS_MISMATCH_TITLE'); + error = t('REQUEST_ERRORS.ADDRESS_MISMATCH'); + } + textAlignment = 'left'; + } else if (payload.network.type !== network.type) { + error = t('REQUEST_ERRORS.NETWORK_MISMATCH'); } - if (payload.network.type !== network.type) { + + if (error) { navigate('/tx-status', { state: { txid: '', currency: 'BTC', - error: t('REQUEST_ERRORS.NETWORK_MISMATCH'), + errorTitle, + error, browserTx: true, + textAlignment, }, }); } diff --git a/src/app/screens/mintRune/useMintRequest.ts b/src/app/screens/mintRune/useMintRequest.ts index 52c203847..0c5a2266c 100644 --- a/src/app/screens/mintRune/useMintRequest.ts +++ b/src/app/screens/mintRune/useMintRequest.ts @@ -1,4 +1,5 @@ import { makeRPCError, makeRpcSuccessResponse, sendRpcResponse } from '@common/utils/rpc/helpers'; +import useBtcClient from '@hooks/apiClients/useBtcClient'; import useOrdinalsServiceApi from '@hooks/apiClients/useOrdinalsServiceApi'; import useRunesApi from '@hooks/apiClients/useRunesApi'; import useTransactionContext from '@hooks/useTransactionContext'; @@ -40,6 +41,7 @@ const useMintRequest = (): { const txContext = useTransactionContext(); const ordinalsServiceApi = useOrdinalsServiceApi(); const runesApi = useRunesApi(); + const btcClient = useBtcClient(); const [mintError, setMintError] = useState<{ code: number | undefined; @@ -139,11 +141,8 @@ const useMintRequest = (): { const payAndConfirmMintRequest = async (ledgerTransport?: Transport) => { try { setIsExecuting(true); - const txid = await orderTx?.transaction.broadcast({ - ledgerTransport, - rbfEnabled: false, - }); - if (!txid) { + + if (!orderTx) { const response = makeRPCError(requestId, { code: RpcErrorCode.INTERNAL_ERROR, message: 'Failed to broadcast transaction', @@ -151,6 +150,21 @@ const useMintRequest = (): { sendRpcResponse(+tabId, response); return; } + + // TODO: make enhancedTransaction class use a passed in btcClient and use: + /* + orderTx.transaction.broadcast({ + ledgerTransport, + rbfEnabled: false, + }); + */ + + const { hex: transactionHex, id: txid } = await orderTx.transaction.getTransactionHexAndId({ + ledgerTransport, + rbfEnabled: false, + }); + await btcClient.sendRawTransaction(transactionHex); + await ordinalsServiceApi.executeMint(orderId, txid); const mintRequestResponse = makeRpcSuccessResponse<'runes_mint'>(requestId, { fundingAddress: txContext.paymentAddress.address, diff --git a/src/app/screens/signBatchPsbtRequest/index.tsx b/src/app/screens/signBatchPsbtRequest/index.tsx index 23ebfcc36..fcadbddf3 100644 --- a/src/app/screens/signBatchPsbtRequest/index.tsx +++ b/src/app/screens/signBatchPsbtRequest/index.tsx @@ -65,15 +65,30 @@ function SignBatchPsbtRequest() { input.address !== selectedAccount.btcAddress && input.address !== selectedAccount.ordinalsAddress ) { + let errorTitle = ''; + let error = ''; + if ( + selectedAccount.btcAddresses.native?.address === input.address || + selectedAccount.btcAddresses.nested?.address === input.address + ) { + errorTitle = t('ADDRESS_TYPE_MISMATCH_TITLE'); + error = t('ADDRESS_TYPE_MISMATCH'); + } else { + errorTitle = t('ADDRESS_MISMATCH_TITLE'); + error = t('ADDRESS_MISMATCH'); + } + navigate('/tx-status', { state: { txid: '', currency: 'BTC', - error: t('ADDRESS_MISMATCH'), + errorTitle, + error, browserTx: true, textAlignment: 'left', }, }); + return true; } return false; diff --git a/src/app/screens/signMessageRequest/useSignMessageRequest.ts b/src/app/screens/signMessageRequest/useSignMessageRequest.ts index 06376c521..155bf0a05 100644 --- a/src/app/screens/signMessageRequest/useSignMessageRequest.ts +++ b/src/app/screens/signMessageRequest/useSignMessageRequest.ts @@ -56,15 +56,15 @@ export const useSignMessageValidation = (requestPayload: SignMessagePayload | un const [validationError, setValidationError] = useState(null); const { t } = useTranslation('translation', { keyPrefix: 'REQUEST_ERRORS' }); const selectedAccount = useSelectedAccount(); - const { accountsList, network } = useWalletSelector(); + const { accountsList, network, btcPaymentAddressType } = useWalletSelector(); const { switchAccount } = useWalletReducer(); const checkAddressAvailability = () => { const account = accountsList.filter( (acc) => - selectedAccount.btcAddress === acc.btcAddresses.native?.address || - selectedAccount.btcAddress === acc.btcAddresses.nested?.address || - selectedAccount.btcAddress === acc.btcAddresses.taproot.address, + requestPayload?.address === acc.btcAddresses.native?.address || + requestPayload?.address === acc.btcAddresses.nested?.address || + requestPayload?.address === acc.btcAddresses.taproot.address, ); return isHardwareAccount(selectedAccount) ? account[0] || selectedAccount : account[0]; }; @@ -86,13 +86,25 @@ export const useSignMessageValidation = (requestPayload: SignMessagePayload | un return; } - if ( - selectedAccount.btcAddress === account.btcAddresses.native?.address || - selectedAccount.btcAddress === account.btcAddresses.nested?.address - ) + if (selectedAccount.ordinalsAddress !== account.btcAddresses.taproot.address) { + switchAccount(account); + } + + if (requestPayload?.address === account.btcAddresses.taproot.address) { return; + } - switchAccount(account); + // ensure we have the correct address type signing on payment address + if ( + (btcPaymentAddressType === 'native' && + requestPayload?.address !== account.btcAddresses.native?.address) || + (btcPaymentAddressType === 'nested' && + requestPayload?.address !== account.btcAddresses.nested?.address) + ) { + setValidationError({ + error: t('ADDRESS_TYPE_MISMATCH'), + }); + } }; useEffect(() => { diff --git a/src/app/screens/signPsbtRequest/useSignPsbtValidationGate.ts b/src/app/screens/signPsbtRequest/useSignPsbtValidationGate.ts index 66e19b240..2558f8062 100644 --- a/src/app/screens/signPsbtRequest/useSignPsbtValidationGate.ts +++ b/src/app/screens/signPsbtRequest/useSignPsbtValidationGate.ts @@ -17,7 +17,7 @@ type ValidationError = { }; const useSignPsbtValidationGate = ({ payload, parsedPsbt }: Props) => { - const { btcAddress, ordinalsAddress } = useSelectedAccount(); + const { btcAddress, btcAddresses, ordinalsAddress } = useSelectedAccount(); const { network } = useWalletSelector(); const { t } = useTranslation('translation', { keyPrefix: 'REQUEST_ERRORS' }); const [validationError, setValidationError] = useState(null); @@ -40,10 +40,22 @@ const useSignPsbtValidationGate = ({ payload, parsedPsbt }: Props) => { if (payload.inputsToSign) { payload.inputsToSign.forEach((input) => { if (input.address !== btcAddress && input.address !== ordinalsAddress) { - setValidationError({ - error: t('ADDRESS_MISMATCH'), - alignment: 'left', - }); + if ( + input.address === btcAddresses.native?.address || + input.address === btcAddresses.nested?.address + ) { + setValidationError({ + errorTitle: t('ADDRESS_TYPE_MISMATCH_TITLE'), + error: t('ADDRESS_TYPE_MISMATCH'), + alignment: 'left', + }); + } else { + setValidationError({ + error: t('ADDRESS_MISMATCH'), + errorTitle: t('ADDRESS_MISMATCH_TITLE'), + alignment: 'left', + }); + } } }); return; diff --git a/src/app/screens/transactionStatus/index.tsx b/src/app/screens/transactionStatus/index.tsx index b8560a634..0862199f6 100644 --- a/src/app/screens/transactionStatus/index.tsx +++ b/src/app/screens/transactionStatus/index.tsx @@ -24,7 +24,8 @@ const TxStatusContainer = styled.div((props) => ({ background: props.theme.colors.elevation0, display: 'flex', flexDirection: 'column', - minHeight: 600, + minHeight: 570, + height: '100%', })); const OuterContainer = styled.div((_props) => ({ @@ -238,6 +239,7 @@ function TransactionStatus() { sendNetworkMismatchMessage({ tabId, messageId }); if ( (error === tReqErrors('ADDRESS_MISMATCH') || + error === tReqErrors('ADDRESS_TYPE_MISMATCH') || error === tReqErrors('ADDRESS_MISMATCH_STX')) && tabId && messageId diff --git a/src/app/stores/index.ts b/src/app/stores/index.ts index d12bae4c3..d60e32185 100644 --- a/src/app/stores/index.ts +++ b/src/app/stores/index.ts @@ -126,16 +126,18 @@ const migrations = { return migratedIds; }; - markAlertsForShow( - 'native_segwit_intro', - 'co:panel:address_changed_to_native', - 'co:receive:address_change_button', - 'co:receive:address_changed_to_native', - ); + if ((state as unknown as WalletStateV5).btcPaymentAddressType === undefined) { + markAlertsForShow( + 'native_segwit_intro', + 'co:panel:address_changed_to_native', + 'co:receive:address_change_button', + 'co:receive:address_changed_to_native', + ); + } return { ...state, - btcPaymentAddressType: 'nested', + btcPaymentAddressType: (state as unknown as WalletStateV5).btcPaymentAddressType || 'nested', accountsList: state.accountsList.map(migrateAccount('software')), ledgerAccountsList: state.ledgerAccountsList.map(migrateAccount('ledger')), allowNestedSegWitAddress: true, diff --git a/src/locales/en.json b/src/locales/en.json index 1fe7ae463..c10d7298a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1599,7 +1599,10 @@ "REQUEST_ERRORS": { "INVALID_REQUEST": "Invalid request", "MISSING_ARGUMENTS": "Contract function call missing arguments", - "ADDRESS_MISMATCH": "The app is requesting a signature from an address that doesn't match the one currently active in your Xverse wallet.\n\nTo fix this:\n - Ensure that the account you're using in Xverse matches the account you're logged in with on the app.\n- Confirm that the address type in Xverse matches the one required by the app (Native SegWit or Nested SegWit).", + "ADDRESS_MISMATCH_TITLE": "Address Mismatch", + "ADDRESS_MISMATCH": "The app is requesting a signature from an address that doesn't match the account currently active in your wallet.\n\nTo fix this:\n- Make sure the account in Xverse matches the one connected to the app\n- Try reconnecting to the app", + "ADDRESS_TYPE_MISMATCH_TITLE": "Address Type Mismatch", + "ADDRESS_TYPE_MISMATCH": "The app is requesting a signature from a Bitcoin payment address type that doesn't match the one currently active in your wallet.\n\nTo fix this:\n- Make sure the Bitcoin payment address type (Native SegWit or Nested SegWit) selected in Xverse matches the app\n- Try reconnecting to the app", "ADDRESS_MISMATCH_STX": "The app is requesting a signature from an address that doesn't match the one currently active in your Xverse wallet.", "NETWORK_MISMATCH": "There's a mismatch between your active network and the network you're logged in with on the app.", "PSBT_CANT_PARSE_ERROR_TITLE": "Transaction Error", From e59f18ff85edb4da43d8c938a2c898b5a6a746f2 Mon Sep 17 00:00:00 2001 From: Victor Kirov Date: Thu, 24 Oct 2024 10:01:15 +0300 Subject: [PATCH 209/227] Fix styling --- src/app/components/batchPsbtSigning/index.styled.ts | 2 +- src/app/components/requests/requestError.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/components/batchPsbtSigning/index.styled.ts b/src/app/components/batchPsbtSigning/index.styled.ts index c5e46a2f6..efcd21c01 100644 --- a/src/app/components/batchPsbtSigning/index.styled.ts +++ b/src/app/components/batchPsbtSigning/index.styled.ts @@ -69,7 +69,7 @@ export const StyledSheet = styled(BottomModal)` flex-direction: column; height: 100%; max-height: 100% !important; - background-color: #181818 !important; + background-color: ${(props) => props.theme.colors.elevation0} !important; `; export const ButtonsContainer = styled.div((props) => ({ diff --git a/src/app/components/requests/requestError.tsx b/src/app/components/requests/requestError.tsx index 52627c1b6..3076095eb 100644 --- a/src/app/components/requests/requestError.tsx +++ b/src/app/components/requests/requestError.tsx @@ -11,8 +11,8 @@ const Container = styled.div((props) => ({ display: 'flex', flexDirection: 'column', height: '100%', - paddingLeft: props.theme.space.m, - paddingRight: props.theme.space.m, + marginLeft: props.theme.space.m, + marginRight: props.theme.space.m, })); const Image = styled.img({ From cbc0fad46897f8d801c5b09d8f99e24f9831a1af Mon Sep 17 00:00:00 2001 From: Victor Kirov Date: Thu, 24 Oct 2024 10:27:34 +0300 Subject: [PATCH 210/227] Make styling consistent --- src/app/components/requests/requestError.tsx | 4 ++-- src/app/screens/transactionStatus/index.tsx | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/app/components/requests/requestError.tsx b/src/app/components/requests/requestError.tsx index 3076095eb..2e13c5770 100644 --- a/src/app/components/requests/requestError.tsx +++ b/src/app/components/requests/requestError.tsx @@ -22,14 +22,14 @@ const Image = styled.img({ }); const HeadingText = styled.h1((props) => ({ - ...props.theme.typography.headline_xs, + ...props.theme.typography.headline_s, color: props.theme.colors.white_0, textAlign: 'center', marginTop: props.theme.space.m, })); const BodyText = styled.h1<{ $textAlignment: 'center' | 'left' }>((props) => ({ - ...props.theme.typography.body_l, + ...props.theme.typography.body_m, color: props.theme.colors.white_200, marginTop: props.theme.space.m, textAlign: props.$textAlignment, diff --git a/src/app/screens/transactionStatus/index.tsx b/src/app/screens/transactionStatus/index.tsx index 0862199f6..e8693020d 100644 --- a/src/app/screens/transactionStatus/index.tsx +++ b/src/app/screens/transactionStatus/index.tsx @@ -7,7 +7,6 @@ import { sendMissingFunctionArgumentsMessage, sendNetworkMismatchMessage, } from '@common/utils/rpc/responseMessages/errors'; -import ActionButton from '@components/button'; import CopyButton from '@components/copyButton'; import InfoContainer from '@components/infoContainer'; import useWalletSelector from '@hooks/useWalletSelector'; @@ -112,7 +111,7 @@ const BodyText = styled.h1<{ $textAlignment: 'center' | 'left' }>((props) => ({ })); const TxIDText = styled.h1((props) => ({ - ...props.theme.headline_category_s, + ...props.theme.typography.body_medium_s, color: props.theme.colors.white_400, marginTop: props.theme.space.m, textTransform: 'uppercase', @@ -343,12 +342,12 @@ function TransactionStatus() { {isSwapTransaction && isSponsorServiceError ? ( - - +