From 8c18929fabcbfab1a6d42a06d06728c49c552fb2 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Thu, 12 Oct 2023 16:39:28 +0200 Subject: [PATCH] feat: extended add-network page with the option to add bitcoin network too feat: modified add network tests --- regtest-new.patch | 1245 +++++++++++++++++ src/app/common/hooks/use-bitcoin-contracts.ts | 5 +- src/app/features/add-network/add-network.tsx | 257 +++- .../ledger-request-bitcoin-keys.tsx | 7 +- .../psbt-signer/hooks/use-parsed-inputs.tsx | 2 +- .../psbt-signer/hooks/use-parsed-outputs.tsx | 2 +- .../bitcoin-contract-request.tsx | 2 +- .../use-sign-bip322-message.ts | 2 +- .../hooks/use-send-inscription-form.tsx | 2 +- .../form/brc-20/use-brc20-send-form.tsx | 2 +- .../form/btc/btc-send-form.tsx | 2 +- .../form/btc/use-btc-send-form.tsx | 2 +- .../bitcoin/address/utxos-by-address.query.ts | 2 +- .../bitcoin/ordinals/brc20/use-brc-20.ts | 4 +- .../bitcoin/ordinals/inscriptions.query.ts | 4 +- src/app/query/bitcoin/ordinalsbot-client.ts | 3 +- .../blockchain/bitcoin/bitcoin-keychain.ts | 4 +- .../blockchain/bitcoin/bitcoin.hooks.ts | 2 +- .../bitcoin/native-segwit-account.hooks.ts | 2 +- .../bitcoin/taproot-account.hooks.ts | 4 +- src/app/store/common/api-clients.hooks.ts | 2 +- src/app/store/networks/networks.hooks.ts | 22 +- src/app/store/networks/networks.slice.ts | 1 + src/app/store/networks/networks.utils.ts | 21 +- src/shared/constants.ts | 24 +- tests/page-object-models/network.page.ts | 20 +- tests/selectors/network.selectors.ts | 10 +- tests/specs/network/add-network.spec.ts | 45 +- 28 files changed, 1564 insertions(+), 136 deletions(-) create mode 100644 regtest-new.patch diff --git a/regtest-new.patch b/regtest-new.patch new file mode 100644 index 00000000000..64d198707e6 --- /dev/null +++ b/regtest-new.patch @@ -0,0 +1,1245 @@ +diff --git a/src/app/common/hooks/use-bitcoin-contracts.ts b/src/app/common/hooks/use-bitcoin-contracts.ts +index 0526bee611..66bf335fc6 100644 +--- a/src/app/common/hooks/use-bitcoin-contracts.ts ++++ b/src/app/common/hooks/use-bitcoin-contracts.ts +@@ -4,7 +4,11 @@ import { RpcErrorCode } from '@btckit/types'; + import { JsDLCInterface } from '@dlc-link/dlc-tools'; + import { bytesToHex } from '@stacks/common'; + +-import { BITCOIN_API_BASE_URL_MAINNET, BITCOIN_API_BASE_URL_TESTNET } from '@shared/constants'; ++import { ++ BITCOIN_API_BASE_URL_MAINNET, ++ BITCOIN_API_BASE_URL_SIGNET, ++ BITCOIN_API_BASE_URL_TESTNET, ++} from '@shared/constants'; + import { + deriveAddressIndexKeychainFromAccount, + extractAddressIndexFromPath, +@@ -25,6 +29,7 @@ import { + useCurrentAccountNativeSegwitSigner, + useNativeSegwitAccountBuilder, + } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; ++import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; + + import { initialSearchParams } from '../initial-search-params'; + import { i18nFormatCurrency } from '../money/format-money'; +@@ -57,6 +62,7 @@ export function useBitcoinContracts() { + const getNativeSegwitSigner = useCurrentAccountNativeSegwitSigner(); + const currentIndex = useCurrentAccountIndex(); + const nativeSegwitPrivateKeychain = useNativeSegwitAccountBuilder()?.(currentIndex); ++ const currentNetwork = useCurrentNetwork(); + + async function getBitcoinContractInterface( + attestorURLs: string[] +@@ -78,8 +84,8 @@ export function useBitcoinContracts() { + const blockchainAPI = whenBitcoinNetwork(currentBitcoinNetwork)({ + mainnet: BITCOIN_API_BASE_URL_MAINNET, + testnet: BITCOIN_API_BASE_URL_TESTNET, +- regtest: BITCOIN_API_BASE_URL_TESTNET, +- signet: BITCOIN_API_BASE_URL_TESTNET, ++ regtest: currentNetwork.chain.bitcoin.bitcoinUrl, ++ signet: BITCOIN_API_BASE_URL_SIGNET, + }); + + const bitcoinContractInterface = JsDLCInterface.new( +diff --git a/src/app/features/add-network/add-network.tsx b/src/app/features/add-network/add-network.tsx +index fecd6d57c6..d188aaaff9 100644 +--- a/src/app/features/add-network/add-network.tsx ++++ b/src/app/features/add-network/add-network.tsx +@@ -1,13 +1,15 @@ +-import { useState } from 'react'; ++import { useEffect, useState } from 'react'; + import { useNavigate } from 'react-router-dom'; + ++import { SelectContent, SelectItem, SelectRoot, SelectTrigger } from '@radix-ui/themes'; + import { ChainID } from '@stacks/transactions'; + import { Input, Stack } from '@stacks/ui'; + import { NetworkSelectors } from '@tests/selectors/network.selectors'; +-import { Formik } from 'formik'; ++import { Formik, useFormik } from 'formik'; ++import { css } from 'leather-styles/css'; + import { token } from 'leather-styles/tokens'; + +-import { DefaultNetworkConfigurations } from '@shared/constants'; ++import { BitcoinNetworkModes, DefaultNetworkConfigurations } from '@shared/constants'; + import { RouteUrls } from '@shared/route-urls'; + import { isValidUrl } from '@shared/utils/validate-url'; + +@@ -17,7 +19,7 @@ import { LeatherButton } from '@app/components/button/button'; + import { CenteredPageContainer } from '@app/components/centered-page-container'; + import { ErrorLabel } from '@app/components/error-label'; + import { Header } from '@app/components/header'; +-import { Text } from '@app/components/typography'; ++import { Text, Title } from '@app/components/typography'; + import { + useCurrentStacksNetworkState, + useNetworksActions, +@@ -35,9 +37,15 @@ enum PeerNetworkID { + interface AddNetworkFormValues { + key: string; + name: string; +- url: string; ++ stacksUrl: string; ++ bitcoinUrl: string; + } +-const addNetworkFormValues: AddNetworkFormValues = { key: '', name: '', url: '' }; ++const addNetworkFormValues: AddNetworkFormValues = { ++ key: '', ++ name: '', ++ stacksUrl: '', ++ bitcoinUrl: '', ++}; + + export function AddNetwork() { + const [loading, setLoading] = useState(false); +@@ -45,125 +53,229 @@ export function AddNetwork() { + const navigate = useNavigate(); + const network = useCurrentStacksNetworkState(); + const networksActions = useNetworksActions(); ++ const [bitcoinApi, setBitcoinApi] = useState('mainnet'); ++ ++ const formikProps = useFormik({ ++ initialValues: addNetworkFormValues, ++ onSubmit: () => {}, ++ }); + + useRouteHeader(
navigate(RouteUrls.Home)} />); + ++ const handleApiChange = (newValue: BitcoinNetworkModes) => { ++ setBitcoinApi(newValue); ++ }; ++ ++ useEffect(() => { ++ switch (bitcoinApi) { ++ case 'mainnet': ++ formikProps.setFieldValue('stacksUrl', 'https://api.hiro.so'); ++ formikProps.setFieldValue('bitcoinUrl', 'https://blockstream.info/api'); ++ break; ++ case 'testnet': ++ formikProps.setFieldValue('stacksUrl', 'https://api.testnet.hiro.so'); ++ formikProps.setFieldValue('bitcoinUrl', 'https://blockstream.info/testnet/api'); ++ break; ++ case 'signet': ++ formikProps.setFieldValue('stacksUrl', 'https://api.testnet.hiro.so'); ++ formikProps.setFieldValue('bitcoinUrl', 'https://mempool.space/signet/api'); ++ break; ++ case 'regtest': ++ formikProps.setFieldValue('stacksUrl', 'https://api.testnet.hiro.so'); ++ formikProps.setFieldValue('bitcoinUrl', 'https://mempool.space/testnet/api'); ++ break; ++ } ++ }, [bitcoinApi]); ++ ++ const setCustomNetwork = async (values: AddNetworkFormValues) => { ++ const { name, key, stacksUrl, bitcoinUrl } = values; ++ setError(''); ++ ++ if (!isValidUrl(stacksUrl) || !isValidUrl(bitcoinUrl)) { ++ setError('Enter a valid URL'); ++ return; ++ } ++ ++ const bitcoinPath = removeTrailingSlash(new URL(bitcoinUrl).href); ++ const bitcoinResponse = await network.fetchFn(`${bitcoinPath}/mempool/recent`); ++ const bitcoinResponseJSON = await bitcoinResponse.json(); ++ if (!Array.isArray(bitcoinResponseJSON)) { ++ setError('Unable to fetch info from node.'); ++ throw new Error('Unable to fetch info from node'); ++ } ++ ++ const stacksPath = removeTrailingSlash(new URL(stacksUrl).href); ++ const stacksResponse = await network.fetchFn(`${stacksPath}/v2/info`); ++ const stacksChainInfo = await stacksResponse.json(); ++ if (!stacksChainInfo) { ++ setError('Unable to fetch info from node.'); ++ throw new Error('Unable to fetch info from node'); ++ } ++ ++ if (!key) { ++ setError('Enter a unique key'); ++ return; ++ } ++ ++ // Attention: ++ // For mainnet/testnet the v2/info response `.network_id` refers to the chain ID ++ // For subnets the v2/info response `.network_id` refers to the network ID and the chain ID (they are the same for subnets) ++ // The `.parent_network_id` refers to the actual peer network ID in both cases ++ const { network_id: chainId, parent_network_id: parentNetworkId } = stacksChainInfo; ++ ++ const isSubnet = typeof stacksChainInfo.l1_subnet_governing_contract === 'string'; ++ const isFirstLevelSubnet = ++ isSubnet && ++ (parentNetworkId === PeerNetworkID.Mainnet || parentNetworkId === PeerNetworkID.Testnet); ++ ++ // Currently, only subnets of mainnet and testnet are supported in the wallet ++ if (isFirstLevelSubnet) { ++ const parentChainId = ++ parentNetworkId === PeerNetworkID.Mainnet ? ChainID.Mainnet : ChainID.Testnet; ++ networksActions.addNetwork({ ++ id: key as DefaultNetworkConfigurations, ++ name, ++ chainId: parentChainId, // Used for differentiating control flow in the wallet ++ subnetChainId: chainId, // Used for signing transactions (via the network object, not to be confused with the NetworkConfigurations) ++ url: stacksPath, ++ bitcoinNetwork: bitcoinApi, ++ bitcoinUrl: bitcoinPath, ++ }); ++ navigate(RouteUrls.Home); ++ return; ++ } ++ ++ if (chainId === ChainID.Mainnet || chainId === ChainID.Testnet) { ++ networksActions.addNetwork({ ++ id: key as DefaultNetworkConfigurations, ++ name, ++ chainId: chainId, ++ url: stacksPath, ++ bitcoinNetwork: bitcoinApi, ++ bitcoinUrl: bitcoinPath, ++ }); ++ navigate(RouteUrls.Home); ++ return; ++ } ++ }; ++ + return ( + ++ ++ ++ Use this form to add a new instance of the{' '} ++ ++ Stacks Blockchain API ++ {' '} ++ or{' '} ++ ++ Bitcoin Blockchain API ++ ++ . Make sure you review and trust the host before you add it. ++ ++ + { +- const { name, url, key } = values; +- if (!isValidUrl(url)) { +- setError('Enter a valid URL'); +- return; +- } +- if (!key) { +- setError('Enter a unique key'); +- return; +- } +- ++ onSubmit={async () => { + setLoading(true); + setError(''); + +- try { +- const path = removeTrailingSlash(new URL(url).href); +- const response = await network.fetchFn(`${path}/v2/info`); +- const chainInfo = await response.json(); +- if (!chainInfo) throw new Error('Unable to fetch info from node'); +- +- // Attention: +- // For mainnet/testnet the v2/info response `.network_id` refers to the chain ID +- // For subnets the v2/info response `.network_id` refers to the network ID and the chain ID (they are the same for subnets) +- // The `.parent_network_id` refers to the actual peer network ID in both cases +- const { network_id: chainId, parent_network_id: parentNetworkId } = chainInfo; +- +- const isSubnet = typeof chainInfo.l1_subnet_governing_contract === 'string'; +- const isFirstLevelSubnet = +- isSubnet && +- (parentNetworkId === PeerNetworkID.Mainnet || +- parentNetworkId === PeerNetworkID.Testnet); +- +- // Currently, only subnets of mainnet and testnet are supported in the wallet +- if (isFirstLevelSubnet) { +- const parentChainId = +- parentNetworkId === PeerNetworkID.Mainnet ? ChainID.Mainnet : ChainID.Testnet; +- networksActions.addNetwork({ +- chainId: parentChainId, // Used for differentiating control flow in the wallet +- subnetChainId: chainId, // Used for signing transactions (via the network object, not to be confused with the NetworkConfigurations) +- id: key as DefaultNetworkConfigurations, +- name, +- url: path, +- }); +- navigate(RouteUrls.Home); +- return; +- } +- +- if (chainId === ChainID.Mainnet || chainId === ChainID.Testnet) { +- networksActions.addNetwork({ +- chainId, +- id: key as DefaultNetworkConfigurations, +- name, +- url: path, +- }); +- navigate(RouteUrls.Home); +- return; +- } +- +- setError('Unable to determine chainID from node.'); +- } catch (error) { +- setError('Unable to fetch info from node.'); +- } ++ setCustomNetwork(formikProps.values).catch(e => { ++ setError(e.message); ++ }); ++ + setLoading(false); + }} + > +- {({ handleSubmit, values, handleChange }) => ( ++ {({ handleSubmit }) => ( +
+ +- +- Use this form to add a new instance of the{' '} +- +- Stacks Blockchain API +- +- . Make sure you review and trust the host before you add it. +- + ++ Bitcoin API ++ ++ ++ ++ ++ Mainnet ++ ++ ++ Testnet ++ ++ ++ Signet ++ ++ ++ Regtest ++ ++ ++ ++ Stacks Address ++ ++ Bitcoin Address + + +diff --git a/src/app/features/ledger/flows/request-bitcoin-keys/ledger-request-bitcoin-keys.tsx b/src/app/features/ledger/flows/request-bitcoin-keys/ledger-request-bitcoin-keys.tsx +index bd23694707..f8764009d3 100644 +--- a/src/app/features/ledger/flows/request-bitcoin-keys/ledger-request-bitcoin-keys.tsx ++++ b/src/app/features/ledger/flows/request-bitcoin-keys/ledger-request-bitcoin-keys.tsx +@@ -35,7 +35,7 @@ function LedgerRequestBitcoinKeys() { + }, + async pullKeysFromDevice(app) { + const { keys } = await pullBitcoinKeysFromLedgerDevice(app)({ +- network: bitcoinNetworkModeToCoreNetworkMode(network.chain.bitcoin.network), ++ network: bitcoinNetworkModeToCoreNetworkMode(network.chain.bitcoin.bitcoinNetwork), + onRequestKey(index) { + if (index <= 4) { + ledgerNavigate.toDeviceBusyStep( +diff --git a/src/app/features/ledger/flows/request-bitcoin-keys/request-bitcoin-keys.utils.ts b/src/app/features/ledger/flows/request-bitcoin-keys/request-bitcoin-keys.utils.ts +index 636afcf599..816df07bba 100644 +--- a/src/app/features/ledger/flows/request-bitcoin-keys/request-bitcoin-keys.utils.ts ++++ b/src/app/features/ledger/flows/request-bitcoin-keys/request-bitcoin-keys.utils.ts +@@ -1,6 +1,6 @@ + import BitcoinApp, { DefaultWalletPolicy } from 'ledger-bitcoin'; + +-import { BitcoinNetworkModes, NetworkModes } from '@shared/constants'; ++import { BitcoinNetworkModes } from '@shared/constants'; + import { createWalletIdDecoratedPath } from '@shared/crypto/bitcoin/bitcoin.utils'; + import { getTaprootAccountDerivationPath } from '@shared/crypto/bitcoin/p2tr-address-gen'; + import { getNativeSegwitAccountDerivationPath } from '@shared/crypto/bitcoin/p2wpkh-address-gen'; +@@ -20,7 +20,7 @@ interface GetPolicyForPaymentTypeFactoryArgs { + interface GetExtendedPublicKeyFactoryArgs { + bitcoinApp: BitcoinApp; + fingerprint: string; +- network: NetworkModes; ++ network: BitcoinNetworkModes; + accountIndex: number; + } + function getPolicyForPaymentType({ +@@ -52,7 +52,7 @@ const getTaprootExtendedPublicKey = getPolicyForPaymentType({ + + interface PullBitcoinKeysFromLedgerDeviceArgs { + onRequestKey?(keyIndex: number): void; +- network: NetworkModes; ++ network: BitcoinNetworkModes; + } + export function pullBitcoinKeysFromLedgerDevice(bitcoinApp: BitcoinApp) { + return async ({ onRequestKey, network }: PullBitcoinKeysFromLedgerDeviceArgs) => { +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 a8e89101f7..1a21d76828 100644 +--- a/src/app/features/psbt-signer/hooks/use-parsed-inputs.tsx ++++ b/src/app/features/psbt-signer/hooks/use-parsed-inputs.tsx +@@ -50,7 +50,7 @@ interface UseParsedInputsArgs { + } + export function useParsedInputs({ inputs, indexesToSign }: UseParsedInputsArgs) { + const network = useCurrentNetwork(); +- const bitcoinNetwork = getBtcSignerLibNetworkConfigByMode(network.chain.bitcoin.network); ++ const bitcoinNetwork = getBtcSignerLibNetworkConfigByMode(network.chain.bitcoin.bitcoinNetwork); + const bitcoinAddressNativeSegwit = useCurrentAccountNativeSegwitIndexZeroSigner().address; + const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner(); + const inscriptions = useGetInscriptionsByOutputQueries(inputs).map(query => { +diff --git a/src/app/features/psbt-signer/hooks/use-parsed-outputs.tsx b/src/app/features/psbt-signer/hooks/use-parsed-outputs.tsx +index feb2c695f2..7f47a05ecb 100644 +--- a/src/app/features/psbt-signer/hooks/use-parsed-outputs.tsx ++++ b/src/app/features/psbt-signer/hooks/use-parsed-outputs.tsx +@@ -26,7 +26,7 @@ interface UseParsedOutputsArgs { + export function useParsedOutputs({ isPsbtMutable, outputs, network }: UseParsedOutputsArgs) { + const bitcoinAddressNativeSegwit = useCurrentAccountNativeSegwitIndexZeroSigner().address; + const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner(); +- const bitcoinNetwork = getBtcSignerLibNetworkConfigByMode(network.chain.bitcoin.network); ++ const bitcoinNetwork = getBtcSignerLibNetworkConfigByMode(network.chain.bitcoin.bitcoinNetwork); + + return useMemo( + () => +diff --git a/src/app/pages/bitcoin-contract-request/bitcoin-contract-request.tsx b/src/app/pages/bitcoin-contract-request/bitcoin-contract-request.tsx +index 19b5accd83..71497a0d00 100644 +--- a/src/app/pages/bitcoin-contract-request/bitcoin-contract-request.tsx ++++ b/src/app/pages/bitcoin-contract-request/bitcoin-contract-request.tsx +@@ -58,7 +58,7 @@ export function BitcoinContractRequest() { + + const currentBitcoinNetwork = bitcoinAccountDetails.network; + +- if (currentBitcoinNetwork !== 'testnet') { ++ if (!['testnet', 'regtest'].includes(currentBitcoinNetwork)) { + navigate(RouteUrls.BitcoinContractLockError, { + state: { + error: new Error('Invalid Network'), +diff --git a/src/app/pages/rpc-sign-bip322-message/use-sign-bip322-message.ts b/src/app/pages/rpc-sign-bip322-message/use-sign-bip322-message.ts +index 3bc1c13f5f..5a3bc872d7 100644 +--- a/src/app/pages/rpc-sign-bip322-message/use-sign-bip322-message.ts ++++ b/src/app/pages/rpc-sign-bip322-message/use-sign-bip322-message.ts +@@ -89,7 +89,7 @@ function useSignBip322MessageFactory({ address, signPsbt }: SignBip322MessageFac + message, + address, + signPsbt, +- network: network.chain.bitcoin.network, ++ network: network.chain.bitcoin.bitcoinNetwork, + }); + + await shortPauseBeforeToast(); +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 aab40dcee0..dabc860b19 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 +@@ -118,7 +118,7 @@ export function useSendInscriptionForm() { + .string() + .required(FormErrorMessages.AddressRequired) + .concat(btcAddressValidator()) +- .concat(btcAddressNetworkValidator(currentNetwork.chain.bitcoin.network)), ++ .concat(btcAddressNetworkValidator(currentNetwork.chain.bitcoin.bitcoinNetwork)), + }), + }; + } +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 6c7de9111f..6bd72eb217 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 +@@ -64,7 +64,7 @@ export function useBrc20SendForm({ balance, tick, decimals }: UseBrc20SendFormAr + recipient: yup + .string() + .concat(btcAddressValidator()) +- .concat(btcAddressNetworkValidator(currentNetwork.chain.bitcoin.network)), ++ .concat(btcAddressNetworkValidator(currentNetwork.chain.bitcoin.bitcoinNetwork)), + // .concat(notCurrentAddressValidator(currentAccountBtcAddress || '')), + }); + const { onFormStateChange } = useUpdatePersistedSendFormValues(); +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 1fa15be9b5..5332473bb9 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 +@@ -90,7 +90,7 @@ export function BtcSendForm() { + symbol={symbol} + /> + +- {currentNetwork.chain.bitcoin.network === 'testnet' && } ++ {currentNetwork.chain.bitcoin.bitcoinNetwork === 'testnet' && } + + + +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 d502119d4d..57ebcbfe9a 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 +@@ -77,7 +77,7 @@ export function useBtcSendForm() { + recipient: yup + .string() + .concat(btcAddressValidator()) +- .concat(btcAddressNetworkValidator(currentNetwork.chain.bitcoin.network)) ++ .concat(btcAddressNetworkValidator(currentNetwork.chain.bitcoin.bitcoinNetwork)) + .concat(notCurrentAddressValidator(nativeSegwitSigner.address || '')) + .required('Enter a bitcoin 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 df64ef345a..ced1322e95 100644 +--- a/src/app/query/bitcoin/address/utxos-by-address.query.ts ++++ b/src/app/query/bitcoin/address/utxos-by-address.query.ts +@@ -56,7 +56,7 @@ export function useTaprootAccountUtxosQuery() { + const address = getTaprootAddress({ + index: addressIndexCounter.getValue(), + keychain: account?.keychain, +- network: network.chain.bitcoin.network, ++ network: network.chain.bitcoin.bitcoinNetwork, + }); + + const unspentTransactions = await client.addressApi.getUtxosByAddress(address); +diff --git a/src/app/query/bitcoin/ordinals/brc20/use-brc-20.ts b/src/app/query/bitcoin/ordinals/brc20/use-brc-20.ts +index c56d230c91..0add7a01da 100644 +--- a/src/app/query/bitcoin/ordinals/brc20/use-brc-20.ts ++++ b/src/app/query/bitcoin/ordinals/brc20/use-brc-20.ts +@@ -20,8 +20,8 @@ export function useBrc20FeatureFlag() { + } + + const supportedNetwork = +- currentNetwork.chain.bitcoin.network === 'mainnet' || +- currentNetwork.chain.bitcoin.network === 'signet'; ++ currentNetwork.chain.bitcoin.bitcoinNetwork === 'mainnet' || ++ currentNetwork.chain.bitcoin.bitcoinNetwork === 'signet'; + + if (!supportedNetwork) return { enabled: false, reason: 'Unsupported network' } as const; + +diff --git a/src/app/query/bitcoin/ordinals/inscriptions.query.ts b/src/app/query/bitcoin/ordinals/inscriptions.query.ts +index 474a9f8f4a..67e34e9025 100644 +--- a/src/app/query/bitcoin/ordinals/inscriptions.query.ts ++++ b/src/app/query/bitcoin/ordinals/inscriptions.query.ts +@@ -63,7 +63,7 @@ export function useGetInscriptionsInfiniteQuery() { + const address = getTaprootAddress({ + index: i, + keychain: account?.keychain, +- network: network.chain.bitcoin.network, ++ network: network.chain.bitcoin.bitcoinNetwork, + }); + acc[address] = i; + return acc; +@@ -71,7 +71,7 @@ export function useGetInscriptionsInfiniteQuery() { + {} + ); + }, +- [account, network.chain.bitcoin.network] ++ [account, network.chain.bitcoin.bitcoinNetwork] + ); + + const query = useInfiniteQuery({ +diff --git a/src/app/query/bitcoin/ordinals/ordinals-aware-utxo.query.ts b/src/app/query/bitcoin/ordinals/ordinals-aware-utxo.query.ts +new file mode 100644 +index 0000000000..d6a3baeb2e +--- /dev/null ++++ b/src/app/query/bitcoin/ordinals/ordinals-aware-utxo.query.ts +@@ -0,0 +1,81 @@ ++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-taproot-address-utxos.query.ts b/src/app/query/bitcoin/ordinals/use-taproot-address-utxos.query.ts +new file mode 100644 +index 0000000000..034f803082 +--- /dev/null ++++ b/src/app/query/bitcoin/ordinals/use-taproot-address-utxos.query.ts +@@ -0,0 +1,72 @@ ++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.bitcoinNetwork, ++ }); ++ ++ 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 +new file mode 100644 +index 0000000000..ff28688a8f +--- /dev/null ++++ b/src/app/query/bitcoin/ordinals/use-zero-index-taproot-address.ts +@@ -0,0 +1,21 @@ ++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.bitcoinNetwork, ++ }); ++ ++ return address; ++} +diff --git a/src/app/query/bitcoin/ordinalsbot-client.ts b/src/app/query/bitcoin/ordinalsbot-client.ts +index 1c763ebb77..5ef39fcaf1 100644 +--- a/src/app/query/bitcoin/ordinalsbot-client.ts ++++ b/src/app/query/bitcoin/ordinalsbot-client.ts +@@ -142,7 +142,8 @@ function useOrdinalsbotApiUrl() { + const currentNetwork = useCurrentNetwork(); + const ordinalsbotConfig = useConfigOrdinalsbot(); + +- if (currentNetwork.chain.bitcoin.network === 'mainnet') return ordinalsbotConfig.mainnetApiUrl; ++ if (currentNetwork.chain.bitcoin.bitcoinNetwork === 'mainnet') ++ return ordinalsbotConfig.mainnetApiUrl; + return ordinalsbotConfig.signetApiUrl; + } + +diff --git a/src/app/store/accounts/blockchain/bitcoin/bitcoin-keychain.ts b/src/app/store/accounts/blockchain/bitcoin/bitcoin-keychain.ts +index f3863e6ae6..2f0f22246e 100644 +--- a/src/app/store/accounts/blockchain/bitcoin/bitcoin-keychain.ts ++++ b/src/app/store/accounts/blockchain/bitcoin/bitcoin-keychain.ts +@@ -59,7 +59,7 @@ export function bitcoinAccountBuilderFactory( + + export function useBitcoinScureLibNetworkConfig() { + const network = useCurrentNetwork(); +- return getBtcSignerLibNetworkConfigByMode(network.chain.bitcoin.network); ++ return getBtcSignerLibNetworkConfigByMode(network.chain.bitcoin.bitcoinNetwork); + } + + export function useBitcoinExtendedPublicKeyVersions(): Versions | undefined { +@@ -71,7 +71,7 @@ export function useBitcoinExtendedPublicKeyVersions(): Versions | undefined { + whenWallet({ + software: undefined, + ledger: getHdKeyVersionsFromNetwork( +- bitcoinNetworkModeToCoreNetworkMode(network.chain.bitcoin.network) ++ bitcoinNetworkModeToCoreNetworkMode(network.chain.bitcoin.bitcoinNetwork) + ), + }), + [network, whenWallet] +diff --git a/src/app/store/accounts/blockchain/bitcoin/bitcoin-signer.ts b/src/app/store/accounts/blockchain/bitcoin/bitcoin-signer.ts +index 1c53d3dc69..295d8ef454 100644 +--- a/src/app/store/accounts/blockchain/bitcoin/bitcoin-signer.ts ++++ b/src/app/store/accounts/blockchain/bitcoin/bitcoin-signer.ts +@@ -114,8 +114,9 @@ function createSignersForAllNetworkTypes { + const mainnetAccount = mainnetKeychainFn(accountIndex); + const testnetAccount = testnetKeychainFn(accountIndex); ++ const regtestAccount = testnetKeychainFn(accountIndex); + +- if (!mainnetAccount || !testnetAccount) throw new Error('No account found'); ++ if (!mainnetAccount || !testnetAccount || !regtestAccount) throw new Error('No account found'); + + function makeNetworkSigner(keychain: HDKey, network: BitcoinNetworkModes) { + return bitcoinAddressIndexSignerFactory({ +@@ -129,7 +130,7 @@ function createSignersForAllNetworkTypes nativeSegwitKeychains[network.chain.bitcoin.network] ++ (nativeSegwitKeychains, network) => nativeSegwitKeychains[network.chain.bitcoin.bitcoinNetwork] + ); + + export function useNativeSegwitAccountBuilder() { +@@ -53,9 +53,10 @@ export function useCurrentNativeSegwitAccount() { + } + + export function useNativeSegwitNetworkSigners() { +- const { mainnet: mainnetKeychain, testnet: testnetKeychain } = useSelector( +- selectNativeSegwitAccountBuilder +- ); ++ const { ++ mainnet: mainnetKeychain, ++ testnet: testnetKeychain, ++ } = useSelector(selectNativeSegwitAccountBuilder); + + return useMakeBitcoinNetworkSignersForPaymentType( + mainnetKeychain, +diff --git a/src/app/store/accounts/blockchain/bitcoin/taproot-account.hooks.ts b/src/app/store/accounts/blockchain/bitcoin/taproot-account.hooks.ts +index 3822447a22..4479452d4f 100644 +--- a/src/app/store/accounts/blockchain/bitcoin/taproot-account.hooks.ts ++++ b/src/app/store/accounts/blockchain/bitcoin/taproot-account.hooks.ts +@@ -32,7 +32,7 @@ const selectTaprootAccountBuilder = bitcoinAccountBuilderFactory( + const selectCurrentNetworkTaprootAccountBuilder = createSelector( + selectTaprootAccountBuilder, + selectCurrentNetwork, +- (taprootKeychains, network) => taprootKeychains[network.chain.bitcoin.network] ++ (taprootKeychains, network) => taprootKeychains[network.chain.bitcoin.bitcoinNetwork] + ); + const selectCurrentTaprootAccount = createSelector( + selectCurrentNetworkTaprootAccountBuilder, +@@ -53,9 +53,10 @@ export function useCurrentTaprootAccount() { + } + + export function useTaprootNetworkSigners() { +- const { mainnet: mainnetKeychain, testnet: testnetKeychain } = useSelector( +- selectTaprootAccountBuilder +- ); ++ const { ++ mainnet: mainnetKeychain, ++ testnet: testnetKeychain, ++ } = useSelector(selectTaprootAccountBuilder); + return useMakeBitcoinNetworkSignersForPaymentType( + mainnetKeychain, + testnetKeychain, +@@ -90,5 +91,5 @@ export function useCurrentAccountTaprootIndexZeroSigner() { + export function useCurrentAccountTaprootSigner() { + const currentAccountIndex = useCurrentAccountIndex(); + const network = useCurrentNetwork(); +- return useTaprootSigner(currentAccountIndex, network.chain.bitcoin.network); ++ return useTaprootSigner(currentAccountIndex, network.chain.bitcoin.bitcoinNetwork); + } +diff --git a/src/app/store/common/api-clients.hooks.ts b/src/app/store/common/api-clients.hooks.ts +index 4940fcde95..8b734dbdf4 100644 +--- a/src/app/store/common/api-clients.hooks.ts ++++ b/src/app/store/common/api-clients.hooks.ts +@@ -25,10 +25,10 @@ import { useCurrentNetworkState } from '../networks/networks.hooks'; + export function useBitcoinClient() { + const network = useCurrentNetworkState(); + +- const baseUrl = whenBitcoinNetwork(network.chain.bitcoin.network)({ ++ const baseUrl = whenBitcoinNetwork(network.chain.bitcoin.bitcoinNetwork)({ + mainnet: BITCOIN_API_BASE_URL_MAINNET, + testnet: BITCOIN_API_BASE_URL_TESTNET, +- regtest: BITCOIN_API_BASE_URL_TESTNET, ++ regtest: network.chain.bitcoin.bitcoinUrl, + signet: BITCOIN_API_BASE_URL_SIGNET, + }); + +diff --git a/src/app/store/networks/networks.hooks.ts b/src/app/store/networks/networks.hooks.ts +index 2df503a673..0b962b54fe 100644 +--- a/src/app/store/networks/networks.hooks.ts ++++ b/src/app/store/networks/networks.hooks.ts +@@ -50,8 +50,26 @@ export function useNetworksActions() { + + return useMemo( + () => ({ +- addNetwork({ chainId, subnetChainId, id, name, url }: PersistedNetworkConfiguration) { +- dispatch(networksActions.addNetwork({ chainId, subnetChainId, id, name, url })); ++ addNetwork({ ++ id, ++ name, ++ chainId, ++ subnetChainId, ++ url, ++ bitcoinNetwork, ++ bitcoinUrl, ++ }: PersistedNetworkConfiguration) { ++ dispatch( ++ networksActions.addNetwork({ ++ id, ++ name, ++ chainId, ++ subnetChainId, ++ url, ++ bitcoinNetwork, ++ bitcoinUrl, ++ }) ++ ); + dispatch(networksActions.changeNetwork(id)); + return; + }, +diff --git a/src/app/store/networks/networks.slice.ts b/src/app/store/networks/networks.slice.ts +index 2ce6d1fe3f..8be7e67b1f 100644 +--- a/src/app/store/networks/networks.slice.ts ++++ b/src/app/store/networks/networks.slice.ts +@@ -11,6 +11,7 @@ export type PersistedNetworkConfiguration = Omit< + Pick['chain']['stacks'], + 'blockchain' + > & ++ Omit['chain']['bitcoin'], 'blockchain'> & + Pick; + + export const networksAdapter = createEntityAdapter(); +diff --git a/src/app/store/networks/networks.utils.ts b/src/app/store/networks/networks.utils.ts +index 4f8a05cf41..fe3e74e15f 100644 +--- a/src/app/store/networks/networks.utils.ts ++++ b/src/app/store/networks/networks.utils.ts +@@ -1,13 +1,7 @@ + import { Dictionary } from '@reduxjs/toolkit'; + import { ChainID } from '@stacks/transactions'; + +-import { +- BITCOIN_API_BASE_URL_MAINNET, +- BITCOIN_API_BASE_URL_TESTNET, +- NetworkConfiguration, +-} from '@shared/constants'; +- +-import { whenStacksChainId } from '@app/common/utils'; ++import { NetworkConfiguration } from '@shared/constants'; + + import { PersistedNetworkConfiguration } from './networks.slice'; + +@@ -50,7 +44,8 @@ export function transformNetworkStateToMultichainStucture( + Object.entries(state) + .map(([key, network]) => { + if (!network) return ['', null]; +- const { id, name, chainId, subnetChainId, url } = network; ++ const { id, name, chainId, subnetChainId, url, bitcoinNetwork, bitcoinUrl } = network; ++ + return [ + key, + { +@@ -59,18 +54,14 @@ export function transformNetworkStateToMultichainStucture( + chain: { + stacks: { + blockchain: 'stacks', +- url, ++ url: url, + chainId, + subnetChainId, + }, + bitcoin: { + blockchain: 'bitcoin', +- network: ChainID[chainId] ? ChainID[chainId].toLowerCase() : 'testnet', +- url: +- whenStacksChainId(chainId)({ +- [ChainID.Mainnet]: BITCOIN_API_BASE_URL_MAINNET, +- [ChainID.Testnet]: BITCOIN_API_BASE_URL_TESTNET, +- }) || BITCOIN_API_BASE_URL_TESTNET, ++ bitcoinNetwork, ++ bitcoinUrl, + }, + }, + }, +diff --git a/src/shared/constants.ts b/src/shared/constants.ts +index 320012157f..2c918d7ce3 100644 +--- a/src/shared/constants.ts ++++ b/src/shared/constants.ts +@@ -57,8 +57,8 @@ interface BaseChainConfig { + + interface BitcoinChainConfig extends BaseChainConfig { + blockchain: 'bitcoin'; +- url: string; +- network: BitcoinNetworkModes; ++ bitcoinUrl: string; ++ bitcoinNetwork: BitcoinNetworkModes; + } + + interface StacksChainConfig extends BaseChainConfig { +@@ -98,8 +98,8 @@ const networkMainnet: NetworkConfiguration = { + }, + bitcoin: { + blockchain: 'bitcoin', +- network: 'mainnet', +- url: BITCOIN_API_BASE_URL_MAINNET, ++ bitcoinNetwork: 'mainnet', ++ bitcoinUrl: BITCOIN_API_BASE_URL_MAINNET, + }, + }, + }; +@@ -115,8 +115,8 @@ const networkTestnet: NetworkConfiguration = { + }, + bitcoin: { + blockchain: 'bitcoin', +- network: 'testnet', +- url: BITCOIN_API_BASE_URL_TESTNET, ++ bitcoinNetwork: 'testnet', ++ bitcoinUrl: BITCOIN_API_BASE_URL_TESTNET, + }, + }, + }; +@@ -132,8 +132,8 @@ const networkSignet: NetworkConfiguration = { + }, + bitcoin: { + blockchain: 'bitcoin', +- network: 'signet', +- url: BITCOIN_API_BASE_URL_SIGNET, ++ bitcoinNetwork: 'signet', ++ bitcoinUrl: BITCOIN_API_BASE_URL_SIGNET, + }, + }, + }; +@@ -149,8 +149,8 @@ const networkSbtcDevenv: NetworkConfiguration = { + }, + bitcoin: { + blockchain: 'bitcoin', +- network: 'regtest', +- url: 'http://localhost:8999/api', ++ bitcoinNetwork: 'regtest', ++ bitcoinUrl: 'http://localhost:8999/api', + }, + }, + }; +@@ -166,8 +166,8 @@ const networkDevnet: NetworkConfiguration = { + }, + bitcoin: { + blockchain: 'bitcoin', +- network: 'regtest', +- url: 'http://localhost:18443', ++ bitcoinNetwork: 'regtest', ++ bitcoinUrl: 'http://localhost:18443', + }, + }, + }; +diff --git a/src/shared/crypto/bitcoin/bitcoin.utils.ts b/src/shared/crypto/bitcoin/bitcoin.utils.ts +index 4cb2705f81..dd6cfd9daa 100644 +--- a/src/shared/crypto/bitcoin/bitcoin.utils.ts ++++ b/src/shared/crypto/bitcoin/bitcoin.utils.ts +@@ -4,9 +4,8 @@ import { HDKey, Versions } from '@scure/bip32'; + import * as btc from '@scure/btc-signer'; + + import { BitcoinNetworkModes, NetworkModes } from '@shared/constants'; +-import { whenNetwork } from '@shared/utils'; + import { defaultWalletKeyId } from '@shared/utils'; +- ++import { whenBitcoinNetwork } from '@app/common/utils'; + import { DerivationPathDepth } from '../derivation-path.utils'; + import { BtcSignerNetwork } from './bitcoin.network'; + import { getTaprootPayment } from './p2tr-address-gen'; +@@ -19,19 +18,22 @@ export interface BitcoinAccount { + network: BitcoinNetworkModes; + } + +-const bitcoinNetworkToCoreNetworkMap: Record = { ++const bitcoinNetworkToCoreNetworkMap: Record = { + mainnet: 'mainnet', + testnet: 'testnet', +- regtest: 'testnet', +- signet: 'testnet', ++ regtest: 'regtest', ++ signet: 'signet', + }; ++ + export function bitcoinNetworkModeToCoreNetworkMode(mode: BitcoinNetworkModes) { + return bitcoinNetworkToCoreNetworkMap[mode]; + } + +-const coinTypeMap: Record = { ++const coinTypeMap: Record = { + mainnet: 0, + testnet: 1, ++ regtest: 1, ++ signet: 1, + }; + + export function getBitcoinCoinTypeIndexByNetwork(network: BitcoinNetworkModes) { +@@ -161,13 +163,15 @@ export function createWalletIdDecoratedPath(policy: string, walletId: string) { + + // Primarily used to get the correct `Version` when passing Ledger Bitcoin + // extended public keys to the HDKey constructor +-export function getHdKeyVersionsFromNetwork(network: NetworkModes) { +- return whenNetwork(network)({ ++export function getHdKeyVersionsFromNetwork(network: BitcoinNetworkModes) { ++ return whenBitcoinNetwork(network)({ + mainnet: undefined, + testnet: { + private: 0x00000000, + public: 0x043587cf, + } as Versions, ++ regtest: undefined, ++ signet: undefined, + }); + } + +diff --git a/tests-legacy/integration/network.selectors.ts b/tests-legacy/integration/network.selectors.ts +new file mode 100644 +index 0000000000..06b5b7ac1d +--- /dev/null ++++ b/tests-legacy/integration/network.selectors.ts +@@ -0,0 +1,10 @@ ++export const NetworkSelectors = { ++ NetworkName: 'network-name', ++ NetworkStacksAddress: 'network-stacks-address', ++ NetworkBitcoinAddress: 'network-bitcoin-address', ++ NetworkKey: 'network-key', ++ BtnAddNetwork: 'btn-add-network', ++ ErrorText: 'error-text', ++ EmptyAddressError: 'Enter a valid URL', ++ NoNodeFetch: 'Unable to fetch info from node.', ++}; +diff --git a/tests/page-object-models/network.page.ts b/tests/page-object-models/network.page.ts +index 6a7f08440f..1eeeab0008 100644 +--- a/tests/page-object-models/network.page.ts ++++ b/tests/page-object-models/network.page.ts +@@ -4,7 +4,10 @@ import { createTestSelector } from '@tests/utils'; + + export class NetworkPage { + readonly networkNameSelector = createTestSelector(NetworkSelectors.NetworkName); +- readonly networkAddressSelector = createTestSelector(NetworkSelectors.NetworkAddress); ++ readonly networkStacksAddressSelector = createTestSelector(NetworkSelectors.NetworkStacksAddress); ++ readonly networkBitcoinAddressSelector = createTestSelector( ++ NetworkSelectors.NetworkBitcoinAddress ++ ); + readonly networkKeySelector = createTestSelector(NetworkSelectors.NetworkKey); + readonly btnAddNetworkSelector = createTestSelector(NetworkSelectors.BtnAddNetwork); + readonly errorTextSelector = createTestSelector(NetworkSelectors.ErrorText); +@@ -16,8 +19,13 @@ export class NetworkPage { + await field?.type(input); + } + +- async inputNetworkAddressField(input: string) { +- const field = this.page.locator(this.networkAddressSelector); ++ async inputNetworkStacksAddressField(input: string) { ++ const field = this.page.locator(this.networkStacksAddressSelector); ++ await field?.type(input); ++ } ++ ++ async inputNetworkBitcoinAddressField(input: string) { ++ const field = this.page.locator(this.networkBitcoinAddressSelector); + await field?.type(input); + } + +diff --git a/tests/selectors/network.selectors.ts b/tests/selectors/network.selectors.ts +index 38aa40ca29..84777c6586 100644 +--- a/tests/selectors/network.selectors.ts ++++ b/tests/selectors/network.selectors.ts +@@ -1,6 +1,7 @@ + export enum NetworkSelectors { + NetworkName = 'network-name', +- NetworkAddress = 'network-address', ++ NetworkStacksAddress = 'network-stacks-address', ++ NetworkBitcoinAddress = 'network-bitcoin-address', + NetworkKey = 'network-key', + BtnAddNetwork = 'btn-add-network', + ErrorText = 'error-text', +diff --git a/tests/specs/network/add-network.spec.ts b/tests/specs/network/add-network.spec.ts +index fcb1220f57..3911a9dd00 100644 +--- a/tests/specs/network/add-network.spec.ts ++++ b/tests/specs/network/add-network.spec.ts +@@ -22,7 +22,7 @@ test.describe('Networks tests', () => { + }); + + test('unable to fetch info from node', async ({ networkPage }) => { +- await networkPage.inputNetworkAddressField('https://www.google.com/'); ++ await networkPage.inputNetworkStacksAddressField('https://www.google.com/'); + await networkPage.inputNetworkKeyField('test-network'); + await networkPage.clickAddNetwork(); + await networkPage.waitForErrorMessage(); diff --git a/src/app/common/hooks/use-bitcoin-contracts.ts b/src/app/common/hooks/use-bitcoin-contracts.ts index ec7a5c93d90..74e928ea38a 100644 --- a/src/app/common/hooks/use-bitcoin-contracts.ts +++ b/src/app/common/hooks/use-bitcoin-contracts.ts @@ -80,8 +80,9 @@ export function useBitcoinContracts() { const bitcoinContractInterface = await JsDLCInterface.new( bytesToHex(currentAddressPrivateKey), currentAddress, - currentBitcoinNetwork.chain.bitcoin.network, - currentBitcoinNetwork.chain.bitcoin.url + currentBitcoinNetwork.chain.bitcoin.bitcoinNetwork, + currentBitcoinNetwork.chain.bitcoin.bitcoinUrl, + JSON.stringify(attestorURLs) ); return bitcoinContractInterface; diff --git a/src/app/features/add-network/add-network.tsx b/src/app/features/add-network/add-network.tsx index 2cf653bce86..139b5a26e5b 100644 --- a/src/app/features/add-network/add-network.tsx +++ b/src/app/features/add-network/add-network.tsx @@ -1,13 +1,16 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; +import { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; +import { SelectContent, SelectItem, SelectRoot, SelectTrigger } from '@radix-ui/themes'; import { ChainID } from '@stacks/transactions'; import { Input, Stack } from '@stacks/ui'; import { NetworkSelectors } from '@tests/selectors/network.selectors'; -import { Formik } from 'formik'; +import { Formik, useFormik } from 'formik'; +import { css } from 'leather-styles/css'; import { token } from 'leather-styles/tokens'; -import { DefaultNetworkConfigurations } from '@shared/constants'; +import { BitcoinNetworkModes, DefaultNetworkConfigurations } from '@shared/constants'; import { RouteUrls } from '@shared/route-urls'; import { isValidUrl } from '@shared/utils/validate-url'; @@ -17,7 +20,7 @@ import { LeatherButton } from '@app/components/button/button'; import { CenteredPageContainer } from '@app/components/centered-page-container'; import { ErrorLabel } from '@app/components/error-label'; import { Header } from '@app/components/header'; -import { Text } from '@app/components/typography'; +import { Text, Title } from '@app/components/typography'; import { useCurrentStacksNetworkState, useNetworksActions, @@ -35,9 +38,15 @@ enum PeerNetworkID { interface AddNetworkFormValues { key: string; name: string; - url: string; + stacksUrl: string; + bitcoinUrl: string; } -const addNetworkFormValues: AddNetworkFormValues = { key: '', name: '', url: '' }; +const addNetworkFormValues: AddNetworkFormValues = { + key: '', + name: '', + stacksUrl: '', + bitcoinUrl: '', +}; export function AddNetwork() { const [loading, setLoading] = useState(false); @@ -45,19 +54,73 @@ export function AddNetwork() { const navigate = useNavigate(); const network = useCurrentStacksNetworkState(); const networksActions = useNetworksActions(); + const [bitcoinApi, setBitcoinApi] = useState('mainnet'); + + const formikProps = useFormik({ + initialValues: addNetworkFormValues, + onSubmit: () => {}, + }); + + const { setFieldValue } = formikProps; useRouteHeader(
navigate(RouteUrls.Home)} />); + const handleApiChange = (newValue: BitcoinNetworkModes) => { + setBitcoinApi(newValue); + }; + + const setStacksUrl = useCallback( + (value: string) => { + setFieldValue('stacksUrl', value); + }, + [setFieldValue] + ); + + const setBitcoinUrl = useCallback( + (value: string) => { + setFieldValue('bitcoinUrl', value); + }, + [setFieldValue] + ); + + useEffect(() => { + switch (bitcoinApi) { + case 'mainnet': + setStacksUrl('https://api.hiro.so'); + setBitcoinUrl('https://blockstream.info/api'); + break; + case 'testnet': + setStacksUrl('https://api.testnet.hiro.so'); + setBitcoinUrl('https://blockstream.info/testnet/api'); + break; + case 'signet': + setStacksUrl('https://api.testnet.hiro.so'); + setBitcoinUrl('https://mempool.space/signet/api'); + break; + case 'regtest': + setStacksUrl('https://api.testnet.hiro.so'); + setBitcoinUrl('https://mempool.space/testnet/api'); + break; + } + }, [bitcoinApi, setStacksUrl, setBitcoinUrl]); + return ( { - const { name, url, key } = values; - if (!isValidUrl(url)) { - setError('Enter a valid URL'); + onSubmit={async () => { + const { name, stacksUrl, bitcoinUrl, key } = formikProps.values; + + if (!isValidUrl(stacksUrl)) { + setError('Enter a valid Stacks API URL'); + return; + } + + if (!isValidUrl(bitcoinUrl)) { + setError('Enter a valid Bitcoin API URL'); return; } + if (!key) { setError('Enter a unique key'); return; @@ -66,63 +129,80 @@ export function AddNetwork() { setLoading(true); setError(''); + const stacksPath = removeTrailingSlash(new URL(formikProps.values.stacksUrl).href); + const bitcoinPath = removeTrailingSlash(new URL(formikProps.values.bitcoinUrl).href); + try { - const path = removeTrailingSlash(new URL(url).href); - const response = await network.fetchFn(`${path}/v2/info`); - const chainInfo = await response.json(); - if (!chainInfo) throw new Error('Unable to fetch info from node'); - - // Attention: - // For mainnet/testnet the v2/info response `.network_id` refers to the chain ID - // For subnets the v2/info response `.network_id` refers to the network ID and the chain ID (they are the same for subnets) - // The `.parent_network_id` refers to the actual peer network ID in both cases - const { network_id: chainId, parent_network_id: parentNetworkId } = chainInfo; - - const isSubnet = typeof chainInfo.l1_subnet_governing_contract === 'string'; - const isFirstLevelSubnet = - isSubnet && - (parentNetworkId === PeerNetworkID.Mainnet || - parentNetworkId === PeerNetworkID.Testnet); - - // Currently, only subnets of mainnet and testnet are supported in the wallet - if (isFirstLevelSubnet) { - const parentChainId = - parentNetworkId === PeerNetworkID.Mainnet ? ChainID.Mainnet : ChainID.Testnet; - networksActions.addNetwork({ - chainId: parentChainId, // Used for differentiating control flow in the wallet - subnetChainId: chainId, // Used for signing transactions (via the network object, not to be confused with the NetworkConfigurations) - id: key as DefaultNetworkConfigurations, - name, - url: path, - }); - navigate(RouteUrls.Home); - return; - } - - if (chainId === ChainID.Mainnet || chainId === ChainID.Testnet) { - networksActions.addNetwork({ - chainId, - id: key as DefaultNetworkConfigurations, - name, - url: path, - }); - navigate(RouteUrls.Home); - return; - } + const bitcoinResponse = await network.fetchFn(`${bitcoinPath}/mempool/recent`); + if (!bitcoinResponse.ok) throw new Error('Unable to fetch mempool from bitcoin node'); + const bitcoinMempool = await bitcoinResponse.json(); + if (!Array.isArray(bitcoinMempool)) + throw new Error('Unable to fetch mempool from bitcoin node'); + } catch (error) { + setError('Unable to fetch mempool from bitcoin node'); + setLoading(false); + return; + } - setError('Unable to determine chainID from node.'); + let stacksChainInfo: any; + try { + const stacksResponse = await network.fetchFn(`${stacksPath}/v2/info`); + stacksChainInfo = await stacksResponse.json(); + if (!stacksChainInfo) throw new Error('Unable to fetch info from stacks node'); } catch (error) { - setError('Unable to fetch info from node.'); + setError('Unable to fetch info from stacks node'); + setLoading(false); + return; + } + + // Attention: + // For mainnet/testnet the v2/info response `.network_id` refers to the chain ID + // For subnets the v2/info response `.network_id` refers to the network ID and the chain ID (they are the same for subnets) + // The `.parent_network_id` refers to the actual peer network ID in both cases + const { network_id: chainId, parent_network_id: parentNetworkId } = stacksChainInfo; + + const isSubnet = typeof stacksChainInfo.l1_subnet_governing_contract === 'string'; + const isFirstLevelSubnet = + isSubnet && + (parentNetworkId === PeerNetworkID.Mainnet || + parentNetworkId === PeerNetworkID.Testnet); + + // Currently, only subnets of mainnet and testnet are supported in the wallet + if (isFirstLevelSubnet) { + const parentChainId = + parentNetworkId === PeerNetworkID.Mainnet ? ChainID.Mainnet : ChainID.Testnet; + networksActions.addNetwork({ + id: key as DefaultNetworkConfigurations, + name: name, + chainId: parentChainId, // Used for differentiating control flow in the wallet + subnetChainId: chainId, // Used for signing transactions (via the network object, not to be confused with the NetworkConfigurations) + url: stacksPath, + bitcoinNetwork: bitcoinApi, + bitcoinUrl: bitcoinPath, + }); + navigate(RouteUrls.Home); + } else if (chainId === ChainID.Mainnet || chainId === ChainID.Testnet) { + networksActions.addNetwork({ + id: key as DefaultNetworkConfigurations, + name: name, + chainId: chainId, + url: stacksPath, + bitcoinNetwork: bitcoinApi, + bitcoinUrl: bitcoinPath, + }); + navigate(RouteUrls.Home); + } else { + setError('Unable to determine chainID from node.'); } setLoading(false); }} > - {({ handleSubmit, values, handleChange }) => ( + {({ handleSubmit }) => ( @@ -133,6 +213,10 @@ export function AddNetwork() { rel="noreferrer" > Stacks Blockchain API + {' '} + or{' '} + + Bitcoin Blockchain API . Make sure you review and trust the host before you add it. @@ -140,30 +224,73 @@ export function AddNetwork() { autoFocus borderRadius="10px" height="64px" - onChange={handleChange} + onChange={formikProps.handleChange} name="name" placeholder="Name" - value={values.name} + value={formikProps.values.name} width="100%" data-testid={NetworkSelectors.NetworkName} /> + Bitcoin API + + + + + Mainnet + + + Testnet + + + Signet + + + Regtest + + + + Stacks API URL + + Bitcoin API URL diff --git a/src/app/features/ledger/flows/request-bitcoin-keys/ledger-request-bitcoin-keys.tsx b/src/app/features/ledger/flows/request-bitcoin-keys/ledger-request-bitcoin-keys.tsx index c947f9c8f10..357c5974f21 100644 --- a/src/app/features/ledger/flows/request-bitcoin-keys/ledger-request-bitcoin-keys.tsx +++ b/src/app/features/ledger/flows/request-bitcoin-keys/ledger-request-bitcoin-keys.tsx @@ -34,11 +34,8 @@ function LedgerRequestBitcoinKeys() { navigate('/', { replace: true }); }, async pullKeysFromDevice(app) { - const { keys } = await pullBitcoinKeysFromLedgerDevice( - app, - latestDeviceResponse?.targetId - )({ - network: bitcoinNetworkModeToCoreNetworkMode(network.chain.bitcoin.network), + const { keys } = await pullBitcoinKeysFromLedgerDevice(app)({ + network: bitcoinNetworkModeToCoreNetworkMode(network.chain.bitcoin.bitcoinNetwork), onRequestKey(index) { if (index <= 4) { ledgerNavigate.toDeviceBusyStep( 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 a8e89101f78..1a21d76828e 100644 --- a/src/app/features/psbt-signer/hooks/use-parsed-inputs.tsx +++ b/src/app/features/psbt-signer/hooks/use-parsed-inputs.tsx @@ -50,7 +50,7 @@ interface UseParsedInputsArgs { } export function useParsedInputs({ inputs, indexesToSign }: UseParsedInputsArgs) { const network = useCurrentNetwork(); - const bitcoinNetwork = getBtcSignerLibNetworkConfigByMode(network.chain.bitcoin.network); + const bitcoinNetwork = getBtcSignerLibNetworkConfigByMode(network.chain.bitcoin.bitcoinNetwork); const bitcoinAddressNativeSegwit = useCurrentAccountNativeSegwitIndexZeroSigner().address; const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner(); const inscriptions = useGetInscriptionsByOutputQueries(inputs).map(query => { diff --git a/src/app/features/psbt-signer/hooks/use-parsed-outputs.tsx b/src/app/features/psbt-signer/hooks/use-parsed-outputs.tsx index feb2c695f26..7f47a05ecb4 100644 --- a/src/app/features/psbt-signer/hooks/use-parsed-outputs.tsx +++ b/src/app/features/psbt-signer/hooks/use-parsed-outputs.tsx @@ -26,7 +26,7 @@ interface UseParsedOutputsArgs { export function useParsedOutputs({ isPsbtMutable, outputs, network }: UseParsedOutputsArgs) { const bitcoinAddressNativeSegwit = useCurrentAccountNativeSegwitIndexZeroSigner().address; const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner(); - const bitcoinNetwork = getBtcSignerLibNetworkConfigByMode(network.chain.bitcoin.network); + const bitcoinNetwork = getBtcSignerLibNetworkConfigByMode(network.chain.bitcoin.bitcoinNetwork); return useMemo( () => diff --git a/src/app/pages/bitcoin-contract-request/bitcoin-contract-request.tsx b/src/app/pages/bitcoin-contract-request/bitcoin-contract-request.tsx index 08cf710fd9c..c59eae606cf 100644 --- a/src/app/pages/bitcoin-contract-request/bitcoin-contract-request.tsx +++ b/src/app/pages/bitcoin-contract-request/bitcoin-contract-request.tsx @@ -54,7 +54,7 @@ export function BitcoinContractRequest() { const currentBitcoinNetwork = bitcoinAccountDetails.network; - if (currentBitcoinNetwork !== 'testnet') { + if (!['testnet', 'regtest'].includes(currentBitcoinNetwork)) { navigate(RouteUrls.BitcoinContractLockError, { state: { error: new Error('Invalid Network'), diff --git a/src/app/pages/rpc-sign-bip322-message/use-sign-bip322-message.ts b/src/app/pages/rpc-sign-bip322-message/use-sign-bip322-message.ts index bdc348f35ad..d3812bb78ff 100644 --- a/src/app/pages/rpc-sign-bip322-message/use-sign-bip322-message.ts +++ b/src/app/pages/rpc-sign-bip322-message/use-sign-bip322-message.ts @@ -90,7 +90,7 @@ function useSignBip322MessageFactory({ address, signPsbt }: SignBip322MessageFac message, address, signPsbt, - network: network.chain.bitcoin.network, + network: network.chain.bitcoin.bitcoinNetwork, }); await shortPauseBeforeToast(); 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 1f2cf93cdb8..0286a5e3438 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 @@ -124,7 +124,7 @@ export function useSendInscriptionForm() { .string() .required(FormErrorMessages.AddressRequired) .concat(btcAddressValidator()) - .concat(btcAddressNetworkValidator(currentNetwork.chain.bitcoin.network)), + .concat(btcAddressNetworkValidator(currentNetwork.chain.bitcoin.bitcoinNetwork)), }), }; } 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 6c7de9111f5..6bd72eb217f 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 @@ -64,7 +64,7 @@ export function useBrc20SendForm({ balance, tick, decimals }: UseBrc20SendFormAr recipient: yup .string() .concat(btcAddressValidator()) - .concat(btcAddressNetworkValidator(currentNetwork.chain.bitcoin.network)), + .concat(btcAddressNetworkValidator(currentNetwork.chain.bitcoin.bitcoinNetwork)), // .concat(notCurrentAddressValidator(currentAccountBtcAddress || '')), }); const { onFormStateChange } = useUpdatePersistedSendFormValues(); 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 1fa15be9b5d..5332473bb93 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 @@ -90,7 +90,7 @@ export function BtcSendForm() { symbol={symbol} /> - {currentNetwork.chain.bitcoin.network === 'testnet' && } + {currentNetwork.chain.bitcoin.bitcoinNetwork === 'testnet' && } 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 d502119d4da..57ebcbfe9a3 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 @@ -77,7 +77,7 @@ export function useBtcSendForm() { recipient: yup .string() .concat(btcAddressValidator()) - .concat(btcAddressNetworkValidator(currentNetwork.chain.bitcoin.network)) + .concat(btcAddressNetworkValidator(currentNetwork.chain.bitcoin.bitcoinNetwork)) .concat(notCurrentAddressValidator(nativeSegwitSigner.address || '')) .required('Enter a bitcoin 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 df64ef345a8..ced1322e951 100644 --- a/src/app/query/bitcoin/address/utxos-by-address.query.ts +++ b/src/app/query/bitcoin/address/utxos-by-address.query.ts @@ -56,7 +56,7 @@ export function useTaprootAccountUtxosQuery() { const address = getTaprootAddress({ index: addressIndexCounter.getValue(), keychain: account?.keychain, - network: network.chain.bitcoin.network, + network: network.chain.bitcoin.bitcoinNetwork, }); const unspentTransactions = await client.addressApi.getUtxosByAddress(address); diff --git a/src/app/query/bitcoin/ordinals/brc20/use-brc-20.ts b/src/app/query/bitcoin/ordinals/brc20/use-brc-20.ts index c56d230c91c..0add7a01da7 100644 --- a/src/app/query/bitcoin/ordinals/brc20/use-brc-20.ts +++ b/src/app/query/bitcoin/ordinals/brc20/use-brc-20.ts @@ -20,8 +20,8 @@ export function useBrc20FeatureFlag() { } const supportedNetwork = - currentNetwork.chain.bitcoin.network === 'mainnet' || - currentNetwork.chain.bitcoin.network === 'signet'; + currentNetwork.chain.bitcoin.bitcoinNetwork === 'mainnet' || + currentNetwork.chain.bitcoin.bitcoinNetwork === 'signet'; if (!supportedNetwork) return { enabled: false, reason: 'Unsupported network' } as const; diff --git a/src/app/query/bitcoin/ordinals/inscriptions.query.ts b/src/app/query/bitcoin/ordinals/inscriptions.query.ts index 474a9f8f4a5..67e34e90253 100644 --- a/src/app/query/bitcoin/ordinals/inscriptions.query.ts +++ b/src/app/query/bitcoin/ordinals/inscriptions.query.ts @@ -63,7 +63,7 @@ export function useGetInscriptionsInfiniteQuery() { const address = getTaprootAddress({ index: i, keychain: account?.keychain, - network: network.chain.bitcoin.network, + network: network.chain.bitcoin.bitcoinNetwork, }); acc[address] = i; return acc; @@ -71,7 +71,7 @@ export function useGetInscriptionsInfiniteQuery() { {} ); }, - [account, network.chain.bitcoin.network] + [account, network.chain.bitcoin.bitcoinNetwork] ); const query = useInfiniteQuery({ diff --git a/src/app/query/bitcoin/ordinalsbot-client.ts b/src/app/query/bitcoin/ordinalsbot-client.ts index 1c763ebb775..5ef39fcaf1d 100644 --- a/src/app/query/bitcoin/ordinalsbot-client.ts +++ b/src/app/query/bitcoin/ordinalsbot-client.ts @@ -142,7 +142,8 @@ function useOrdinalsbotApiUrl() { const currentNetwork = useCurrentNetwork(); const ordinalsbotConfig = useConfigOrdinalsbot(); - if (currentNetwork.chain.bitcoin.network === 'mainnet') return ordinalsbotConfig.mainnetApiUrl; + if (currentNetwork.chain.bitcoin.bitcoinNetwork === 'mainnet') + return ordinalsbotConfig.mainnetApiUrl; return ordinalsbotConfig.signetApiUrl; } diff --git a/src/app/store/accounts/blockchain/bitcoin/bitcoin-keychain.ts b/src/app/store/accounts/blockchain/bitcoin/bitcoin-keychain.ts index c5d8c1d1495..40b2edf076a 100644 --- a/src/app/store/accounts/blockchain/bitcoin/bitcoin-keychain.ts +++ b/src/app/store/accounts/blockchain/bitcoin/bitcoin-keychain.ts @@ -58,7 +58,7 @@ export function bitcoinAccountBuilderFactory( export function useBitcoinScureLibNetworkConfig() { const network = useCurrentNetwork(); - return getBtcSignerLibNetworkConfigByMode(network.chain.bitcoin.network); + return getBtcSignerLibNetworkConfigByMode(network.chain.bitcoin.bitcoinNetwork); } export function useBitcoinExtendedPublicKeyVersions(): Versions | undefined { @@ -70,7 +70,7 @@ export function useBitcoinExtendedPublicKeyVersions(): Versions | undefined { whenWallet({ software: undefined, ledger: getHdKeyVersionsFromNetwork( - bitcoinNetworkModeToCoreNetworkMode(network.chain.bitcoin.network) + bitcoinNetworkModeToCoreNetworkMode(network.chain.bitcoin.bitcoinNetwork) ), }), [network, whenWallet] diff --git a/src/app/store/accounts/blockchain/bitcoin/bitcoin.hooks.ts b/src/app/store/accounts/blockchain/bitcoin/bitcoin.hooks.ts index 9c7e4e0c3f5..54543e7a2be 100644 --- a/src/app/store/accounts/blockchain/bitcoin/bitcoin.hooks.ts +++ b/src/app/store/accounts/blockchain/bitcoin/bitcoin.hooks.ts @@ -25,7 +25,7 @@ export function useZeroIndexTaprootAddress(accIndex?: number) { const address = getTaprootAddress({ index: 0, keychain: account.keychain, - network: network.chain.bitcoin.network, + network: network.chain.bitcoin.bitcoinNetwork, }); return address; diff --git a/src/app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks.ts b/src/app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks.ts index 1500d3ead0d..d0a7aa5f076 100644 --- a/src/app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks.ts +++ b/src/app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks.ts @@ -35,7 +35,7 @@ const selectNativeSegwitAccountBuilder = bitcoinAccountBuilderFactory( const selectCurrentNetworkNativeSegwitAccountBuilder = createSelector( selectNativeSegwitAccountBuilder, selectCurrentNetwork, - (nativeSegwitKeychains, network) => nativeSegwitKeychains[network.chain.bitcoin.network] + (nativeSegwitKeychains, network) => nativeSegwitKeychains[network.chain.bitcoin.bitcoinNetwork] ); export function useNativeSegwitAccountBuilder() { diff --git a/src/app/store/accounts/blockchain/bitcoin/taproot-account.hooks.ts b/src/app/store/accounts/blockchain/bitcoin/taproot-account.hooks.ts index 7164f198adf..41ebb36516f 100644 --- a/src/app/store/accounts/blockchain/bitcoin/taproot-account.hooks.ts +++ b/src/app/store/accounts/blockchain/bitcoin/taproot-account.hooks.ts @@ -32,7 +32,7 @@ const selectTaprootAccountBuilder = bitcoinAccountBuilderFactory( const selectCurrentNetworkTaprootAccountBuilder = createSelector( selectTaprootAccountBuilder, selectCurrentNetwork, - (taprootKeychains, network) => taprootKeychains[network.chain.bitcoin.network] + (taprootKeychains, network) => taprootKeychains[network.chain.bitcoin.bitcoinNetwork] ); const selectCurrentTaprootAccount = createSelector( selectCurrentNetworkTaprootAccountBuilder, @@ -90,5 +90,5 @@ export function useCurrentAccountTaprootIndexZeroSigner() { export function useCurrentAccountTaprootSigner() { const currentAccountIndex = useCurrentAccountIndex(); const network = useCurrentNetwork(); - return useTaprootSigner(currentAccountIndex, network.chain.bitcoin.network); + return useTaprootSigner(currentAccountIndex, network.chain.bitcoin.bitcoinNetwork); } diff --git a/src/app/store/common/api-clients.hooks.ts b/src/app/store/common/api-clients.hooks.ts index cb0dc96414a..90482ad8885 100644 --- a/src/app/store/common/api-clients.hooks.ts +++ b/src/app/store/common/api-clients.hooks.ts @@ -18,7 +18,7 @@ import { useCurrentNetworkState } from '../networks/networks.hooks'; export function useBitcoinClient() { const network = useCurrentNetworkState(); - return new BitcoinClient(network.chain.bitcoin.url); + return new BitcoinClient(network.chain.bitcoin.bitcoinUrl); } // Unanchored by default (microblocks) diff --git a/src/app/store/networks/networks.hooks.ts b/src/app/store/networks/networks.hooks.ts index 2df503a673c..0b962b54fea 100644 --- a/src/app/store/networks/networks.hooks.ts +++ b/src/app/store/networks/networks.hooks.ts @@ -50,8 +50,26 @@ export function useNetworksActions() { return useMemo( () => ({ - addNetwork({ chainId, subnetChainId, id, name, url }: PersistedNetworkConfiguration) { - dispatch(networksActions.addNetwork({ chainId, subnetChainId, id, name, url })); + addNetwork({ + id, + name, + chainId, + subnetChainId, + url, + bitcoinNetwork, + bitcoinUrl, + }: PersistedNetworkConfiguration) { + dispatch( + networksActions.addNetwork({ + id, + name, + chainId, + subnetChainId, + url, + bitcoinNetwork, + bitcoinUrl, + }) + ); dispatch(networksActions.changeNetwork(id)); return; }, diff --git a/src/app/store/networks/networks.slice.ts b/src/app/store/networks/networks.slice.ts index 2ce6d1fe3f6..8be7e67b1f8 100644 --- a/src/app/store/networks/networks.slice.ts +++ b/src/app/store/networks/networks.slice.ts @@ -11,6 +11,7 @@ export type PersistedNetworkConfiguration = Omit< Pick['chain']['stacks'], 'blockchain' > & + Omit['chain']['bitcoin'], 'blockchain'> & Pick; export const networksAdapter = createEntityAdapter(); diff --git a/src/app/store/networks/networks.utils.ts b/src/app/store/networks/networks.utils.ts index 4f8a05cf413..fe3e74e15f0 100644 --- a/src/app/store/networks/networks.utils.ts +++ b/src/app/store/networks/networks.utils.ts @@ -1,13 +1,7 @@ import { Dictionary } from '@reduxjs/toolkit'; import { ChainID } from '@stacks/transactions'; -import { - BITCOIN_API_BASE_URL_MAINNET, - BITCOIN_API_BASE_URL_TESTNET, - NetworkConfiguration, -} from '@shared/constants'; - -import { whenStacksChainId } from '@app/common/utils'; +import { NetworkConfiguration } from '@shared/constants'; import { PersistedNetworkConfiguration } from './networks.slice'; @@ -50,7 +44,8 @@ export function transformNetworkStateToMultichainStucture( Object.entries(state) .map(([key, network]) => { if (!network) return ['', null]; - const { id, name, chainId, subnetChainId, url } = network; + const { id, name, chainId, subnetChainId, url, bitcoinNetwork, bitcoinUrl } = network; + return [ key, { @@ -59,18 +54,14 @@ export function transformNetworkStateToMultichainStucture( chain: { stacks: { blockchain: 'stacks', - url, + url: url, chainId, subnetChainId, }, bitcoin: { blockchain: 'bitcoin', - network: ChainID[chainId] ? ChainID[chainId].toLowerCase() : 'testnet', - url: - whenStacksChainId(chainId)({ - [ChainID.Mainnet]: BITCOIN_API_BASE_URL_MAINNET, - [ChainID.Testnet]: BITCOIN_API_BASE_URL_TESTNET, - }) || BITCOIN_API_BASE_URL_TESTNET, + bitcoinNetwork, + bitcoinUrl, }, }, }, diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 322db7c608e..a2ce8da6e46 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -57,8 +57,8 @@ interface BaseChainConfig { interface BitcoinChainConfig extends BaseChainConfig { blockchain: 'bitcoin'; - url: string; - network: BitcoinNetworkModes; + bitcoinUrl: string; + bitcoinNetwork: BitcoinNetworkModes; } interface StacksChainConfig extends BaseChainConfig { @@ -98,8 +98,8 @@ const networkMainnet: NetworkConfiguration = { }, bitcoin: { blockchain: 'bitcoin', - network: 'mainnet', - url: BITCOIN_API_BASE_URL_MAINNET, + bitcoinNetwork: 'mainnet', + bitcoinUrl: BITCOIN_API_BASE_URL_MAINNET, }, }, }; @@ -115,8 +115,8 @@ const networkTestnet: NetworkConfiguration = { }, bitcoin: { blockchain: 'bitcoin', - network: 'testnet', - url: BITCOIN_API_BASE_URL_TESTNET, + bitcoinNetwork: 'testnet', + bitcoinUrl: BITCOIN_API_BASE_URL_TESTNET, }, }, }; @@ -132,8 +132,8 @@ const networkSignet: NetworkConfiguration = { }, bitcoin: { blockchain: 'bitcoin', - network: 'signet', - url: BITCOIN_API_BASE_URL_SIGNET, + bitcoinNetwork: 'signet', + bitcoinUrl: BITCOIN_API_BASE_URL_SIGNET, }, }, }; @@ -149,8 +149,8 @@ const networkSbtcDevenv: NetworkConfiguration = { }, bitcoin: { blockchain: 'bitcoin', - network: 'regtest', - url: 'http://localhost:3002', + bitcoinNetwork: 'regtest', + bitcoinUrl: 'http://localhost:8999/api', }, }, }; @@ -166,8 +166,8 @@ const networkDevnet: NetworkConfiguration = { }, bitcoin: { blockchain: 'bitcoin', - network: 'regtest', - url: 'http://localhost:18443', + bitcoinNetwork: 'regtest', + bitcoinUrl: 'http://localhost:18443', }, }, }; diff --git a/tests/page-object-models/network.page.ts b/tests/page-object-models/network.page.ts index 6a7f08440fb..386d48b537f 100644 --- a/tests/page-object-models/network.page.ts +++ b/tests/page-object-models/network.page.ts @@ -4,7 +4,10 @@ import { createTestSelector } from '@tests/utils'; export class NetworkPage { readonly networkNameSelector = createTestSelector(NetworkSelectors.NetworkName); - readonly networkAddressSelector = createTestSelector(NetworkSelectors.NetworkAddress); + readonly networkStacksAddressSelector = createTestSelector(NetworkSelectors.NetworkStacksAddress); + readonly networkBitcoinAddressSelector = createTestSelector( + NetworkSelectors.NetworkBitcoinAddress + ); readonly networkKeySelector = createTestSelector(NetworkSelectors.NetworkKey); readonly btnAddNetworkSelector = createTestSelector(NetworkSelectors.BtnAddNetwork); readonly errorTextSelector = createTestSelector(NetworkSelectors.ErrorText); @@ -13,17 +16,22 @@ export class NetworkPage { async inputNetworkNameField(input: string) { const field = this.page.locator(this.networkNameSelector); - await field?.type(input); + await field?.fill(input); } - async inputNetworkAddressField(input: string) { - const field = this.page.locator(this.networkAddressSelector); - await field?.type(input); + async inputNetworkStacksAddressField(input: string) { + const field = this.page.locator(this.networkStacksAddressSelector); + await field?.fill(input); + } + + async inputNetworkBitcoinAddressField(input: string) { + const field = this.page.locator(this.networkBitcoinAddressSelector); + await field?.fill(input); } async inputNetworkKeyField(input: string) { const field = this.page.locator(this.networkKeySelector); - await field?.type(input); + await field?.fill(input); } async waitForErrorMessage() { diff --git a/tests/selectors/network.selectors.ts b/tests/selectors/network.selectors.ts index 38aa40ca29d..4908387c28f 100644 --- a/tests/selectors/network.selectors.ts +++ b/tests/selectors/network.selectors.ts @@ -1,9 +1,13 @@ export enum NetworkSelectors { NetworkName = 'network-name', - NetworkAddress = 'network-address', + NetworkStacksAddress = 'network-stacks-address', + NetworkBitcoinAddress = 'network-bitcoin-address', NetworkKey = 'network-key', BtnAddNetwork = 'btn-add-network', ErrorText = 'error-text', - EmptyAddressError = 'Enter a valid URL', - NoNodeFetch = 'Unable to fetch info from node.', + EmptyStacksAddressError = 'Enter a valid Stacks API URL', + EmptyBitcoinURLError = 'Enter a valid Bitcoin API URL', + EmptyKeyError = 'Enter a unique key', + NoStacksNodeFetch = 'Unable to fetch info from stacks node', + NoBitcoinNodeFetch = 'Unable to fetch mempool from bitcoin node', } diff --git a/tests/specs/network/add-network.spec.ts b/tests/specs/network/add-network.spec.ts index fcb1220f579..fbc17ea928f 100644 --- a/tests/specs/network/add-network.spec.ts +++ b/tests/specs/network/add-network.spec.ts @@ -12,22 +12,57 @@ test.describe('Networks tests', () => { await page.getByTestId(SettingsSelectors.BtnAddNetwork).click(); }); - test('validation error when address is empty', async ({ networkPage }) => { + test('validation error when stacks api url is empty', async ({ networkPage }) => { + await networkPage.inputNetworkStacksAddressField(''); + await networkPage.inputNetworkBitcoinAddressField('https://mempool.space/testnet/api'); + await networkPage.inputNetworkKeyField('test-network'); await networkPage.clickAddNetwork(); await networkPage.waitForErrorMessage(); const errorMsgElement = await networkPage.getErrorMessage(); const errorMessage = await errorMsgElement.innerText(); - test.expect(errorMessage).toEqual(NetworkSelectors.EmptyAddressError); + test.expect(errorMessage).toEqual(NetworkSelectors.EmptyStacksAddressError); }); - test('unable to fetch info from node', async ({ networkPage }) => { - await networkPage.inputNetworkAddressField('https://www.google.com/'); + test('validation error when key is empty', async ({ networkPage }) => { + await networkPage.clickAddNetwork(); + await networkPage.waitForErrorMessage(); + + const errorMsgElement = await networkPage.getErrorMessage(); + const errorMessage = await errorMsgElement.innerText(); + test.expect(errorMessage).toEqual(NetworkSelectors.EmptyKeyError); + }); + + test('validation error when bitcoin api url is empty', async ({ networkPage }) => { + await networkPage.inputNetworkBitcoinAddressField(''); await networkPage.inputNetworkKeyField('test-network'); await networkPage.clickAddNetwork(); await networkPage.waitForErrorMessage(); + + const errorMsgElement = await networkPage.getErrorMessage(); + const errorMessage = await errorMsgElement.innerText(); + test.expect(errorMessage).toEqual(NetworkSelectors.EmptyBitcoinURLError); + }); + + test('unable to fetch info from stacks node', async ({ networkPage }) => { + await networkPage.inputNetworkStacksAddressField('https://www.google.com/'); + await networkPage.inputNetworkKeyField('test-network'); + await networkPage.clickAddNetwork(); + await networkPage.waitForErrorMessage(); + + const errorMsgElement = await networkPage.getErrorMessage(); + const errorMessage = await errorMsgElement.innerText(); + test.expect(errorMessage).toEqual(NetworkSelectors.NoStacksNodeFetch); + }); + + test('unable to fetch mempool from bitcoin node', async ({ networkPage }) => { + await networkPage.inputNetworkBitcoinAddressField('https://www.google.com/'); + await networkPage.inputNetworkKeyField('test-network'); + await networkPage.clickAddNetwork(); + await networkPage.waitForErrorMessage(); + const errorMsgElement = await networkPage.getErrorMessage(); const errorMessage = await errorMsgElement.innerText(); - test.expect(errorMessage).toEqual(NetworkSelectors.NoNodeFetch); + test.expect(errorMessage).toEqual(NetworkSelectors.NoBitcoinNodeFetch); }); });