Merge pull request #874 from secretkeylabs/release/v0.48.1

release: v0.48.1 to main
This commit is contained in:
Duska.T
2025-01-22 09:06:57 +01:00
committed by GitHub
27 changed files with 247 additions and 215 deletions

View File

@@ -1,4 +1,4 @@
Copyright Secret Key Labs Limited 2024. All rights reserved.
Copyright Secret Key Labs Limited 2025. All rights reserved.
You acknowledge and agree that Secret Key Labs Limited own all legal right, title and interest in and to the work, software, application, source code, documentation and any other documents in this repository (collectively, the “Program”), including any intellectual property rights which subsist in the Program (whether those rights happen to be registered or not, and wherever in the world those rights may exist), whether in source code or any other form.

18
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "xverse-web-extension",
"version": "0.48.0",
"version": "0.48.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "xverse-web-extension",
"version": "0.48.0",
"version": "0.48.1",
"dependencies": {
"@aryzing/superqs": "0.0.6",
"@ledgerhq/hw-transport-webusb": "6.27.17",
@@ -17,7 +17,7 @@
"@sats-connect/core": "0.5.2",
"@scure/base": "1.1.9",
"@scure/btc-signer": "1.2.1",
"@secretkeylabs/xverse-core": "36.0.1",
"@secretkeylabs/xverse-core": "36.0.2",
"@stacks/connect": "7.9.0",
"@stacks/stacks-blockchain-api-types": "7.14.1",
"@stacks/transactions": "7.0.2",
@@ -1265,9 +1265,9 @@
}
},
"node_modules/@secretkeylabs/xverse-core": {
"version": "36.0.1",
"resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/36.0.1/9b8902ab25b7f7e08d45a885c70c6bc3d9714075",
"integrity": "sha512-6mJWaN2W3qFZ8f++NqdAbuNAkk8VlnwKwDBgh6AVI+t2l9qjIt74p5VVHwuppAZcM6mi3Aiwem3ggdNXU90TUw==",
"version": "36.0.2",
"resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/36.0.2/1e4854a0378c9c6960cf3467634f0ab42991b43f",
"integrity": "sha512-zlPjXcSW2IZOcGBS/dKeONm8cOvphi5AhnNXUZl2vbTPDhXnGII5HWXF7QoJtaRKw7QcNGqNmJ7bK+QfPsyGIw==",
"dependencies": {
"@bitcoinerlab/secp256k1": "1.0.2",
"@noble/curves": "1.3.0",
@@ -14148,9 +14148,9 @@
}
},
"@secretkeylabs/xverse-core": {
"version": "36.0.1",
"resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/36.0.1/9b8902ab25b7f7e08d45a885c70c6bc3d9714075",
"integrity": "sha512-6mJWaN2W3qFZ8f++NqdAbuNAkk8VlnwKwDBgh6AVI+t2l9qjIt74p5VVHwuppAZcM6mi3Aiwem3ggdNXU90TUw==",
"version": "36.0.2",
"resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/36.0.2/1e4854a0378c9c6960cf3467634f0ab42991b43f",
"integrity": "sha512-zlPjXcSW2IZOcGBS/dKeONm8cOvphi5AhnNXUZl2vbTPDhXnGII5HWXF7QoJtaRKw7QcNGqNmJ7bK+QfPsyGIw==",
"requires": {
"@bitcoinerlab/secp256k1": "1.0.2",
"@noble/curves": "1.3.0",

View File

@@ -1,7 +1,7 @@
{
"name": "xverse-web-extension",
"description": "A Bitcoin wallet for Web3",
"version": "0.48.0",
"description": "The Bitcoin Wallet for everyone",
"version": "0.48.1",
"private": true,
"engines": {
"node": "^18.18.2"
@@ -32,6 +32,7 @@
"tsc-files --noEmit src/styled.d.ts src/react-app-env.d.ts"
],
"*.json": [
"node scripts/pin_all_deps.js",
"prettier --write"
]
},
@@ -45,7 +46,7 @@
"@sats-connect/core": "0.5.2",
"@scure/base": "1.1.9",
"@scure/btc-signer": "1.2.1",
"@secretkeylabs/xverse-core": "36.0.1",
"@secretkeylabs/xverse-core": "36.0.2",
"@stacks/connect": "7.9.0",
"@stacks/stacks-blockchain-api-types": "7.14.1",
"@stacks/transactions": "7.0.2",

View File

@@ -8,21 +8,46 @@
*/
const fs = require('fs');
const packageLock = require('../package-lock.json');
const packageJson = require('../package.json');
const path = require('path');
const { execSync } = require('child_process');
for (const packageName in packageJson.dependencies) {
const installedPathKey = `node_modules/${packageName}`;
if (packageJson.dependencies.hasOwnProperty(packageName) && packageLock.packages[installedPathKey]) {
packageJson.dependencies[packageName] = packageLock.packages[installedPathKey].version;
const packageJsonPath = path.resolve(__dirname, '../package.json');
const packageJson = require(packageJsonPath);
const allDependenciesVersions = Object.values(packageJson.dependencies).concat(
Object.values(packageJson.devDependencies),
);
// if any version has a ^ or ~, we need to pin it, otherwise we can skip
const hasUnpinnedVersions = allDependenciesVersions.some(
(version) => version.startsWith('^') || version.startsWith('~'),
);
if (hasUnpinnedVersions) {
const packages = require('../package-lock.json').packages;
for (const packageName in packageJson.dependencies) {
const installedVersion = packages[`node_modules/${packageName}`].version;
if (packageJson.dependencies.hasOwnProperty(packageName) && installedVersion) {
packageJson.dependencies[packageName] = installedVersion;
}
}
}
for (const packageName in packageJson.devDependencies) {
const installedPathKey = `node_modules/${packageName}`;
if (packageJson.devDependencies.hasOwnProperty(packageName) && packageLock.packages[installedPathKey]) {
packageJson.devDependencies[packageName] = packageLock.packages[installedPathKey].version;
for (const packageName in packageJson.devDependencies) {
const installedVersion = packages[`node_modules/${packageName}`].version;
if (packageJson.devDependencies.hasOwnProperty(packageName) && installedVersion) {
packageJson.devDependencies[packageName] = installedVersion;
}
}
}
fs.writeFileSync('../package.json', JSON.stringify(packageJson, null, 2));
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8');
// Run npm install to update package-lock.json
console.log('Running npm install to update package-lock.json...');
execSync('npm install', { stdio: 'inherit' });
// execSync('git add package.json package-lock.json', { stdio: 'inherit' });
console.log('Successfully pinned all dependency versions');
} else {
console.log('All dependencies are already pinned to exact versions');
}

View File

@@ -55,20 +55,14 @@ export const getAmountFromPostCondition = (
if (pc.conditionType === PostConditionType.NonFungible) return '1';
};
function hasAssetInfo(pc: any): pc is { assetInfo: { assetName: { content: string } } } {
return (
pc &&
pc.assetInfo &&
pc.assetInfo.assetName &&
typeof pc.assetInfo.assetName.content === 'string'
);
}
export const getSymbolFromPostCondition = (
pc: STXPostConditionWire | FungiblePostConditionWire | NonFungiblePostConditionWire,
) => {
if (hasAssetInfo(pc)) {
return pc.assetInfo.assetName?.content?.slice(0, 3).toUpperCase();
if (
pc.conditionType === PostConditionType.Fungible ||
pc.conditionType === PostConditionType.NonFungible
) {
return pc.asset.assetName.content.slice(0, 3).toUpperCase();
}
return 'STX';
};
@@ -76,8 +70,11 @@ export const getSymbolFromPostCondition = (
export const getNameFromPostCondition = (
pc: STXPostConditionWire | FungiblePostConditionWire | NonFungiblePostConditionWire,
) => {
if (hasAssetInfo(pc)) {
return pc.assetInfo.assetName.content;
if (
pc.conditionType === PostConditionType.Fungible ||
pc.conditionType === PostConditionType.NonFungible
) {
return pc.asset.assetName.content;
}
return 'STX';
};

View File

@@ -1,10 +1,8 @@
/* eslint-disable no-nested-ternary */
import {
addressToString,
deserializeAddress,
FungibleConditionCode,
NonFungibleConditionCode,
serializePrincipal,
addressToString,
type PostConditionWire,
} from '@stacks/transactions';
import { useTranslation } from 'react-i18next';
@@ -48,9 +46,10 @@ function PostConditionsView({ postCondition, amount, icon }: Props) {
const name = getNameFromPostCondition(postCondition);
const contractName =
'contractName' in postCondition.principal && postCondition.principal.contractName.content;
const address = addressToString(deserializeAddress(serializePrincipal(postCondition?.principal)));
const isSending = address === stxAddress;
const isContractPrincipal = !!contractName || address.includes('.');
const addressString =
'address' in postCondition.principal ? addressToString(postCondition.principal.address) : '';
const isSending = addressString === stxAddress;
const isContractPrincipal = !!contractName || addressString.includes('.');
return (
<TransferAmountComponent
title={`${
@@ -59,7 +58,7 @@ function PostConditionsView({ postCondition, amount, icon }: Props) {
value={`${amount} ${ticker}`}
subValue={name !== 'STX' ? name : ''}
icon={icon}
address={`${address}${contractName ? `.${contractName}` : ''}`}
address={`${addressString}${contractName ? `.${contractName}` : ''}`}
subTitle={`${
isContractPrincipal
? t('CONTRACT_ADDRESS')

View File

@@ -58,6 +58,8 @@ const TestnetContainer = styled.div((props) => ({
background: props.theme.colors.elevation1,
paddingTop: props.theme.spacing(3),
paddingBottom: props.theme.spacing(3),
borderTopLeftRadius: 'inherit',
borderTopRightRadius: 'inherit',
}));
const TestnetText = styled.p((props) => ({

View File

@@ -86,6 +86,7 @@ export default function ContractCallRequest({
onSignTransaction,
}: Props) {
const selectedNetwork = useNetworkSelector();
const navigate = useNavigate();
const [hasTabClosed, setHasTabClosed] = useState(false);
const { t } = useTranslation('translation');
const [fee, setFee] = useState<BigNumber | undefined>(
@@ -115,7 +116,7 @@ export default function ContractCallRequest({
</PostConditionAlertText>
</PostConditionContainer>
);
const navigate = useNavigate();
const broadcastTx = async (
tx: StacksTransactionWire[],
txAttachment: Buffer | undefined = undefined,
@@ -202,7 +203,7 @@ export default function ContractCallRequest({
txid: response.txid,
currency: 'STX',
error: '',
browserTx: false,
browserTx: true,
tabId,
messageId,
rpcMethod,

View File

@@ -3,10 +3,10 @@ import useSelectedAccount from '@hooks/useSelectedAccount';
import useWalletSelector from '@hooks/useWalletSelector';
import type { FungibleToken } from '@secretkeylabs/xverse-core';
import { useQuery } from '@tanstack/react-query';
import { useCallback } from 'react';
import { handleRetries, InvalidParamsError } from '@utils/query';
export default function useRuneUtxosQueryPerMarketplace(
rune: FungibleToken,
rune?: FungibleToken,
backgroundRefetch = true,
) {
const { network } = useWalletSelector();
@@ -16,7 +16,11 @@ export default function useRuneUtxosQueryPerMarketplace(
throw new Error('Only available on Mainnet');
}
const queryFn = useCallback(async () => {
const queryFn = async () => {
if (!rune) {
throw new InvalidParamsError('rune token is required');
}
const res = await xverseApi.listings.getListedUtxos({
address: ordinalsAddress,
rune: {
@@ -52,13 +56,14 @@ export default function useRuneUtxosQueryPerMarketplace(
listedItems,
unlistedItems,
};
}, [xverseApi, ordinalsAddress, rune.name, rune.ticker]);
};
return useQuery({
refetchOnWindowFocus: backgroundRefetch,
refetchOnReconnect: backgroundRefetch,
queryKey: ['get-listed-rune-utxos', ordinalsAddress, rune.name],
enabled: Boolean(ordinalsAddress && rune.name),
queryKey: ['get-listed-rune-utxos', ordinalsAddress, rune?.name],
enabled: Boolean(ordinalsAddress && rune?.name),
queryFn,
retry: handleRetries,
});
}

View File

@@ -18,9 +18,9 @@ const CoinContainer = styled.div({
alignItems: 'center',
});
const CoinTitleText = styled.p<{ isEnabled?: boolean }>((props) => ({
...props.theme.typography[props.isEnabled ? 'body_bold_m' : 'body_m'],
color: props.theme.colors[props.isEnabled ? 'white_0' : 'white_400'],
const CoinTitleText = styled.p<{ $isEnabled?: boolean }>((props) => ({
...props.theme.typography[props.$isEnabled ? 'body_bold_m' : 'body_m'],
color: props.theme.colors[props.$isEnabled ? 'white_0' : 'white_400'],
textAlign: 'left',
marginLeft: props.theme.space.m,
overflow: 'hidden',
@@ -78,7 +78,7 @@ function CoinItem({
}
size={32}
/>
<CoinTitleText aria-label="Coin Title" isEnabled={isEnabled}>
<CoinTitleText aria-label="Coin Title" $isEnabled={isEnabled}>
{name}
</CoinTitleText>
</CoinContainer>

View File

@@ -0,0 +1,77 @@
import { StyledP } from '@ui-library/common.styled';
import styled from 'styled-components';
export const TokenContainer = styled.div`
display: flex;
flex-direction: column;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
margin-bottom: ${(props) => props.theme.space.xl};
> *:not(:last-child) {
border-bottom: 1px solid ${(props) => props.theme.colors.elevation3};
}
`;
export const Container = styled.div({
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
paddingLeft: 16,
paddingRight: 16,
height: '100%',
});
export const ScrollableContainer = styled.div`
flex: 1;
overflow-y: auto;
`;
export const FtInfoContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'row',
marginBottom: props.theme.space.m,
}));
export const Header = styled.h1((props) => ({
...props.theme.typography.headline_xs,
marginBottom: props.theme.space.m,
}));
export const Description = styled.h1((props) => ({
...props.theme.typography.body_m,
color: props.theme.colors.white_200,
marginBottom: props.theme.space.xl,
}));
export const ErrorsText = 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 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};
`;

View File

@@ -21,108 +21,24 @@ import {
setShowSpamTokensAction,
setSip10ManageTokensAction,
} from '@stores/wallet/actions/actionCreators';
import { StyledP } from '@ui-library/common.styled';
import { TabItem } from '@ui-library/tabs';
import { SPAM_OPTIONS_WIDTH } from '@utils/constants';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import styled from 'styled-components';
import Theme from 'theme';
const TokenContainer = styled.div`
display: flex;
flex-direction: column;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
margin-bottom: ${(props) => props.theme.space.xl};
> *:not(:last-child) {
border-bottom: 1px solid ${(props) => props.theme.colors.elevation3};
}
`;
const Container = styled.div({
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
paddingLeft: 16,
paddingRight: 16,
height: '100%',
});
const ScrollableContainer = styled.div`
flex: 1;
overflow-y: auto;
`;
const FtInfoContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'row',
marginBottom: props.theme.spacing(8),
}));
const Button = styled.button<{
isSelected: boolean;
}>((props) => ({
...props.theme.typography.body_bold_l,
fontSize: 12,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: 31,
paddingLeft: props.theme.spacing(6),
paddingRight: props.theme.spacing(6),
marginRight: props.theme.spacing(2),
borderRadius: 44,
background: props.isSelected ? props.theme.colors.elevation3 : 'transparent',
color: props.theme.colors.white_0,
opacity: props.isSelected ? 1 : 0.6,
userSelect: 'none',
}));
const Header = styled.h1((props) => ({
...props.theme.typography.headline_xs,
marginBottom: props.theme.spacing(8),
}));
const Description = styled.h1((props) => ({
...props.theme.typography.body_m,
color: props.theme.colors.white_200,
marginBottom: props.theme.spacing(16),
}));
const ErrorsText = 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 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};
`;
import {
ButtonRow,
Container,
Description,
ErrorsText,
FtInfoContainer,
Header,
ScrollableContainer,
TokenContainer,
TokenText,
} from './index.styled';
function Stacks() {
const { hideStx } = useWalletSelector();
@@ -157,9 +73,7 @@ function ManageTokens() {
data.filter((ft) => ft.showToggle),
);
const [selectedProtocol, setSelectedProtocol] = useState<FungibleTokenProtocol>(
selectedAccount?.stxAddress ? 'stacks' : 'brc-20',
);
const [selectedProtocol, setSelectedProtocol] = useState<FungibleTokenProtocol>('runes');
const navigate = useNavigate();
const dispatch = useDispatch();
@@ -221,6 +135,7 @@ function ManageTokens() {
disabled={false}
toggled={toggled}
enabled={coin.isEnabled}
protocol={selectedProtocol}
/>
))}
{!coins.length && <ErrorsText>{t('NO_COINS')}</ErrorsText>}
@@ -258,26 +173,26 @@ function ManageTokens() {
<Header>{t('ADD_COINS')}</Header>
<Description>{t('DESCRIPTION')}</Description>
<FtInfoContainer>
{selectedAccount?.stxAddress && (
<Button
isSelected={selectedProtocol === 'stacks'}
onClick={() => setSelectedProtocol('stacks')}
>
SIP-10
</Button>
)}
<Button
isSelected={selectedProtocol === 'brc-20'}
onClick={() => setSelectedProtocol('brc-20')}
>
BRC-20
</Button>
<Button
isSelected={selectedProtocol === 'runes'}
<TabItem
$active={selectedProtocol === 'runes'}
onClick={() => setSelectedProtocol('runes')}
>
RUNES
</Button>
</TabItem>
<TabItem
$active={selectedProtocol === 'brc-20'}
onClick={() => setSelectedProtocol('brc-20')}
>
BRC-20
</TabItem>
{selectedAccount?.stxAddress && (
<TabItem
$active={selectedProtocol === 'stacks'}
onClick={() => setSelectedProtocol('stacks')}
>
STACKS
</TabItem>
)}
</FtInfoContainer>
<TokenContainer>{getCoinsList()}</TokenContainer>
</ScrollableContainer>

View File

@@ -32,10 +32,10 @@ function SendBtcScreen() {
const transactionContext = useTransactionContext(overridePaymentType);
const userCanSwitchPayType = useCanUserSwitchPaymentType();
const [recipientAddress, setRecipientAddress] = useState<string>('');
const [recipientAddress, setRecipientAddress] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [amountSats, setAmountSats] = useState<string>('');
const [amountSats, setAmountSats] = useState('');
const [feeRate, setFeeRate] = useState('');
const [sendMax, setSendMax] = useState(false);
@@ -137,7 +137,7 @@ function SendBtcScreen() {
txid: txnId,
currency: 'BTC',
error: '',
browserTx: isInOption,
browserTx: false,
},
});
} catch (e) {
@@ -147,7 +147,7 @@ function SendBtcScreen() {
txid: '',
currency: 'BTC',
error: `${e}`,
browserTx: isInOption,
browserTx: false,
},
});
} finally {

View File

@@ -163,7 +163,7 @@ function SendOrdinalScreen() {
txid: txnId,
currency: 'BTC',
error: '',
browserTx: isInOption,
browserTx: false,
},
});
@@ -180,7 +180,7 @@ function SendOrdinalScreen() {
txid: '',
currency: 'BTC',
error: `${e}`,
browserTx: isInOption,
browserTx: false,
},
});
} finally {

View File

@@ -170,7 +170,7 @@ function SendRuneScreen() {
txid: txnId,
currency: 'BTC',
error: '',
browserTx: isInOption,
browserTx: false,
},
});
} catch (e) {
@@ -180,7 +180,7 @@ function SendRuneScreen() {
txid: '',
currency: 'BTC',
error: `${e}`,
browserTx: isInOption,
browserTx: false,
},
});
} finally {

View File

@@ -206,7 +206,7 @@ function ChangeNetworkScreen() {
isSelected={formInputs.type === 'Signet'}
onNetworkSelected={onNetworkSelected}
disabled={isChangingNetwork}
showDivider={false}
showDivider
/>
<NetworkRow
network={savedRegtest || defaultRegtest}

View File

@@ -70,7 +70,7 @@ function Setting() {
<SettingComponent
text={t('NETWORK')}
onClick={openChangeNetworkScreen}
textDetail={network.type}
textDetail={network.type === 'Testnet' ? 'Testnet3' : network.type}
showDivider
/>

View File

@@ -18,7 +18,10 @@ const useFromTokens = (toToken?: FungibleToken) => {
const filteredTokens = toToken
? sortedTokens.filter(
(token) => token.principal !== toToken.ticker && toToken.protocol === token.protocol,
(token) =>
token.principal !== toToken.ticker &&
token.principal !== toToken.principal &&
toToken.protocol === token.protocol,
)
: sortedTokens;

View File

@@ -79,7 +79,8 @@ const ProtocolItem = styled.button<{ $selected: boolean }>`
const supportedProtocols: Protocol[] = ['runes', 'sip10']; // add more protocols here
const mapProtocolName = (protocol: Protocol) => {
if (protocol === 'sip10') return 'SIP-10';
if (protocol === 'runes') return 'Bitcoin';
if (protocol === 'sip10') return 'Stacks';
return protocol.toUpperCase();
};

View File

@@ -113,7 +113,7 @@ export default function QuoteSummary({
} = usePlaceUtxoOrder();
useEffect(() => {
if (true) {
if (placeOrderError || placeUtxoOrderError) {
onError(placeOrderError ?? placeUtxoOrderError ?? '');
}
}, [placeOrderError, placeUtxoOrderError]);
@@ -299,11 +299,9 @@ export default function QuoteSummary({
.toFixed(2);
})();
const showBadQuoteWarning =
quote.slippageSupported &&
new BigNumber(toTokenFiatValue).isLessThan(
new BigNumber(fromTokenFiatValue).multipliedBy(BAD_QUOTE_PERCENTAGE),
);
const showBadQuoteWarning = new BigNumber(toTokenFiatValue).isLessThan(
new BigNumber(fromTokenFiatValue).multipliedBy(BAD_QUOTE_PERCENTAGE),
);
const valueLossPercentage = new BigNumber(fromTokenFiatValue)
.minus(new BigNumber(toTokenFiatValue))
.dividedBy(new BigNumber(fromTokenFiatValue))
@@ -330,9 +328,13 @@ export default function QuoteSummary({
<CalloutContainer>
<Callout
titleText={t('SWAP_SCREEN.BAD_QUOTE_WARNING_TITLE')}
bodyText={t('SWAP_SCREEN.BAD_QUOTE_WARNING_DESC', {
percentage: valueLossPercentage,
})}
bodyText={
quote.slippageSupported && BigNumber(toTokenFiatValue).isGreaterThan(0)
? t('SWAP_SCREEN.BAD_QUOTE_WARNING_DESC', {
percentage: valueLossPercentage,
})
: t('SWAP_SCREEN.UNKNOWN_QUOTE_VALUE_WARNING_DESC')
}
variant="warning"
/>
</CalloutContainer>

View File

@@ -27,7 +27,8 @@ export default function UnlistRuneScreen() {
const { t } = useTranslation('translation', { keyPrefix: 'LIST_RUNE_SCREEN' });
const navigate = useNavigate();
const { runeId } = useParams();
const { data: runesCoinsList = [] } = useVisibleRuneFungibleTokens();
const { data: runesCoinsList = [], isLoading: isLoadingRunesCoinsList } =
useVisibleRuneFungibleTokens();
const selectedRune = runesCoinsList.find((ft) => ft.principal === runeId);
const showRunesListing =
useHasFeature(FeatureId.RUNES_LISTING) || process.env.NODE_ENV === 'development';
@@ -43,10 +44,10 @@ export default function UnlistRuneScreen() {
isInitialLoading: isFetchingListedItems,
isRefetching: isRefetchingListedItems,
refetch,
} = useRuneUtxosQueryPerMarketplace(selectedRune as FungibleToken, false);
} = useRuneUtxosQueryPerMarketplace(selectedRune, false);
const { listedItems } = data || {};
const isLoading = isFetchingListedItems || isRefetchingListedItems;
const isLoading = isFetchingListedItems || isRefetchingListedItems || isLoadingRunesCoinsList;
const showContent = !isLoading && !!listedItems?.length;
const handleGoBack = () =>

View File

@@ -1528,7 +1528,8 @@
"LIST_YOUR_RUNES": "List your Runes on a marketplace",
"SLIPPAGE_WARNING": "Your transaction may be frontrun and result in an unfavorable trade",
"BAD_QUOTE_WARNING_TITLE": "The quote you are receiving may result in significant value loss.",
"BAD_QUOTE_WARNING_DESC": "The minimum amount you will receive will result in a loss of over {{percentage}}% of your trades value. This is due to low liquidity in the pool, causing discrepancies in pricing. Please review the details carefully before proceeding with the swap."
"BAD_QUOTE_WARNING_DESC": "The minimum amount you will receive will result in a loss of over {{percentage}}% of your trades value. This is due to low liquidity in the pool, causing discrepancies in pricing. Please review the details carefully before proceeding with the swap.",
"UNKNOWN_QUOTE_VALUE_WARNING_DESC": "The market value of the token you will receive is unknown. Review details carefully before proceeding with the swap."
},
"SWAP_CONFIRM_SCREEN": {
"TOKEN_SWAP": "Token swap",

View File

@@ -1,12 +1,12 @@
{
"name": "Xverse Wallet",
"description": "A Bitcoin Wallet for Web3",
"name": "Xverse Wallet: Buy Bitcoin",
"description": "Ordinals, Runes, NFTs & DeFi",
"manifest_version": 3,
"options_page": "options.html",
"action": {
"default_popup": "popup.html",
"default_icon": "xverse_icon.png",
"default_title": "Xverse Wallet"
"default_title": "Xverse Wallet: Buy Bitcoin"
},
"chrome_url_overrides": {},
"background": {

View File

@@ -62,7 +62,7 @@ export default class Wallet {
readonly checkboxTokenInactive: Locator;
readonly buttonSip10: Locator;
readonly buttonStacks: Locator;
readonly buttonBRC20: Locator;
@@ -376,7 +376,7 @@ export default class Wallet {
this.navigationExplore = page.getByTestId('nav-explore');
this.navigationSettings = page.getByTestId('nav-settings');
// this.balance = page.getByTestId('total-balance-value');
this.balance = page.getByLabel(/^Total balance:/);
this.balance = page.getByLabel(/^Total balance/);
this.textCurrency = page.getByTestId('currency-text');
this.allUpperButtons = page.getByTestId('transaction-buttons-row').getByRole('button');
this.buttonTransactionSend = this.allUpperButtons.nth(0);
@@ -439,7 +439,7 @@ export default class Wallet {
this.checkboxToken = page.locator('label[role="checkbox"]');
this.checkboxTokenActive = page.locator('label[role="checkbox"][aria-checked="true"]');
this.checkboxTokenInactive = page.locator('label[role="checkbox"][aria-checked="false"]');
this.buttonSip10 = page.getByRole('button', { name: 'SIP-10' });
this.buttonStacks = page.getByRole('button', { name: 'STACKS' });
this.buttonBRC20 = page.getByRole('button', { name: 'BRC-20' });
this.buttonRunes = page.getByRole('button', { name: 'RUNES' });
this.headingTokens = page.getByRole('heading', { name: 'Manage tokens' });
@@ -1163,13 +1163,15 @@ export default class Wallet {
return totalBalance;
}
async selectLastToken(tokenType: 'BRC20' | 'SIP10'): Promise<string> {
async selectLastToken(tokenType: 'BRC20' | 'STACKS'): Promise<string> {
await this.manageTokenButton.click();
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();
} else if (tokenType === 'STACKS') {
await this.buttonStacks.click();
}
const chosenToken = this.divTokenRow.last();

View File

@@ -15,14 +15,14 @@ test.describe('Token Management', () => {
await wallet.manageTokenButton.click();
expect(page.url()).toContain('manage-tokens');
await expect(wallet.buttonBack).toBeVisible();
await expect(wallet.buttonSip10).toBeVisible();
await expect(wallet.buttonStacks).toBeVisible();
await expect(wallet.buttonBRC20).toBeVisible();
await expect(wallet.buttonRunes).toBeVisible();
await expect(wallet.headingTokens).toBeVisible();
// Check SIP10 token tab - only Stacks and sBTC should be showing when user has no sip10 balances
await test.step('Check SIP10 token tab', async () => {
await wallet.buttonSip10.click();
// Check STACKS tokens tab - only Stacks and sBTC should be showing when user has no sip10 balances
await test.step('Check STACKS tokens tab', async () => {
await wallet.buttonStacks.click();
await expect(wallet.labelCoinTitle).toHaveCount(2);
await expect(wallet.checkboxToken).toHaveCount(2);
await expect(wallet.checkboxTokenActive).toHaveCount(2);
@@ -88,7 +88,7 @@ test.describe('Token Management', () => {
await test.step('Toggle a random token', async () => {
await wallet.manageTokenButton.click();
await wallet.buttonSip10.click();
await wallet.buttonStacks.click();
// NOTE: requires an account with at least 1 sip10 token with balance
await expect(wallet.checkboxTokenActive.first()).toBeVisible();
@@ -106,7 +106,7 @@ test.describe('Token Management', () => {
// enable the token again
await wallet.manageTokenButton.click();
await wallet.buttonSip10.click();
await wallet.buttonStacks.click();
await page.getByTestId(tokenName).locator('label').click();
// expect to be visible again on dashboard

View File

@@ -43,7 +43,7 @@ test.describe('Swap Flow Visuals', () => {
// Select the second Coin
await wallet.buttonDownArrow.nth(1).click();
// Had problems with loading of all tokens so I check that a 'DOG' is loaded
await expect(page.getByText('DOG').first()).toBeVisible();
await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0);
@@ -87,7 +87,7 @@ test.describe('Swap Flow Visuals', () => {
// Select the second Coin
await wallet.buttonDownArrow.nth(1).click();
// Had problems with loading of all tokens so I check that a 'DOG' is loaded
await expect(page.getByText('DOG').first()).toBeVisible();
await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0);

View File

@@ -5,7 +5,7 @@ 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.selectLastToken('SIP10');
const tokenName = await wallet.selectLastToken('STACKS');
await wallet.clickOnSpecificToken(tokenName);