Skip to content

Commit

Permalink
feat: extended add-network page with the option to add bitcoin networ…
Browse files Browse the repository at this point in the history
…k too

feat: modified add network tests

fix: fixed types and removed unused exports
  • Loading branch information
Polybius93 committed Nov 15, 2023
1 parent 35493d5 commit 637667b
Show file tree
Hide file tree
Showing 29 changed files with 1,566 additions and 139 deletions.
1,245 changes: 1,245 additions & 0 deletions regtest-new.patch

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/app/common/hooks/use-bitcoin-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ 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
);

return bitcoinContractInterface;
Expand Down
257 changes: 192 additions & 65 deletions src/app/features/add-network/add-network.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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,
Expand All @@ -35,29 +38,89 @@ 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);
const [error, setError] = useState('');
const navigate = useNavigate();
const network = useCurrentStacksNetworkState();
const networksActions = useNetworksActions();
const [bitcoinApi, setBitcoinApi] = useState<BitcoinNetworkModes>('mainnet');

const formikProps = useFormik({
initialValues: addNetworkFormValues,
onSubmit: () => {},
});

const { setFieldValue } = formikProps;

useRouteHeader(<Header title="Add a network" onClose={() => 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 (
<CenteredPageContainer>
<Formik
initialValues={addNetworkFormValues}
onSubmit={async values => {
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;
Expand All @@ -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 }) => (
<form onSubmit={handleSubmit}>
<Stack
maxWidth={token('sizes.centeredPageFullWidth')}
px={['loose', 'base-loose']}
spacing="loose"
spacing="base-loose"
textAlign={['left', 'center']}
>
<Text>
Expand All @@ -133,37 +213,84 @@ export function AddNetwork() {
rel="noreferrer"
>
Stacks Blockchain API
</a>{' '}
or{' '}
<a href="https://github.com/Blockstream/esplora" target="_blank" rel="noreferrer">
Bitcoin Blockchain API
</a>
. Make sure you review and trust the host before you add it.
</Text>
<Input
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}
/>
<Title as={'h3'}>Bitcoin API</Title>
<SelectRoot onValueChange={handleApiChange} defaultValue="mainnet">
<SelectTrigger
className={css({
backgroundColor: 'accent.background-primary',
borderRadius: '6px',
border: '1px solid accent.border-primary',
})}
></SelectTrigger>
<SelectContent
className={css({
backgroundColor: 'accent.background-primary',
borderRadius: '6px',
border: '1px solid accent.border-primary',
dropShadow: 'lg',
})}
>
<SelectItem key="mainnet" value="mainnet">
Mainnet
</SelectItem>
<SelectItem key="testnet" value="testnet">
Testnet
</SelectItem>
<SelectItem key="signet" value="signet">
Signet
</SelectItem>
<SelectItem key="regtest" value="regtest">
Regtest
</SelectItem>
</SelectContent>
</SelectRoot>
<Title as={'h3'}>Stacks API URL</Title>
<Input
borderRadius="10px"
height="64px"
onChange={formikProps.handleChange}
name="stacksUrl"
placeholder="Stacks Address"
value={formikProps.values.stacksUrl}
width="100%"
data-testid={NetworkSelectors.NetworkStacksAddress}
/>
<Title as={'h3'}>Bitcoin API URL</Title>
<Input
borderRadius="10px"
height="64px"
onChange={handleChange}
name="url"
placeholder="Address"
value={values.url}
onChange={formikProps.handleChange}
name="bitcoinUrl"
placeholder="Bitcoin Address"
value={formikProps.values.bitcoinUrl}
width="100%"
data-testid={NetworkSelectors.NetworkAddress}
data-testid={NetworkSelectors.NetworkBitcoinAddress}
/>
<Input
borderRadius="10px"
height="64px"
onChange={handleChange}
onChange={formikProps.handleChange}
name="key"
placeholder="Key"
value={values.key}
value={formikProps.values.key}
width="100%"
data-testid={NetworkSelectors.NetworkKey}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/app/features/asset-list/asset-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function AssetsList() {
return (
<Stack pb="space.06" gap="space.05" data-testid={HomePageSelectors.BalancesList}>
{/* Temporary duplication during Ledger Bitcoin feature dev */}
{network.chain.bitcoin.network === 'testnet' &&
{network.chain.bitcoin.bitcoinNetwork === 'testnet' &&
whenWallet({
software: <BitcoinContractEntryPoint btcAddress={btcAddress} />,
ledger: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/app/features/psbt-signer/hooks/use-parsed-inputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
2 changes: 1 addition & 1 deletion src/app/features/psbt-signer/hooks/use-parsed-outputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
() =>
Expand Down
Loading

0 comments on commit 637667b

Please sign in to comment.