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 21, 2023
1 parent b035f20 commit 870574a
Show file tree
Hide file tree
Showing 29 changed files with 1,575 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
266 changes: 201 additions & 65 deletions src/app/features/add-network/add-network.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { useState } from 'react';
import { useCallback, 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 { NetworkSelectors } from '@tests/selectors/network.selectors';
import { Formik } from 'formik';
import { Formik, useFormik } from 'formik';
import { css } from 'leather-styles/css';
import { Stack, styled } from 'leather-styles/jsx';

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 @@ -21,6 +23,7 @@ import {
} from '@app/store/networks/networks.hooks';
import { LeatherButton } from '@app/ui/components/button';
import { Input } from '@app/ui/components/input';
import { Title } from '@app/ui/components/typography/title';

/**
* The **peer** network ID.
Expand All @@ -34,29 +37,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 @@ -65,58 +128,75 @@ 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 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;
}

let stacksChainInfo: any;
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 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 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.');
} catch (error) {
setError('Unable to fetch info from node.');
}
setLoading(false);
}}
>
{({ handleSubmit, values, handleChange }) => (
{({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<Stack
gap="space.05"
Expand All @@ -132,30 +212,86 @@ 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.
</styled.span>
<Input
autoFocus
data-testid={NetworkSelectors.NetworkName}
borderRadius="10px"
height="64px"
onChange={formikProps.handleChange}
name="name"
onChange={handleChange}
placeholder="Name"
value={values.name}
value={formikProps.values.name}
width="100%"
data-testid={NetworkSelectors.NetworkName}
/>
<Title>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>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>Bitcoin API URL</Title>
<Input
data-testid={NetworkSelectors.NetworkAddress}
name="url"
onChange={handleChange}
placeholder="Address"
value={values.url}
borderRadius="10px"
height="64px"
onChange={formikProps.handleChange}
name="bitcoinUrl"
placeholder="Bitcoin Address"
value={formikProps.values.bitcoinUrl}
width="100%"
data-testid={NetworkSelectors.NetworkBitcoinAddress}
/>
<Input
data-testid={NetworkSelectors.NetworkKey}
borderRadius="10px"
height="64px"
onChange={formikProps.handleChange}
name="key"
onChange={handleChange}
placeholder="Key"
value={values.key}
value={formikProps.values.key}
width="100%"
data-testid={NetworkSelectors.NetworkKey}
/>
{error ? (
<ErrorLabel data-testid={NetworkSelectors.ErrorText}>{error}</ErrorLabel>
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 @@ -57,7 +57,7 @@ export function AssetsList() {
})}

{/* 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 870574a

Please sign in to comment.