Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bitcoin regtest network option #4270

Merged
merged 3 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
} 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 @@
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);

Check warning on line 73 in src/app/features/add-network/add-network.tsx

View workflow job for this annotation

GitHub Actions / lint-eslint

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
},
[setFieldValue]
);

const setBitcoinUrl = useCallback(
(value: string) => {
setFieldValue('bitcoinUrl', value);

Check warning on line 80 in src/app/features/add-network/add-network.tsx

View workflow job for this annotation

GitHub Actions / lint-eslint

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
},
[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 @@
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 @@
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
Loading