diff --git a/.github/workflows/code-checks.yml b/.github/workflows/code-checks.yml index 1c89d17fe69..ecd3fa273f4 100644 --- a/.github/workflows/code-checks.yml +++ b/.github/workflows/code-checks.yml @@ -116,9 +116,6 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/provision - - name: Build - run: yarn build - - name: Test run: yarn test:unit diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 2417ee7942e..fef3c68d177 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -13,8 +13,8 @@ jobs: - uses: ./.github/actions/provision - - name: Build prod - run: yarn build + - name: Build + run: yarn test:coverage - name: Make badge maker run: npx make-coverage-badge diff --git a/README.md b/README.md index 37dd243abc8..4059843a6b3 100755 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Leather [![Chrome Web Store](https://img.shields.io/chrome-web-store/stars/ldinpeekobnhjjdofggfgjlcehhmanlj?label=Chrome%20Web%20Store)](https://chrome.google.com/webstore/detail/stacks-wallet/ldinpeekobnhjjdofggfgjlcehhmanlj) -[![Mozilla Add-on](https://img.shields.io/amo/stars/hiro-wallet?label=Firefox%20Add-on)](https://addons.mozilla.org/en-US/firefox/addon/hiro-wallet/) -[![coverage](https://raw.githubusercontent.com/hirosystems/wallet/gh-pages/badge.svg)](https://hirosystems.github.io/wallet/) +[![Mozilla Add-on](https://img.shields.io/amo/stars/leather-wallet?label=Firefox%20Add-on)](https://addons.mozilla.org/en-US/firefox/addon/leather-wallet/) +[![coverage](https://raw.githubusercontent.com/leather-wallet/extension/gh-pages/badge.svg)](https://leather-wallet.github.io/extension/) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) Leather is the most popular and trusted wallet for apps built on Bitcoin. Connect to apps and manage assets secured by Bitcoin and Bitcoin L2s with battle-tested wallet for the Stacks blockchain. @@ -66,9 +66,32 @@ yarn playwright install --with-deps Note that the installed browsers are tied to the version of Playwright being used, and it may be necessary to run the above command again in some situations, such as when upgrading Playwright or switching branches. [Read the documentation for more information](https://playwright.dev/docs/cli#install-system-dependencies). +### Integration tests + +All integration tests can be run using: + +```bash +yarn test:integration +``` + +To run a suite of tests you can use: + +```bash +yarn playwright test specs/TEST.spec.ts +yarn playwright test tests/specs --shard=3/8 +``` + +### Unit tests + +Unit tests can be run with vitest using: + +```bash +yarn test:unit +``` + ## Production -[See instructions on Hiro.so for installing from source for production usage.](https://www.hiro.so/wallet/install-web-source) +[See instructions on Leather.io to install from source](https://leather.io/install-extension) Alternatively, the following steps can be taken by _technical_ users with the latest version of node installed on their machines. @@ -98,13 +121,13 @@ We consider the security of our systems a top priority. But no matter how much e If you discover a security vulnerability, please use one of the following means of communications to report it to us: -- Report the security issue to our [HackerOne program](https://hackerone.com/hiro) -- Report the security issue directly at [security@hiro.so](mailto:security@hiro.so) +- Report the security issue to our [HackerOne program](https://hackerone.com/leather_wallet) +- Report the security issue directly at [security@leather.io](mailto:security@leather.io) Please note this email is strictly for reporting security vulnerabilities. For support queries, contact [contact@leather.io](mailto:contact@leather.io). Your efforts to responsibly disclose your findings are sincerely appreciated and will be taken into account to acknowledge your contributions. ### Audit Report -In Q1 2021, Hiro partnered with [Least Authority](https://leastauthority.com/), a leading security consultancy with experience in the crypto space, to audit Leather. On April 29th 2021, after addressing the major concerns described in the initial findings, as well as a concluding sign off from the Least Authority team, a final report was delivered. +In Q1 2021, Leather partnered with [Least Authority](https://leastauthority.com/), a leading security consultancy with experience in the crypto space, to audit Leather. On April 29th 2021, after addressing the major concerns described in the initial findings, as well as a concluding sign off from the Least Authority team, a final report was delivered. -[Download and read the full report here](https://github.com/hirosystems/wallet/blob/main/public/docs/least-authority-security-audit-report.pdf) +[Download and read the full report here](https://github.com/leather-wallet/extension/blob/main/public/docs/least-authority-security-audit-report.pdf) diff --git a/package.json b/package.json index 170e65956f1..64c521ba330 100644 --- a/package.json +++ b/package.json @@ -24,15 +24,14 @@ "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix", "lint:prettier": "prettier --check \"{src,tests}/**/*.{ts,tsx}\" \"*.{js,json}\"", "lint:prettier:fix": "prettier --write \"{src,tests}/**/*.{ts,tsx}\" *.js", - "lint:unused-exports": "ts-unused-exports tsconfig.json --ignoreFiles=leather-styles", + "lint:unused-exports": "ts-unused-exports tsconfig.json --ignoreFiles=leather-styles --ignoreFiles=tests", "lint:remote-wallet-config": "npx ajv-cli validate -s config/wallet-config.schema.json -d config/wallet-config.json", "lint:deps": "dependency-cruise --config .dependency-cruiser.js \"src/**/*.{ts,tsx}\"", "prod:ext": "yarn build", "prod:analyze": "cross-env WALLET_ENVIRONMENT=production ANALYZE=true webpack -p", "test:integration": "playwright test", "test:unit": "vitest run", - "test:coverage": "WALLET_ENVIRONMENT=testing jest --collect-coverage", - "test:watch": "WALLET_ENVIRONMENT=testing jest --watch", + "test:coverage": "vitest run --coverage", "typecheck": "tsc --noEmit", "prepare": "panda codegen" }, @@ -160,13 +159,13 @@ "@stacks/wallet-sdk": "6.9.0", "@stitches/react": "1.2.8", "@styled-system/theme-get": "5.1.2", - "@tanstack/query-sync-storage-persister": "4.35.3", - "@tanstack/react-query": "4.35.3", - "@tanstack/react-query-devtools": "4.35.3", - "@tanstack/react-query-persist-client": "4.35.5", + "@tanstack/query-sync-storage-persister": "4.35.7", + "@tanstack/react-query": "4.35.7", + "@tanstack/react-query-devtools": "4.35.7", + "@tanstack/react-query-persist-client": "4.35.7", "@tippyjs/react": "4.2.6", "@types/lodash.uniqby": "4.7.7", - "@typescript-eslint/eslint-plugin": "6.7.3", + "@typescript-eslint/eslint-plugin": "6.7.4", "@vkontakte/vk-qr": "2.0.13", "@zondax/ledger-stacks": "1.0.4", "are-passive-events-supported": "1.1.1", @@ -188,7 +187,7 @@ "dotenv": "16.3.1", "ecdsa-sig-formatter": "1.0.11", "ecpair": "2.1.0", - "formik": "2.4.3", + "formik": "2.4.5", "jotai": "2.2.1", "jotai-redux": "0.2.1", "jsontokens": "4.0.1", @@ -199,8 +198,8 @@ "mdi-react": "9.2.0", "micro-packed": "0.3.2", "object-hash": "3.0.0", - "observable-hooks": "4.2.2", - "pino": "8.15.1", + "observable-hooks": "4.2.3", + "pino": "8.15.4", "postcss-preset-env": "9.1.4", "prismjs": "1.29.0", "promise-memoize": "1.2.1", @@ -215,7 +214,7 @@ "react-icons": "4.10.1", "react-intersection-observer": "9.5.2", "react-lottie": "1.2.3", - "react-redux": "8.1.2", + "react-redux": "8.1.3", "react-router-dom": "6.16.0", "react-virtuoso": "4.6.0", "redux-persist": "6.0.0", @@ -229,7 +228,7 @@ "valid-url": "1.0.9", "varuint-bitcoin": "1.1.2", "webextension-polyfill": "0.10.0", - "yup": "1.3.1", + "yup": "1.3.2", "zxcvbn": "4.4.2" }, "devDependencies": { @@ -239,7 +238,7 @@ "@babel/preset-typescript": "7.23.0", "@btckit/types": "0.0.19", "@ls-lint/ls-lint": "2.1.0", - "@pandacss/dev": "0.15.3", + "@pandacss/dev": "0.15.4", "@playwright/test": "1.38.1", "@pmmmwh/react-refresh-webpack-plugin": "0.5.11", "@redux-devtools/cli": "3.0.2", @@ -258,13 +257,13 @@ "@types/html-webpack-plugin": "3.2.7", "@types/jsdom": "21.1.3", "@types/lodash.get": "4.4.7", - "@types/node": "20.7.1", + "@types/node": "20.8.2", "@types/object-hash": "3.0.4", "@types/prismjs": "1.26.1", "@types/promise-memoize": "1.2.2", "@types/punycode": "2.1.0", - "@types/qrcode.react": "1.0.2", - "@types/react": "18.2.23", + "@types/qrcode.react": "1.0.3", + "@types/react": "18.2.24", "@types/react-dom": "18.2.8", "@types/react-lottie": "1.2.7", "@types/react-router-dom": "5.3.3", @@ -274,24 +273,23 @@ "@types/webextension-polyfill": "0.10.4", "@types/webpack": "5.28.3", "@types/zxcvbn": "4.4.2", - "@typescript-eslint/parser": "6.7.3", + "@typescript-eslint/parser": "6.7.4", "@vitest/coverage-istanbul": "0.34.6", "audit-ci": "6.6.1", "babel-loader": "9.1.3", "base64-loader": "1.0.0", "bip32": "4.0.0", - "bip39": "3.1.0", "blns": "2.0.4", - "browserslist": "4.22.0", + "browserslist": "4.22.1", "chrome-webstore-upload-cli": "2.2.2", "clean-webpack-plugin": "4.0.0", "concurrently": "8.0.1", - "conventional-changelog-conventionalcommits": "6.1.0", + "conventional-changelog-conventionalcommits": "7.0.2", "copy-webpack-plugin": "11.0.0", "cross-env": "7.0.3", "crypto-browserify": "3.12.0", "deepmerge": "4.3.1", - "dependency-cruiser": "14.0.0", + "dependency-cruiser": "14.1.0", "dotenv-webpack": "8.0.1", "esbuild": "0.19.4", "esbuild-loader": "4.0.2", @@ -314,13 +312,13 @@ "stream-browserify": "3.0.0", "svg-url-loader": "8.0.0", "ts-node": "10.9.1", - "ts-unused-exports": "7.0.3", + "ts-unused-exports": "10.0.1", "tsconfig-paths-webpack-plugin": "4.1.0", "typescript": "5.2.2", "vitest": "0.34.6", "vm-browserify": "1.1.2", "web-ext": "7.8.0", - "web-ext-submit": "7.7.0", + "web-ext-submit": "7.8.0", "webpack": "5.88.2", "webpack-bundle-analyzer": "4.9.1", "webpack-cli": "5.1.4", diff --git a/src/app/common/form-utils.ts b/src/app/common/form-utils.ts index 028449ad501..007185edf59 100644 --- a/src/app/common/form-utils.ts +++ b/src/app/common/form-utils.ts @@ -1,6 +1,6 @@ import { useField, useFormikContext } from 'formik'; -function useIsFieldDirty(name: string) { +export function useIsFieldDirty(name: string) { const [field, meta] = useField(name); return field.value !== meta.initialValue; } diff --git a/src/app/common/hooks/balance/btc/use-btc-balance.ts b/src/app/common/hooks/balance/btc/use-btc-balance.ts index 9019aad28e5..8c8f5deec8b 100644 --- a/src/app/common/hooks/balance/btc/use-btc-balance.ts +++ b/src/app/common/hooks/balance/btc/use-btc-balance.ts @@ -8,7 +8,7 @@ import { baseCurrencyAmountInQuote, subtractMoney } from '@app/common/money/calc import { i18nFormatCurrency } from '@app/common/money/format-money'; import { createBitcoinCryptoCurrencyAssetTypeWrapper } from '@app/query/bitcoin/address/address.utils'; import { useBitcoinPendingTransactionsBalance } from '@app/query/bitcoin/address/transactions-by-address.hooks'; -import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query'; +import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/btc-native-segwit-balance.hooks'; import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks'; export function useBtcAssetBalance(btcAddress: string) { diff --git a/src/app/common/hooks/use-transferable-asset-balances.hooks.ts b/src/app/common/hooks/use-transferable-asset-balances.hooks.ts index 25d266935e8..dac2ffdd232 100644 --- a/src/app/common/hooks/use-transferable-asset-balances.hooks.ts +++ b/src/app/common/hooks/use-transferable-asset-balances.hooks.ts @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import type { AllTransferableCryptoAssetBalances } from '@shared/models/crypto-asset-balance.model'; -import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query'; +import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/btc-native-segwit-balance.hooks'; import { useTransferableStacksFungibleTokenAssetBalances } from '@app/query/stacks/balance/stacks-ft-balances.hooks'; import { createStacksCryptoCurrencyAssetTypeWrapper } from '@app/query/stacks/balance/stacks-ft-balances.utils'; import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; diff --git a/src/app/components/account/account-list-item-layout.tsx b/src/app/components/account/account-list-item-layout.tsx index 106d2f615a0..3c6ed650835 100644 --- a/src/app/components/account/account-list-item-layout.tsx +++ b/src/app/components/account/account-list-item-layout.tsx @@ -1,8 +1,10 @@ -import { Flex, Spinner, Stack, StackProps, color, useMediaQuery } from '@stacks/ui'; +import { Spinner } from '@stacks/ui'; import { truncateMiddle } from '@stacks/ui-utils'; import { SettingsSelectors } from '@tests/selectors/settings.selectors'; -import { HStack, styled } from 'leather-styles/jsx'; +import { Flex, HStack, Stack, StackProps, styled } from 'leather-styles/jsx'; +import { token } from 'leather-styles/tokens'; +import { useViewportMinWidth } from '@app/common/hooks/use-media-query'; import { useConfigBitcoinEnabled } from '@app/query/common/remote-config/remote-config.query'; import { CaptionDotSeparator } from '../caption-dot-separator'; @@ -41,7 +43,7 @@ export function AccountListItemLayout(props: AccountListItemLayoutProps) { ...rest } = props; - const [isNarrowViewport] = useMediaQuery('(max-width: 400px)'); + const isBreakpointSm = useViewportMinWidth('sm'); const isBitcoinEnabled = useConfigBitcoinEnabled(); return ( @@ -54,35 +56,35 @@ export function AccountListItemLayout(props: AccountListItemLayoutProps) { onClick={onSelectAccount} {...rest} > - - + + - + {accountName} {isActive && } - + {isLoading ? ( ) : ( balanceLabel )} - + - {truncateMiddle(stxAddress, isNarrowViewport ? 3 : 4)} + {truncateMiddle(stxAddress, isBreakpointSm ? 4 : 3)} {isBitcoinEnabled && ( {truncateMiddle(btcAddress, 5)} )} - + {children} diff --git a/src/app/components/balance-btc.tsx b/src/app/components/balance-btc.tsx index 275a2bca7cf..db36dfb72dd 100644 --- a/src/app/components/balance-btc.tsx +++ b/src/app/components/balance-btc.tsx @@ -1,6 +1,6 @@ import { formatMoney } from '@app/common/money/format-money'; import { Caption } from '@app/components/typography'; -import { useCurrentNativeSegwitAddressBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query'; +import { useCurrentNativeSegwitAddressBalance } from '@app/query/bitcoin/balance/btc-native-segwit-balance.hooks'; export function BtcBalance() { const balance = useCurrentNativeSegwitAddressBalance(); diff --git a/src/app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee.tsx b/src/app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee.tsx index 699d9d004ee..20836a18cf1 100644 --- a/src/app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee.tsx +++ b/src/app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee.tsx @@ -8,8 +8,8 @@ import { determineUtxosForSpend, determineUtxosForSpendAll, } from '@app/common/transactions/bitcoin/coinselect/local-coin-selection'; -import { useSpendableCurrentNativeSegwitAccountUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks'; -import { useCurrentNativeSegwitAddressBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query'; +import { useCurrentNativeSegwitAccountSpendableUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks'; +import { useCurrentNativeSegwitAddressBalance } from '@app/query/bitcoin/balance/btc-native-segwit-balance.hooks'; import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks'; export const MAX_FEE_RATE_MULTIPLIER = 50; @@ -21,7 +21,7 @@ interface UseBitcoinCustomFeeArgs { } export function useBitcoinCustomFee({ amount, isSendingMax, recipient }: UseBitcoinCustomFeeArgs) { const balance = useCurrentNativeSegwitAddressBalance(); - const { data: utxos = [] } = useSpendableCurrentNativeSegwitAccountUtxos(); + const { data: utxos = [] } = useCurrentNativeSegwitAccountSpendableUtxos(); const btcMarketData = useCryptoCurrencyMarketData('BTC'); return useCallback( diff --git a/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list.ts b/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list.ts index 252758f633c..66826997a3a 100644 --- a/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list.ts +++ b/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list.ts @@ -11,7 +11,7 @@ import { determineUtxosForSpend, determineUtxosForSpendAll, } from '@app/common/transactions/bitcoin/coinselect/local-coin-selection'; -import { useCurrentNativeSegwitAddressBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query'; +import { useCurrentNativeSegwitAddressBalance } from '@app/query/bitcoin/balance/btc-native-segwit-balance.hooks'; import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client'; import { useAverageBitcoinFeeRates } from '@app/query/bitcoin/fees/fee-estimates.hooks'; import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks'; diff --git a/src/app/components/bitcoin-transaction-item/bitcoin-transaction-item.tsx b/src/app/components/bitcoin-transaction-item/bitcoin-transaction-item.tsx index 234c0b03fe3..33ca25356e3 100644 --- a/src/app/components/bitcoin-transaction-item/bitcoin-transaction-item.tsx +++ b/src/app/components/bitcoin-transaction-item/bitcoin-transaction-item.tsx @@ -22,7 +22,7 @@ import { convertInscriptionToSupportedInscriptionType, createInscriptionInfoUrl, } from '@app/query/bitcoin/ordinals/inscription.hooks'; -import { useGetInscriptionsByOutputQuery } from '@app/query/bitcoin/ordinals/use-inscription-by-output.query'; +import { useGetInscriptionsByOutputQuery } from '@app/query/bitcoin/ordinals/inscriptions-by-param.query'; import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { CaptionDotSeparator } from '../caption-dot-separator'; diff --git a/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx b/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx index 25304ba105f..3233dfeb23b 100644 --- a/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx +++ b/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx @@ -5,7 +5,7 @@ import { noop } from '@shared/utils'; import { Brc20TokenAssetItem } from '@app/components/crypto-assets/bitcoin/brc20-token-asset-list/components/brc20-token-asset-item'; import { Tooltip } from '@app/components/tooltip'; -import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query'; +import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/btc-native-segwit-balance.hooks'; import { Brc20Token } from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.query'; import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; diff --git a/src/app/components/inscription-preview-card/components/inscription-text.tsx b/src/app/components/inscription-preview-card/components/inscription-text.tsx index 42a2b218b5c..f0c97d11625 100644 --- a/src/app/components/inscription-preview-card/components/inscription-text.tsx +++ b/src/app/components/inscription-preview-card/components/inscription-text.tsx @@ -2,13 +2,13 @@ import { Box, Spinner, Text } from '@stacks/ui'; import { sanitize } from 'dompurify'; import { figmaTheme } from '@app/common/utils/figma-theme'; -import { useTextInscriptionContentQuery } from '@app/query/bitcoin/ordinals/use-text-ordinal-content.query'; +import { useInscriptionTextContentQuery } from '@app/query/bitcoin/ordinals/inscription-text-content.query'; interface InscriptionTextProps { contentSrc: string; } export function InscriptionText(props: InscriptionTextProps) { - const query = useTextInscriptionContentQuery(props.contentSrc); + const query = useInscriptionTextContentQuery(props.contentSrc); if (query.isLoading) return ; diff --git a/src/app/components/page-title.tsx b/src/app/components/page-title.tsx deleted file mode 100644 index df1be844d25..00000000000 --- a/src/app/components/page-title.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { ReactNode } from 'react'; - -import { BoxProps } from '@stacks/ui'; - -import { Title } from '@app/components/typography'; - -interface PageTitleProps extends BoxProps { - children?: ReactNode; - isHeadline?: boolean; -} -export function PageTitle(props: PageTitleProps) { - const { children, isHeadline, ...rest } = props; - - return ( - - {children} - - ); -} diff --git a/src/app/components/request-password.tsx b/src/app/components/request-password.tsx index 2b87cd60651..d1e36ed7031 100644 --- a/src/app/components/request-password.tsx +++ b/src/app/components/request-password.tsx @@ -1,8 +1,8 @@ import { FormEvent, useCallback, useState } from 'react'; -import { Box, Input, Stack } from '@stacks/ui'; +import { Input } from '@stacks/ui'; import { SettingsSelectors } from '@tests/selectors/settings.selectors'; -import { styled } from 'leather-styles/jsx'; +import { Box, Stack, styled } from 'leather-styles/jsx'; import { token } from 'leather-styles/tokens'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; @@ -52,13 +52,19 @@ export function RequestPassword({ title, caption, onSuccess }: RequestPasswordPr }, [analytics, startWaitingMessage, stopWaitingMessage, unlockWallet, password, onSuccess]); return ( - <> - {title} + + {title} {(isRunning && waitingMessage) || caption} - + Continue - + ); } diff --git a/src/app/components/secret-key/mnemonic-key/mnemonic-input-field.tsx b/src/app/components/secret-key/mnemonic-key/mnemonic-input-field.tsx new file mode 100644 index 00000000000..64de9f7be9a --- /dev/null +++ b/src/app/components/secret-key/mnemonic-key/mnemonic-input-field.tsx @@ -0,0 +1,98 @@ +import { useState } from 'react'; + +import { TextField } from '@radix-ui/themes'; +import { useField } from 'formik'; +import { css } from 'leather-styles/css'; +import { FlexProps, styled } from 'leather-styles/jsx'; + +import { useIsFieldDirty } from '@app/common/form-utils'; + +interface InputFieldProps extends FlexProps { + dataTestId?: string; + name: string; + value: string; + onPaste: (e: React.ClipboardEvent) => void; + onUpdateWord: (word: string) => void; + hasError?: boolean; + wordlist: string[]; +} + +const psuedoBorderStyles = { + content: '""', + zIndex: 1, + top: 0, + right: 0, + bottom: 0, + left: 0, + background: 'transparent', + position: 'absolute', + border: '1px solid', + borderRadius: '4px', +}; + +export function InputField({ dataTestId, name, onPaste, onChange, value }: InputFieldProps) { + const [field, meta] = useField(name); + const [isFocused, setIsFocused] = useState(false); + const isDirty = useIsFieldDirty(name); + + return ( + + + {/* // FIXME #4130: - update this color when available in design system */} + {`${name}.`} + + setIsFocused(!isFocused)} + {...field} + value={value || field.value || ''} + // using onChangeCapture + onBlurCapture to keep Formik validation + onChangeCapture={onChange} + onBlurCapture={() => setIsFocused(!isFocused)} + onPaste={onPaste} + /> + + ); +} diff --git a/src/app/components/secret-key/mnemonic-key/mnemonic-word-input.tsx b/src/app/components/secret-key/mnemonic-key/mnemonic-word-input.tsx new file mode 100644 index 00000000000..f06ec9fa1d5 --- /dev/null +++ b/src/app/components/secret-key/mnemonic-key/mnemonic-word-input.tsx @@ -0,0 +1,43 @@ +import { wordlist } from '@scure/bip39/wordlists/english'; + +import { extractPhraseFromString } from '@app/common/utils'; + +import { InputField } from './mnemonic-input-field'; + +interface MnemonicWordInputProps { + fieldNumber: number; + value: string; + onUpdateWord(word: string): void; + onPasteEntireKey(word: string): void; +} +export function MnemonicWordInput({ + fieldNumber, + value, + onUpdateWord, + onPasteEntireKey, +}: MnemonicWordInputProps) { + return ( + { + const pasteValue = extractPhraseFromString(e.clipboardData.getData('text')); + if (pasteValue.includes(' ')) { + e.preventDefault(); + //assume its a full key + onPasteEntireKey(pasteValue); + } + }} + onChange={(e: any) => { + e.preventDefault(); + onUpdateWord(e.target.value); + }} + wordlist={wordlist} + value={value} + height="3rem" + /> + ); +} diff --git a/src/app/components/secret-key/mnemonic-key/utils/error-handling.ts b/src/app/components/secret-key/mnemonic-key/utils/error-handling.ts new file mode 100644 index 00000000000..978777f271a --- /dev/null +++ b/src/app/components/secret-key/mnemonic-key/utils/error-handling.ts @@ -0,0 +1,26 @@ +import { FormikErrors, FormikTouched, FormikValues } from 'formik'; + +export function hasMnemonicFormValues(values: FormikValues): boolean { + return Object.values(values).filter(value => value !== '').length > 0; +} + +export function getMnemonicErrorMessage(mnemonicErrorFields: string[]): string { + return mnemonicErrorFields.length > 1 + ? `Words ${mnemonicErrorFields + .toString() + .replace(/,(?=[^,]+$)/, ' and')} are incorrect or misspelled` // replace last , with 'and' + : `Word ${mnemonicErrorFields} is incorrect or misspelled`; +} + +export function getMnemonicErrorFields( + errors: FormikErrors, + touched: FormikTouched, + values: FormikValues +): string[] { + return Object.keys(errors) + .filter( + fieldNumber => fieldNumber !== undefined && touched[fieldNumber] && values[fieldNumber] !== '' + ) // make sure field is touched and not empty + .sort((a, b) => +a - +b) // sort numerically for display + .map(fieldNumber => ` ${fieldNumber}`); // prepend space to space them out +} diff --git a/src/app/components/secret-key/mnemonic-key/utils/validation.ts b/src/app/components/secret-key/mnemonic-key/utils/validation.ts new file mode 100644 index 00000000000..381b2904908 --- /dev/null +++ b/src/app/components/secret-key/mnemonic-key/utils/validation.ts @@ -0,0 +1,23 @@ +import { wordlist } from '@scure/bip39/wordlists/english'; +import * as yup from 'yup'; + +function isValidMnemonic(word: string): boolean { + return wordlist.includes(word); +} + +// YUP validation of dynamic field names => https://github.com/jquense/yup/issues/130 +function mapRules(map: Record, rule: T): Record { + return Object.keys(map).reduce((newMap, key) => ({ ...newMap, [key]: rule }), {}); +} + +// field names are dynamically generated +const dynamicObjectValue = yup.string().test({ + test(value) { + if (!value) return false; + return isValidMnemonic(value); + }, +}); + +export const validationSchema = yup.lazy(map => + yup.object(mapRules(map, dynamicObjectValue)) +); diff --git a/src/app/components/secret-key/secret-key-grid.tsx b/src/app/components/secret-key/secret-key-grid.tsx new file mode 100644 index 00000000000..c791d7acd1c --- /dev/null +++ b/src/app/components/secret-key/secret-key-grid.tsx @@ -0,0 +1,24 @@ +import { Grid, Stack } from 'leather-styles/jsx'; + +interface SecretKeyGridProps { + children: React.ReactNode; +} +export function SecretKeyGrid({ children }: SecretKeyGridProps) { + return ( + + + {children} + + + ); +} diff --git a/src/app/components/secret-key/two-column.layout.tsx b/src/app/components/secret-key/two-column.layout.tsx new file mode 100644 index 00000000000..92a41198954 --- /dev/null +++ b/src/app/components/secret-key/two-column.layout.tsx @@ -0,0 +1,54 @@ +import { Flex, Stack, styled } from 'leather-styles/jsx'; + +interface TwoColumnLayoutProps { + leftColumn: React.JSX.Element; + rightColumn: React.JSX.Element; +} + +export function TwoColumnLayout({ + leftColumn, + rightColumn, +}: TwoColumnLayoutProps): React.JSX.Element { + return ( + + + {leftColumn} + + + + + {rightColumn} + + + + ); +} diff --git a/src/app/components/transaction/transaction-title.tsx b/src/app/components/transaction/transaction-title.tsx index 28967d83b7b..d535beb1dc5 100644 --- a/src/app/components/transaction/transaction-title.tsx +++ b/src/app/components/transaction/transaction-title.tsx @@ -1,6 +1,7 @@ import { useRef, useState } from 'react'; import { useOnResizeListener } from '@app/common/hooks/use-on-resize-listener'; +import { spamFilter } from '@app/common/utils/spam-filter'; import { Tooltip } from '@app/components/tooltip'; import { Title } from '@app/components/typography'; @@ -27,7 +28,7 @@ export function TransactionTitle(props: TransactionTitleProps) { textOverflow="ellipsis" whiteSpace="nowrap" > - {title} + {spamFilter(title)} ); diff --git a/src/app/features/activity-list/activity-list.tsx b/src/app/features/activity-list/activity-list.tsx index 97679936285..a8bde52abc6 100644 --- a/src/app/features/activity-list/activity-list.tsx +++ b/src/app/features/activity-list/activity-list.tsx @@ -5,10 +5,10 @@ import uniqby from 'lodash.uniqby'; import { LoadingSpinner } from '@app/components/loading-spinner'; import { useBitcoinPendingTransactions } from '@app/query/bitcoin/address/transactions-by-address.hooks'; import { useGetBitcoinTransactionsByAddressesQuery } from '@app/query/bitcoin/address/transactions-by-address.query'; -import { useZeroIndexTaprootAddress } from '@app/query/bitcoin/ordinals/use-zero-index-taproot-address'; import { useConfigBitcoinEnabled } from '@app/query/common/remote-config/remote-config.query'; import { useStacksPendingTransactions } from '@app/query/stacks/mempool/mempool.hooks'; import { useGetAccountTransactionsWithTransfersQuery } from '@app/query/stacks/transactions/transactions-with-transfers.query'; +import { useZeroIndexTaprootAddress } from '@app/store/accounts/blockchain/bitcoin/bitcoin.hooks'; import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useSubmittedTransactions } from '@app/store/submitted-transactions/submitted-transactions.selectors'; diff --git a/src/app/features/bitcoin-choose-fee/bitcoin-choose-fee.tsx b/src/app/features/bitcoin-choose-fee/bitcoin-choose-fee.tsx index b2b9e5593ff..27a1367d8b8 100644 --- a/src/app/features/bitcoin-choose-fee/bitcoin-choose-fee.tsx +++ b/src/app/features/bitcoin-choose-fee/bitcoin-choose-fee.tsx @@ -11,7 +11,7 @@ import { AvailableBalance } from '@app/components/available-balance'; import { BitcoinCustomFee } from '@app/components/bitcoin-custom-fee/bitcoin-custom-fee'; import { MAX_FEE_RATE_MULTIPLIER } from '@app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee'; import { OnChooseFeeArgs } from '@app/components/bitcoin-fees-list/bitcoin-fees-list'; -import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query'; +import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/btc-native-segwit-balance.hooks'; import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { BitcoinChooseFeeLayout } from './components/bitcoin-choose-fee.layout'; diff --git a/src/app/features/bitcoin-choose-fee/hooks/use-validate-bitcoin-spend.ts b/src/app/features/bitcoin-choose-fee/hooks/use-validate-bitcoin-spend.ts index bbeba0f602b..526de3835bd 100644 --- a/src/app/features/bitcoin-choose-fee/hooks/use-validate-bitcoin-spend.ts +++ b/src/app/features/bitcoin-choose-fee/hooks/use-validate-bitcoin-spend.ts @@ -3,7 +3,7 @@ import { useState } from 'react'; import { Money, createMoney } from '@shared/models/money.model'; import { subtractMoney, sumMoney } from '@app/common/money/calculate-money'; -import { useCurrentNativeSegwitAddressBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query'; +import { useCurrentNativeSegwitAddressBalance } from '@app/query/bitcoin/balance/btc-native-segwit-balance.hooks'; export function useValidateBitcoinSpend(amount?: Money, isSendingMax?: boolean) { const [showInsufficientBalanceError, setShowInsufficientBalanceError] = useState(false); diff --git a/src/app/features/collectibles/components/bitcoin/inscription-text.tsx b/src/app/features/collectibles/components/bitcoin/inscription-text.tsx index 23a9d0d7242..b3342a8dd9d 100644 --- a/src/app/features/collectibles/components/bitcoin/inscription-text.tsx +++ b/src/app/features/collectibles/components/bitcoin/inscription-text.tsx @@ -1,5 +1,5 @@ import { OrdinalMinimalIcon } from '@app/components/icons/ordinal-minimal-icon'; -import { useTextInscriptionContentQuery } from '@app/query/bitcoin/ordinals/use-text-ordinal-content.query'; +import { useInscriptionTextContentQuery } from '@app/query/bitcoin/ordinals/inscription-text-content.query'; import { CollectibleText } from '../_collectible-types/collectible-text'; @@ -23,7 +23,7 @@ export function InscriptionText({ onClickCallToAction, onClickSend, }: InscriptionTextProps) { - const query = useTextInscriptionContentQuery(contentSrc); + const query = useInscriptionTextContentQuery(contentSrc); if (query.isLoading || query.isError) return null; diff --git a/src/app/features/collectibles/components/bitcoin/ordinals.tsx b/src/app/features/collectibles/components/bitcoin/ordinals.tsx index 3cb280ff800..af0dc434d16 100644 --- a/src/app/features/collectibles/components/bitcoin/ordinals.tsx +++ b/src/app/features/collectibles/components/bitcoin/ordinals.tsx @@ -4,7 +4,7 @@ import { useInView } from 'react-intersection-observer'; import { Box } from '@stacks/ui'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; -import { useTaprootInscriptionsInfiniteQuery } from '@app/query/bitcoin/ordinals/use-inscriptions.query'; +import { useGetInscriptionsInfiniteQuery } from '@app/query/bitcoin/ordinals/inscriptions.query'; import { Inscription } from './inscription'; @@ -13,7 +13,7 @@ interface OrdinalsProps { } export function Ordinals({ setIsLoadingMore }: OrdinalsProps) { - const query = useTaprootInscriptionsInfiniteQuery(); + const query = useGetInscriptionsInfiniteQuery(); const pages = query.data?.pages; const analytics = useAnalytics(); const { ref: intersectionSentinel, inView } = useInView({ diff --git a/src/app/features/collectibles/components/taproot-balance-displayer.tsx b/src/app/features/collectibles/components/taproot-balance-displayer.tsx index b6e5c44e7d2..5f6125fe568 100644 --- a/src/app/features/collectibles/components/taproot-balance-displayer.tsx +++ b/src/app/features/collectibles/components/taproot-balance-displayer.tsx @@ -1,7 +1,7 @@ import { formatMoney } from '@app/common/money/format-money'; import { Tooltip } from '@app/components/tooltip'; import { Caption } from '@app/components/typography'; -import { useCurrentTaprootAccountBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query'; +import { useCurrentTaprootAccountBalance } from '@app/query/bitcoin/balance/btc-taproot-balance.hooks'; const taprootSpendNotSupportedYetMsg = ` Total amount of BTC in your Taproot account addresses. Click to diff --git a/src/app/features/pending-brc-20-transfers/pending-brc-20-transfers.tsx b/src/app/features/pending-brc-20-transfers/pending-brc-20-transfers.tsx index 686480c423f..7fc899ed8f7 100644 --- a/src/app/features/pending-brc-20-transfers/pending-brc-20-transfers.tsx +++ b/src/app/features/pending-brc-20-transfers/pending-brc-20-transfers.tsx @@ -12,10 +12,10 @@ import { StatusPending } from '@app/components/status-pending'; import { StatusReady } from '@app/components/status-ready'; import { Tooltip } from '@app/components/tooltip'; import { Caption } from '@app/components/typography'; -import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query'; +import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/btc-native-segwit-balance.hooks'; import { useCheckOrderStatuses } from '@app/query/bitcoin/ordinals/brc20/use-check-order-status'; +import { fetchInscripionById } from '@app/query/bitcoin/ordinals/inscription-by-id.query'; import { convertInscriptionToSupportedInscriptionType } from '@app/query/bitcoin/ordinals/inscription.hooks'; -import { fetchInscripionById } from '@app/query/bitcoin/ordinals/use-inscription-by-id'; import { useOrdinalsbotClient } from '@app/query/bitcoin/ordinalsbot-client'; import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { diff --git a/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/components/psbt-address-receive-totals.tsx b/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/components/psbt-address-receive-totals.tsx index 64949a2b735..e5bb48fcf0d 100644 --- a/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/components/psbt-address-receive-totals.tsx +++ b/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/components/psbt-address-receive-totals.tsx @@ -28,7 +28,9 @@ export function PsbtAddressReceiveTotals({ showTaprootTotal }: PsbtAddressTotals /> ) : null} {isReceivingInscriptions - ? accountInscriptionsBeingReceived.map(path => ) + ? accountInscriptionsBeingReceived.map(inscription => ( + + )) : null} ); diff --git a/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/components/psbt-address-transfer-totals.tsx b/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/components/psbt-address-transfer-totals.tsx index 53585216ed2..25c9fe3f395 100644 --- a/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/components/psbt-address-transfer-totals.tsx +++ b/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/components/psbt-address-transfer-totals.tsx @@ -11,13 +11,8 @@ interface PsbtAddressTotalsProps { showNativeSegwitTotal: boolean; } export function PsbtAddressTransferTotals({ showNativeSegwitTotal }: PsbtAddressTotalsProps) { - const { - accountInscriptionsBeingTransferred, - - addressNativeSegwit, - - addressNativeSegwitTotal, - } = usePsbtSignerContext(); + const { accountInscriptionsBeingTransferred, addressNativeSegwit, addressNativeSegwitTotal } = + usePsbtSignerContext(); const calculateBitcoinFiatValue = useCalculateBitcoinFiatValue(); const isTransferringInscriptions = accountInscriptionsBeingTransferred?.length; @@ -33,8 +28,8 @@ export function PsbtAddressTransferTotals({ showNativeSegwitTotal }: PsbtAddress /> ) : null} {isTransferringInscriptions - ? accountInscriptionsBeingTransferred.map(path => ( - + ? accountInscriptionsBeingTransferred.map(inscription => ( + )) : null} diff --git a/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/components/psbt-inscription.tsx b/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/components/psbt-inscription.tsx index 57d4359205b..8f06e8b7106 100644 --- a/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/components/psbt-inscription.tsx +++ b/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/components/psbt-inscription.tsx @@ -1,24 +1,24 @@ +import { Inscription } from '@shared/models/inscription.model'; import { isUndefined } from '@shared/utils'; import { openInNewTab } from '@app/common/utils/open-in-new-tab'; import { OrdinalIcon } from '@app/components/icons/ordinal-icon'; import { InscriptionPreview } from '@app/components/inscription-preview-card/components/inscription-preview'; -import { useInscription } from '@app/query/bitcoin/ordinals/inscription.hooks'; +import { + createInscriptionInfoUrl, + useInscription, +} from '@app/query/bitcoin/ordinals/inscription.hooks'; import { PsbtAddressTotalItem } from './psbt-address-total-item'; interface PsbtInscriptionProps { - path: string; + inscription: Inscription; } -export function PsbtInscription({ path }: PsbtInscriptionProps) { - const { - isLoading, - isError, - data: inscription, - } = useInscription(path.replace('/inscription/', '')); +export function PsbtInscription({ inscription }: PsbtInscriptionProps) { + const { isLoading, isError, data: supportedInscription } = useInscription(inscription?.id ?? ''); if (isLoading) return null; - if (isError || isUndefined(inscription)) + if (isError || isUndefined(supportedInscription)) return ( } @@ -27,12 +27,13 @@ export function PsbtInscription({ path }: PsbtInscriptionProps) { /> ); + // return ( } + image={} title="Inscription" - value={`#${inscription.number}`} - valueAction={() => openInNewTab(inscription.infoUrl)} + value={`#${inscription?.number}`} + valueAction={() => openInNewTab(createInscriptionInfoUrl(inscription?.id))} /> ); } diff --git a/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/psbt-inputs-outputs-totals.tsx b/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/psbt-inputs-outputs-totals.tsx index 0289bda378b..7ff20f7a228 100644 --- a/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/psbt-inputs-outputs-totals.tsx +++ b/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/psbt-inputs-outputs-totals.tsx @@ -1,7 +1,5 @@ -import { color } from '@stacks/ui-utils'; -import { Box } from 'leather-styles/jsx'; +import { Box, styled } from 'leather-styles/jsx'; -import { Hr } from '@app/components/hr'; import { usePsbtSignerContext } from '@app/features/psbt-signer/psbt-signer.context'; import { PsbtRequestDetailsSectionHeader } from '../psbt-request-details-section-header'; @@ -34,7 +32,9 @@ export function PsbtInputsOutputsTotals() { ) : null} - {showDivider ?
: null} + {showDivider ? ( + + ) : null} {isReceiving ? ( diff --git a/src/app/features/psbt-signer/components/psbt-request-details-section.layout.tsx b/src/app/features/psbt-signer/components/psbt-request-details-section.layout.tsx index dc9e9640cb6..4d8d63e29e9 100644 --- a/src/app/features/psbt-signer/components/psbt-request-details-section.layout.tsx +++ b/src/app/features/psbt-signer/components/psbt-request-details-section.layout.tsx @@ -1,15 +1,15 @@ -import { Stack, StackProps, color } from '@stacks/ui'; +import { Stack, StackProps } from 'leather-styles/jsx'; import { HasChildren } from '@app/common/has-children'; export function PsbtRequestDetailsSectionLayout({ children, ...props }: HasChildren & StackProps) { return ( diff --git a/src/app/features/psbt-signer/hooks/find-outputs-receiving-inscriptions.spec.ts b/src/app/features/psbt-signer/hooks/find-outputs-receiving-inscriptions.spec.ts index b54cef592c1..a1e354863eb 100644 --- a/src/app/features/psbt-signer/hooks/find-outputs-receiving-inscriptions.spec.ts +++ b/src/app/features/psbt-signer/hooks/find-outputs-receiving-inscriptions.spec.ts @@ -1,6 +1,5 @@ +import { mockInscription1, mockInscription2 } from '@tests/mocks/mock-inscriptions'; import { - mockInscriptions1, - mockInscriptions2, mockPsbtInputs1, mockPsbtInputs2, mockPsbtOutputs1, @@ -12,27 +11,23 @@ import { findOutputsReceivingInscriptions } from './find-outputs-receiving-inscr describe('find outputs receiving inscriptions', () => { test('that the correct output receives the inscription (scenario 1)', () => { const outputsReceivingInscriptions = findOutputsReceivingInscriptions({ - inscriptions: mockInscriptions1, psbtInputs: mockPsbtInputs1, psbtOutputs: mockPsbtOutputs1, }); expect(outputsReceivingInscriptions[0]).toEqual({ address: 'bc1p9pnzvq52956jht5deha82qp96pxw0a0tvey6fhdea7vwhf33tarskqq3nr', - inscription: - '/inscription/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0', + inscription: mockInscription1, }); }); test('that the correct output receives the inscription (scenario 2)', () => { const outputsReceivingInscriptions = findOutputsReceivingInscriptions({ - inscriptions: mockInscriptions2, psbtInputs: mockPsbtInputs2, psbtOutputs: mockPsbtOutputs2, }); expect(outputsReceivingInscriptions[0]).toEqual({ address: 'bc1p9pnzvq52956jht5deha82qp96pxw0a0tvey6fhdea7vwhf33tarskqq3nr', - inscription: - '/inscription/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0', + inscription: mockInscription2, }); }); }); diff --git a/src/app/features/psbt-signer/hooks/find-outputs-receiving-inscriptions.ts b/src/app/features/psbt-signer/hooks/find-outputs-receiving-inscriptions.ts index 7e78d846f71..1aa2c453504 100644 --- a/src/app/features/psbt-signer/hooks/find-outputs-receiving-inscriptions.ts +++ b/src/app/features/psbt-signer/hooks/find-outputs-receiving-inscriptions.ts @@ -1,13 +1,11 @@ import BigNumber from 'bignumber.js'; -import { Inscription } from '@shared/models/inscription.model'; import { isDefined } from '@shared/utils'; import { PsbtInput } from './use-parsed-inputs'; import { PsbtOutput } from './use-parsed-outputs'; interface FindOutputsReceivingInscriptionsArgs { - inscriptions: Inscription[]; psbtInputs: PsbtInput[]; psbtOutputs: PsbtOutput[]; } @@ -15,7 +13,6 @@ interface FindOutputsReceivingInscriptionsArgs { // up from total input sats position to total output sats position. By doing this, // we can predict where the inscription will be sent. export function findOutputsReceivingInscriptions({ - inscriptions, psbtInputs, psbtOutputs, }: FindOutputsReceivingInscriptionsArgs) { @@ -23,13 +20,9 @@ export function findOutputsReceivingInscriptions({ return psbtInputs .flatMap(input => { - if (input.inscription) { - const inscription = inscriptions.find( - inscription => input.inscription?.includes(inscription.id) - ); - + if (isDefined(input.inscription)) { // Offset is zero indexed, so 1 is added here to match the sats total - const inscriptionTotalOffset = inputsSatsTotal.plus(Number(inscription?.offset) + 1); + const inscriptionTotalOffset = inputsSatsTotal.plus(Number(input.inscription.offset) + 1); let outputsSatsTotal = new BigNumber(0); diff --git a/src/app/features/psbt-signer/hooks/use-parsed-inputs.tsx b/src/app/features/psbt-signer/hooks/use-parsed-inputs.tsx index f173a5ebb98..a8e89101f78 100644 --- a/src/app/features/psbt-signer/hooks/use-parsed-inputs.tsx +++ b/src/app/features/psbt-signer/hooks/use-parsed-inputs.tsx @@ -8,9 +8,10 @@ import { getBtcSignerLibNetworkConfigByMode, } from '@shared/crypto/bitcoin/bitcoin.network'; import { getAddressFromOutScript } from '@shared/crypto/bitcoin/bitcoin.utils'; +import { Inscription } from '@shared/models/inscription.model'; import { isDefined, isUndefined } from '@shared/utils'; -import { useOrdinalsAwareUtxoQueries } from '@app/query/bitcoin/ordinals/ordinals-aware-utxo.query'; +import { useGetInscriptionsByOutputQueries } from '@app/query/bitcoin/ordinals/inscriptions-by-param.query'; import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useCurrentAccountTaprootIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; @@ -18,7 +19,7 @@ import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; export interface PsbtInput { address: string; index?: number; - inscription?: string; + inscription?: Inscription; isMutable: boolean; toSign: boolean; txid: string; @@ -52,7 +53,9 @@ export function useParsedInputs({ inputs, indexesToSign }: UseParsedInputsArgs) const bitcoinNetwork = getBtcSignerLibNetworkConfigByMode(network.chain.bitcoin.network); const bitcoinAddressNativeSegwit = useCurrentAccountNativeSegwitIndexZeroSigner().address; const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner(); - const utxosWithInscriptions = useOrdinalsAwareUtxoQueries(inputs).map(query => query.data); + const inscriptions = useGetInscriptionsByOutputQueries(inputs).map(query => { + return query.data?.results[0]; + }); const signAll = isUndefined(indexesToSign); const psbtInputs = useMemo( @@ -74,7 +77,7 @@ export function useParsedInputs({ inputs, indexesToSign }: UseParsedInputsArgs) return { address: inputAddress, index: input.index, - inscription: utxosWithInscriptions[i]?.inscriptions, + inscription: inscriptions[i], isMutable: canChange, toSign: toSignAll || toSignIndex, txid: input.txid ? bytesToHex(input.txid) : '', @@ -87,8 +90,8 @@ export function useParsedInputs({ inputs, indexesToSign }: UseParsedInputsArgs) bitcoinNetwork, indexesToSign, inputs, + inscriptions, signAll, - utxosWithInscriptions, ] ); diff --git a/src/app/features/psbt-signer/hooks/use-psbt-inscriptions.tsx b/src/app/features/psbt-signer/hooks/use-psbt-inscriptions.tsx index eeae3ac9256..2d3b98f481c 100644 --- a/src/app/features/psbt-signer/hooks/use-psbt-inscriptions.tsx +++ b/src/app/features/psbt-signer/hooks/use-psbt-inscriptions.tsx @@ -2,7 +2,6 @@ import { useMemo } from 'react'; import { isDefined } from '@shared/utils'; -import { useGetInscriptionQueries } from '@app/query/bitcoin/ordinals/inscription.query'; import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useCurrentAccountTaprootIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; @@ -13,20 +12,14 @@ import { PsbtOutput } from './use-parsed-outputs'; export function usePsbtInscriptions(psbtInputs: PsbtInput[], psbtOutputs: PsbtOutput[]) { const bitcoinAddressNativeSegwit = useCurrentAccountNativeSegwitIndexZeroSigner().address; const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner(); - const inscriptions = useGetInscriptionQueries( - psbtInputs.map(utxo => utxo.inscription?.replace('/inscription/', '') ?? '') - ) - .map(query => query.data) - .filter(isDefined); const outputsReceivingInscriptions = useMemo( () => findOutputsReceivingInscriptions({ - inscriptions, psbtInputs, psbtOutputs, }), - [inscriptions, psbtInputs, psbtOutputs] + [psbtInputs, psbtOutputs] ); return useMemo( diff --git a/src/app/features/psbt-signer/psbt-signer.context.ts b/src/app/features/psbt-signer/psbt-signer.context.ts index 09641493c15..5e58fd0c552 100644 --- a/src/app/features/psbt-signer/psbt-signer.context.ts +++ b/src/app/features/psbt-signer/psbt-signer.context.ts @@ -1,13 +1,14 @@ import { createContext, useContext } from 'react'; +import { Inscription } from '@shared/models/inscription.model'; import { Money } from '@shared/models/money.model'; import { PsbtInput } from './hooks/use-parsed-inputs'; import { PsbtOutput } from './hooks/use-parsed-outputs'; export interface PsbtSignerContext { - accountInscriptionsBeingTransferred: string[]; - accountInscriptionsBeingReceived: string[]; + accountInscriptionsBeingTransferred: Inscription[]; + accountInscriptionsBeingReceived: Inscription[]; addressNativeSegwit: string; addressTaproot: string; addressNativeSegwitTotal: Money; diff --git a/src/app/features/retrieve-taproot-to-native-segwit/retrieve-taproot-to-native-segwit.tsx b/src/app/features/retrieve-taproot-to-native-segwit/retrieve-taproot-to-native-segwit.tsx index 0343ee80916..51a844fe102 100644 --- a/src/app/features/retrieve-taproot-to-native-segwit/retrieve-taproot-to-native-segwit.tsx +++ b/src/app/features/retrieve-taproot-to-native-segwit/retrieve-taproot-to-native-segwit.tsx @@ -15,7 +15,7 @@ import { InfoCard, InfoCardRow, InfoCardSeparator } from '@app/components/info-c import { useCurrentTaprootAccountBalance, useCurrentTaprootAccountUninscribedUtxos, -} from '@app/query/bitcoin/balance/bitcoin-balances.query'; +} from '@app/query/bitcoin/balance/btc-taproot-balance.hooks'; import { useBitcoinBroadcastTransaction } from '@app/query/bitcoin/transaction/use-bitcoin-broadcast-transaction'; import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; diff --git a/src/app/features/retrieve-taproot-to-native-segwit/use-generate-retrieve-taproot-funds-tx.tsx b/src/app/features/retrieve-taproot-to-native-segwit/use-generate-retrieve-taproot-funds-tx.tsx index 86c5fcf7981..4cdf04826f3 100644 --- a/src/app/features/retrieve-taproot-to-native-segwit/use-generate-retrieve-taproot-funds-tx.tsx +++ b/src/app/features/retrieve-taproot-to-native-segwit/use-generate-retrieve-taproot-funds-tx.tsx @@ -6,9 +6,9 @@ import { Money, createMoney } from '@shared/models/money.model'; import { sumNumbers } from '@app/common/math/helpers'; import { BtcSizeFeeEstimator } from '@app/common/transactions/bitcoin/fees/btc-size-fee-estimator'; -import { useCurrentTaprootAccountUninscribedUtxos } from '@app/query/bitcoin/balance/bitcoin-balances.query'; +import { useCurrentTaprootAccountUninscribedUtxos } from '@app/query/bitcoin/balance/btc-taproot-balance.hooks'; import { useAverageBitcoinFeeRates } from '@app/query/bitcoin/fees/fee-estimates.hooks'; -import { getNumberOfInscriptionOnUtxo } from '@app/query/bitcoin/ordinals/ordinals-aware-utxo.query'; +import { useNumberOfInscriptionsOnUtxo } from '@app/query/bitcoin/ordinals/inscriptions.hooks'; import { useBitcoinScureLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain'; import { useCurrentAccountTaprootSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; @@ -17,6 +17,7 @@ export function useGenerateRetrieveTaprootFundsTx() { const uninscribedUtxos = useCurrentTaprootAccountUninscribedUtxos(); const createSigner = useCurrentAccountTaprootSigner(); const { data: feeRates } = useAverageBitcoinFeeRates(); + const getNumberOfInscriptionOnUtxo = useNumberOfInscriptionsOnUtxo(); const fee = useMemo(() => { if (!feeRates) return createMoney(0, 'BTC'); @@ -65,7 +66,7 @@ export function useGenerateRetrieveTaprootFundsTx() { tx.finalize(); return tx.hex; }, - [createSigner, networkMode, uninscribedUtxos] + [createSigner, getNumberOfInscriptionOnUtxo, networkMode, uninscribedUtxos] ); return { generateRetrieveTaprootFundsTx, fee }; diff --git a/src/app/features/secret-key-displayer/components/secret-key-word.tsx b/src/app/features/secret-key-displayer/components/secret-key-word.tsx index 00554c2e21a..08966613c0d 100644 --- a/src/app/features/secret-key-displayer/components/secret-key-word.tsx +++ b/src/app/features/secret-key-displayer/components/secret-key-word.tsx @@ -5,11 +5,26 @@ interface SecretKeyWordProps extends FlexProps { word: string; num: number; } -export function SecretKeyWord({ word, num, ...rest }: SecretKeyWordProps) { +export function SecretKeyWord({ word, num }: SecretKeyWordProps) { return ( - - {num}. - + + {/* FIXME #4130: need to fix this color color: var(--color-grey-light-8, #BBB); */} + + {num}. + + {word} diff --git a/src/app/features/secret-key-displayer/secret-key-displayer.layout.tsx b/src/app/features/secret-key-displayer/secret-key-displayer.layout.tsx index 907e9a32a15..cbfa7f92052 100644 --- a/src/app/features/secret-key-displayer/secret-key-displayer.layout.tsx +++ b/src/app/features/secret-key-displayer/secret-key-displayer.layout.tsx @@ -1,10 +1,14 @@ import { useState } from 'react'; +import { FiEye, FiEyeOff } from 'react-icons/fi'; +import { OnboardingSelectors } from '@tests/selectors/onboarding.selectors'; import { SettingsSelectors } from '@tests/selectors/settings.selectors'; -import { Box, Flex, Stack, styled } from 'leather-styles/jsx'; +import { Flex, styled } from 'leather-styles/jsx'; import { LeatherButton } from '@app/components/button/button'; +import { CopyIcon } from '@app/components/icons/copy-icon'; +import { SecretKeyGrid } from '../../components/secret-key/secret-key-grid'; import { SecretKeyWord } from './components/secret-key-word'; interface SecretKeyDisplayerLayoutProps { @@ -12,55 +16,62 @@ interface SecretKeyDisplayerLayoutProps { onCopyToClipboard(): void; secretKeyWords: string[] | undefined; showTitleAndIllustration: boolean; + onBackedUpSecretKey(): void; } export function SecretKeyDisplayerLayout(props: SecretKeyDisplayerLayoutProps) { - const { hasCopied, onCopyToClipboard, secretKeyWords, showTitleAndIllustration } = props; + const { hasCopied, onCopyToClipboard, onBackedUpSecretKey, secretKeyWords } = props; const [showSecretKey, setShowSecretKey] = useState(false); return ( - - + + {secretKeyWords?.map((word, index) => ( + + ))} + + + setShowSecretKey(!showSecretKey)} + > + {showSecretKey ? : } + {showSecretKey ? 'Hide key' : 'Show key'} + + + + {!hasCopied ? ' Copy' : 'Copied!'} + + + - {showTitleAndIllustration ? ( - - Your Secret Key - - ) : null} - - - {secretKeyWords?.map((word, index) => ( - - ))} - - - setShowSecretKey(!showSecretKey)} - > - {showSecretKey ? 'Hide key' : 'Show key'} - - - {!hasCopied ? ' Copy to clipboard' : 'Copied!'} - - - - + I've backed it up + + ); } diff --git a/src/app/features/secret-key-displayer/secret-key-displayer.tsx b/src/app/features/secret-key-displayer/secret-key-displayer.tsx index 58a38e71cac..62ab47cd5fa 100644 --- a/src/app/features/secret-key-displayer/secret-key-displayer.tsx +++ b/src/app/features/secret-key-displayer/secret-key-displayer.tsx @@ -1,5 +1,5 @@ import { memo, useMemo } from 'react'; -import { Outlet, useLocation } from 'react-router-dom'; +import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import { useClipboard } from '@stacks/ui'; @@ -16,6 +16,7 @@ export const SecretKeyDisplayer = memo(({ secretKey }: SecretKeyDisplayerProps) const { onCopy, hasCopied } = useClipboard(secretKey || ''); const { pathname } = useLocation(); const analytics = useAnalytics(); + const navigate = useNavigate(); const copyToClipboard = () => { void analytics.track('copy_secret_key_to_clipboard'); @@ -32,6 +33,7 @@ export const SecretKeyDisplayer = memo(({ secretKey }: SecretKeyDisplayerProps) onCopyToClipboard={copyToClipboard} secretKeyWords={secretKeyWords} showTitleAndIllustration={showTitleAndIllustration} + onBackedUpSecretKey={() => navigate(RouteUrls.SetPassword)} /> diff --git a/src/app/pages/choose-account/components/accounts.tsx b/src/app/pages/choose-account/components/accounts.tsx index 4118e48cee4..bc481e66e59 100644 --- a/src/app/pages/choose-account/components/accounts.tsx +++ b/src/app/pages/choose-account/components/accounts.tsx @@ -52,56 +52,57 @@ interface ChooseAccountItemProps extends FlexProps { account: StacksAccount; onSelectAccount(index: number): void; } -const ChooseAccountItem = memo((props: ChooseAccountItemProps) => { - const { selectedAddress, account, isLoading, onSelectAccount, ...rest } = props; - - const [component, bind] = usePressable(true); - const { decodedAuthRequest } = useOnboardingState(); - const name = useAccountDisplayName(account); - const btcAddress = useNativeSegwitAccountIndexAddressIndexZero(account.index); - - const showLoadingProps = !!selectedAddress || !decodedAuthRequest; - - const accountSlug = useMemo(() => slugify(`Account ${account?.index + 1}`), [account?.index]); - - return ( - // Padding required on outer element to prevent jumpy list behaviours in - // virtualised list library - - - } - > - - - } - avatar={ - - } - balanceLabel={} - isLoading={isLoading} - onSelectAccount={() => onSelectAccount(account.index)} - data-testid={`account-${accountSlug}-${account.index}`} - {...bind} - {...rest} - > - {component} - - - ); -}); +const ChooseAccountItem = memo( + ({ selectedAddress, account, isLoading, onSelectAccount }: ChooseAccountItemProps) => { + const [component, bind] = usePressable(true); + const { decodedAuthRequest } = useOnboardingState(); + const name = useAccountDisplayName(account); + const btcAddress = useNativeSegwitAccountIndexAddressIndexZero(account.index); + + const showLoadingProps = !!selectedAddress || !decodedAuthRequest; + + const accountSlug = useMemo(() => slugify(`Account ${account?.index + 1}`), [account?.index]); + + return ( + // Padding required on outer element to prevent jumpy list behaviours in + // virtualised list library + + + } + > + + + } + avatar={ + + } + balanceLabel={ + + } + isLoading={isLoading} + onSelectAccount={() => onSelectAccount(account.index)} + data-testid={`account-${accountSlug}-${account.index}`} + {...bind} + > + {component} + + + ); + } +); const AddAccountAction = memo(() => { const [component, bind] = usePressable(true); diff --git a/src/app/pages/fund/components/fiat-providers-list.tsx b/src/app/pages/fund/components/fiat-providers-list.tsx index adbc9ea7015..362d388f71a 100644 --- a/src/app/pages/fund/components/fiat-providers-list.tsx +++ b/src/app/pages/fund/components/fiat-providers-list.tsx @@ -1,6 +1,6 @@ import { useNavigate } from 'react-router-dom'; -import { Grid } from '@stacks/ui'; +import { Grid } from 'leather-styles/jsx'; import { RouteUrls } from '@shared/route-urls'; @@ -35,12 +35,20 @@ export function FiatProvidersList(props: FiatProvidersProps) { return ( diff --git a/src/app/pages/fund/fund.layout.tsx b/src/app/pages/fund/fund.layout.tsx index 83d0a35e925..16a53833d80 100644 --- a/src/app/pages/fund/fund.layout.tsx +++ b/src/app/pages/fund/fund.layout.tsx @@ -1,20 +1,11 @@ -import { Flex, Stack, useMediaQuery } from '@stacks/ui'; -import { styled } from 'leather-styles/jsx'; -import { token } from 'leather-styles/tokens'; - -import { PageTitle } from '@app/components/page-title'; -import { Text } from '@app/components/typography'; +import { Flex, Stack, styled } from 'leather-styles/jsx'; import { FiatProvidersList } from './components/fiat-providers-list'; interface FundLayoutProps { address: string; } -export function FundLayout(props: FundLayoutProps) { - const { address } = props; - - const [desktopViewport] = useMediaQuery(`(min-width: ${token('sizes.desktopViewportMinWidth')})`); - +export function FundLayout({ address }: FundLayoutProps) { return ( - - {/* TODO: Investigate removing / refactoring - {desktopViewport ? 'Let’s get STX into your wallet' : 'Get STX'} - - - - - Choose an exchange to fund your account with Stacks (STX) or deposit from elsewhere. - Exchanges with “Fast checkout” make it easier to purchase STX for direct deposit into - your wallet with a credit card. - - + + Let's get funds into your wallet + + + + Choose an exchange to fund your account with Stacks (STX) or deposit from elsewhere. + Exchanges with “Fast checkout” make it easier to purchase STX for direct deposit into your + wallet with a credit card. + diff --git a/src/app/pages/home/components/account-info-card.tsx b/src/app/pages/home/components/account-info-card.tsx index 657f41a613d..7e9ba9cecb7 100644 --- a/src/app/pages/home/components/account-info-card.tsx +++ b/src/app/pages/home/components/account-info-card.tsx @@ -1,4 +1,3 @@ -import { useMediaQuery } from '@stacks/ui'; import { SettingsSelectors } from '@tests/selectors/settings.selectors'; import { Box, Divider, Flex, styled } from 'leather-styles/jsx'; @@ -14,7 +13,6 @@ import { AccountActions } from './account-actions'; export function AccountInfoCard() { const name = useCurrentAccountDisplayName(); - const [isNarrowViewport] = useMediaQuery('(max-width: 600px)'); const account = useCurrentStacksAccount(); const btcAddress = useCurrentAccountNativeSegwitAddressIndexZero(); @@ -40,7 +38,7 @@ export function AccountInfoCard() { {name} diff --git a/src/app/pages/home/components/home.layout.tsx b/src/app/pages/home/components/home.layout.tsx index e66b919172a..06ac7e45cb7 100644 --- a/src/app/pages/home/components/home.layout.tsx +++ b/src/app/pages/home/components/home.layout.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { useMediaQuery } from '@stacks/ui'; import { HomePageSelectors } from '@tests/selectors/home.selectors'; import { Stack } from 'leather-styles/jsx'; @@ -8,8 +7,6 @@ import { AccountInfoCard } from './account-info-card'; type HomeLayoutProps = Record<'currentAccount' | 'children', React.ReactNode>; export function HomeLayout({ children }: HomeLayoutProps) { - const [isNarrowViewport] = useMediaQuery('(max-width: 600px)'); - return ( {children} diff --git a/src/app/pages/onboarding/allow-diagnostics/allow-diagnostics-layout.tsx b/src/app/pages/onboarding/allow-diagnostics/allow-diagnostics-layout.tsx index eaf153b1f6d..fa9e2b2a6fa 100644 --- a/src/app/pages/onboarding/allow-diagnostics/allow-diagnostics-layout.tsx +++ b/src/app/pages/onboarding/allow-diagnostics/allow-diagnostics-layout.tsx @@ -2,8 +2,8 @@ import { FiCheck } from 'react-icons/fi'; import { Dialog } from '@radix-ui/themes'; import { OnboardingSelectors } from '@tests/selectors/onboarding.selectors'; +import { css } from 'leather-styles/css'; import { Box, Flex, HStack, Stack, styled } from 'leather-styles/jsx'; -import { token } from 'leather-styles/tokens'; import { LeatherButton } from '@app/components/button/button'; import { LeatherIcon } from '@app/components/icons/leather-icon'; @@ -31,12 +31,11 @@ export function AllowDiagnosticsLayout(props: AllowDiagnosticsLayoutProps) { return ( diff --git a/src/app/pages/onboarding/back-up-secret-key/back-up-secret-key.layout.tsx b/src/app/pages/onboarding/back-up-secret-key/back-up-secret-key.layout.tsx deleted file mode 100644 index db58511b635..00000000000 --- a/src/app/pages/onboarding/back-up-secret-key/back-up-secret-key.layout.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useMediaQuery } from '@stacks/ui'; -import { Box, Flex, Stack, styled } from 'leather-styles/jsx'; -import { token } from 'leather-styles/tokens'; - -import { BackUpSecretKeyActions } from './components/back-up-secret-key-actions'; - -interface BackUpSecretKeyLayoutProps { - secretKeyDisplay: React.JSX.Element; - onBackedUpSecretKey(): void; -} -export function BackUpSecretKeyLayout(props: BackUpSecretKeyLayoutProps): React.JSX.Element { - const { secretKeyDisplay, onBackedUpSecretKey } = props; - - const [desktopViewport] = useMediaQuery(`(min-width: ${token('sizes.desktopViewportMinWidth')})`); - - return ( - - - - Back up your Secret Key - - Here's your Secret Key: 24 words that give you access to your new wallet. - - - You'll need it to access your wallet on a new device, or this one if you lose your - password — so back it up somewhere safe! - - - {desktopViewport && ( - - - - )} - - - - {secretKeyDisplay} - - {!desktopViewport && ( - - - - )} - - ); -} diff --git a/src/app/pages/onboarding/back-up-secret-key/back-up-secret-key.tsx b/src/app/pages/onboarding/back-up-secret-key/back-up-secret-key.tsx index 2fc995042fa..210ab4f6ab0 100644 --- a/src/app/pages/onboarding/back-up-secret-key/back-up-secret-key.tsx +++ b/src/app/pages/onboarding/back-up-secret-key/back-up-secret-key.tsx @@ -5,10 +5,11 @@ import { RouteUrls } from '@shared/route-urls'; import { useRouteHeader } from '@app/common/hooks/use-route-header'; import { Header } from '@app/components/header'; +import { TwoColumnLayout } from '@app/components/secret-key/two-column.layout'; import { SecretKeyDisplayer } from '@app/features/secret-key-displayer/secret-key-displayer'; import { useDefaultWalletSecretKey } from '@app/store/in-memory-key/in-memory-key.selectors'; -import { BackUpSecretKeyLayout } from './back-up-secret-key.layout'; +import { BackUpSecretKeyContent } from './components/back-up-secret-key.content'; export const BackUpSecretKeyPage = memo(() => { const secretKey = useDefaultWalletSecretKey(); @@ -23,9 +24,9 @@ export const BackUpSecretKeyPage = memo(() => { if (!secretKey) return null; return ( - } - onBackedUpSecretKey={() => navigate(RouteUrls.SetPassword)} + } + rightColumn={} /> ); }); diff --git a/src/app/pages/onboarding/back-up-secret-key/components/back-up-secret-key-actions.tsx b/src/app/pages/onboarding/back-up-secret-key/components/back-up-secret-key-actions.tsx deleted file mode 100644 index 7eb9190f324..00000000000 --- a/src/app/pages/onboarding/back-up-secret-key/components/back-up-secret-key-actions.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { FiEyeOff, FiLock, FiRotateCcw } from 'react-icons/fi'; - -import { Box, Stack } from '@stacks/ui'; -import { OnboardingSelectors } from '@tests/selectors/onboarding.selectors'; -import { styled } from 'leather-styles/jsx'; - -import { LeatherButton } from '@app/components/button/button'; - -interface BackUpSecretKeyLayoutProps { - onBackedUpSecretKey(): void; -} - -export function BackUpSecretKeyActions(props: BackUpSecretKeyLayoutProps): React.JSX.Element { - const { onBackedUpSecretKey } = props; - - return ( - <> - - - Your Secret Key gives access to your wallet - - - - Never share your Secret Key with anyone - - - - Store it somewhere 100% private and secure - - - - I've backed it up - - - ); -} diff --git a/src/app/pages/onboarding/back-up-secret-key/components/back-up-secret-key.content.tsx b/src/app/pages/onboarding/back-up-secret-key/components/back-up-secret-key.content.tsx new file mode 100644 index 00000000000..6d70cf968a3 --- /dev/null +++ b/src/app/pages/onboarding/back-up-secret-key/components/back-up-secret-key.content.tsx @@ -0,0 +1,39 @@ +import { FiEyeOff, FiLock, FiRotateCcw } from 'react-icons/fi'; + +import { HStack, Stack, styled } from 'leather-styles/jsx'; + +export function BackUpSecretKeyContent(): React.JSX.Element { + return ( + <> + + Back up your
Secret Key +
+ + Here's your Secret Key: 24 words that give you access to your new wallet. + + + You'll need it to access your wallet on a new device, or this one if you lose your password + — so back it up somewhere safe! + + + + + + Your Secret Key gives access to your wallet + + + + Never share your Secret Key with anyone + + + + Store it somewhere 100% private and secure + + + + ); +} diff --git a/src/app/pages/onboarding/set-password/set-password.tsx b/src/app/pages/onboarding/set-password/set-password.tsx index a6ee3d38b93..441fcdc0613 100644 --- a/src/app/pages/onboarding/set-password/set-password.tsx +++ b/src/app/pages/onboarding/set-password/set-password.tsx @@ -3,8 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { OnboardingSelectors } from '@tests/selectors/onboarding.selectors'; import { Form, Formik } from 'formik'; -import { Flex, Stack, styled } from 'leather-styles/jsx'; -import { token } from 'leather-styles/tokens'; +import { styled } from 'leather-styles/jsx'; import { debounce } from 'ts-debounce'; import * as yup from 'yup'; @@ -22,6 +21,7 @@ import { } from '@app/common/validation/validate-password'; import { LeatherButton } from '@app/components/button/button'; import { Header } from '@app/components/header'; +import { TwoColumnLayout } from '@app/components/secret-key/two-column.layout'; import { OnboardingGate } from '@app/routes/onboarding-gate'; import { useStacksAccounts } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; @@ -113,7 +113,6 @@ function SetPasswordPage() { }, 60) as unknown as yup.TestFunction, }), }); - return ( {({ dirty, isSubmitting, isValid }) => (
- - - Set a password - - Your password protects your Secret Key on this device only. - - - You'll need just your Secret Key to access your wallet on another device, or this - one if you lose your password. - - - - - - Your password - - - - Continue - - - + + + Set a password + + + Your password protects your Secret Key on this device only. + + + You'll need just your Secret Key to access your wallet on another device, or this + one if you lose your password. + + + } + rightColumn={ + <> + + Your password + + + + Continue + + + } + /> )}
diff --git a/src/app/pages/onboarding/sign-in/components/sign-in.content.tsx b/src/app/pages/onboarding/sign-in/components/sign-in.content.tsx new file mode 100644 index 00000000000..8b5fd38393b --- /dev/null +++ b/src/app/pages/onboarding/sign-in/components/sign-in.content.tsx @@ -0,0 +1,32 @@ +import { styled } from 'leather-styles/jsx'; + +import { LeatherButton } from '@app/components/button/button'; + +export function SignInContent({ + onClick, + twentyFourWordMode, +}: { + onClick: () => void; + twentyFourWordMode: boolean; +}): React.JSX.Element { + return ( + <> + + Sign in
with your
+ Secret Key +
+ + Speed things up by pasting your entire Secret Key in one go. + + + {twentyFourWordMode ? 'Have a 12-word Secret Key?' : 'Use 24 word Secret Key'} + + + ); +} diff --git a/src/app/pages/onboarding/sign-in/hooks/use-sign-in.ts b/src/app/pages/onboarding/sign-in/hooks/use-sign-in.ts index 5cff1db3f9d..ad2327be68f 100644 --- a/src/app/pages/onboarding/sign-in/hooks/use-sign-in.ts +++ b/src/app/pages/onboarding/sign-in/hooks/use-sign-in.ts @@ -1,7 +1,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { validateMnemonic } from 'bip39'; +import { validateMnemonic } from '@scure/bip39'; +import { wordlist } from '@scure/bip39/wordlists/english'; import { RouteUrls } from '@shared/route-urls'; @@ -50,7 +51,7 @@ export function useSignIn() { handleSetError('Entering your Secret Key is required.'); } - if (!validateMnemonic(parsedKeyInput)) { + if (!validateMnemonic(parsedKeyInput, wordlist)) { handleSetError(); return; } diff --git a/src/app/pages/onboarding/sign-in/mnemonic-form.tsx b/src/app/pages/onboarding/sign-in/mnemonic-form.tsx new file mode 100644 index 00000000000..84463dc68b1 --- /dev/null +++ b/src/app/pages/onboarding/sign-in/mnemonic-form.tsx @@ -0,0 +1,127 @@ +import { OnboardingSelectors } from '@tests/selectors/onboarding.selectors'; +import { Form, Formik } from 'formik'; +import { css } from 'leather-styles/css'; +import { Flex, styled } from 'leather-styles/jsx'; + +import { isEmpty } from '@shared/utils'; + +import { createNullArrayOfLength } from '@app/common/utils'; +import { LeatherButton } from '@app/components/button/button'; +import { ErrorLabel } from '@app/components/error-label'; +import { SecretKeyGrid } from '@app/components/secret-key/secret-key-grid'; +import { useSignIn } from '@app/pages/onboarding/sign-in/hooks/use-sign-in'; + +import { MnemonicWordInput } from '../../../components/secret-key/mnemonic-key/mnemonic-word-input'; +import { + getMnemonicErrorFields, + getMnemonicErrorMessage, + hasMnemonicFormValues, +} from '../../../components/secret-key/mnemonic-key/utils/error-handling'; +import { validationSchema } from '../../../components/secret-key/mnemonic-key/utils/validation'; + +interface MnemonicFormProps { + mnemonic: (string | null)[]; + setMnemonic: (mnemonic: (string | null)[]) => void; + twentyFourWordMode: boolean; +} +export function MnemonicForm({ mnemonic, setMnemonic, twentyFourWordMode }: MnemonicFormProps) { + const { submitMnemonicForm, error, isLoading } = useSignIn(); + + function mnemonicWordUpdate(index: number, word: string) { + const newMnemonic = [...mnemonic]; + newMnemonic[index] = word; + setMnemonic(newMnemonic); + } + + function updateEntireKey(key: string, setFieldValue: (name: string, value: number) => void) { + const newKey = key.split(' '); + newKey.map((index, value) => setFieldValue(`${index + 1}`, value)); + setMnemonic(newKey); + void submitMnemonicForm(key); + } + + function handleSubmit() { + return void submitMnemonicForm(mnemonic.join(' ')); + } + + const mnemonicFieldArray = mnemonic + ? mnemonic + : createNullArrayOfLength(twentyFourWordMode ? 24 : 12); + + // set initialValues to avoid throwing uncontrolled inputs error + const initialValues = mnemonicFieldArray.reduce( + (accumulator, _, index) => ((accumulator[`${index + 1}`] = ''), accumulator), + {} + ); + return ( + + {({ errors, touched, setFieldValue, values, isValid }) => { + const hasFormValues = hasMnemonicFormValues(values); + const mnemonicErrorFields = getMnemonicErrorFields(errors, touched, values); + const showMnemonicErrors = !isEmpty(mnemonicErrorFields) && hasFormValues; + const mnemonicErrorMessage = getMnemonicErrorMessage(mnemonicErrorFields); + + return ( +
+ + Your Secret Key + + + {mnemonicFieldArray.map((_, i) => ( + { + (document.activeElement as HTMLInputElement).blur(); + updateEntireKey(key, setFieldValue); + }} + onUpdateWord={w => mnemonicWordUpdate(i, w)} + /> + ))} + + + {(showMnemonicErrors || error) && ( + // #4274 TODO migrate ErrorLabel + + + {showMnemonicErrors ? mnemonicErrorMessage : error} + + + )} + + { + e.preventDefault(); + return handleSubmit(); + }} + > + Continue + + +
+ ); + }} +
+ ); +} diff --git a/src/app/pages/onboarding/sign-in/sign-in.tsx b/src/app/pages/onboarding/sign-in/sign-in.tsx index 9c9f34fbd33..6b430dae857 100644 --- a/src/app/pages/onboarding/sign-in/sign-in.tsx +++ b/src/app/pages/onboarding/sign-in/sign-in.tsx @@ -1,178 +1,44 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Grid, Input } from '@stacks/ui'; -import { OnboardingSelectors } from '@tests/selectors/onboarding.selectors'; -import { Box, Flex, Stack, styled } from 'leather-styles/jsx'; -import { token } from 'leather-styles/tokens'; -import { useFocus } from 'use-events'; - import { RouteUrls } from '@shared/route-urls'; import { useRouteHeader } from '@app/common/hooks/use-route-header'; -import { createNullArrayOfLength, extractPhraseFromString } from '@app/common/utils'; -import { LeatherButton } from '@app/components/button/button'; -import { ErrorLabel } from '@app/components/error-label'; +import { createNullArrayOfLength } from '@app/common/utils'; import { Header } from '@app/components/header'; -import { useSignIn } from '@app/pages/onboarding/sign-in/hooks/use-sign-in'; - -interface MnemonicWordInputProps { - index: number; - value: string; - onUpdateWord(word: string): void; - onPasteEntireKey(word: string): void; -} -function MnemonicWordInput({ - index, - value, - onUpdateWord, - onPasteEntireKey, -}: MnemonicWordInputProps) { - const [isFocused, bind] = useFocus(); +import { TwoColumnLayout } from '@app/components/secret-key/two-column.layout'; +import { MnemonicForm } from '@app/pages/onboarding/sign-in/mnemonic-form'; - return ( - - - {index + 1}. - - { - const pasteValue = extractPhraseFromString(e.clipboardData.getData('text')); - if (pasteValue.includes(' ')) { - e.preventDefault(); - //assume its a full key - onPasteEntireKey(pasteValue); - } - }} - onChange={(e: any) => { - e.preventDefault(); - onUpdateWord(e.target.value); - }} - {...bind} - /> - - ); -} +import { SignInContent } from './components/sign-in.content'; export function SignIn() { - const { submitMnemonicForm, error, isLoading } = useSignIn(); const navigate = useNavigate(); const [twentyFourWordMode, setTwentyFourWordMode] = useState(true); - - useRouteHeader(
navigate(RouteUrls.Onboarding)} hideActions />); - const [mnemonic, setMnemonic] = useState<(string | null)[]>(() => createNullArrayOfLength(24)); - function mnemonicWordUpdate(index: number, word: string) { - const newMnemonic = [...mnemonic]; - newMnemonic[index] = word; - setMnemonic(newMnemonic); - } - - function updateEntireKey(key: string) { - const newKey = key.split(' '); - setMnemonic(newKey); - void submitMnemonicForm(key); - } + useRouteHeader(
navigate(RouteUrls.Onboarding)} hideActions />); return ( - - - Sign in with your Secret Key - - - Enter your Secret Key to sign in with an existing wallet - - - - Tip: You can paste in your entire Secret Key at once - - - { - setTwentyFourWordMode(!twentyFourWordMode); - setMnemonic(createNullArrayOfLength(twentyFourWordMode ? 24 : 12)); - }} - > - {twentyFourWordMode ? 'Have a 12-word Secret Key?' : 'Use 24 word Secret Key'} - - - - - Your Secret Key - - - - {createNullArrayOfLength(twentyFourWordMode ? 24 : 12).map((_, i) => ( - { - (document.activeElement as HTMLInputElement).blur(); - updateEntireKey(key); - }} - onUpdateWord={w => mnemonicWordUpdate(i, w)} - /> - ))} - - - - {error && ( - - - {error} - - - )} - { - e.preventDefault(); - void submitMnemonicForm(mnemonic.join(' ')); + <> + { + setTwentyFourWordMode(!twentyFourWordMode); + setMnemonic(createNullArrayOfLength(twentyFourWordMode ? 24 : 12)); }} - > - Continue - - - - + twentyFourWordMode={twentyFourWordMode} + /> + } + rightColumn={ + + } + /> + ); } diff --git a/src/app/pages/receive/receive-modal.tsx b/src/app/pages/receive/receive-modal.tsx index 2e56ba1acc3..834c6fc1dce 100644 --- a/src/app/pages/receive/receive-modal.tsx +++ b/src/app/pages/receive/receive-modal.tsx @@ -14,7 +14,7 @@ import { BaseDrawer } from '@app/components/drawer/base-drawer'; import { BtcIcon } from '@app/components/icons/btc-icon'; import { BtcStampsIcon } from '@app/components/icons/btc-stamps-icon'; import { OrdinalIcon } from '@app/components/icons/ordinal-icon'; -import { useZeroIndexTaprootAddress } from '@app/query/bitcoin/ordinals/use-zero-index-taproot-address'; +import { useZeroIndexTaprootAddress } from '@app/store/accounts/blockchain/bitcoin/bitcoin.hooks'; import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useCurrentAccountStxAddressState } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; diff --git a/src/app/pages/rpc-send-transfer/use-rpc-send-transfer.ts b/src/app/pages/rpc-send-transfer/use-rpc-send-transfer.ts index f11b07ed1f4..66804cb59eb 100644 --- a/src/app/pages/rpc-send-transfer/use-rpc-send-transfer.ts +++ b/src/app/pages/rpc-send-transfer/use-rpc-send-transfer.ts @@ -8,7 +8,7 @@ import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-s import { useOnMount } from '@app/common/hooks/use-on-mount'; import { initialSearchParams } from '@app/common/initial-search-params'; import { useWalletType } from '@app/common/use-wallet-type'; -import { useSpendableCurrentNativeSegwitAccountUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks'; +import { useCurrentNativeSegwitAccountSpendableUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks'; export function useRpcSendTransferRequestParams() { const defaultParams = useDefaultRequestParams(); @@ -27,7 +27,7 @@ export function useRpcSendTransfer() { const navigate = useNavigate(); const { whenWallet } = useWalletType(); const { address, amount, origin } = useRpcSendTransferRequestParams(); - const { data: utxos = [], refetch } = useSpendableCurrentNativeSegwitAccountUtxos(); + const { data: utxos = [], refetch } = useCurrentNativeSegwitAccountSpendableUtxos(); // Forcing a refetch to ensure UTXOs are fresh useOnMount(() => refetch()); diff --git a/src/app/pages/send/ordinal-inscription/coinselect/select-inscription-coins.ts b/src/app/pages/send/ordinal-inscription/coinselect/select-inscription-coins.ts index fc39849d7f7..0c06401b2cc 100644 --- a/src/app/pages/send/ordinal-inscription/coinselect/select-inscription-coins.ts +++ b/src/app/pages/send/ordinal-inscription/coinselect/select-inscription-coins.ts @@ -4,8 +4,7 @@ import { isDefined } from '@shared/utils'; import { sumNumbers } from '@app/common/math/helpers'; import { BtcSizeFeeEstimator } from '@app/common/transactions/bitcoin/fees/btc-size-fee-estimator'; import { createCounter } from '@app/common/utils/counter'; -import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client'; -import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query'; +import { TaprootUtxo, UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client'; const idealInscriptionValue = 10_000; diff --git a/src/app/pages/send/ordinal-inscription/components/create-utxo-from-inscription.ts b/src/app/pages/send/ordinal-inscription/components/create-utxo-from-inscription.ts index 9f8208964cb..8d15df29f91 100644 --- a/src/app/pages/send/ordinal-inscription/components/create-utxo-from-inscription.ts +++ b/src/app/pages/send/ordinal-inscription/components/create-utxo-from-inscription.ts @@ -1,6 +1,6 @@ import { Inscription } from '@shared/models/inscription.model'; -import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query'; +import { TaprootUtxo } from '@app/query/bitcoin/bitcoin-client'; export function createUtxoFromInscription(inscription: Inscription): TaprootUtxo { const { genesis_block_hash, genesis_timestamp, genesis_block_height, value, addressIndex } = diff --git a/src/app/pages/send/ordinal-inscription/components/send-inscription-container.tsx b/src/app/pages/send/ordinal-inscription/components/send-inscription-container.tsx index ac4738988bb..ddff09d76fa 100644 --- a/src/app/pages/send/ordinal-inscription/components/send-inscription-container.tsx +++ b/src/app/pages/send/ordinal-inscription/components/send-inscription-container.tsx @@ -7,7 +7,7 @@ import { AverageBitcoinFeeRates, BtcFeeType } from '@shared/models/fees/bitcoin- import { SupportedInscription } from '@shared/models/inscription.model'; import { useOnMount } from '@app/common/hooks/use-on-mount'; -import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query'; +import { TaprootUtxo } from '@app/query/bitcoin/bitcoin-client'; import { useSendInscriptionRouteState } from '../hooks/use-send-inscription-route-state'; import { createUtxoFromInscription } from './create-utxo-from-inscription'; diff --git a/src/app/pages/send/ordinal-inscription/hooks/use-generate-ordinal-tx.ts b/src/app/pages/send/ordinal-inscription/hooks/use-generate-ordinal-tx.ts index aebaa91c705..3c75946786f 100644 --- a/src/app/pages/send/ordinal-inscription/hooks/use-generate-ordinal-tx.ts +++ b/src/app/pages/send/ordinal-inscription/hooks/use-generate-ordinal-tx.ts @@ -6,8 +6,7 @@ import { OrdinalSendFormValues } from '@shared/models/form.model'; import { determineUtxosForSpend } from '@app/common/transactions/bitcoin/coinselect/local-coin-selection'; import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks'; -import { NativeSegwitUtxo } from '@app/query/bitcoin/bitcoin-client'; -import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query'; +import { NativeSegwitUtxo, TaprootUtxo } from '@app/query/bitcoin/bitcoin-client'; import { useBitcoinScureLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain'; import { useCurrentAccountNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useCurrentAccountTaprootSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; diff --git a/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-fees-list.ts b/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-fees-list.ts index d299faf5e9e..c7d60fd12d8 100644 --- a/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-fees-list.ts +++ b/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-fees-list.ts @@ -7,8 +7,8 @@ import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money'; import { formatMoneyPadded, i18nFormatCurrency } from '@app/common/money/format-money'; import { FeesListItem } from '@app/components/bitcoin-fees-list/bitcoin-fees-list'; import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks'; +import { TaprootUtxo } from '@app/query/bitcoin/bitcoin-client'; import { useAverageBitcoinFeeRates } from '@app/query/bitcoin/fees/fee-estimates.hooks'; -import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query'; import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks'; import { useCurrentAccountNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; diff --git a/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-form.tsx b/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-form.tsx index 59c505c5705..aab40dcee01 100644 --- a/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-form.tsx +++ b/src/app/pages/send/ordinal-inscription/hooks/use-send-inscription-form.tsx @@ -16,7 +16,7 @@ import { btcAddressNetworkValidator, btcAddressValidator, } from '@app/common/validation/forms/address-validators'; -import { getNumberOfInscriptionOnUtxo } from '@app/query/bitcoin/ordinals/ordinals-aware-utxo.query'; +import { useNumberOfInscriptionsOnUtxo } from '@app/query/bitcoin/ordinals/inscriptions.hooks'; import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; import { useSendInscriptionState } from '../components/send-inscription-container'; @@ -33,6 +33,7 @@ export function useSendInscriptionForm() { const currentNetwork = useCurrentNetwork(); const { coverFeeFromAdditionalUtxos } = useGenerateSignedOrdinalTx(utxo); + const getNumberOfInscriptionOnUtxo = useNumberOfInscriptionsOnUtxo(); return { currentError, @@ -57,7 +58,7 @@ export function useSendInscriptionForm() { return; } - const numInscriptionsOnUtxo = await getNumberOfInscriptionOnUtxo(utxo.txid, utxo.vout); + const numInscriptionsOnUtxo = getNumberOfInscriptionOnUtxo(utxo.txid, utxo.vout); if (numInscriptionsOnUtxo > 1) { setShowError('Sending inscription from utxo with multiple inscriptions is unsupported'); return; diff --git a/src/app/pages/send/send-crypto-asset-form/family/bitcoin/hooks/use-calculate-max-spend.ts b/src/app/pages/send/send-crypto-asset-form/family/bitcoin/hooks/use-calculate-max-spend.ts index 7f6f7c41b88..c63aecbe149 100644 --- a/src/app/pages/send/send-crypto-asset-form/family/bitcoin/hooks/use-calculate-max-spend.ts +++ b/src/app/pages/send/send-crypto-asset-form/family/bitcoin/hooks/use-calculate-max-spend.ts @@ -7,7 +7,7 @@ import { createMoney } from '@shared/models/money.model'; import { satToBtc } from '@app/common/money/unit-conversion'; import { BtcSizeFeeEstimator } from '@app/common/transactions/bitcoin/fees/btc-size-fee-estimator'; -import { useCurrentNativeSegwitAddressBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query'; +import { useCurrentNativeSegwitAddressBalance } from '@app/query/bitcoin/balance/btc-native-segwit-balance.hooks'; import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client'; import { useAverageBitcoinFeeRates } from '@app/query/bitcoin/fees/fee-estimates.hooks'; diff --git a/src/app/pages/send/send-crypto-asset-form/form/brc-20/use-brc20-send-form.tsx b/src/app/pages/send/send-crypto-asset-form/form/brc-20/use-brc20-send-form.tsx index 8ea77edd1ea..6c7de9111f5 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/brc-20/use-brc20-send-form.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/brc-20/use-brc20-send-form.tsx @@ -20,7 +20,7 @@ import { import { tokenAmountValidator } from '@app/common/validation/forms/amount-validators'; import { currencyAmountValidator } from '@app/common/validation/forms/currency-validators'; import { useUpdatePersistedSendFormValues } from '@app/features/popup-send-form-restoration/use-update-persisted-send-form-values'; -import { useSpendableCurrentNativeSegwitAccountUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks'; +import { useCurrentNativeSegwitAccountSpendableUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks'; import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; @@ -44,7 +44,7 @@ export function useBrc20SendForm({ balance, tick, decimals }: UseBrc20SendFormAr const navigate = useNavigate(); const currentNetwork = useCurrentNetwork(); const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner(); - const { data: utxos = [], refetch } = useSpendableCurrentNativeSegwitAccountUtxos(); + const { data: utxos = [], refetch } = useCurrentNativeSegwitAccountSpendableUtxos(); // Forcing a refetch to ensure UTXOs are fresh useOnMount(() => refetch()); diff --git a/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form.tsx b/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form.tsx index 704c0f462e6..1fa15be9b5d 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form.tsx @@ -9,7 +9,7 @@ import { CryptoCurrencies } from '@shared/models/currencies.model'; import { BtcIcon } from '@app/components/icons/btc-icon'; import { HighFeeDrawer } from '@app/features/high-fee-drawer/high-fee-drawer'; -import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query'; +import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/btc-native-segwit-balance.hooks'; import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks'; import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; diff --git a/src/app/pages/send/send-crypto-asset-form/form/btc/use-btc-send-form.tsx b/src/app/pages/send/send-crypto-asset-form/form/btc/use-btc-send-form.tsx index 37d63786322..d502119d4da 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/btc/use-btc-send-form.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/btc/use-btc-send-form.tsx @@ -24,8 +24,8 @@ import { currencyAmountValidator, } from '@app/common/validation/forms/currency-validators'; import { useUpdatePersistedSendFormValues } from '@app/features/popup-send-form-restoration/use-update-persisted-send-form-values'; -import { useSpendableCurrentNativeSegwitAccountUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks'; -import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query'; +import { useCurrentNativeSegwitAccountSpendableUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks'; +import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/btc-native-segwit-balance.hooks'; import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; @@ -37,7 +37,7 @@ export function useBtcSendForm() { const formRef = useRef>(null); const currentNetwork = useCurrentNetwork(); const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner(); - const { data: utxos = [], refetch } = useSpendableCurrentNativeSegwitAccountUtxos(); + const { data: utxos = [], refetch } = useCurrentNativeSegwitAccountSpendableUtxos(); const btcCryptoCurrencyAssetBalance = useNativeSegwitBalance(nativeSegwitSigner.address); const { whenWallet } = useWalletType(); const sendFormNavigate = useSendFormNavigate(); diff --git a/src/app/pages/sign-out-confirm/sign-out-confirm.layout.tsx b/src/app/pages/sign-out-confirm/sign-out-confirm.layout.tsx index 405b3045c39..52e1dc9d5db 100644 --- a/src/app/pages/sign-out-confirm/sign-out-confirm.layout.tsx +++ b/src/app/pages/sign-out-confirm/sign-out-confirm.layout.tsx @@ -40,7 +40,7 @@ export function SignOutConfirmLayout(props: SignOutConfirmLayoutProps) { ledger: ` you'll need to reconnect your Ledger to sign back into your wallet.`, })} - + {whenWallet({ software: ( } spacing="tight"> @@ -49,7 +49,7 @@ export function SignOutConfirmLayout(props: SignOutConfirmLayoutProps) { ), ledger: <>, })} - + )} - - - + + ); diff --git a/src/app/pages/view-secret-key/components/view-secret-key.content.tsx b/src/app/pages/view-secret-key/components/view-secret-key.content.tsx new file mode 100644 index 00000000000..ea124ff4a10 --- /dev/null +++ b/src/app/pages/view-secret-key/components/view-secret-key.content.tsx @@ -0,0 +1,24 @@ +import { styled } from 'leather-styles/jsx'; + +export function ViewSecretKeyContent(): React.JSX.Element { + return ( + <> + + Your
Secret Key +
+ + These 24 words are your Secret Key. They create your account, and you sign in on different + devices with them. Make sure to save these somewhere safe. + + +
+ + If you lose these words, you lose your account. + + + ); +} diff --git a/src/app/pages/view-secret-key/view-secret-key.tsx b/src/app/pages/view-secret-key/view-secret-key.tsx index 5ad276733fd..20d61109b05 100644 --- a/src/app/pages/view-secret-key/view-secret-key.tsx +++ b/src/app/pages/view-secret-key/view-secret-key.tsx @@ -1,21 +1,18 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Stack } from '@stacks/ui'; -import { styled } from 'leather-styles/jsx'; -import { token } from 'leather-styles/tokens'; - import { RouteUrls } from '@shared/route-urls'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { useRouteHeader } from '@app/common/hooks/use-route-header'; -import { LeatherButton } from '@app/components/button/button'; -import { CenteredPageContainer } from '@app/components/centered-page-container'; import { Header } from '@app/components/header'; import { RequestPassword } from '@app/components/request-password'; +import { TwoColumnLayout } from '@app/components/secret-key/two-column.layout'; import { SecretKeyDisplayer } from '@app/features/secret-key-displayer/secret-key-displayer'; import { useDefaultWalletSecretKey } from '@app/store/in-memory-key/in-memory-key.selectors'; +import { ViewSecretKeyContent } from './components/view-secret-key.content'; + export function ViewSecretKey() { const analytics = useAnalytics(); const navigate = useNavigate(); @@ -28,39 +25,20 @@ export function ViewSecretKey() { void analytics.page('view', '/save-secret-key'); }, [analytics]); - return ( - - - {!showSecretKey ? ( - setShowSecretKey(true)} - /> - ) : ( - <> - Your Secret Key - - These 24 words are your Secret Key. They create your account, and you sign in on - different devices with them. Make sure to save these somewhere safe. - + if (showSecretKey) { + return ( + } + rightColumn={} + /> + ); + } - - If you lose these words, you lose your account. - - - - navigate(RouteUrls.Home)}>I've saved it - - )} - - + return ( + setShowSecretKey(true)} + /> ); } diff --git a/src/app/query/bitcoin/address/address.utils.ts b/src/app/query/bitcoin/address/address.utils.ts index 152c520cf1d..633a062978b 100644 --- a/src/app/query/bitcoin/address/address.utils.ts +++ b/src/app/query/bitcoin/address/address.utils.ts @@ -1,6 +1,9 @@ import { BTC_DECIMALS } from '@shared/constants'; import { BitcoinCryptoCurrencyAssetBalance } from '@shared/models/crypto-asset-balance.model'; import type { Money } from '@shared/models/money.model'; +import { isEmptyArray } from '@shared/utils'; + +import { UtxoResponseItem } from '../bitcoin-client'; export function createBitcoinCryptoCurrencyAssetTypeWrapper( balance: Money @@ -17,3 +20,7 @@ export function createBitcoinCryptoCurrencyAssetTypeWrapper( type: 'crypto-currency', }; } + +export function hasInscriptions(utxos: UtxoResponseItem[]) { + return !isEmptyArray(utxos); +} diff --git a/src/app/query/bitcoin/address/tests/transactions-by-address.spec.ts b/src/app/query/bitcoin/address/tests/transactions-by-address.spec.ts deleted file mode 100644 index 17b37b3e847..00000000000 --- a/src/app/query/bitcoin/address/tests/transactions-by-address.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { calculateOutboundPendingTxsValue } from '../transactions-by-address.hooks'; -import { mockAddress, mockPendingTxs1, mockPendingTxs2, mockPendingTxs3 } from './mock-txs'; - -describe(calculateOutboundPendingTxsValue.name, () => { - test('should return 0 if no pending txs', () => { - expect(calculateOutboundPendingTxsValue([], mockAddress)).toEqual(0); - }); - - test('should return sum of pending txs', () => { - expect(calculateOutboundPendingTxsValue(mockPendingTxs1, mockAddress)).toEqual(14165); - expect(calculateOutboundPendingTxsValue(mockPendingTxs2, mockAddress)).toEqual(28330); - expect(calculateOutboundPendingTxsValue(mockPendingTxs3, mockAddress)).toEqual(14165); - }); -}); diff --git a/src/app/query/bitcoin/address/transactions-by-address.hooks.ts b/src/app/query/bitcoin/address/transactions-by-address.hooks.ts index 5a30410613d..862e81db879 100644 --- a/src/app/query/bitcoin/address/transactions-by-address.hooks.ts +++ b/src/app/query/bitcoin/address/transactions-by-address.hooks.ts @@ -10,7 +10,7 @@ import { useGetBitcoinTransactionsByAddressQuery, useGetBitcoinTransactionsByAddressesQuery, } from './transactions-by-address.query'; -import { useAllSpendableNativeSegwitUtxos } from './utxos-by-address.hooks'; +import { useAllSpendableUtxosByAddress } from './utxos-by-address.hooks'; function useFilterAddressPendingTransactions() { return useCallback((txs: BitcoinTx[]) => { @@ -72,7 +72,7 @@ function filterMissingUtxosPendingTxs( export function useBitcoinPendingTransactionsBalance(address: string) { const filterPendingTransactions = useFilterAddressPendingTransactions(); - const { data: utxos } = useAllSpendableNativeSegwitUtxos(address); + const { data: utxos } = useAllSpendableUtxosByAddress(address); return useGetBitcoinTransactionsByAddressQuery(address, { select(txs) { diff --git a/src/app/query/bitcoin/address/transactions-by-address.spec.ts b/src/app/query/bitcoin/address/transactions-by-address.spec.ts new file mode 100644 index 00000000000..94f3de555ea --- /dev/null +++ b/src/app/query/bitcoin/address/transactions-by-address.spec.ts @@ -0,0 +1,26 @@ +import { + mockBitcoinTestnetAddress, + mockPendingTxs1, + mockPendingTxs2, + mockPendingTxs3, +} from '@tests/mocks/mock-btc-txs'; + +import { calculateOutboundPendingTxsValue } from './transactions-by-address.hooks'; + +describe(calculateOutboundPendingTxsValue.name, () => { + test('that it returns 0 if there are no pending txs', () => { + expect(calculateOutboundPendingTxsValue([], mockBitcoinTestnetAddress)).toEqual(0); + }); + + test('that it returns the correct total sum of pending txs', () => { + expect(calculateOutboundPendingTxsValue(mockPendingTxs1, mockBitcoinTestnetAddress)).toEqual( + 14165 + ); + expect(calculateOutboundPendingTxsValue(mockPendingTxs2, mockBitcoinTestnetAddress)).toEqual( + 28330 + ); + expect(calculateOutboundPendingTxsValue(mockPendingTxs3, mockBitcoinTestnetAddress)).toEqual( + 14165 + ); + }); +}); diff --git a/src/app/query/bitcoin/address/utxos-by-address.hooks.ts b/src/app/query/bitcoin/address/utxos-by-address.hooks.ts index 50cde93aa8b..b824117ee05 100644 --- a/src/app/query/bitcoin/address/utxos-by-address.hooks.ts +++ b/src/app/query/bitcoin/address/utxos-by-address.hooks.ts @@ -1,27 +1,41 @@ import { useCallback } from 'react'; +import { InscriptionResponseItem } from '@shared/models/inscription.model'; + import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; -import { UtxoResponseItem } from '../bitcoin-client'; -import { useInscriptionByAddressQuery } from '../ordinals/use-inscriptions.query'; +import { TaprootUtxo, UtxoResponseItem } from '../bitcoin-client'; +import { useInscriptionsByAddressQuery } from '../ordinals/inscriptions.query'; import { useBitcoinPendingTransactionsInputs } from './transactions-by-address.hooks'; import { useGetUtxosByAddressQuery } from './utxos-by-address.query'; +export function filterUtxosWithInscriptions( + inscriptions: InscriptionResponseItem[], + utxos: TaprootUtxo[] | UtxoResponseItem[] +) { + return utxos.filter( + utxo => + !inscriptions?.some( + inscription => `${utxo.txid}:${utxo.vout.toString()}` === inscription.output + ) + ); +} + /** * Warning: ⚠️ These are **all** UTXOs, including Stamped and Inscribed UTXOs. - * You should probably use `useSpendableCurrentNativeSegwitAccountUtxos` instead. + * You should probably use `useCurrentNativeSegwitAccountSpendableUtxos` instead. */ export function useCurrentNativeSegwitUtxos() { const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner(); return useGetUtxosByAddressQuery(nativeSegwitSigner.address); } -function useFilterAddressNativeSegwitInscriptions(address: string) { +function useFilterInscriptionsByAddress(address: string) { const { - data: inscriptions, + data: inscriptionsList, hasNextPage: hasMoreInscriptionsToLoad, isLoading: isLoadingInscriptions, - } = useInscriptionByAddressQuery(address); + } = useInscriptionsByAddressQuery(address); return useCallback( (utxos: UtxoResponseItem[]) => { @@ -29,20 +43,15 @@ function useFilterAddressNativeSegwitInscriptions(address: string) { // are loading, assume nothing is spendable if (hasMoreInscriptionsToLoad || isLoadingInscriptions) return []; - const inscribedUtxos = inscriptions?.pages.flatMap(page => page.results) ?? []; - return utxos.filter( - utxo => - !inscribedUtxos.some( - inscription => - utxo.txid === inscription.tx_id && utxo.vout === Number(inscription.offset) - ) - ); + const inscriptions = inscriptionsList?.pages.flatMap(page => page.results) ?? []; + + return filterUtxosWithInscriptions(inscriptions, utxos); }, - [hasMoreInscriptionsToLoad, inscriptions?.pages, isLoadingInscriptions] + [hasMoreInscriptionsToLoad, inscriptionsList?.pages, isLoadingInscriptions] ); } -function useFilterAddressNativeSegwitPendingTxsUtxos(address: string) { +function useFilterPendingUtxosByAddress(address: string) { const { data: pendingInputs = [] } = useBitcoinPendingTransactionsInputs(address); return useCallback( @@ -58,8 +67,8 @@ function useFilterAddressNativeSegwitPendingTxsUtxos(address: string) { ); } -export function useAllSpendableNativeSegwitUtxos(address: string) { - const filterOutInscriptions = useFilterAddressNativeSegwitInscriptions(address); +export function useAllSpendableUtxosByAddress(address: string) { + const filterOutInscriptions = useFilterInscriptionsByAddress(address); return useGetUtxosByAddressQuery(address, { select(utxos) { return filterOutInscriptions(utxos); @@ -67,9 +76,9 @@ export function useAllSpendableNativeSegwitUtxos(address: string) { }); } -function useSpendableAndNotPendingNativeSegwitUtxos(address: string) { - const filterOutInscriptions = useFilterAddressNativeSegwitInscriptions(address); - const filterOutPendingTxsUtxos = useFilterAddressNativeSegwitPendingTxsUtxos(address); +function useSpendableAndNotPendingUtxosByAddress(address: string) { + const filterOutInscriptions = useFilterInscriptionsByAddress(address); + const filterOutPendingTxsUtxos = useFilterPendingUtxosByAddress(address); return useGetUtxosByAddressQuery(address, { select(utxos) { @@ -78,7 +87,7 @@ function useSpendableAndNotPendingNativeSegwitUtxos(address: string) { }); } -export function useSpendableCurrentNativeSegwitAccountUtxos() { +export function useCurrentNativeSegwitAccountSpendableUtxos() { const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner(); - return useSpendableAndNotPendingNativeSegwitUtxos(nativeSegwitSigner.address); + return useSpendableAndNotPendingUtxosByAddress(nativeSegwitSigner.address); } diff --git a/src/app/query/bitcoin/address/utxos-by-address.query.ts b/src/app/query/bitcoin/address/utxos-by-address.query.ts index 01413e667f4..df64ef345a8 100644 --- a/src/app/query/bitcoin/address/utxos-by-address.query.ts +++ b/src/app/query/bitcoin/address/utxos-by-address.query.ts @@ -1,9 +1,16 @@ import { useQuery } from '@tanstack/react-query'; +import { getTaprootAddress } from '@shared/crypto/bitcoin/bitcoin.utils'; + +import { createCounter } from '@app/common/utils/counter'; import { AppUseQueryConfig } from '@app/query/query-config'; +import { useCurrentAccountIndex } from '@app/store/accounts/account'; +import { useCurrentTaprootAccount } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; import { useBitcoinClient } from '@app/store/common/api-clients.hooks'; +import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; -import { UtxoResponseItem } from '../bitcoin-client'; +import { TaprootUtxo, UtxoResponseItem } from '../bitcoin-client'; +import { hasInscriptions } from './address.utils'; const staleTime = 3 * 60 * 1000; @@ -22,3 +29,61 @@ export function useGetUtxosByAddressQuery { + let currentNumberOfAddressesWithoutOrdinals = 0; + const addressIndexCounter = createCounter(0); + let foundUnspentTransactions: TaprootUtxo[] = []; + while ( + currentNumberOfAddressesWithoutOrdinals < stopSearchAfterNumberAddressesWithoutOrdinals + ) { + const address = getTaprootAddress({ + index: addressIndexCounter.getValue(), + keychain: account?.keychain, + network: network.chain.bitcoin.network, + }); + + const unspentTransactions = await client.addressApi.getUtxosByAddress(address); + + if (!hasInscriptions(unspentTransactions)) { + currentNumberOfAddressesWithoutOrdinals += 1; + addressIndexCounter.increment(); + continue; + } + + foundUnspentTransactions = [ + ...unspentTransactions.map(utxo => ({ + // adds addresss index of which utxo belongs + ...utxo, + addressIndex: addressIndexCounter.getValue(), + })), + ...foundUnspentTransactions, + ]; + + currentNumberOfAddressesWithoutOrdinals = 0; + addressIndexCounter.increment(); + } + return foundUnspentTransactions; + }, + { + refetchInterval: 15000, + refetchOnWindowFocus: false, + } + ); +} diff --git a/src/app/query/bitcoin/address/utxos-by-address.spec.tsx b/src/app/query/bitcoin/address/utxos-by-address.spec.tsx new file mode 100644 index 00000000000..170f005ea2d --- /dev/null +++ b/src/app/query/bitcoin/address/utxos-by-address.spec.tsx @@ -0,0 +1,11 @@ +import { mockInscriptionsList } from '@tests/mocks/mock-inscriptions'; +import { mockUtxos } from '@tests/mocks/mock-utxos'; + +import { filterUtxosWithInscriptions } from './utxos-by-address.hooks'; + +describe(filterUtxosWithInscriptions, () => { + test('that it filters out utxos with inscriptions so they are not spent', () => { + const filteredUtxos = filterUtxosWithInscriptions(mockInscriptionsList, mockUtxos); + expect(filteredUtxos).toEqual([]); + }); +}); diff --git a/src/app/query/bitcoin/balance/bitcoin-balances.query.ts b/src/app/query/bitcoin/balance/bitcoin-balances.query.ts deleted file mode 100644 index af78a0a16d5..00000000000 --- a/src/app/query/bitcoin/balance/bitcoin-balances.query.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { useMemo } from 'react'; - -import BigNumber from 'bignumber.js'; - -import { createMoney } from '@shared/models/money.model'; -import { isDefined, isUndefined } from '@shared/utils'; - -import { sumNumbers } from '@app/common/math/helpers'; -import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; - -import { createBitcoinCryptoCurrencyAssetTypeWrapper } from '../address/address.utils'; -import { useAllSpendableNativeSegwitUtxos } from '../address/utxos-by-address.hooks'; -import { useOrdinalsAwareUtxoQueries } from '../ordinals/ordinals-aware-utxo.query'; -import { useTaprootAccountUtxosQuery } from '../ordinals/use-taproot-address-utxos.query'; - -function useGetBitcoinBalanceByAddress(address: string) { - const { data: utxos } = useAllSpendableNativeSegwitUtxos(address); - - return useMemo(() => { - if (isUndefined(utxos)) return createMoney(new BigNumber(0), 'BTC'); - return createMoney(sumNumbers(utxos.map(utxo => utxo.value)), 'BTC'); - }, [utxos]); -} - -// While wallet is in address reuse mode, it's simple enough to establish -// balance from a single query -export function useNativeSegwitBalance(address: string) { - const balance = useGetBitcoinBalanceByAddress(address); - return useMemo(() => createBitcoinCryptoCurrencyAssetTypeWrapper(balance), [balance]); -} - -export function useCurrentNativeSegwitAddressBalance() { - const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner(); - return useGetBitcoinBalanceByAddress(nativeSegwitSigner.address); -} - -export function useCurrentTaprootAccountUninscribedUtxos() { - const { data: utxos = [] } = useTaprootAccountUtxosQuery(); - const utxoQueries = useOrdinalsAwareUtxoQueries(utxos); - - return useMemo( - () => - utxoQueries - .map(query => query.data) - .filter(isDefined) - // If tx isn't confirmed, we can't tell yet whether or not it has - // inscriptions - .filter(utxo => utxo.status.confirmed) - .filter(utxo => !utxo.inscriptions), - [utxoQueries] - ); -} - -// Must be ordinals-aware and exclude utxo values that contain inscriptions -export function useCurrentTaprootAccountBalance() { - const uninscribedUtxos = useCurrentTaprootAccountUninscribedUtxos(); - return useMemo( - () => createMoney(sumNumbers(uninscribedUtxos.map(utxo => Number(utxo.value))), 'BTC'), - [uninscribedUtxos] - ); -} diff --git a/src/app/query/bitcoin/balance/btc-balance.hooks.ts b/src/app/query/bitcoin/balance/btc-balance.hooks.ts new file mode 100644 index 00000000000..99d6724f33a --- /dev/null +++ b/src/app/query/bitcoin/balance/btc-balance.hooks.ts @@ -0,0 +1,19 @@ +import { useMemo } from 'react'; + +import BigNumber from 'bignumber.js'; + +import { createMoney } from '@shared/models/money.model'; +import { isUndefined } from '@shared/utils'; + +import { sumNumbers } from '@app/common/math/helpers'; + +import { useAllSpendableUtxosByAddress } from '../address/utxos-by-address.hooks'; + +export function useGetBitcoinBalanceByAddress(address: string) { + const { data: utxos } = useAllSpendableUtxosByAddress(address); + + return useMemo(() => { + if (isUndefined(utxos)) return createMoney(new BigNumber(0), 'BTC'); + return createMoney(sumNumbers(utxos.map(utxo => utxo.value)), 'BTC'); + }, [utxos]); +} diff --git a/src/app/query/bitcoin/balance/btc-native-segwit-balance.hooks.ts b/src/app/query/bitcoin/balance/btc-native-segwit-balance.hooks.ts new file mode 100644 index 00000000000..ba76ea285cd --- /dev/null +++ b/src/app/query/bitcoin/balance/btc-native-segwit-balance.hooks.ts @@ -0,0 +1,17 @@ +import { useMemo } from 'react'; + +import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; + +import { createBitcoinCryptoCurrencyAssetTypeWrapper } from '../address/address.utils'; +import { useGetBitcoinBalanceByAddress } from './btc-balance.hooks'; + +// Balance is derived from a single query in address reuse mode +export function useNativeSegwitBalance(address: string) { + const balance = useGetBitcoinBalanceByAddress(address); + return useMemo(() => createBitcoinCryptoCurrencyAssetTypeWrapper(balance), [balance]); +} + +export function useCurrentNativeSegwitAddressBalance() { + const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner(); + return useGetBitcoinBalanceByAddress(nativeSegwitSigner.address); +} diff --git a/src/app/query/bitcoin/balance/btc-taproot-balance.hooks.ts b/src/app/query/bitcoin/balance/btc-taproot-balance.hooks.ts new file mode 100644 index 00000000000..9453ee5dde2 --- /dev/null +++ b/src/app/query/bitcoin/balance/btc-taproot-balance.hooks.ts @@ -0,0 +1,32 @@ +import { useMemo } from 'react'; + +import { createMoney } from '@shared/models/money.model'; + +import { sumNumbers } from '@app/common/math/helpers'; + +import { filterUtxosWithInscriptions } from '../address/utxos-by-address.hooks'; +import { useTaprootAccountUtxosQuery } from '../address/utxos-by-address.query'; +import { TaprootUtxo } from '../bitcoin-client'; +import { useGetInscriptionsInfiniteQuery } from '../ordinals/inscriptions.query'; + +export function useCurrentTaprootAccountUninscribedUtxos() { + const { data: utxos = [] } = useTaprootAccountUtxosQuery(); + const query = useGetInscriptionsInfiniteQuery(); + + return useMemo(() => { + const inscriptions = query.data?.pages?.flatMap(page => page.inscriptions) ?? []; + return filterUtxosWithInscriptions( + inscriptions, + utxos.filter(utxo => utxo.status.confirmed) + ) as TaprootUtxo[]; + }, [query.data?.pages, utxos]); +} + +export function useCurrentTaprootAccountBalance() { + const uninscribedUtxos = useCurrentTaprootAccountUninscribedUtxos(); + + return useMemo( + () => createMoney(sumNumbers(uninscribedUtxos.map(utxo => Number(utxo.value))), 'BTC'), + [uninscribedUtxos] + ); +} diff --git a/src/app/query/bitcoin/bitcoin-client.ts b/src/app/query/bitcoin/bitcoin-client.ts index 6eea63722c9..fb85849007c 100644 --- a/src/app/query/bitcoin/bitcoin-client.ts +++ b/src/app/query/bitcoin/bitcoin-client.ts @@ -20,6 +20,10 @@ export interface NativeSegwitUtxo extends UtxoResponseItem { addressIndex: number; } +export interface TaprootUtxo extends UtxoResponseItem { + addressIndex: number; +} + class AddressApi { constructor(public configuration: Configuration) {} diff --git a/src/app/query/bitcoin/ordinals/brc20/use-check-order-status.ts b/src/app/query/bitcoin/ordinals/brc20/use-check-order-status.ts index 693772dae64..f5d57cbd8e6 100644 --- a/src/app/query/bitcoin/ordinals/brc20/use-check-order-status.ts +++ b/src/app/query/bitcoin/ordinals/brc20/use-check-order-status.ts @@ -9,7 +9,7 @@ import { } from '@app/store/ordinals/ordinals.slice'; import { useOrdinalsbotClient } from '../../ordinalsbot-client'; -import { fetchInscripionById } from '../use-inscription-by-id'; +import { fetchInscripionById } from '../inscription-by-id.query'; export function useCheckOrderStatuses(ids: string[]) { const ordinalsbotClient = useOrdinalsbotClient(); diff --git a/src/app/query/bitcoin/ordinals/use-inscription-by-id.ts b/src/app/query/bitcoin/ordinals/inscription-by-id.query.ts similarity index 77% rename from src/app/query/bitcoin/ordinals/use-inscription-by-id.ts rename to src/app/query/bitcoin/ordinals/inscription-by-id.query.ts index d0ce9a161d5..6a609fe22b7 100644 --- a/src/app/query/bitcoin/ordinals/use-inscription-by-id.ts +++ b/src/app/query/bitcoin/ordinals/inscription-by-id.query.ts @@ -1,9 +1,8 @@ import axios from 'axios'; +import { HIRO_INSCRIPTIONS_API_URL } from '@shared/constants'; import { InscriptionResponseItem } from '@shared/models/inscription.model'; -import { HIRO_INSCRIPTIONS_API_URL } from '@app/query/query-config'; - export async function fetchInscripionById(id: string) { return axios.get(`${HIRO_INSCRIPTIONS_API_URL}}/${id}`); } diff --git a/src/app/query/bitcoin/ordinals/use-text-ordinal-content.query.ts b/src/app/query/bitcoin/ordinals/inscription-text-content.query.ts similarity index 69% rename from src/app/query/bitcoin/ordinals/use-text-ordinal-content.query.ts rename to src/app/query/bitcoin/ordinals/inscription-text-content.query.ts index 725509c8977..47cf1e2004b 100644 --- a/src/app/query/bitcoin/ordinals/use-text-ordinal-content.query.ts +++ b/src/app/query/bitcoin/ordinals/inscription-text-content.query.ts @@ -2,16 +2,16 @@ import { useQuery } from '@tanstack/react-query'; import { QueryPrefixes } from '@app/query/query-prefixes'; -async function getTextInscriptionContent(src: string) { +async function getInscriptionTextContent(src: string) { const res = await fetch(src); if (!res.ok) throw new Error('Failed to fetch ordinal text content'); return res.text(); } -export function useTextInscriptionContentQuery(contentSrc: string) { +export function useInscriptionTextContentQuery(contentSrc: string) { return useQuery( [QueryPrefixes.OrdinalTextContent, contentSrc], - () => getTextInscriptionContent(contentSrc), + () => getInscriptionTextContent(contentSrc), { cacheTime: Infinity, staleTime: Infinity, diff --git a/src/app/query/bitcoin/ordinals/inscription.hooks.ts b/src/app/query/bitcoin/ordinals/inscription.hooks.ts index 23c0228917b..6c99bfab88e 100644 --- a/src/app/query/bitcoin/ordinals/inscription.hooks.ts +++ b/src/app/query/bitcoin/ordinals/inscription.hooks.ts @@ -1,11 +1,10 @@ +import { HIRO_INSCRIPTIONS_API_URL } from '@shared/constants'; import { Inscription, SupportedInscription, whenInscriptionType, } from '@shared/models/inscription.model'; -import { HIRO_INSCRIPTIONS_API_URL } from '@app/query/query-config'; - import { useGetInscriptionQuery } from './inscription.query'; export function createInscriptionInfoUrl(id: string) { diff --git a/src/app/query/bitcoin/ordinals/inscription.query.ts b/src/app/query/bitcoin/ordinals/inscription.query.ts index e4488fa82f7..721ff17b106 100644 --- a/src/app/query/bitcoin/ordinals/inscription.query.ts +++ b/src/app/query/bitcoin/ordinals/inscription.query.ts @@ -1,8 +1,9 @@ -import { useQueries, useQuery } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import { HIRO_INSCRIPTIONS_API_URL } from '@shared/constants'; import { Inscription } from '@shared/models/inscription.model'; -import { AppUseQueryConfig, HIRO_INSCRIPTIONS_API_URL } from '@app/query/query-config'; +import { AppUseQueryConfig } from '@app/query/query-config'; import { QueryPrefixes } from '@app/query/query-prefixes'; const inscriptionQueryOptions = { @@ -36,16 +37,3 @@ export function useGetInscriptionQuery ...options, }); } - -export function useGetInscriptionQueries(ids: string[]) { - return useQueries({ - queries: ids.map(id => { - return { - enabled: !!id, - queryKey: [QueryPrefixes.InscriptionMetadata, id], - queryFn: () => fetchInscription()(id), - ...inscriptionQueryOptions, - }; - }), - }); -} diff --git a/src/app/query/bitcoin/ordinals/inscriptions-by-param.query.ts b/src/app/query/bitcoin/ordinals/inscriptions-by-param.query.ts new file mode 100644 index 00000000000..8eed26a5b3d --- /dev/null +++ b/src/app/query/bitcoin/ordinals/inscriptions-by-param.query.ts @@ -0,0 +1,66 @@ +import * as btc from '@scure/btc-signer'; +import { bytesToHex } from '@stacks/common'; +import { useQueries, useQuery } from '@tanstack/react-query'; + +import { HIRO_INSCRIPTIONS_API_URL } from '@shared/constants'; +import { Paginated } from '@shared/models/api-types'; +import { Inscription } from '@shared/models/inscription.model'; +import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model'; + +import { AppUseQueryConfig } from '@app/query/query-config'; + +type FetchInscriptionResp = Awaited>>; + +function fetchInscriptionsByParam() { + return async (param: string) => { + const res = await fetch(`${HIRO_INSCRIPTIONS_API_URL}?${param}`); + if (!res.ok) throw new Error('Error retrieving inscription metadata'); + const data = await res.json(); + return data as Paginated; + }; +} + +const weekInMs = 1000 * 60 * 60 * 24 * 7; +const inscriptionsByOutputQueryOptions = { + staleTime: weekInMs, + cacheTime: weekInMs, +} as const; + +export function useGetInscriptionsByOutputQuery( + transaction: BitcoinTx, + options?: AppUseQueryConfig +) { + const inputsLength = transaction.vin.length; + const index = inputsLength === 1 ? 0 : inputsLength - 2; + const isPending = !transaction.status.confirmed; + const id = isPending ? transaction.vin[index].txid : transaction.txid; + const param = `output=${id}:${index}`; + + return useQuery({ + enabled: !!param, + queryKey: ['inscription-by-param', isPending, param], + queryFn: () => fetchInscriptionsByParam()(param), + ...inscriptionsByOutputQueryOptions, + ...options, + }); +} + +const inscriptionsByOutputQueriesOptions = { + cacheTime: Infinity, + staleTime: 15 * 60 * 1000, + refetchOnWindowFocus: false, +} as const; + +export function useGetInscriptionsByOutputQueries(inputs: btc.TransactionInput[]) { + return useQueries({ + queries: inputs.map(input => { + const param = input.txid ? `output=${bytesToHex(input.txid)}:${input.index}` : ''; + + return { + queryKey: ['inscription-by-param', false, param], + queryFn: () => fetchInscriptionsByParam()(param), + ...inscriptionsByOutputQueriesOptions, + }; + }), + }); +} diff --git a/src/app/query/bitcoin/ordinals/inscriptions.hooks.ts b/src/app/query/bitcoin/ordinals/inscriptions.hooks.ts new file mode 100644 index 00000000000..bdf404ea130 --- /dev/null +++ b/src/app/query/bitcoin/ordinals/inscriptions.hooks.ts @@ -0,0 +1,32 @@ +import { useCallback } from 'react'; + +import { InscriptionResponseItem } from '@shared/models/inscription.model'; +import { isUndefined } from '@shared/utils'; + +import { useGetInscriptionsInfiniteQuery } from './inscriptions.query'; + +interface FindInscriptionsOnUtxoArgs { + index: number; + inscriptions: InscriptionResponseItem[]; + txId: string; +} +export function findInscriptionsOnUtxo({ index, inscriptions, txId }: FindInscriptionsOnUtxoArgs) { + return inscriptions?.filter(inscription => { + return `${txId}:${index.toString()}` === inscription.output; + }); +} + +export function useNumberOfInscriptionsOnUtxo() { + const query = useGetInscriptionsInfiniteQuery(); + const inscriptions = query.data?.pages?.flatMap(page => page.inscriptions); + + return useCallback( + (txId: string, index: number) => { + if (isUndefined(inscriptions)) return 0; + const foundInscriptionsOnUtxo = findInscriptionsOnUtxo({ index, inscriptions, txId }); + if (isUndefined(foundInscriptionsOnUtxo)) return 0; + return foundInscriptionsOnUtxo?.length; + }, + [inscriptions] + ); +} diff --git a/src/app/query/bitcoin/ordinals/use-inscriptions.query.ts b/src/app/query/bitcoin/ordinals/inscriptions.query.ts similarity index 92% rename from src/app/query/bitcoin/ordinals/use-inscriptions.query.ts rename to src/app/query/bitcoin/ordinals/inscriptions.query.ts index 3891767a61c..474a9f8f4a5 100644 --- a/src/app/query/bitcoin/ordinals/use-inscriptions.query.ts +++ b/src/app/query/bitcoin/ordinals/inscriptions.query.ts @@ -2,31 +2,23 @@ import { useCallback, useEffect } from 'react'; import { useInfiniteQuery } from '@tanstack/react-query'; +import { HIRO_INSCRIPTIONS_API_URL } from '@shared/constants'; +import { getTaprootAddress } from '@shared/crypto/bitcoin/bitcoin.utils'; import { InscriptionResponseItem } from '@shared/models/inscription.model'; import { ensureArray } from '@shared/utils'; import { createNumArrayOfRange } from '@app/common/utils'; -import { HIRO_INSCRIPTIONS_API_URL } from '@app/query/query-config'; import { QueryPrefixes } from '@app/query/query-prefixes'; import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useCurrentTaprootAccount } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; -import { getTaprootAddress } from './utils'; - const stopSearchAfterNumberAddressesWithoutOrdinals = 20; const addressesSimultaneousFetchLimit = 5; -// max limit value in Hiro API - 60 +// Hiro API max limit = 60 const inscriptionsLazyLoadLimit = 20; -interface InscriptionsQueryResponse { - results: InscriptionResponseItem[]; - limit: number; - offset: number; - total: number; -} - interface InfiniteQueryPageParam { pageParam?: { fromIndex: number; @@ -36,6 +28,13 @@ interface InfiniteQueryPageParam { }; } +interface InscriptionsQueryResponse { + results: InscriptionResponseItem[]; + limit: number; + offset: number; + total: number; +} + async function fetchInscriptions(addresses: string | string[], offset = 0, limit = 60) { const params = new URLSearchParams(); ensureArray(addresses).forEach(address => params.append('address', address)); @@ -49,9 +48,9 @@ async function fetchInscriptions(addresses: string | string[], offset = 0, limit } /** - * Returns all inscriptions for the user's current taproot account + * Returns all inscriptions for the user's current account */ -export function useTaprootInscriptionsInfiniteQuery() { +export function useGetInscriptionsInfiniteQuery() { const network = useCurrentNetwork(); const account = useCurrentTaprootAccount(); const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner(); @@ -185,7 +184,7 @@ export function useTaprootInscriptionsInfiniteQuery() { return query; } -export function useInscriptionByAddressQuery(address: string) { +export function useInscriptionsByAddressQuery(address: string) { const network = useCurrentNetwork(); const query = useInfiniteQuery({ @@ -193,9 +192,9 @@ export function useInscriptionByAddressQuery(address: string) { async queryFn({ pageParam = 0 }) { return fetchInscriptions(address, pageParam); }, - getNextPageParam(prevInscriptionQuery) { - if (prevInscriptionQuery.offset >= prevInscriptionQuery.total) return undefined; - return prevInscriptionQuery.offset + 60; + getNextPageParam(prevInscriptionsQuery) { + if (prevInscriptionsQuery.offset >= prevInscriptionsQuery.total) return undefined; + return prevInscriptionsQuery.offset + 60; }, refetchOnMount: false, refetchOnReconnect: false, diff --git a/src/app/query/bitcoin/ordinals/inscriptions.spec.ts b/src/app/query/bitcoin/ordinals/inscriptions.spec.ts new file mode 100644 index 00000000000..f1f31963598 --- /dev/null +++ b/src/app/query/bitcoin/ordinals/inscriptions.spec.ts @@ -0,0 +1,14 @@ +import { mockInscriptionsList } from '@tests/mocks/mock-inscriptions'; + +import { findInscriptionsOnUtxo } from './inscriptions.hooks'; + +describe(findInscriptionsOnUtxo, () => { + test('that it finds an inscription on a utxo', () => { + const foundInscriptions = findInscriptionsOnUtxo({ + index: 0, + inscriptions: mockInscriptionsList, + txId: '58d44000884f0ba4cdcbeb1ac082e6c802d300c16b0d3251738e8cf6a57397ce', + }); + expect(foundInscriptions.length).toEqual(1); + }); +}); diff --git a/src/app/query/bitcoin/ordinals/ordinals-aware-utxo.query.ts b/src/app/query/bitcoin/ordinals/ordinals-aware-utxo.query.ts deleted file mode 100644 index d6a3baeb2e3..00000000000 --- a/src/app/query/bitcoin/ordinals/ordinals-aware-utxo.query.ts +++ /dev/null @@ -1,81 +0,0 @@ -import * as btc from '@scure/btc-signer'; -import { bytesToHex } from '@stacks/common'; -import { useQueries } from '@tanstack/react-query'; -import * as yup from 'yup'; - -import { isDefined, isTypedArray } from '@shared/utils'; -import { Prettify } from '@shared/utils/type-utils'; - -import { QueryPrefixes } from '@app/query/query-prefixes'; - -import { TaprootUtxo } from './use-taproot-address-utxos.query'; - -/** - * Schema of data used from the `GET https://ordapi.xyz/output/:tx` endpoint. Additional data - * that is not currently used by the app may be returned by this endpoint. - */ -const ordApiGetTransactionOutput = yup - .object({ - address: yup.string(), - all_inscriptions: yup.array().of(yup.string()).optional(), - inscriptions: yup.string(), - script_pubkey: yup.string(), - transaction: yup.string(), - value: yup.string().required(), - }) - .required(); - -type OrdApiInscriptionTxOutput = Prettify>; - -export async function getNumberOfInscriptionOnUtxo(id: string, index: number) { - const resp = await fetchOrdinalsAwareUtxo(id, index); - if (resp.all_inscriptions) return resp.all_inscriptions.length; - if (resp.inscriptions) return 1; - return 0; -} - -function getQueryArgsWithDefaults(utxo: TaprootUtxo | btc.TransactionInput) { - const txId = isTypedArray(utxo.txid) ? bytesToHex(utxo.txid) : utxo.txid ?? ''; - const txIndex = 'vout' in utxo ? utxo.vout : utxo.index ?? 0; - return { txId, txIndex }; -} - -async function fetchOrdinalsAwareUtxo( - txid: string, - index: number -): Promise { - const res = await fetch(`https://ordapi.xyz/output/${txid}:${index}`); - - if (!res.ok) throw new Error('Failed to fetch txid metadata'); - - const data = await res.json(); - if (data.error) throw new Error(data.error); - if (Object.keys(data).length === 0) throw new Error('No output data found'); - return ordApiGetTransactionOutput.validate(data); -} - -function makeOrdinalsAwareUtxoQueryKey(txId: string, txIndex?: number) { - return [QueryPrefixes.InscriptionFromTxid, txId, txIndex] as const; -} - -const queryOptions = { - cacheTime: Infinity, - staleTime: 15 * 60 * 1000, - refetchOnWindowFocus: false, -} as const; - -export function useOrdinalsAwareUtxoQueries(utxos: TaprootUtxo[] | btc.TransactionInput[]) { - return useQueries({ - queries: utxos.map(utxo => { - const { txId, txIndex } = getQueryArgsWithDefaults(utxo); - return { - enable: txId !== '' && isDefined(txIndex), - queryKey: makeOrdinalsAwareUtxoQueryKey(txId, txIndex), - queryFn: () => fetchOrdinalsAwareUtxo(txId, txIndex), - select: (resp: OrdApiInscriptionTxOutput) => - ({ ...utxo, ...resp }) as TaprootUtxo & OrdApiInscriptionTxOutput, - ...queryOptions, - }; - }), - }); -} diff --git a/src/app/query/bitcoin/ordinals/use-inscription-by-output.query.ts b/src/app/query/bitcoin/ordinals/use-inscription-by-output.query.ts deleted file mode 100644 index cd28a2c1c65..00000000000 --- a/src/app/query/bitcoin/ordinals/use-inscription-by-output.query.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; - -import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model'; - -import { AppUseQueryConfig } from '@app/query/query-config'; - -import { FetchInscriptionResp, fetchInscriptionByParam } from './use-inscription-by-param'; - -const weekInMs = 1000 * 60 * 60 * 24 * 7; -const inscriptionsByOutputQueryOptions = { - staleTime: weekInMs, - cacheTime: weekInMs, -} as const; - -export function useGetInscriptionsByOutputQuery( - transaction: BitcoinTx, - options?: AppUseQueryConfig -) { - const inputsLength = transaction.vin.length; - const index = inputsLength === 1 ? 0 : inputsLength - 2; - const isPending = !transaction.status.confirmed; - const id = isPending ? transaction.vin[index].txid : transaction.txid; - const param = `output=${id}:${index}`; - - return useQuery({ - enabled: !!param, - queryKey: ['inscription-by-param', isPending, param], - queryFn: () => fetchInscriptionByParam()(param), - ...inscriptionsByOutputQueryOptions, - ...options, - }); -} diff --git a/src/app/query/bitcoin/ordinals/use-inscription-by-param.ts b/src/app/query/bitcoin/ordinals/use-inscription-by-param.ts deleted file mode 100644 index 7e331e31755..00000000000 --- a/src/app/query/bitcoin/ordinals/use-inscription-by-param.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Paginated } from '@shared/models/api-types'; -import { Inscription } from '@shared/models/inscription.model'; - -import { HIRO_INSCRIPTIONS_API_URL } from '@app/query/query-config'; - -export function fetchInscriptionByParam() { - return async (param: string) => { - const res = await fetch(`${HIRO_INSCRIPTIONS_API_URL}?${param}`); - if (!res.ok) throw new Error('Error retrieving inscription metadata'); - const data = await res.json(); - return data as Paginated; - }; -} - -export type FetchInscriptionResp = Awaited>>; diff --git a/src/app/query/bitcoin/ordinals/use-taproot-address-utxos.query.ts b/src/app/query/bitcoin/ordinals/use-taproot-address-utxos.query.ts deleted file mode 100644 index 7fd936f3b96..00000000000 --- a/src/app/query/bitcoin/ordinals/use-taproot-address-utxos.query.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; - -import { createCounter } from '@app/common/utils/counter'; -import { useCurrentAccountIndex } from '@app/store/accounts/account'; -import { useCurrentTaprootAccount } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; -import { useBitcoinClient } from '@app/store/common/api-clients.hooks'; -import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; - -import { UtxoResponseItem } from '../bitcoin-client'; -import { getTaprootAddress, hasInscriptions } from './utils'; - -const stopSearchAfterNumberAddressesWithoutOrdinals = 20; - -export interface TaprootUtxo extends UtxoResponseItem { - addressIndex: number; -} - -/** - * Returns all utxos for the user's current taproot account. The search for - * utxos iterates through all addresses until a sufficiently large number of - * empty addresses is found. - */ -export function useTaprootAccountUtxosQuery() { - const network = useCurrentNetwork(); - const account = useCurrentTaprootAccount(); - const client = useBitcoinClient(); - - const currentAccountIndex = useCurrentAccountIndex(); - - return useQuery( - ['taproot-address-utxos-metadata', currentAccountIndex, network.id], - async () => { - let currentNumberOfAddressesWithoutOrdinals = 0; - const addressIndexCounter = createCounter(0); - let foundUnspentTransactions: TaprootUtxo[] = []; - while ( - currentNumberOfAddressesWithoutOrdinals < stopSearchAfterNumberAddressesWithoutOrdinals - ) { - const address = getTaprootAddress({ - index: addressIndexCounter.getValue(), - keychain: account?.keychain, - network: network.chain.bitcoin.network, - }); - - const unspentTransactions = await client.addressApi.getUtxosByAddress(address); - - if (!hasInscriptions(unspentTransactions)) { - currentNumberOfAddressesWithoutOrdinals += 1; - addressIndexCounter.increment(); - continue; - } - - foundUnspentTransactions = [ - ...unspentTransactions.map(utxo => ({ - // adds addresss index of which utxo belongs - ...utxo, - addressIndex: addressIndexCounter.getValue(), - })), - ...foundUnspentTransactions, - ]; - - currentNumberOfAddressesWithoutOrdinals = 0; - addressIndexCounter.increment(); - } - return foundUnspentTransactions; - }, - { - refetchInterval: 15000, - refetchOnWindowFocus: false, - } - ); -} diff --git a/src/app/query/bitcoin/ordinals/use-zero-index-taproot-address.ts b/src/app/query/bitcoin/ordinals/use-zero-index-taproot-address.ts deleted file mode 100644 index d09f2aa2db7..00000000000 --- a/src/app/query/bitcoin/ordinals/use-zero-index-taproot-address.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { getTaprootAddress } from '@app/query/bitcoin/ordinals/utils'; -import { useCurrentAccountIndex } from '@app/store/accounts/account'; -import { useTaprootAccount } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; -import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; - -// Temporary - remove with privacy mode -export function useZeroIndexTaprootAddress(accIndex?: number) { - const network = useCurrentNetwork(); - const currentAccountIndex = useCurrentAccountIndex(); - const account = useTaprootAccount(accIndex ?? currentAccountIndex); - - if (!account) throw new Error('Expected keychain to be provided'); - - const address = getTaprootAddress({ - index: 0, - keychain: account.keychain, - network: network.chain.bitcoin.network, - }); - - return address; -} diff --git a/src/app/query/bitcoin/ordinals/utils.ts b/src/app/query/bitcoin/ordinals/utils.ts deleted file mode 100644 index bf1cad29f48..00000000000 --- a/src/app/query/bitcoin/ordinals/utils.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { HDKey } from '@scure/bip32'; - -import { BitcoinNetworkModes } from '@shared/constants'; -import { deriveAddressIndexKeychainFromAccount } from '@shared/crypto/bitcoin/bitcoin.utils'; -import { getTaprootPayment } from '@shared/crypto/bitcoin/p2tr-address-gen'; -import { DerivationPathDepth } from '@shared/crypto/derivation-path.utils'; - -export function hasInscriptions(data: unknown[]) { - return data.length !== 0; -} - -interface GetTaprootAddressArgs { - index: number; - keychain?: HDKey; - network: BitcoinNetworkModes; -} -export function getTaprootAddress({ index, keychain, network }: GetTaprootAddressArgs) { - if (!keychain) throw new Error('Expected keychain to be provided'); - - if (keychain.depth !== DerivationPathDepth.Account) - throw new Error('Expects keychain to be on the account index'); - - const addressIndex = deriveAddressIndexKeychainFromAccount(keychain)(index); - - if (!addressIndex.publicKey) { - throw new Error('Expected publicKey to be defined'); - } - - const payment = getTaprootPayment(addressIndex.publicKey!, network); - - if (!payment.address) throw new Error('Expected address to be defined'); - - return payment.address; -} diff --git a/src/app/query/query-config.ts b/src/app/query/query-config.ts index 3f033b780fb..a5ede1495ab 100644 --- a/src/app/query/query-config.ts +++ b/src/app/query/query-config.ts @@ -9,5 +9,3 @@ export type AppUseQueryConfig = Pick< UseQueryOptions, AllowedReactQueryConfigOptions >; - -export const HIRO_INSCRIPTIONS_API_URL = 'https://api.hiro.so/ordinals/v1/inscriptions'; diff --git a/src/app/store/accounts/blockchain/bitcoin/bitcoin.hooks.ts b/src/app/store/accounts/blockchain/bitcoin/bitcoin.hooks.ts index 8d42bcb952a..9c7e4e0c3f5 100644 --- a/src/app/store/accounts/blockchain/bitcoin/bitcoin.hooks.ts +++ b/src/app/store/accounts/blockchain/bitcoin/bitcoin.hooks.ts @@ -1,3 +1,9 @@ +import { getTaprootAddress } from '@shared/crypto/bitcoin/bitcoin.utils'; + +import { useCurrentAccountIndex } from '@app/store/accounts/account'; +import { useTaprootAccount } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; +import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; + import { useCurrentNativeSegwitAccount } from './native-segwit-account.hooks'; import { useCurrentTaprootAccount } from './taproot-account.hooks'; @@ -7,3 +13,20 @@ export function useHasCurrentBitcoinAccount() { const taproot = useCurrentTaprootAccount(); return !!nativeSegwit && !!taproot; } + +// Temporary - remove with privacy mode +export function useZeroIndexTaprootAddress(accIndex?: number) { + const network = useCurrentNetwork(); + const currentAccountIndex = useCurrentAccountIndex(); + const account = useTaprootAccount(accIndex ?? currentAccountIndex); + + if (!account) throw new Error('Expected keychain to be provided'); + + const address = getTaprootAddress({ + index: 0, + keychain: account.keychain, + network: network.chain.bitcoin.network, + }); + + return address; +} diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 249453e45c8..de5b3469a13 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -80,6 +80,7 @@ export interface NetworkConfiguration { export const HIRO_API_BASE_URL_MAINNET = 'https://api.hiro.so'; export const HIRO_API_BASE_URL_TESTNET = 'https://api.testnet.hiro.so'; +export const HIRO_INSCRIPTIONS_API_URL = 'https://api.hiro.so/ordinals/v1/inscriptions'; export const BITCOIN_API_BASE_URL_MAINNET = 'https://blockstream.info/api'; export const BITCOIN_API_BASE_URL_TESTNET = 'https://blockstream.info/testnet/api'; diff --git a/src/shared/crypto/bitcoin/bitcoin.utils.ts b/src/shared/crypto/bitcoin/bitcoin.utils.ts index d06ad66d349..5b9b2e27592 100644 --- a/src/shared/crypto/bitcoin/bitcoin.utils.ts +++ b/src/shared/crypto/bitcoin/bitcoin.utils.ts @@ -8,6 +8,7 @@ import { whenNetwork } from '@shared/utils'; import { DerivationPathDepth } from '../derivation-path.utils'; import { BtcSignerNetwork } from './bitcoin.network'; +import { getTaprootPayment } from './p2tr-address-gen'; export interface BitcoinAccount { type: PaymentTypes; @@ -195,3 +196,27 @@ function initBitcoinAccount(derivationPath: string, policy: string): BitcoinAcco accountIndex: extractAccountIndexFromPath(derivationPath), }; } + +interface GetTaprootAddressArgs { + index: number; + keychain?: HDKey; + network: BitcoinNetworkModes; +} +export function getTaprootAddress({ index, keychain, network }: GetTaprootAddressArgs) { + if (!keychain) throw new Error('Expected keychain to be provided'); + + if (keychain.depth !== DerivationPathDepth.Account) + throw new Error('Expects keychain to be on the account index'); + + const addressIndex = deriveAddressIndexKeychainFromAccount(keychain)(index); + + if (!addressIndex.publicKey) { + throw new Error('Expected publicKey to be defined'); + } + + const payment = getTaprootPayment(addressIndex.publicKey!, network); + + if (!payment.address) throw new Error('Expected address to be defined'); + + return payment.address; +} diff --git a/src/shared/crypto/bitcoin/p2wsh-p2sh-address-gen.spec.ts b/src/shared/crypto/bitcoin/p2wsh-p2sh-address-gen.spec.ts index 553c429fde7..bf98a066119 100644 --- a/src/shared/crypto/bitcoin/p2wsh-p2sh-address-gen.spec.ts +++ b/src/shared/crypto/bitcoin/p2wsh-p2sh-address-gen.spec.ts @@ -4,9 +4,9 @@ import ecc from '@bitcoinerlab/secp256k1'; import { sha256 } from '@noble/hashes/sha256'; import { base58check } from '@scure/base'; import { HDKey } from '@scure/bip32'; +import { mnemonicToSeedSync } from '@scure/bip39'; import { hashP2WPKH } from '@stacks/transactions'; import { BIP32Factory } from 'bip32'; -import * as bip39 from 'bip39'; import * as bitcoin from 'bitcoinjs-lib'; import { @@ -22,35 +22,6 @@ import { describe('Bitcoin SegWit (P2WPKH-P2SH) address generation', () => { const bip32 = BIP32Factory(ecc); - // - // https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/bip32.spec.ts - describe('Sanity check tests copied from `bitcoinjs-lib` vs other libs', () => { - test('can create a BIP49, bitcoin testnet, account 0, external address', async () => { - const mnemonic = - 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; - const seed = bip39.mnemonicToSeedSync(mnemonic); - expect(seed).toEqual(Buffer.from(await deriveBtcBip49SeedFromMnemonic(mnemonic))); - - const root = bip32.fromSeed(seed); - const keychain = deriveRootBtcKeychain(seed); - expect(root.privateKey?.toString('hex')).toEqual( - Buffer.from(keychain.privateKey!).toString('hex') - ); - - const path = "m/49'/1'/0'/0/0"; - const child = root.derivePath(path); - - const bitcoinPayment = bitcoin.payments.p2sh({ - redeem: bitcoin.payments.p2wpkh({ - pubkey: child.publicKey, - network: bitcoin.networks.testnet, - }), - network: bitcoin.networks.testnet, - }); - - expect(bitcoinPayment.address).toEqual('2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2'); - }); - }); const phrase = 'above view guide write long gift chimney own guide mirror word ski code monster gauge bracket until stem feed scale smart truth toy limb'; @@ -110,8 +81,8 @@ describe('Bitcoin SegWit (P2WPKH-P2SH) address generation', () => { ] as const; describe.each(keys)('Core libraries: bip32, bip39, bitcoinjs-lib', key => { - const seed = bip39.mnemonicToSeedSync(phrase); - const root = bip32.fromSeed(seed); + const seed = mnemonicToSeedSync(phrase); + const root = bip32.fromSeed(Buffer.from(seed)); const child = root.derivePath(key.path); describe(key.path, () => { diff --git a/src/shared/environment.ts b/src/shared/environment.ts index 777ad7d36a9..84bb4bc6904 100644 --- a/src/shared/environment.ts +++ b/src/shared/environment.ts @@ -12,4 +12,3 @@ export const WALLET_ENVIRONMENT = process.env.WALLET_ENVIRONMENT ?? 'unknown'; export const LEDGER_BITCOIN_ENABLED = process.env.LEDGER_BITCOIN_ENABLED === 'true'; // ts-unused-exports:disable-next-line export const SWAP_ENABLED = process.env.SWAP_ENABLED === 'true'; -export const TEST_ACCOUNT_SECRET_KEY = process.env.TEST_ACCOUNT_SECRET_KEY ?? ''; diff --git a/src/shared/models/inscription.model.ts b/src/shared/models/inscription.model.ts index de6a0c2ad71..0726a313f0d 100644 --- a/src/shared/models/inscription.model.ts +++ b/src/shared/models/inscription.model.ts @@ -1,31 +1,26 @@ export interface InscriptionResponseItem { address: string; - content: string; - content_length: string; + content_length: number; content_type: string; - genesis_fee: string; - genesis_height: string; - genesis_transaction: string; - id: string; - inscription_number: number; - location: string; - offset: string; - output: string; - output_value: string; - preview: string; - sat: string; - timestamp: string; - // Outdated props? + curse_type: string | null; genesis_address: string; genesis_block_hash: string; genesis_block_height: number; + genesis_fee: string; genesis_timestamp: number; genesis_tx_id: string; + id: string; + location: string; mime_type: string; number: number; + offset: string; + output: string; + recursive: boolean; + recursion_refs: string | null; sat_coinbase_height: number; sat_ordinal: string; sat_rarity: string; + timestamp: number; tx_id: string; value: string; } diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 3671b6e048f..77611acbe31 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -51,3 +51,7 @@ type NetworkMap = Record; export function whenNetwork(mode: NetworkModes) { return >(networkMap: T) => networkMap[mode] as T[NetworkModes]; } + +export function isEmptyArray(data: unknown[]) { + return data.length === 0; +} diff --git a/src/shared/utils/type-utils.ts b/src/shared/utils/type-utils.ts index 998efa137f0..d426dc3a336 100644 --- a/src/shared/utils/type-utils.ts +++ b/src/shared/utils/type-utils.ts @@ -1,7 +1,3 @@ -export type Prettify = { - [K in keyof T]: T[K]; -}; - export type ValueOf = T[keyof T]; type Primitive = null | undefined | string | number | boolean | symbol | bigint; diff --git a/src/app/query/bitcoin/address/tests/mock-txs.ts b/tests/mocks/mock-btc-txs.ts similarity index 99% rename from src/app/query/bitcoin/address/tests/mock-txs.ts rename to tests/mocks/mock-btc-txs.ts index e747040a7fa..929ae30bfb6 100644 --- a/src/app/query/bitcoin/address/tests/mock-txs.ts +++ b/tests/mocks/mock-btc-txs.ts @@ -1,6 +1,6 @@ import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model'; -export const mockAddress = 'tb1qxy5r9rlmpcxgwp92x2594q3gg026y4kdv2rsl8'; +export const mockBitcoinTestnetAddress = 'tb1qxy5r9rlmpcxgwp92x2594q3gg026y4kdv2rsl8'; // multiple inputs and outputs export const mockPendingTxs1: BitcoinTx[] = [ diff --git a/tests/mocks/mock-inscriptions.ts b/tests/mocks/mock-inscriptions.ts new file mode 100644 index 00000000000..3f5e5eb510e --- /dev/null +++ b/tests/mocks/mock-inscriptions.ts @@ -0,0 +1,86 @@ +import { Inscription } from '@shared/models/inscription.model'; + +export const mockInscription1: Inscription = { + address: 'bc1pwrmewwprc8k8l2k63x4advg0nx0jk50xzqnee996lm87mcuza7kq6drg2k', + addressIndex: 0, + content_length: 55, + content_type: 'image/png', + curse_type: '', + genesis_address: '', + genesis_block_hash: '', + genesis_block_height: 0, + genesis_fee: '11130', + genesis_timestamp: 0, + genesis_tx_id: '', + id: 'ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0', + location: 'ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202:0:0', + mime_type: '', + number: 10875335, + offset: '200', + output: '/output/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202:0', + recursive: false, + recursion_refs: '', + sat_coinbase_height: 0, + sat_ordinal: '', + sat_rarity: '', + timestamp: 1696185309000, + tx_id: '5c3206e2d7655758e4db05a251571bbc902db7e5c1a1e6f99ca7d6e71bde450b', + value: '10000', +}; + +export const mockInscription2: Inscription = { + address: 'bc1pwrmewwprc8k8l2k63x4advg0nx0jk50xzqnee996lm87mcuza7kq6drg2k', + addressIndex: 0, + content_length: 55, + content_type: 'image/png', + curse_type: '', + genesis_address: '', + genesis_block_hash: '', + genesis_block_height: 0, + genesis_fee: '11130', + genesis_timestamp: 0, + genesis_tx_id: '', + id: 'ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0', + location: 'ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202:0:0', + mime_type: '', + number: 10875335, + offset: '600', + output: '/output/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202:0', + recursive: false, + recursion_refs: '', + sat_coinbase_height: 0, + sat_ordinal: '', + sat_rarity: '', + timestamp: 1696185309000, + tx_id: '42e90f6c10331d9409225015875decd1793406b3b7ee8f9d17b0abc8c4080c98', + value: '10000', +}; + +export const mockInscriptionsList = [ + { + address: 'bc1q530dz4h80kwlzywlhx2qn0k6vdtftd93c499yq', + id: 'a5ab63799f0bbd2571d1b90de9ebff815f7526787e27263d2f604e22f9118d0ci0', + content_length: 55, + content_type: 'text/plain;charset=utf-8', + curse_type: null, + genesis_address: 'bc1p9pnzvq52956jht5deha82qp96pxw0a0tvey6fhdea7vwhf33tarskqq3nr', + genesis_block_hash: '00000000000000000003fb85f8ae82f194786416cf699961b04d2953fbbd63d4', + genesis_block_height: 792337, + genesis_fee: '5738', + genesis_timestamp: 1685595657000, + genesis_tx_id: 'a5ab63799f0bbd2571d1b90de9ebff815f7526787e27263d2f604e22f9118d0c', + location: '58d44000884f0ba4cdcbeb1ac082e6c802d300c16b0d3251738e8cf6a57397ce:0:0', + mime_type: 'text/plain', + number: 10371348, + offset: '0', + output: '58d44000884f0ba4cdcbeb1ac082e6c802d300c16b0d3251738e8cf6a57397ce:0', + recursive: false, + recursion_refs: null, + sat_coinbase_height: 287150, + sat_ordinal: '1242877032342951', + sat_rarity: 'common', + timestamp: 1696185309000, + tx_id: '58d44000884f0ba4cdcbeb1ac082e6c802d300c16b0d3251738e8cf6a57397ce', + value: '546', + }, +]; diff --git a/tests/mocks/mock-psbts.ts b/tests/mocks/mock-psbts.ts index 7ae125061f1..45c6048dd8b 100644 --- a/tests/mocks/mock-psbts.ts +++ b/tests/mocks/mock-psbts.ts @@ -1,38 +1,4 @@ -export const mockInscriptions1 = [ - { - address: 'bc1pwrmewwprc8k8l2k63x4advg0nx0jk50xzqnee996lm87mcuza7kq6drg2k', - addressIndex: 0, - content: '/content/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0', - content_length: '519 bytes', - content_type: 'image/png', - genesis_fee: '11130', - genesis_height: '/block/792967', - genesis_transaction: '/tx/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202', - id: 'ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0', - inscription_number: 10875335, - location: 'ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202:0:0', - offset: '200', - output: '/output/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202:0', - output_value: '10000', - preview: '/preview/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0', - sat: '/sat/516510429895340', - timestamp: '2023-06-05 13:28:12 UTC', - title: 'Inscription 10875335', - // Outdated props? - genesis_address: '', - genesis_block_hash: '', - genesis_block_height: 0, - genesis_timestamp: 0, - genesis_tx_id: '', - mime_type: '', - number: 0, - sat_coinbase_height: 0, - sat_ordinal: '', - sat_rarity: '', - tx_id: '', - value: '', - }, -]; +import { mockInscription1, mockInscription2 } from './mock-inscriptions'; export const mockPsbtInputs1 = [ { @@ -54,7 +20,7 @@ export const mockPsbtInputs1 = [ { address: 'bc1pwrmewwprc8k8l2k63x4advg0nx0jk50xzqnee996lm87mcuza7kq6drg2k', index: 0, - inscription: '/inscription/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0', + inscription: mockInscription1, isMutable: false, toSign: false, txid: 'ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202', @@ -97,42 +63,6 @@ export const mockPsbtOutputs1 = [ }, ]; -export const mockInscriptions2 = [ - { - address: 'bc1pwrmewwprc8k8l2k63x4advg0nx0jk50xzqnee996lm87mcuza7kq6drg2k', - addressIndex: 0, - content: '/content/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0', - content_length: '519 bytes', - content_type: 'image/png', - genesis_fee: '11130', - genesis_height: '/block/792967', - genesis_transaction: '/tx/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202', - id: 'ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0', - inscription_number: 10875335, - location: 'ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202:0:0', - offset: '600', - output: '/output/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202:0', - output_value: '10000', - preview: '/preview/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0', - sat: '/sat/516510429895340', - timestamp: '2023-06-05 13:28:12 UTC', - title: 'Inscription 10875335', - // Outdated props? - genesis_address: '', - genesis_block_hash: '', - genesis_block_height: 0, - genesis_timestamp: 0, - genesis_tx_id: '', - mime_type: '', - number: 0, - sat_coinbase_height: 0, - sat_ordinal: '', - sat_rarity: '', - tx_id: '', - value: '', - }, -]; - export const mockPsbtInputs2 = [ { address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6', @@ -145,7 +75,7 @@ export const mockPsbtInputs2 = [ { address: 'bc1pwrmewwprc8k8l2k63x4advg0nx0jk50xzqnee996lm87mcuza7kq6drg2k', index: 0, - inscription: '/inscription/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0', + inscription: mockInscription2, isMutable: true, toSign: false, txid: 'ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202', diff --git a/tests/mocks/mock-utxos.ts b/tests/mocks/mock-utxos.ts new file mode 100644 index 00000000000..7f661345db1 --- /dev/null +++ b/tests/mocks/mock-utxos.ts @@ -0,0 +1,13 @@ +export const mockUtxos = [ + { + txid: '58d44000884f0ba4cdcbeb1ac082e6c802d300c16b0d3251738e8cf6a57397ce', + vout: 0, + status: { + confirmed: true, + block_height: 810183, + block_hash: '000000000000000000040caf3f438bb15731f6dc0f5ce1eaa6cd2ee8213d72dd', + block_time: 1696185309, + }, + value: 546, + }, +]; diff --git a/tests/page-object-models/onboarding.page.ts b/tests/page-object-models/onboarding.page.ts index 44fde74ba5a..7cb9dfd04f5 100644 --- a/tests/page-object-models/onboarding.page.ts +++ b/tests/page-object-models/onboarding.page.ts @@ -3,9 +3,10 @@ import { TEST_PASSWORD } from '@tests/mocks/constants'; import { HomePageSelectors } from '@tests/selectors/home.selectors'; import { OnboardingSelectors } from '@tests/selectors/onboarding.selectors'; -import { TEST_ACCOUNT_SECRET_KEY } from '@shared/environment'; import { RouteUrls } from '@shared/route-urls'; +const TEST_ACCOUNT_SECRET_KEY = process.env.TEST_ACCOUNT_SECRET_KEY ?? ''; + // If default wallet state changes, we'll need to update this export const testSoftwareAccountDefaultWalletState = { analytics: { hasStxDeposits: { '1': true, '2147483648': true } }, @@ -117,25 +118,36 @@ export class OnboardingPage { await this.page.getByTestId(OnboardingSelectors.BackUpSecretKeyBtn).click(); await this.setPassword(); } + async initiateSignIn() { + await this.denyAnalytics(); + await this.page.getByTestId(OnboardingSelectors.SignInLink).click(); + } /** * Use this to test the onboarding flow by going through step-by-step */ - async signInExistingUser() { - await this.denyAnalytics(); - await this.page.getByTestId(OnboardingSelectors.SignInLink).click(); - - const key = TEST_ACCOUNT_SECRET_KEY.split(' '); - for (let i = 0; i < key.length; i++) { - await this.page.getByTestId(`mnemonic-input-${i}`).fill(key[i]); - } - + async signInExistingUser(secretKey = TEST_ACCOUNT_SECRET_KEY) { + await this.initiateSignIn(); + await this.enterMnemonicKey(secretKey); await this.page.getByTestId(OnboardingSelectors.SignInBtn).click(); await this.setPassword(); await this.page.waitForURL('**' + RouteUrls.Home); await this.page.getByTestId(HomePageSelectors.HomePageContainer).waitFor(); } + async signInMnemonicKey(secretKey = TEST_ACCOUNT_SECRET_KEY) { + await this.initiateSignIn(); + await this.enterMnemonicKey(secretKey); + } + async enterMnemonicKey(secretKey: string) { + // NOTE: TEST_ACCOUNT_SECRET_KEY needs to be obtained and set in .env + if (!secretKey) throw new Error('No key found'); + const key = secretKey.split(' '); + for (let i = 0; i < key.length; i++) { + await this.page.getByTestId(`mnemonic-input-${i + 1}`).fill(key[i]); + } + } + /** * Use this for tests that just need to be signed in. This will skip the * onboarding flow and initialise the wallet in a signed in state for the test diff --git a/tests/selectors/onboarding.selectors.ts b/tests/selectors/onboarding.selectors.ts index b60e6289659..9371afc424a 100644 --- a/tests/selectors/onboarding.selectors.ts +++ b/tests/selectors/onboarding.selectors.ts @@ -14,4 +14,5 @@ export enum OnboardingSelectors { StepItemDone = 'step-item-done', StepItemStart = 'step-item-start', StepsList = 'steps-list', + SignInSeedError = 'sign-in-seed-error', } diff --git a/tests/specs/onboarding/onboarding.spec.ts b/tests/specs/onboarding/onboarding.spec.ts index e8b98aa01a5..93aabe19aa8 100644 --- a/tests/specs/onboarding/onboarding.spec.ts +++ b/tests/specs/onboarding/onboarding.spec.ts @@ -4,6 +4,7 @@ import { TEST_ACCOUNT_1_TAPROOT_ADDRESS, } from '@tests/mocks/constants'; import { testSoftwareAccountDefaultWalletState } from '@tests/page-object-models/onboarding.page'; +import { OnboardingSelectors } from '@tests/selectors/onboarding.selectors'; import { test } from '../../fixtures/fixtures'; @@ -31,6 +32,37 @@ test.describe('Onboarding an existing user', () => { test.expect(walletState).toEqual(testSoftwareAccountDefaultWalletState); }); + test('mnemonic key validation: should show error for invalid mnemonic key words', async ({ + extensionId, + globalPage, + onboardingPage, + }) => { + await globalPage.setupAndUseApiCalls(extensionId); + // enter some invalid key + const invalidKey = 'some incorrect data'; + await onboardingPage.signInMnemonicKey(invalidKey); + const signInButton = await onboardingPage.page.getByTestId(OnboardingSelectors.SignInBtn); + const error = onboardingPage.page.getByText('Words 1 and 2 are incorrect or misspelled'); + await test.expect(error).toBeVisible(); + await test.expect(signInButton).toBeDisabled(); + }); + test('mnemonic key validation: should not show error for valid mnemonic key words', async ({ + extensionId, + globalPage, + onboardingPage, + }) => { + await globalPage.setupAndUseApiCalls(extensionId); + // enter some key partial + const validPartialKey = 'shoulder any pencil'; + await onboardingPage.signInMnemonicKey(validPartialKey); + const signInButton = await onboardingPage.page.getByTestId(OnboardingSelectors.SignInBtn); + const signInSeedError = await onboardingPage.page.getByTestId( + OnboardingSelectors.SignInSeedError + ); + await test.expect(signInSeedError).not.toBeVisible(); + await test.expect(signInButton).toBeDisabled(); + }); + test('Activity tab', async ({ extensionId, globalPage, onboardingPage, homePage }) => { await globalPage.setupAndUseApiCalls(extensionId); await onboardingPage.signUpNewUser(); diff --git a/tsconfig.json b/tsconfig.json index 17b20659c70..a5d607a3c1f 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,7 +29,6 @@ "@inpage/*": ["inpage/*"], "@app/*": ["app/*"], "@tests/*": ["../tests/*"], - "leather-styles/*": ["../leather-styles/*"] }, "allowSyntheticDefaultImports": true, diff --git a/yarn.lock b/yarn.lock index 1702c3d9d53..f050a2d3ad6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2016,32 +2016,32 @@ mkdirp "^1.0.4" rimraf "^3.0.2" -"@pandacss/config@0.15.3", "@pandacss/config@^0.15.3": - version "0.15.3" - resolved "https://registry.yarnpkg.com/@pandacss/config/-/config-0.15.3.tgz#1b08a43034c785b92160d7f75cb7eec6583b66c8" - integrity sha512-gWp3diyfy4NEMG1Su7qtEJPXm327ZQeeT//hCm6glNpKgm+5D9urfRUGv+hQEFDeQokGmurlSDUTT1PVmYqx/g== - dependencies: - "@pandacss/error" "0.15.3" - "@pandacss/logger" "0.15.3" - "@pandacss/preset-base" "0.15.3" - "@pandacss/preset-panda" "0.15.3" - "@pandacss/types" "0.15.3" +"@pandacss/config@0.15.4", "@pandacss/config@^0.15.4": + version "0.15.4" + resolved "https://registry.yarnpkg.com/@pandacss/config/-/config-0.15.4.tgz#3300288a182e02d5f6e1f2907948b396f62b760f" + integrity sha512-YNNXoGwQggd4LFSP3TdyNyjnH8Vfis3Ip7E+o1MdeuNqiFZRedWmCTynBYDxGRsanO2h9nFGuUMuhG2IbSqOVg== + dependencies: + "@pandacss/error" "0.15.4" + "@pandacss/logger" "0.15.4" + "@pandacss/preset-base" "0.15.4" + "@pandacss/preset-panda" "0.15.4" + "@pandacss/types" "0.15.4" bundle-n-require "^1.0.1" escalade "3.1.1" jiti "^1.19.1" merge-anything "^5.1.7" typescript "^5.2.2" -"@pandacss/core@0.15.3": - version "0.15.3" - resolved "https://registry.yarnpkg.com/@pandacss/core/-/core-0.15.3.tgz#8514c3ec5ff9c149c0dc7ded1b41ae4d033f1370" - integrity sha512-cfVyF0aR1+n64IrMVx3cf8+LlFgBCz3cvtE8WzBeHmHOHWqO7V7lld0ub0Typ/levAYL4yfOEI7s9i3fznMITg== +"@pandacss/core@0.15.4": + version "0.15.4" + resolved "https://registry.yarnpkg.com/@pandacss/core/-/core-0.15.4.tgz#3761e3ddc48425aafd91db57bbe54914d6501459" + integrity sha512-LkGgNngMs3LrotzdvhUlmjy4T5pHn9uE1waApT+OfxDsPJcq9W4T/TilRAI5KXxwZGCPt3HcG3grZEQj+iz9mQ== dependencies: - "@pandacss/error" "0.15.3" - "@pandacss/logger" "0.15.3" - "@pandacss/shared" "0.15.3" - "@pandacss/token-dictionary" "0.15.3" - "@pandacss/types" "0.15.3" + "@pandacss/error" "0.15.4" + "@pandacss/logger" "0.15.4" + "@pandacss/shared" "0.15.4" + "@pandacss/token-dictionary" "0.15.4" + "@pandacss/types" "0.15.4" autoprefixer "10.4.15" hookable "5.5.3" lodash.merge "4.6.2" @@ -2055,52 +2055,52 @@ postcss-selector-parser "^6.0.13" ts-pattern "5.0.5" -"@pandacss/dev@0.15.3": - version "0.15.3" - resolved "https://registry.yarnpkg.com/@pandacss/dev/-/dev-0.15.3.tgz#a1cf6c7db9ab4706b253097408eeb2ef8c981d74" - integrity sha512-hOl3xDomY2/SziK7jRKDpvlNWgQtuSp+57oxk24imYZ6LzIrWlczQxlq6qhuIUt7mIef/xuXQCeO7QmgQJlH+w== +"@pandacss/dev@0.15.4": + version "0.15.4" + resolved "https://registry.yarnpkg.com/@pandacss/dev/-/dev-0.15.4.tgz#ab30e7f579e71391d851832d6726ac799c35cf60" + integrity sha512-4rEYnvyl4ITojpzJUZRFEv1mUZqlDaMaAUHvtvpjwSQyK2Z2dNFyHDkznzJ3lRKSbm4DGbUhc82+2VSjzpuKIg== dependencies: "@clack/prompts" "^0.6.3" - "@pandacss/config" "0.15.3" - "@pandacss/error" "0.15.3" - "@pandacss/logger" "0.15.3" - "@pandacss/node" "0.15.3" - "@pandacss/postcss" "0.15.3" - "@pandacss/preset-panda" "0.15.3" - "@pandacss/shared" "0.15.3" - "@pandacss/studio" "0.15.3" - "@pandacss/token-dictionary" "0.15.3" - "@pandacss/types" "0.15.3" + "@pandacss/config" "0.15.4" + "@pandacss/error" "0.15.4" + "@pandacss/logger" "0.15.4" + "@pandacss/node" "0.15.4" + "@pandacss/postcss" "0.15.4" + "@pandacss/preset-panda" "0.15.4" + "@pandacss/shared" "0.15.4" + "@pandacss/studio" "0.15.4" + "@pandacss/token-dictionary" "0.15.4" + "@pandacss/types" "0.15.4" cac "6.7.14" pathe "1.1.1" perfect-debounce "^1.0.0" -"@pandacss/error@0.15.3": - version "0.15.3" - resolved "https://registry.yarnpkg.com/@pandacss/error/-/error-0.15.3.tgz#09c7be0718cad6f05e99ed66cb49155c59932ee0" - integrity sha512-rORr/mH4tUv1kljkvPnT4mlzBkQrrB7x5ej7EYCN/KUSzB8K5r04hBO8w9uNX8UvJTQwnrFu3GWdnKe+dRLRag== +"@pandacss/error@0.15.4": + version "0.15.4" + resolved "https://registry.yarnpkg.com/@pandacss/error/-/error-0.15.4.tgz#a8646aa34ff868269407d8079a4ac73509af81eb" + integrity sha512-P83ikz9hmk4pOeWzrPeG//vV7tPeP0xJLjx/hJYAIX17xgso5UGoiRTjmKKdFoU4u7DwPXUX0r1nJ7K1gKhbyw== -"@pandacss/extractor@0.15.3": - version "0.15.3" - resolved "https://registry.yarnpkg.com/@pandacss/extractor/-/extractor-0.15.3.tgz#8835ac7d7c6fd101e66ac737f4c473a54447cb87" - integrity sha512-Yp96OdgJIM9m3vLBXWGF7SILh+rclzgmoQIHSFIg2VZT2lzgzyvJjgx7+687jRKb04nUsekQMaBoFsmN2gEYLw== +"@pandacss/extractor@0.15.4": + version "0.15.4" + resolved "https://registry.yarnpkg.com/@pandacss/extractor/-/extractor-0.15.4.tgz#f4a46801973421ccbcb47ef1f3d4347984fcd22e" + integrity sha512-g02WCselnex7c3g70zJ0zMUihzeopDwpZFATUklSu8jILxRmZmI5n4iSZ5dmrXxqFafWJdOSXRRSnPpZDstyzg== dependencies: lil-fp "1.4.5" ts-evaluator "^1.1.0" ts-morph "19.0.0" ts-pattern "5.0.5" -"@pandacss/generator@0.15.3": - version "0.15.3" - resolved "https://registry.yarnpkg.com/@pandacss/generator/-/generator-0.15.3.tgz#0bdc1b776ed9094aee65a254b779a61f41b481bd" - integrity sha512-coDd1njBBydwFyBwF4v9+6EKyZM9hg9k3wfUfyZ0P8JUChWbCBT3wtG52DvPWm7kpxUw7bheqTnEo0LdAYxMPg== - dependencies: - "@pandacss/core" "0.15.3" - "@pandacss/is-valid-prop" "0.15.3" - "@pandacss/logger" "0.15.3" - "@pandacss/shared" "0.15.3" - "@pandacss/token-dictionary" "0.15.3" - "@pandacss/types" "0.15.3" +"@pandacss/generator@0.15.4": + version "0.15.4" + resolved "https://registry.yarnpkg.com/@pandacss/generator/-/generator-0.15.4.tgz#48d7cbb110feed77825da050ccc5864a5221a31b" + integrity sha512-zhsMCVLQPGZj9uI3p8puTpcL0+w4SOSDxFutSzbPu0FVQend6hUjFb634ntnntuKkXBGV+G6MNYIq1RwKY9tew== + dependencies: + "@pandacss/core" "0.15.4" + "@pandacss/is-valid-prop" "0.15.4" + "@pandacss/logger" "0.15.4" + "@pandacss/shared" "0.15.4" + "@pandacss/token-dictionary" "0.15.4" + "@pandacss/types" "0.15.4" javascript-stringify "2.1.0" lil-fp "1.4.5" outdent " ^0.8.0" @@ -2108,35 +2108,35 @@ postcss "8.4.27" ts-pattern "5.0.5" -"@pandacss/is-valid-prop@0.15.3": - version "0.15.3" - resolved "https://registry.yarnpkg.com/@pandacss/is-valid-prop/-/is-valid-prop-0.15.3.tgz#ecfee19b0a64e8ec514097b3e35b8c42647dbc9b" - integrity sha512-uE3G0CO74+C70h76RrzwldqTxC20ICftpVStvISD5NaB7aCJp3qSFnzxvIUMJU2c76S9KToegfo3jCAoyONJgw== +"@pandacss/is-valid-prop@0.15.4": + version "0.15.4" + resolved "https://registry.yarnpkg.com/@pandacss/is-valid-prop/-/is-valid-prop-0.15.4.tgz#81f2aa52c4b2c3c02a8626daf194d5e16c5cf4ff" + integrity sha512-aqs4YjmwR0ATDlJ/l8cv3RYPdo3XTUSs425BcR5/UiqV0axCKsVs+qsMPGKl04yuD8CXMsYY1jFcUWgTKeZshg== -"@pandacss/logger@0.15.3": - version "0.15.3" - resolved "https://registry.yarnpkg.com/@pandacss/logger/-/logger-0.15.3.tgz#401632788a08e14f6dcc5567d0aae1fab2771e76" - integrity sha512-d91dQrhtgWmmTDVelC62Vni9JlnIDXv+BhiJX+ExAVCgSCpdJ45taiEs9PznVMeTgPxKSbex9YT0UrUlvfgC4g== +"@pandacss/logger@0.15.4": + version "0.15.4" + resolved "https://registry.yarnpkg.com/@pandacss/logger/-/logger-0.15.4.tgz#8f75e16a4aeb395c23de6d34e0792c7484639e7d" + integrity sha512-eqSIIf2lUiJTo/Aqv5Pc/Jg6XHScLqp4lYWzAcgo/gwATGd7ELp7JhvVxjI1CLbEyqOcN/IbkmTW7zKltKaZPg== dependencies: kleur "^4.1.5" lil-fp "1.4.5" -"@pandacss/node@0.15.3": - version "0.15.3" - resolved "https://registry.yarnpkg.com/@pandacss/node/-/node-0.15.3.tgz#878e163780467d6cc0e260fc366efc43835c8bf5" - integrity sha512-tti8aINiUf1ZBCsBkpkA7sJId9FyUrzba9mAmQ+icafWrCwcAKf/53nlWAzoY9Pq6abdauUKL4CUdWQT6Rd4/g== - dependencies: - "@pandacss/config" "0.15.3" - "@pandacss/core" "0.15.3" - "@pandacss/error" "0.15.3" - "@pandacss/extractor" "0.15.3" - "@pandacss/generator" "0.15.3" - "@pandacss/is-valid-prop" "0.15.3" - "@pandacss/logger" "0.15.3" - "@pandacss/parser" "0.15.3" - "@pandacss/shared" "0.15.3" - "@pandacss/token-dictionary" "0.15.3" - "@pandacss/types" "0.15.3" +"@pandacss/node@0.15.4": + version "0.15.4" + resolved "https://registry.yarnpkg.com/@pandacss/node/-/node-0.15.4.tgz#cbfb92e54b563e915ab3e7c3e925ed2e95048dfe" + integrity sha512-gTOXrTnJR/tiNep2XxIPW2JLMOPrmjmr64W40i6J6Cx//3iRbQpnh9oyT/tpw8Cu7MQBh0/MWwk8bWi+7mOvCQ== + dependencies: + "@pandacss/config" "0.15.4" + "@pandacss/core" "0.15.4" + "@pandacss/error" "0.15.4" + "@pandacss/extractor" "0.15.4" + "@pandacss/generator" "0.15.4" + "@pandacss/is-valid-prop" "0.15.4" + "@pandacss/logger" "0.15.4" + "@pandacss/parser" "0.15.4" + "@pandacss/shared" "0.15.4" + "@pandacss/token-dictionary" "0.15.4" + "@pandacss/types" "0.15.4" chokidar "^3.5.3" fast-glob "^3.3.1" file-size "^1.0.0" @@ -2158,82 +2158,82 @@ ts-pattern "5.0.5" tsconfck "^2.1.2" -"@pandacss/parser@0.15.3": - version "0.15.3" - resolved "https://registry.yarnpkg.com/@pandacss/parser/-/parser-0.15.3.tgz#821dbf59efb076925b0c949c9ff01e3151ef56b6" - integrity sha512-nHZUurilvDmWGDxJg+Rtuv2aUWHgF5OkhsxM4fsnu8t9jMtBnIAq55t8u9hf+6MF5NwXBZ45yIgR9gxv/E5HWg== - dependencies: - "@pandacss/config" "^0.15.3" - "@pandacss/extractor" "0.15.3" - "@pandacss/is-valid-prop" "0.15.3" - "@pandacss/logger" "0.15.3" - "@pandacss/shared" "0.15.3" - "@pandacss/types" "0.15.3" +"@pandacss/parser@0.15.4": + version "0.15.4" + resolved "https://registry.yarnpkg.com/@pandacss/parser/-/parser-0.15.4.tgz#c50d523882b3e05338d9b9fdcba10bf226f4203d" + integrity sha512-fTa67aTdbmhwVtJ2QAhmsWuH21ezcsNXBqaVQLkFKeRN5x1hlUqmaXXHTzq4FwsfoZz+UbaOnCc2Ahsi2n/rMw== + dependencies: + "@pandacss/config" "^0.15.4" + "@pandacss/extractor" "0.15.4" + "@pandacss/is-valid-prop" "0.15.4" + "@pandacss/logger" "0.15.4" + "@pandacss/shared" "0.15.4" + "@pandacss/types" "0.15.4" "@vue/compiler-sfc" "^3.3.4" lil-fp "1.4.5" magic-string "^0.30.2" ts-morph "19.0.0" ts-pattern "5.0.5" -"@pandacss/postcss@0.15.3": - version "0.15.3" - resolved "https://registry.yarnpkg.com/@pandacss/postcss/-/postcss-0.15.3.tgz#0ffc333947febf5a7588f5ec509a8c9ed7f00960" - integrity sha512-mrfw6Sor6Z+u7L9wSYTuih94ZLs2BUTH/OcDL84EatWqBVE7micbnpUT0LT46kAfVryrJwLlU8ng7qk1+98oiw== +"@pandacss/postcss@0.15.4": + version "0.15.4" + resolved "https://registry.yarnpkg.com/@pandacss/postcss/-/postcss-0.15.4.tgz#ce0f8d1dc94b3cab34122864fa9665ad4859619b" + integrity sha512-EZoXB+YNCxnIhPqxPcdJodzhhsvQukrUySZAjAAmyVArOmKZGW6V5aiGSLGb4jQlK2sjnRrW7lyVGSDou4qbvQ== dependencies: - "@pandacss/node" "0.15.3" + "@pandacss/node" "0.15.4" postcss "^8.4.27" -"@pandacss/preset-base@0.15.3": - version "0.15.3" - resolved "https://registry.yarnpkg.com/@pandacss/preset-base/-/preset-base-0.15.3.tgz#1909dddbbbd8ada41cfc35a9ec51489bd8cbc2d9" - integrity sha512-Wz64FyuY49bp9Hxcnp/a/HxrSCz0dtM8Eh2fbpi791F7PBmtNUynQiQxBY5aooSI2V0JWU92jL2nbs9kLo9OzA== +"@pandacss/preset-base@0.15.4": + version "0.15.4" + resolved "https://registry.yarnpkg.com/@pandacss/preset-base/-/preset-base-0.15.4.tgz#a6a395fe0737061dbd1358a17beb6003e6c61e0f" + integrity sha512-LusGmKgEX8jjWhi4iXM5qJPcrb1hqhIIsIYhvj/Sf6m3ntExJedJGJvPWEHbbb8rviKnPWPzg9KGOT5cgGnIIA== dependencies: - "@pandacss/types" "0.15.3" + "@pandacss/types" "0.15.4" -"@pandacss/preset-panda@0.15.3": - version "0.15.3" - resolved "https://registry.yarnpkg.com/@pandacss/preset-panda/-/preset-panda-0.15.3.tgz#4a37bd89fe15d6ef75621d229580e9621fdf2e51" - integrity sha512-ezhyi+GU3JOpN13O5wGMBE+0c5FBLyj0FSELJ5UHW2xx488k0YwKCNnkTR0rND3a2RwA+SIH05/zJerjt1VnTw== +"@pandacss/preset-panda@0.15.4": + version "0.15.4" + resolved "https://registry.yarnpkg.com/@pandacss/preset-panda/-/preset-panda-0.15.4.tgz#e5da2296f9787f6dc11695a05348bee2aae5ac90" + integrity sha512-VMYlzBp4YcrM9w5mncEDOgJCGJv5lfzxw4GIbpgWXhs5URSX0iLgXT8ar8djdXPX2TbFmR87IX2A11ipLNdhAg== dependencies: - "@pandacss/types" "0.15.3" + "@pandacss/types" "0.15.4" -"@pandacss/shared@0.15.3": - version "0.15.3" - resolved "https://registry.yarnpkg.com/@pandacss/shared/-/shared-0.15.3.tgz#c015c6a64f19d7b2ea2af94597b47ccebebbb4f1" - integrity sha512-UCOjhIlOyB/xN2JeBEjf1npNwvFIvICBDETMt3Fi/uDuZN7YyXXpXzA3MLeP0U6IE/r1Wg7FNQXf11XYzPTtXQ== +"@pandacss/shared@0.15.4": + version "0.15.4" + resolved "https://registry.yarnpkg.com/@pandacss/shared/-/shared-0.15.4.tgz#e67be7b6920fe2de6b97ea3b1b3f658f8ab20afd" + integrity sha512-uG2JEf+8ftO190lHeZXkzTVklFX+kmFK6f7gS0JmMsBbHCRaL6J80RdMyd8p9crxxTJhsCqE3uEQEFtyLXfJVA== -"@pandacss/studio@0.15.3": - version "0.15.3" - resolved "https://registry.yarnpkg.com/@pandacss/studio/-/studio-0.15.3.tgz#27c500f31bf2693b701f8b8de39b8b74763b40de" - integrity sha512-mqQbLiW9C1ElN9+iTWQg1XyotMmKQMzl4ZIGRsVl4n6G3ZRHukZ2ZwmNolE2dlGdilKAMUiNhM0JdMD2OypW0A== +"@pandacss/studio@0.15.4": + version "0.15.4" + resolved "https://registry.yarnpkg.com/@pandacss/studio/-/studio-0.15.4.tgz#942ea6b89f3ba916cdb25288fce90d244d41e14e" + integrity sha512-W8fHmfC13ZDCTHBVIJ5WqkT7mj37wp5GRXsdJ3ZwhLF4NYhPTJP3ovPLehVbdpoKubj0tDRUVrv5ky8MobxH/w== dependencies: "@ark-ui/react" "0.9.0" "@astrojs/react" "2.2.1" - "@pandacss/config" "0.15.3" - "@pandacss/logger" "0.15.3" - "@pandacss/node" "0.15.3" - "@pandacss/shared" "0.15.3" - "@pandacss/token-dictionary" "0.15.3" - "@pandacss/types" "0.15.3" + "@pandacss/config" "0.15.4" + "@pandacss/logger" "0.15.4" + "@pandacss/node" "0.15.4" + "@pandacss/shared" "0.15.4" + "@pandacss/token-dictionary" "0.15.4" + "@pandacss/types" "0.15.4" astro "2.9.6" javascript-stringify "2.1.0" react "18.2.0" react-dom "18.2.0" vite "4.4.9" -"@pandacss/token-dictionary@0.15.3": - version "0.15.3" - resolved "https://registry.yarnpkg.com/@pandacss/token-dictionary/-/token-dictionary-0.15.3.tgz#676da648bcfed273002606fab481760671d759ee" - integrity sha512-Qhc6Is5KUKPlGR4aPg1KUpXUOSBu4vsglCcviPoG5lSum84zpMQD7IVJZQGwAZClnYXMdThv7gJO4CJ0tF30Kw== +"@pandacss/token-dictionary@0.15.4": + version "0.15.4" + resolved "https://registry.yarnpkg.com/@pandacss/token-dictionary/-/token-dictionary-0.15.4.tgz#003ad377a1e259173de186ad006d6452cb62cd0a" + integrity sha512-a0vvF/nJeEElzf6jIaZ+VkKTZakpt4xwMRKjNWojv2YoOz0m8meJ9rwhiHz0I/8yLICTmNXNWoj2Z/JTy1PD4g== dependencies: - "@pandacss/shared" "0.15.3" - "@pandacss/types" "0.15.3" + "@pandacss/shared" "0.15.4" + "@pandacss/types" "0.15.4" ts-pattern "5.0.5" -"@pandacss/types@0.15.3": - version "0.15.3" - resolved "https://registry.yarnpkg.com/@pandacss/types/-/types-0.15.3.tgz#ed62a6b785110b40f0b0948ccbec6ebf6b766d61" - integrity sha512-ImumT7zPWb69Yt/j/e0UQO0qQWxEkEcO25GlbbvWiuOEXByat7H4Tyc7mcf4NM4021HlLMZ3/FSBvWiAdlCe+w== +"@pandacss/types@0.15.4": + version "0.15.4" + resolved "https://registry.yarnpkg.com/@pandacss/types/-/types-0.15.4.tgz#26ee315b38aae0c6aaea21c0948ac3d181e2c4e7" + integrity sha512-oFJpM+FwA19Ts+U+2Rta4V0oB4urXBgkfGR/RRRguuxeOf5V+1AK/zi8fyAEXxux1GZkTxypStcD5AcCS1z6+Q== "@pkgjs/parseargs@^0.11.0": version "0.11.0" @@ -5901,47 +5901,47 @@ dependencies: remove-accents "0.4.2" -"@tanstack/query-core@4.35.3": - version "4.35.3" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.35.3.tgz#1c127e66b4ad1beeac052db83a812d7cb67369e1" - integrity sha512-PS+WEjd9wzKTyNjjQymvcOe1yg8f3wYc6mD+vb6CKyZAKvu4sIJwryfqfBULITKCla7P9C4l5e9RXePHvZOZeQ== +"@tanstack/query-core@4.35.7": + version "4.35.7" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.35.7.tgz#31d6f520ab8abedb6024d2d870af8afca764d048" + integrity sha512-PgDJtX75ubFS0WCYFM7DqEoJ4QbxU3S5OH3gJSI40xr7UVVax3/J4CM3XUMOTs+EOT5YGEfssi3tfRVGte4DEw== -"@tanstack/query-persist-client-core@4.35.3": - version "4.35.3" - resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-4.35.3.tgz#61a57d9360cb35a75acb57f96795071dd1c4611e" - integrity sha512-UlUMsvmy12qgPzphIq8iyFtwxuv/vaEyFQEFDVVCvyrqj2G020qMZiCA1vj3+gasmCXh59EraiC2eY4Iqo0/PA== +"@tanstack/query-persist-client-core@4.35.7": + version "4.35.7" + resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-4.35.7.tgz#37a4ccfa991c2a06a5d453d7eafd6d1d72db5d0e" + integrity sha512-RhnvVtjR2owK50XosSWJh3SD7516c/cQZO3FhPOPKfnCP7b7NqJbeiznKqxIIR88rvNlIOmZ2BIerdvvW7WQ5g== dependencies: - "@tanstack/query-core" "4.35.3" + "@tanstack/query-core" "4.35.7" -"@tanstack/query-sync-storage-persister@4.35.3": - version "4.35.3" - resolved "https://registry.yarnpkg.com/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-4.35.3.tgz#0b5be86608e1448dacc17fbb88270dd4c257b9bc" - integrity sha512-q9axt4iJkRnhR9R9qou+Q2+T2S21jwgf/7carYs9DQGLoE9r9YnwxgbmDE72yQd1glcsGF26UqqO6WO8ziNCrQ== +"@tanstack/query-sync-storage-persister@4.35.7": + version "4.35.7" + resolved "https://registry.yarnpkg.com/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-4.35.7.tgz#aa9c07ca0cef639e674698b04a2f87f8df8a2537" + integrity sha512-LwOV4FfbRQmCH3MUXU2xngnws5AzRJ8mtc9JlDogRVwutlDEeY2bYSiu7xV6NDRLPf0/+0KCMzWeBx6Tlgnkgw== dependencies: - "@tanstack/query-persist-client-core" "4.35.3" + "@tanstack/query-persist-client-core" "4.35.7" -"@tanstack/react-query-devtools@4.35.3": - version "4.35.3" - resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-4.35.3.tgz#f03b39b613782a97794a42798d979c02ba51a7df" - integrity sha512-UvLT7qPzCuCZ3NfjwsOqDUVN84JvSOuW6ukrjZmSqgjPqVxD6ra/HUp1CEOatQY2TRvKCp8y1lTVu+trXM30fg== +"@tanstack/react-query-devtools@4.35.7": + version "4.35.7" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-4.35.7.tgz#782a09b4ebf29fda445b018b462aacc96e864c2f" + integrity sha512-oe3reHNvXBTUvNb9jwLY8EYOXyp8Oq8/c40iwpXBnEkAtJI+RryrCXaGKFTivg72roPcYHzKILQHR9jbX8sn1Q== dependencies: "@tanstack/match-sorter-utils" "^8.7.0" superjson "^1.10.0" use-sync-external-store "^1.2.0" -"@tanstack/react-query-persist-client@4.35.5": - version "4.35.5" - resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-4.35.5.tgz#095258f30382efa643441fb76185bac2c55c9f41" - integrity sha512-d8pFzvMD6AD2guGXLP2A4r0mfCRuZ8C4VMBl5+EZvka/e4o9/DRfizZB4S9w0Za+sOQJ4/SdD3OOk2BwYEykkQ== +"@tanstack/react-query-persist-client@4.35.7": + version "4.35.7" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-4.35.7.tgz#a23d76973b061f0574db0697170a8b674f675b2b" + integrity sha512-ziEZPMQO7bTjloCbnd4PBhL7U6hlAuivLqWyoHSNNNJYG1ZtjFfbpqA8XGw6t07PYSuASs/6oMRRzAQe0NkS2w== dependencies: - "@tanstack/query-persist-client-core" "4.35.3" + "@tanstack/query-persist-client-core" "4.35.7" -"@tanstack/react-query@4.35.3": - version "4.35.3" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.35.3.tgz#03d726ef6a19d426166427c6539b003dd9606d1b" - integrity sha512-UgTPioip/rGG3EQilXfA2j4BJkhEQsR+KAbF+KIuvQ7j4MkgnTCJF01SfRpIRNtQTlEfz/+IL7+jP8WA8bFbsw== +"@tanstack/react-query@4.35.7": + version "4.35.7" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.35.7.tgz#35a1db50156411b2a9eb68a020bae416948939db" + integrity sha512-0MankquP/6EOM2ATfEov6ViiKemey5uTbjGlFMX1xGotwNaqC76YKDMJdHumZupPbZcZPWAeoPGEHQmVKIKoOQ== dependencies: - "@tanstack/query-core" "4.35.3" + "@tanstack/query-core" "4.35.7" use-sync-external-store "^1.2.0" "@tippyjs/react@4.2.6", "@tippyjs/react@^4.2.4": @@ -6664,11 +6664,16 @@ "@types/node" "*" form-data "^4.0.0" -"@types/node@*", "@types/node@20.7.1": +"@types/node@*": version "20.7.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.7.1.tgz#06d732ead0bd5ad978ef0ea9cbdeb24dc8717514" integrity sha512-LT+OIXpp2kj4E2S/p91BMe+VgGX2+lfO+XTpfXhh+bCk2LkQtHZSub8ewFBMGP5ClysPjTDFa4sMI8Q3n4T0wg== +"@types/node@20.8.2": + version "20.8.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.2.tgz#d76fb80d87d0d8abfe334fc6d292e83e5524efc4" + integrity sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w== + "@types/node@^17.0.36": version "17.0.45" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" @@ -6719,10 +6724,10 @@ resolved "https://registry.yarnpkg.com/@types/punycode/-/punycode-2.1.0.tgz#89e4f3d09b3f92e87a80505af19be7e0c31d4e83" integrity sha512-PG5aLpW6PJOeV2fHRslP4IOMWn+G+Uq8CfnyJ+PDS8ndCbU+soO+fB3NKCKo0p/Jh2Y4aPaiQZsrOXFdzpcA6g== -"@types/qrcode.react@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@types/qrcode.react/-/qrcode.react-1.0.2.tgz#f892432cc41b5dac52e3ca8873b717c8bfea6002" - integrity sha512-I9Oq5Cjlkgy3Tw7krCnCXLw2/zMhizkTere49OOcta23tkvH0xBTP0yInimTh0gstLRtb8Ki9NZVujE5UI6ffQ== +"@types/qrcode.react@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/qrcode.react/-/qrcode.react-1.0.3.tgz#ce6852ebc1bbe4f43d1854eb19982cff30351025" + integrity sha512-gl7Ozf3BRQwfDUAU2xx7sWRBe/s7TqO0HAJukSQHbEVfQrFo5WKgZl0BHlN8u9W1DHXb4elgKRolHLZkgETXyA== dependencies: "@types/react" "*" @@ -6781,7 +6786,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@18.2.23", "@types/react@^18.2.21": +"@types/react@*", "@types/react@^18.2.21": version "18.2.23" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.23.tgz#60ad6cf4895e93bed858db0e03bcc4ff97d0410e" integrity sha512-qHLW6n1q2+7KyBEYnrZpcsAmU/iiCh9WGCKgXvMxx89+TYdJWRjZohVIo9XTcoLhfX3+/hP0Pbulu3bCZQ9PSA== @@ -6790,6 +6795,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@18.2.24": + version "18.2.24" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.24.tgz#3c7d68c02e0205a472f04abe4a0c1df35d995c05" + integrity sha512-Ee0Jt4sbJxMu1iDcetZEIKQr99J1Zfb6D4F3qfUWoR1JpInkY1Wdg4WwCyBjL257D0+jGqSl1twBjV8iCaC0Aw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/redux-devtools-themes@^1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/redux-devtools-themes/-/redux-devtools-themes-1.0.1.tgz#7ad5a75195896cf28db4b8088868a4525549bc96" @@ -7011,16 +7025,16 @@ resolved "https://registry.yarnpkg.com/@types/zxcvbn/-/zxcvbn-4.4.2.tgz#7b16c35330a15471cd7b9d3576503a40b6bd2416" integrity sha512-T7SEL8b/eN7AEhHQ8oFt7c6Y+l3p8OpH7KwJIe+5oBOPLMMioPeMsUTB3huNgEnXhiittV8Ohdw21Jg8E/f70Q== -"@typescript-eslint/eslint-plugin@6.7.3": - version "6.7.3" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.3.tgz#d98046e9f7102d49a93d944d413c6055c47fafd7" - integrity sha512-vntq452UHNltxsaaN+L9WyuMch8bMd9CqJ3zhzTPXXidwbf5mqqKCVXEuvRZUqLJSTLeWE65lQwyXsRGnXkCTA== +"@typescript-eslint/eslint-plugin@6.7.4": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.4.tgz#057338df21b6062c2f2fc5999fbea8af9973ac6d" + integrity sha512-DAbgDXwtX+pDkAHwiGhqP3zWUGpW49B7eqmgpPtg+BKJXwdct79ut9+ifqOFPJGClGKSHXn2PTBatCnldJRUoA== dependencies: "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.7.3" - "@typescript-eslint/type-utils" "6.7.3" - "@typescript-eslint/utils" "6.7.3" - "@typescript-eslint/visitor-keys" "6.7.3" + "@typescript-eslint/scope-manager" "6.7.4" + "@typescript-eslint/type-utils" "6.7.4" + "@typescript-eslint/utils" "6.7.4" + "@typescript-eslint/visitor-keys" "6.7.4" debug "^4.3.4" graphemer "^1.4.0" ignore "^5.2.4" @@ -7028,15 +7042,15 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/parser@6.7.3": - version "6.7.3" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.7.3.tgz#aaf40092a32877439e5957e18f2d6a91c82cc2fd" - integrity sha512-TlutE+iep2o7R8Lf+yoer3zU6/0EAUc8QIBB3GYBc1KGz4c4TRm83xwXUZVPlZ6YCLss4r77jbu6j3sendJoiQ== +"@typescript-eslint/parser@6.7.4": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.7.4.tgz#23d1dd4fe5d295c7fa2ab651f5406cd9ad0bd435" + integrity sha512-I5zVZFY+cw4IMZUeNCU7Sh2PO5O57F7Lr0uyhgCJmhN/BuTlnc55KxPonR4+EM3GBdfiCyGZye6DgMjtubQkmA== dependencies: - "@typescript-eslint/scope-manager" "6.7.3" - "@typescript-eslint/types" "6.7.3" - "@typescript-eslint/typescript-estree" "6.7.3" - "@typescript-eslint/visitor-keys" "6.7.3" + "@typescript-eslint/scope-manager" "6.7.4" + "@typescript-eslint/types" "6.7.4" + "@typescript-eslint/typescript-estree" "6.7.4" + "@typescript-eslint/visitor-keys" "6.7.4" debug "^4.3.4" "@typescript-eslint/scope-manager@6.7.3": @@ -7047,13 +7061,21 @@ "@typescript-eslint/types" "6.7.3" "@typescript-eslint/visitor-keys" "6.7.3" -"@typescript-eslint/type-utils@6.7.3": - version "6.7.3" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.7.3.tgz#c2c165c135dda68a5e70074ade183f5ad68f3400" - integrity sha512-Fc68K0aTDrKIBvLnKTZ5Pf3MXK495YErrbHb1R6aTpfK5OdSFj0rVN7ib6Tx6ePrZ2gsjLqr0s98NG7l96KSQw== +"@typescript-eslint/scope-manager@6.7.4": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.7.4.tgz#a484a17aa219e96044db40813429eb7214d7b386" + integrity sha512-SdGqSLUPTXAXi7c3Ob7peAGVnmMoGzZ361VswK2Mqf8UOYcODiYvs8rs5ILqEdfvX1lE7wEZbLyELCW+Yrql1A== dependencies: - "@typescript-eslint/typescript-estree" "6.7.3" - "@typescript-eslint/utils" "6.7.3" + "@typescript-eslint/types" "6.7.4" + "@typescript-eslint/visitor-keys" "6.7.4" + +"@typescript-eslint/type-utils@6.7.4": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.7.4.tgz#847cd3b59baf948984499be3e0a12ff07373e321" + integrity sha512-n+g3zi1QzpcAdHFP9KQF+rEFxMb2KxtnJGID3teA/nxKHOVi3ylKovaqEzGBbVY2pBttU6z85gp0D00ufLzViQ== + dependencies: + "@typescript-eslint/typescript-estree" "6.7.4" + "@typescript-eslint/utils" "6.7.4" debug "^4.3.4" ts-api-utils "^1.0.1" @@ -7062,6 +7084,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.7.3.tgz#0402b5628a63f24f2dc9d4a678e9a92cc50ea3e9" integrity sha512-4g+de6roB2NFcfkZb439tigpAMnvEIg3rIjWQ+EM7IBaYt/CdJt6em9BJ4h4UpdgaBWdmx2iWsafHTrqmgIPNw== +"@typescript-eslint/types@6.7.4": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.7.4.tgz#5d358484d2be986980c039de68e9f1eb62ea7897" + integrity sha512-o9XWK2FLW6eSS/0r/tgjAGsYasLAnOWg7hvZ/dGYSSNjCh+49k5ocPN8OmG5aZcSJ8pclSOyVKP2x03Sj+RrCA== + "@typescript-eslint/typescript-estree@6.7.3": version "6.7.3" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.3.tgz#ec5bb7ab4d3566818abaf0e4a8fa1958561b7279" @@ -7075,7 +7102,33 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/utils@6.7.3", "@typescript-eslint/utils@^6.0.0": +"@typescript-eslint/typescript-estree@6.7.4": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.4.tgz#f2baece09f7bb1df9296e32638b2e1130014ef1a" + integrity sha512-ty8b5qHKatlNYd9vmpHooQz3Vki3gG+3PchmtsA4TgrZBKWHNjWfkQid7K7xQogBqqc7/BhGazxMD5vr6Ha+iQ== + dependencies: + "@typescript-eslint/types" "6.7.4" + "@typescript-eslint/visitor-keys" "6.7.4" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/utils@6.7.4": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.7.4.tgz#2236f72b10e38277ee05ef06142522e1de470ff2" + integrity sha512-PRQAs+HUn85Qdk+khAxsVV+oULy3VkbH3hQ8hxLRJXWBEd7iI+GbQxH5SEUSH7kbEoTp6oT1bOwyga24ELALTA== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.7.4" + "@typescript-eslint/types" "6.7.4" + "@typescript-eslint/typescript-estree" "6.7.4" + semver "^7.5.4" + +"@typescript-eslint/utils@^6.0.0": version "6.7.3" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.7.3.tgz#96c655816c373135b07282d67407cb577f62e143" integrity sha512-vzLkVder21GpWRrmSR9JxGZ5+ibIUSudXlW52qeKpzUEQhRSmyZiVDDj3crAth7+5tmN1ulvgKaCU2f/bPRCzg== @@ -7096,6 +7149,14 @@ "@typescript-eslint/types" "6.7.3" eslint-visitor-keys "^3.4.1" +"@typescript-eslint/visitor-keys@6.7.4": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.4.tgz#80dfecf820fc67574012375859085f91a4dff043" + integrity sha512-pOW37DUhlTZbvph50x5zZCkFn3xzwkGtNoJHzIM3svpiSkJzwOYr/kVBaXmf+RAQiUDs1AHEZVNPg6UJCJpwRA== + dependencies: + "@typescript-eslint/types" "6.7.4" + eslint-visitor-keys "^3.4.1" + "@vitest/coverage-istanbul@0.34.6": version "0.34.6" resolved "https://registry.yarnpkg.com/@vitest/coverage-istanbul/-/coverage-istanbul-0.34.6.tgz#4fc91eac4d7cfc5b91850e9f82bcb986990b79a4" @@ -8803,13 +8864,6 @@ bip32@4.0.0, bip32@^4.0.0: typeforce "^1.11.5" wif "^2.0.6" -bip39@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.1.0.tgz#c55a418deaf48826a6ceb34ac55b3ee1577e18a3" - integrity sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A== - dependencies: - "@noble/hashes" "^1.2.0" - bip68@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/bip68/-/bip68-1.0.4.tgz#78a95c7a43fad183957995cc2e08d79b0c372c4d" @@ -9052,7 +9106,17 @@ browserify-sign@^4.0.0: readable-stream "^3.6.0" safe-buffer "^5.2.0" -browserslist@4.22.0, browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.21.4, browserslist@^4.21.9: +browserslist@4.22.1: + version "4.22.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619" + integrity sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ== + dependencies: + caniuse-lite "^1.0.30001541" + electron-to-chromium "^1.4.535" + node-releases "^2.0.13" + update-browserslist-db "^1.0.13" + +browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.21.4, browserslist@^4.21.9: version "4.22.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.0.tgz#6adc8116589ccea8a99d0df79c5de2436199abdb" integrity sha512-v+Jcv64L2LbfTC6OnRcaxtqJNJuQAVhZKSJfR/6hn7lhnChUXl4amwVviqN1k411BB+3rRoKMitELRn1CojeRA== @@ -9314,6 +9378,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001520, caniuse-lite@^1.0.30001538, can resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001541.tgz#b1aef0fadd87fb72db4dcb55d220eae17b81cdb1" integrity sha512-bLOsqxDgTqUBkzxbNlSBt8annkDpQB9NdzdTbO2ooJ+eC/IQcvDspDc058g84ejCelF7vHUx57KIOjEecOHXaw== +caniuse-lite@^1.0.30001541: + version "1.0.30001543" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001543.tgz#478a3e9dddbb353c5ab214b0ecb0dbed529ed1d8" + integrity sha512-qxdO8KPWPQ+Zk6bvNpPeQIOH47qZSYdFZd6dXQzb2KzhnSXju4Kd7H1PkSJx6NICSMgo/IhRZRhhfPTHYpJUCA== + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -9853,10 +9922,10 @@ content-type@~1.0.4, content-type@~1.0.5: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== -conventional-changelog-conventionalcommits@6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-6.1.0.tgz#3bad05f4eea64e423d3d90fc50c17d2c8cf17652" - integrity sha512-3cS3GEtR78zTfMzk0AizXKKIdN4OvSh7ibNz6/DPbhWWQu7LqE/8+/GqSodV+sywUR2gpJAdP/1JFf4XtN7Zpw== +conventional-changelog-conventionalcommits@7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz#aa5da0f1b2543094889e8cf7616ebe1a8f5c70d5" + integrity sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w== dependencies: compare-func "^2.0.0" @@ -10686,10 +10755,10 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== -dependency-cruiser@14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/dependency-cruiser/-/dependency-cruiser-14.0.0.tgz#d6c77b577abdd2954a25bd9a50af7c647956d49d" - integrity sha512-AAPiYPLAkJBhfcsxBp6nbR5gyfLZBAbCMwYI1hscuaBjbKt6FRzpMitl+f7mkzEWxyPGi6d0S9Us86Jx2CN55A== +dependency-cruiser@14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/dependency-cruiser/-/dependency-cruiser-14.1.0.tgz#7312f17e905f6183e3e47298ed2019b20534b217" + integrity sha512-JF7F0SFG4K5vXmUMvgYHKQnMuU2JzO18/+r/hTuaGEr3KTlMYkR16WNc+WDqS0y5fjq8khDy/WKO4bR5xhw2sQ== dependencies: acorn "8.10.0" acorn-jsx "5.3.2" @@ -11020,6 +11089,11 @@ electron-to-chromium@^1.4.530: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.532.tgz#44454731e26f2c8c14e88cca0d073f0761784f5e" integrity sha512-piIR0QFdIGKmOJTSNg5AwxZRNWQSXlRYycqDB9Srstx4lip8KpcmRxVP6zuFWExWziHYZpJ0acX7TxqX95KBpg== +electron-to-chromium@^1.4.535: + version "1.4.540" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.540.tgz#c685f2f035e93eb21dd6a9cfe2c735bad8f77401" + integrity sha512-aoCqgU6r9+o9/S7wkcSbmPRFi7OWZWiXS9rtjEd+Ouyu/Xyw5RSq2XN8s5Qp8IaFOLiRrhQCphCIjAxgG3eCAg== + electron@^26.1.0: version "26.2.3" resolved "https://registry.yarnpkg.com/electron/-/electron-26.2.3.tgz#4d198ba8d42aebdf65de75ce944c667ddd0c10b4" @@ -12034,11 +12108,12 @@ formdata-polyfill@^4.0.10: dependencies: fetch-blob "^3.1.2" -formik@2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/formik/-/formik-2.4.3.tgz#6020e85eb3e3e8415b3b19d6f4f65793ab754b24" - integrity sha512-2Dy79Szw3zlXmZiokUdKsn+n1ow4G8hRrC/n92cOWHNTWXCRpQXlyvz6HcjW7aSQZrldytvDOavYjhfmDnUq8Q== +formik@2.4.5: + version "2.4.5" + resolved "https://registry.yarnpkg.com/formik/-/formik-2.4.5.tgz#f899b5b7a6f103a8fabb679823e8fafc7e0ee1b4" + integrity sha512-Gxlht0TD3vVdzMDHwkiNZqJ7Mvg77xQNfmBRrNtvzcHZs72TJppSTDKHpImCMJZwcWPBJ8jSQQ95GJzXFf1nAQ== dependencies: + "@types/hoist-non-react-statics" "^3.3.1" deepmerge "^2.1.1" hoist-non-react-statics "^3.3.0" lodash "^4.17.21" @@ -15799,10 +15874,10 @@ object.values@^1.1.6: define-properties "^1.2.0" es-abstract "^1.22.1" -observable-hooks@4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/observable-hooks/-/observable-hooks-4.2.2.tgz#0882d3a829d622bbf51783d6c5a56aac01993c92" - integrity sha512-4zJLzYephHp3hHnu+lsAJ8pcIaiKla3yZMOTnCBBWjGV05HG2Nw9So47c26xnA8d3+YEaUo8DveGh7yRe6ZubA== +observable-hooks@4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/observable-hooks/-/observable-hooks-4.2.3.tgz#69e3353caafd7887ad9030bd440b053304e8d2d1" + integrity sha512-d6fYTIU+9sg1V+CT0GhgoE/ntjIqcy9DGaYGE6ELGVP4ojaWIEsaLvL/05hLOM+AL7aySN4DCTLvj6dDF9T8XA== obuf@^1.0.0, obuf@^1.1.2: version "1.1.2" @@ -16317,10 +16392,10 @@ pino@8.15.0: sonic-boom "^3.1.0" thread-stream "^2.0.0" -pino@8.15.1: - version "8.15.1" - resolved "https://registry.yarnpkg.com/pino/-/pino-8.15.1.tgz#04b815ff7aa4e46b1bbab88d8010aaa2b17eaba4" - integrity sha512-Cp4QzUQrvWCRJaQ8Lzv0mJzXVk4z2jlq8JNKMGaixC2Pz5L4l2p95TkuRvYbrEbe85NQsDKrAd4zalf7Ml6WiA== +pino@8.15.4: + version "8.15.4" + resolved "https://registry.yarnpkg.com/pino/-/pino-8.15.4.tgz#fd9a0c7bacf04af8fef48972ebbc8ea83fb2193d" + integrity sha512-3s+SfSxeugMt8QeBVXprIJAgXuGDeGuHBfquXKEXKnpghlXzMGMjoa8tOSyzz00iBfQX3xlZvm2yJQ+d6SrVsg== dependencies: atomic-sleep "^1.0.0" fast-redact "^3.1.1" @@ -17269,7 +17344,19 @@ react-lottie@1.2.3: babel-runtime "^6.26.0" lottie-web "^5.1.3" -react-redux@8.1.2, react-redux@^8.1.2: +react-redux@8.1.3: + version "8.1.3" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.3.tgz#4fdc0462d0acb59af29a13c27ffef6f49ab4df46" + integrity sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw== + dependencies: + "@babel/runtime" "^7.12.1" + "@types/hoist-non-react-statics" "^3.3.1" + "@types/use-sync-external-store" "^0.0.3" + hoist-non-react-statics "^3.3.2" + react-is "^18.0.0" + use-sync-external-store "^1.0.0" + +react-redux@^8.1.2: version "8.1.2" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.2.tgz#9076bbc6b60f746659ad6d51cb05de9c5e1e9188" integrity sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw== @@ -19291,10 +19378,10 @@ ts-pattern@5.0.5: resolved "https://registry.yarnpkg.com/ts-pattern/-/ts-pattern-5.0.5.tgz#20f82d50ea56c1d9cd1aa772911f7bb545d9607d" integrity sha512-tL0w8U/pgaacOmkb9fRlYzWEUDCfVjjv9dD4wHTgZ61MjhuMt46VNWTG747NqW6vRzoWIKABVhFSOJ82FvXrfA== -ts-unused-exports@7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/ts-unused-exports/-/ts-unused-exports-7.0.3.tgz#37a06d103d9d5b8619807dbd50d89f698e8cebf1" - integrity sha512-D0VdTiTfrmZM7tViQEMuzG0+giU5z5crn4vjK+f1dnxTKcNx23Vc2lpMgd1vP3lYrwnvJofZmCnvEuJ7XUeV2Q== +ts-unused-exports@10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/ts-unused-exports/-/ts-unused-exports-10.0.1.tgz#df7afc39ca7a8baa99f802ef89e95a62677635f1" + integrity sha512-nWG8Y96pKem01Hw4j4+Mwuy+L0/9sKT7D61Q+OS3cii9ocQACuV6lu00B9qpiPhF4ReVWw3QYHDqV8+to2wbsg== dependencies: chalk "^4.0.0" tsconfig-paths "^3.9.0" @@ -20127,14 +20214,14 @@ wcwidth@^1.0.0, wcwidth@^1.0.1: dependencies: defaults "^1.0.3" -web-ext-submit@7.7.0: - version "7.7.0" - resolved "https://registry.yarnpkg.com/web-ext-submit/-/web-ext-submit-7.7.0.tgz#eb31007d67d72cbc65410c8e697ab927d53edf45" - integrity sha512-ZLPqQ6f7F3QimwRmp+M1KxPWXA5wBbKlx08mV60LucuI3fJ0m/udVLFxMCsKE3KXjOGcaBky28gch1YTAEeXUg== +web-ext-submit@7.8.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/web-ext-submit/-/web-ext-submit-7.8.0.tgz#f8c9bb97d16286f5f509e53d122d2781987ed6ff" + integrity sha512-fHZKvbZXGea5fQMOdriRfsu61F3BNmYeZpxNH2BFP2cdP3fBGc2r9DJsLcId13mn9cZGCZvNSAp0tjzVAljRHw== dependencies: - web-ext "^7.7.0" + web-ext "^7.8.0" -web-ext@7.8.0, web-ext@^7.7.0: +web-ext@7.8.0, web-ext@^7.8.0: version "7.8.0" resolved "https://registry.yarnpkg.com/web-ext/-/web-ext-7.8.0.tgz#710cd73af79e3ac26fd4883b8894a3f6fd692d5d" integrity sha512-ta+VjbNfK5q0pqRmrlH/DQMdJ89sAJFfgcvpqoUoe2gPxLu18zZ5mIakGvyjmfjrkJLOZ2NCNF7suj/qd6xaEQ== @@ -20743,10 +20830,10 @@ yocto-queue@^1.0.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== -yup@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/yup/-/yup-1.3.1.tgz#4ab52d00555f0f18af09fe1356fdae35f804e421" - integrity sha512-2stNyEF96SnPUxzRL99kt1bEHWytnvC2stwmTTqjoFXZRf63JtYK2pQt2AJvWcQvkrAzr/pcXvc6c5vrqsBzDg== +yup@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/yup/-/yup-1.3.2.tgz#afffc458f1513ed386e6aaf4bcaa4e67a9e270dc" + integrity sha512-6KCM971iQtJ+/KUaHdrhVr2LDkfhBtFPRnsG1P8F4q3uUVQ2RfEM9xekpha9aA4GXWJevjM10eDcPQ1FfWlmaQ== dependencies: property-expr "^2.0.5" tiny-case "^1.0.3"