Merge pull request #3901 from hirosystems/release/psbt-updates

Release/psbt updates
This commit is contained in:
kyranjamie
2023-06-21 13:53:42 +02:00
committed by GitHub
45 changed files with 682 additions and 264 deletions

View File

@@ -22,7 +22,7 @@ RUN apt-get update -y \
&& ./build-ext.sh /stacks-wallet-chromium.zip
FROM alpine:3.15.3
FROM alpine:3.16
COPY --from=builder /stacks-wallet-chromium.zip .
# Wait for extension.zip to be copied into local

View File

@@ -11,21 +11,19 @@ import {
} from '@app/common/transactions/bitcoin/coinselect/local-coin-selection';
import { useSpendableCurrentNativeSegwitAccountUtxos } from '@app/query/bitcoin/address/use-spendable-native-segwit-utxos';
import { useBitcoinScureLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain';
import {
useCurrentAccountNativeSegwitAddressIndexZero,
useCurrentAccountNativeSegwitSigner,
useCurrentBitcoinNativeSegwitAddressIndexPublicKeychain,
} from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
interface GenerateNativeSegwitTxValues {
amount: Money;
recipient: string;
}
export function useGenerateSignedNativeSegwitTx() {
const currentAccountBtcAddress = useCurrentAccountNativeSegwitAddressIndexZero();
const { data: utxos } = useSpendableCurrentNativeSegwitAccountUtxos();
const currentAddressIndexKeychain = useCurrentBitcoinNativeSegwitAddressIndexPublicKeychain();
const createSigner = useCurrentAccountNativeSegwitSigner();
const {
address,
publicKeychain: currentAddressIndexKeychain,
sign,
} = useCurrentAccountNativeSegwitIndexZeroSigner();
const networkMode = useBitcoinScureLibNetworkConfig();
@@ -33,11 +31,8 @@ export function useGenerateSignedNativeSegwitTx() {
(values: GenerateNativeSegwitTxValues, feeRate: number, isSendingMax?: boolean) => {
if (!utxos) return;
if (!feeRate) return;
if (!createSigner) return;
try {
const signer = createSigner(0);
const tx = new btc.Transaction();
const amountAsNumber = values.amount.amount.toNumber();
@@ -77,12 +72,12 @@ export function useGenerateSignedNativeSegwitTx() {
// When coin selection returns output with no address we assume it is
// a change output
if (!output.address) {
tx.addOutputAddress(currentAccountBtcAddress, BigInt(output.value), networkMode);
tx.addOutputAddress(address, BigInt(output.value), networkMode);
return;
}
tx.addOutputAddress(values.recipient, BigInt(output.value), networkMode);
});
signer.sign(tx);
sign(tx);
tx.finalize();
return { hex: tx.hex, fee };
@@ -92,12 +87,6 @@ export function useGenerateSignedNativeSegwitTx() {
return null;
}
},
[
createSigner,
currentAccountBtcAddress,
currentAddressIndexKeychain?.publicKey,
networkMode,
utxos,
]
[address, currentAddressIndexKeychain?.publicKey, networkMode, sign, utxos]
);
}

View File

@@ -1,15 +1,18 @@
import {
Brc20Token,
useBrc20TokensByAddressQuery,
useBrc20TokensQuery,
} from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.query';
import { useCurrentAccountTaprootAddressIndexZeroPayment } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
interface Brc20TokensLoaderProps {
children(brc20Tokens: Brc20Token[]): JSX.Element;
}
export function Brc20TokensLoader({ children }: Brc20TokensLoaderProps) {
const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootAddressIndexZeroPayment();
const { data: brc20Tokens } = useBrc20TokensByAddressQuery(bitcoinAddressTaproot);
if (!bitcoinAddressTaproot || !brc20Tokens) return null;
const { data: allBrc20TokensResponse } = useBrc20TokensQuery();
const brc20Tokens = allBrc20TokensResponse?.pages
.flatMap(page => page.brc20Tokens)
.filter(token => token.length > 0)
.flatMap(token => token);
if (!brc20Tokens) return null;
return children(brc20Tokens);
}

View File

@@ -3,7 +3,10 @@ import { BitcoinTransactionItem } from '@app/features/activity-list/components/t
import { StacksTransaction } from './stacks-transaction/stacks-transaction';
import { TransactionListTxs } from './transaction-list.model';
function renderTransaction(tx: TransactionListTxs) {
interface TransactionListItemProps {
tx: TransactionListTxs;
}
export function TransactionListItem({ tx }: TransactionListItemProps) {
switch (tx.blockchain) {
case 'bitcoin':
return <BitcoinTransactionItem transaction={tx.transaction} />;
@@ -13,10 +16,3 @@ function renderTransaction(tx: TransactionListTxs) {
return null;
}
}
interface TransactionListItemProps {
tx: TransactionListTxs;
}
export function TransactionListItem({ tx }: TransactionListItemProps) {
return renderTransaction(tx);
}

View File

@@ -1,5 +1,10 @@
import * as btc from '@scure/btc-signer';
import {
PsbtDecodedUtxosMainnet,
PsbtDecodedUtxosTestnet,
} from '@app/features/psbt-signer/hooks/use-psbt-decoded-utxos';
import { PsbtPlaceholderNode } from '../psbt-decoded-request-node/psbt-placeholder-node';
import { PsbtUnsignedInputList } from '../psbt-unsigned-input-list/psbt-unsigned-input-list';
import { PsbtUnsignedOutputList } from '../psbt-unsigned-output-list/psbt-unsigned-output-list';
@@ -7,16 +12,17 @@ import { PsbtUnsignedOutputList } from '../psbt-unsigned-output-list/psbt-unsign
interface PsbtDecodedRequestSimpleProps {
bitcoinAddressNativeSegwit: string;
bitcoinAddressTaproot: string;
inputs: btc.TransactionInputRequired[];
inputs: PsbtDecodedUtxosMainnet | PsbtDecodedUtxosTestnet;
outputs: btc.TransactionOutputRequired[];
showPlaceholder: boolean;
}
export function PsbtDecodedRequestSimple({
bitcoinAddressNativeSegwit,
bitcoinAddressTaproot,
inputs,
outputs,
showPlaceholder,
inputs,
}: PsbtDecodedRequestSimpleProps) {
if (showPlaceholder) return <PsbtPlaceholderNode />;

View File

@@ -2,7 +2,7 @@ import * as btc from '@scure/btc-signer';
import { Stack, color } from '@stacks/ui';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountTaprootAddressIndexZeroPayment } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { useCurrentAccountTaprootIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { usePsbtDecodedRequest } from '../../hooks/use-psbt-decoded-request';
import { DecodedPsbt } from '../../hooks/use-psbt-signer';
@@ -15,7 +15,7 @@ interface PsbtDecodedRequestProps {
}
export function PsbtDecodedRequest({ psbt }: PsbtDecodedRequestProps) {
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootAddressIndexZeroPayment();
const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner();
const unsignedInputs: btc.TransactionInputRequired[] = psbt.global.unsignedTx?.inputs ?? [];
const unsignedOutputs: btc.TransactionOutputRequired[] = psbt.global.unsignedTx?.outputs ?? [];
@@ -24,6 +24,7 @@ export function PsbtDecodedRequest({ psbt }: PsbtDecodedRequestProps) {
shouldDefaultToAdvancedView,
shouldShowPlaceholder,
showAdvancedView,
unsignedUtxos,
} = usePsbtDecodedRequest({
unsignedInputs,
unsignedOutputs,
@@ -45,7 +46,7 @@ export function PsbtDecodedRequest({ psbt }: PsbtDecodedRequestProps) {
<PsbtDecodedRequestSimple
bitcoinAddressNativeSegwit={nativeSegwitSigner.address}
bitcoinAddressTaproot={bitcoinAddressTaproot}
inputs={unsignedInputs}
inputs={unsignedUtxos}
outputs={unsignedOutputs}
showPlaceholder={shouldShowPlaceholder}
/>

View File

@@ -0,0 +1,49 @@
import { truncateMiddle } from '@stacks/ui-utils';
import { logger } from '@shared/logger';
import { i18nFormatCurrency } from '@app/common/money/format-money';
import { satToBtc } from '@app/common/money/unit-conversion';
import { OrdApiInscriptionTxOutput } from '@app/query/bitcoin/ordinals/ordinals-aware-utxo.query';
import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query';
import { useCalculateBitcoinFiatValue } from '@app/query/common/market-data/market-data.hooks';
import { PsbtDecodedNodeLayout } from '../../psbt-decoded-request-node/psbt-decoded-node.layout';
import { PsbtUnsignedInputWithInscription } from './psbt-unsigned-input-with-inscription';
interface PsbtUnsignedInputItemWithPossibleInscriptionProps {
addressNativeSegwit: string;
addressTaproot: string;
utxo: TaprootUtxo & OrdApiInscriptionTxOutput;
}
export function PsbtUnsignedInputItemWithPossibleInscription({
addressNativeSegwit,
addressTaproot,
utxo,
}: PsbtUnsignedInputItemWithPossibleInscriptionProps) {
const calculateBitcoinFiatValue = useCalculateBitcoinFiatValue();
const isInputCurrentAddress =
utxo.address === addressNativeSegwit || utxo.address === addressTaproot;
const inputValue = satToBtc(utxo.value).toString();
if (!utxo.address) {
logger.error('UTXO does not have an address');
return null;
}
return utxo.inscriptions ? (
<PsbtUnsignedInputWithInscription
address={utxo.address}
inputValue={inputValue}
path={utxo.inscriptions}
/>
) : (
<PsbtDecodedNodeLayout
hoverLabel={utxo.address}
subtitle={truncateMiddle(utxo.address)}
subValue={i18nFormatCurrency(calculateBitcoinFiatValue(inputValue))}
value={`${isInputCurrentAddress ? '-' : '+'}${inputValue}`}
/>
);
}

View File

@@ -1,18 +1,17 @@
import { truncateMiddle } from '@stacks/ui-utils';
import { BitcoinTransactionVectorOutput } from '@shared/models/transactions/bitcoin-transaction.model';
import { i18nFormatCurrency } from '@app/common/money/format-money';
import { satToBtc } from '@app/common/money/unit-conversion';
import { OrdApiInscriptionTxOutput } from '@app/query/bitcoin/ordinals/ordinals-aware-utxo.query';
import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query';
import { useCalculateBitcoinFiatValue } from '@app/query/common/market-data/market-data.hooks';
import { PsbtDecodedNodeLayout } from '../../psbt-decoded-request-node/psbt-decoded-node.layout';
import { PsbtUnsignedInputWithInscription } from './psbt-unsigned-input-with-inscription';
interface PsbtUnsignedInputItemProps {
addressNativeSegwit: string;
addressTaproot: string;
utxo: TaprootUtxo & OrdApiInscriptionTxOutput;
utxo: BitcoinTransactionVectorOutput;
}
export function PsbtUnsignedInputItem({
addressNativeSegwit,
@@ -22,24 +21,15 @@ export function PsbtUnsignedInputItem({
const calculateBitcoinFiatValue = useCalculateBitcoinFiatValue();
const isInputCurrentAddress =
utxo.address === addressNativeSegwit || utxo.address === addressTaproot;
utxo.scriptpubkey_address === addressNativeSegwit ||
utxo.scriptpubkey_address === addressTaproot;
const inputValue = satToBtc(utxo.value).toString();
const fiatValue = i18nFormatCurrency(calculateBitcoinFiatValue(utxo.value));
const inscription = utxo.inscriptions;
if (!utxo.address) return null;
return inscription ? (
<PsbtUnsignedInputWithInscription
address={utxo.address}
inputValue={inputValue}
path={inscription}
/>
) : (
return (
<PsbtDecodedNodeLayout
hoverLabel={utxo.address}
subtitle={truncateMiddle(utxo.address)}
subValue={`${fiatValue} USD`}
hoverLabel={utxo.scriptpubkey_address}
subtitle={truncateMiddle(utxo.scriptpubkey_address)}
subValue={i18nFormatCurrency(calculateBitcoinFiatValue(inputValue))}
value={`${isInputCurrentAddress ? '-' : '+'}${inputValue}`}
/>
);

View File

@@ -0,0 +1,12 @@
import { Box, Text } from '@stacks/ui';
import { HasChildren } from '@app/common/has-children';
export function PsbtUnsignedInputListLayout({ children }: HasChildren) {
return (
<Box background="white" borderTopLeftRadius="16px" borderTopRightRadius="16px" p="loose">
<Text fontWeight={500}>Inputs</Text>
{children}
</Box>
);
}

View File

@@ -1,40 +1,62 @@
import * as btc from '@scure/btc-signer';
import { Box, Text } from '@stacks/ui';
import { isUndefined } from '@shared/utils';
import { useOrdinalsAwareUtxoQueries } from '@app/query/bitcoin/ordinals/ordinals-aware-utxo.query';
import {
PsbtDecodedUtxosMainnet,
PsbtDecodedUtxosTestnet,
} from '@app/features/psbt-signer/hooks/use-psbt-decoded-utxos';
import { PsbtDecodedNodeLayout } from '../psbt-decoded-request-node/psbt-decoded-node.layout';
import { PsbtUnsignedInputItem } from './components/psbt-unsigned-input-item';
import { PsbtUnsignedInputItemWithPossibleInscription } from './components/psbt-unsigned-input-item-with-possible-inscription';
import { PsbtUnsignedInputListLayout } from './components/psbt-unsigned-input-list.layout';
interface PsbtUnsignedInputListProps {
addressNativeSegwit: string;
addressTaproot: string;
inputs: btc.TransactionInputRequired[];
inputs: PsbtDecodedUtxosMainnet | PsbtDecodedUtxosTestnet;
}
export function PsbtUnsignedInputList({
addressNativeSegwit,
addressTaproot,
inputs,
}: PsbtUnsignedInputListProps) {
const unsignedUtxos = useOrdinalsAwareUtxoQueries(inputs).map(query => query.data);
if (!inputs.utxos.length)
return (
<PsbtUnsignedInputListLayout>
<PsbtDecodedNodeLayout value="No inputs found" />
</PsbtUnsignedInputListLayout>
);
return (
<Box background="white" borderTopLeftRadius="16px" borderTopRightRadius="16px" p="loose">
<Text fontWeight={500}>Inputs</Text>
{unsignedUtxos.map(utxo => {
if (isUndefined(utxo)) return <PsbtDecodedNodeLayout value="No input data found" />;
return (
<PsbtUnsignedInputItem
addressNativeSegwit={addressNativeSegwit}
addressTaproot={addressTaproot}
key={utxo.transaction}
utxo={utxo}
/>
);
})}
</Box>
);
switch (inputs.network) {
case 'mainnet':
return (
<PsbtUnsignedInputListLayout>
{inputs.utxos.map((utxo, i) => {
return (
<PsbtUnsignedInputItemWithPossibleInscription
addressNativeSegwit={addressNativeSegwit}
addressTaproot={addressTaproot}
key={i}
utxo={utxo}
/>
);
})}
</PsbtUnsignedInputListLayout>
);
case 'testnet':
return (
<PsbtUnsignedInputListLayout>
{inputs.utxos.map((utxo, i) => {
return (
<PsbtUnsignedInputItem
addressNativeSegwit={addressNativeSegwit}
addressTaproot={addressTaproot}
key={i}
utxo={utxo}
/>
);
})}
</PsbtUnsignedInputListLayout>
);
default:
return null;
}
}

View File

@@ -33,7 +33,7 @@ export function PsbtUnsignedOutputItem({
<PsbtDecodedNodeLayout
hoverLabel={addressFromScript}
subtitle={truncateMiddle(addressFromScript)}
subValue={`${i18nFormatCurrency(calculateBitcoinFiatValue(outputValue))} USD`}
subValue={i18nFormatCurrency(calculateBitcoinFiatValue(outputValue))}
value={`${isOutputCurrentAddress ? '+' : ' '}${outputValue}`}
/>
);

View File

@@ -0,0 +1,12 @@
import { Box, Text } from '@stacks/ui';
import { HasChildren } from '@app/common/has-children';
export function PsbtUnsignedOutputListLayout({ children }: HasChildren) {
return (
<Box background="white" borderTopLeftRadius="16px" borderTopRightRadius="16px" p="loose">
<Text fontWeight={500}>Outputs</Text>
{children}
</Box>
);
}

View File

@@ -1,7 +1,8 @@
import * as btc from '@scure/btc-signer';
import { Box, Text } from '@stacks/ui';
import { PsbtDecodedNodeLayout } from '../psbt-decoded-request-node/psbt-decoded-node.layout';
import { PsbtUnsignedOutputItem } from './components/psbt-unsigned-output-item';
import { PsbtUnsignedOutputListLayout } from './components/psbt-unsigned-output-list.layout';
interface PsbtUnsignedOutputListProps {
addressNativeSegwit: string;
@@ -13,9 +14,15 @@ export function PsbtUnsignedOutputList({
addressTaproot,
outputs,
}: PsbtUnsignedOutputListProps) {
if (!outputs.length)
return (
<PsbtUnsignedOutputListLayout>
<PsbtDecodedNodeLayout value="No outputs found" />
</PsbtUnsignedOutputListLayout>
);
return (
<Box background="white" borderBottomLeftRadius="16px" borderBottomRightRadius="16px" p="loose">
<Text fontWeight={500}>Outputs</Text>
<PsbtUnsignedOutputListLayout>
{outputs.map((output, i) => {
return (
<PsbtUnsignedOutputItem
@@ -26,6 +33,6 @@ export function PsbtUnsignedOutputList({
/>
);
})}
</Box>
</PsbtUnsignedOutputListLayout>
);
}

View File

@@ -16,8 +16,10 @@ export function PsbtRequestHeader({ origin }: PsbtRequestHeaderProps) {
Sign transaction
</Title>
{caption && (
<Flag align="middle" img={<Favicon origin={origin} />} pl="tight">
<Caption wordBreak="break-word">{caption}</Caption>
<Flag align="top" img={<Favicon origin={origin} />} pl="tight">
<Caption wordBreak="break-word" lineHeight={1.3}>
{caption}
</Caption>
</Flag>
)}
</Flex>

View File

@@ -1,6 +1,6 @@
import { WarningLabel } from '@app/components/warning-label';
export function PsbtRequestWarningLabel(props: { appName?: string }) {
export function PsbtRequestAppWarningLabel(props: { appName?: string }) {
const { appName } = props;
const title = `Do not proceed unless you trust ${appName ?? 'Unknown'}!`;

View File

@@ -4,29 +4,41 @@ import * as btc from '@scure/btc-signer';
import { BitcoinNetworkModes } from '@shared/constants';
import { getAddressFromOutScript } from '@shared/crypto/bitcoin/bitcoin.utils';
import { isUndefined } from '@shared/utils';
import {
OrdApiInscriptionTxOutput,
useOrdinalsAwareUtxoQueries,
} from '@app/query/bitcoin/ordinals/ordinals-aware-utxo.query';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
import {
PsbtDecodedUtxosMainnet,
PsbtDecodedUtxosTestnet,
usePsbtDecodedUtxos,
} from './use-psbt-decoded-utxos';
function isPlaceholderTransaction(
address: string,
inputs: (OrdApiInscriptionTxOutput | undefined)[],
outputs: btc.TransactionOutputRequired[],
network: BitcoinNetworkModes
network: BitcoinNetworkModes,
unsignedOutputs: btc.TransactionOutputRequired[],
unsignedUtxos: PsbtDecodedUtxosMainnet | PsbtDecodedUtxosTestnet
) {
const inputsNotFromCurrentAddress = inputs.filter(input => {
return input?.address !== address;
});
const outputsNotToCurrentAddress = outputs.filter(output => {
let utxosNotFromCurrentAddress = [];
switch (unsignedUtxos.network) {
case 'mainnet':
utxosNotFromCurrentAddress = unsignedUtxos.utxos.filter(utxo => utxo.address !== address);
break;
case 'testnet':
utxosNotFromCurrentAddress = unsignedUtxos.utxos.filter(
vo => vo.scriptpubkey_address !== address
);
break;
}
const outputsNotToCurrentAddress = unsignedOutputs.filter(output => {
const addressFromScript = getAddressFromOutScript(output.script, network);
return addressFromScript !== address;
});
return inputsNotFromCurrentAddress.length === 0 && outputsNotToCurrentAddress.length === 0;
return utxosNotFromCurrentAddress.length === 0 && outputsNotToCurrentAddress.length === 0;
}
interface UsePsbtDecodedRequestArgs {
@@ -40,20 +52,20 @@ export function usePsbtDecodedRequest({
const [showAdvancedView, setShowAdvancedView] = useState(false);
const network = useCurrentNetwork();
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const unsignedUtxos = useOrdinalsAwareUtxoQueries(unsignedInputs).map(query => query.data);
const unsignedUtxos = usePsbtDecodedUtxos(unsignedInputs);
const defaultToAdvancedView = useCallback(() => {
const noInputs = isUndefined(unsignedUtxos) || !unsignedUtxos.length;
const noOutputs = isUndefined(unsignedOutputs) || !unsignedOutputs.length;
const noInputs = !unsignedInputs.length;
const noOutputs = !unsignedOutputs.length;
return noInputs || noOutputs;
}, [unsignedOutputs, unsignedUtxos]);
}, [unsignedInputs.length, unsignedOutputs.length]);
const showPlaceholder = useCallback(() => {
return isPlaceholderTransaction(
nativeSegwitSigner.address,
unsignedUtxos,
network.chain.bitcoin.network,
unsignedOutputs,
network.chain.bitcoin.network
unsignedUtxos
);
}, [nativeSegwitSigner.address, network.chain.bitcoin.network, unsignedOutputs, unsignedUtxos]);
@@ -62,5 +74,6 @@ export function usePsbtDecodedRequest({
shouldDefaultToAdvancedView: defaultToAdvancedView(),
shouldShowPlaceholder: showPlaceholder(),
showAdvancedView,
unsignedUtxos,
};
}

View File

@@ -0,0 +1,44 @@
import * as btc from '@scure/btc-signer';
import { WalletDefaultNetworkConfigurationIds } from '@shared/constants';
import { BitcoinTransactionVectorOutput } from '@shared/models/transactions/bitcoin-transaction.model';
import { isDefined } from '@shared/utils';
import {
OrdApiInscriptionTxOutput,
useOrdinalsAwareUtxoQueries,
} from '@app/query/bitcoin/ordinals/ordinals-aware-utxo.query';
import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query';
import { useGetBitcoinTransactionQueries } from '@app/query/bitcoin/transaction/transaction.query';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
export interface PsbtDecodedUtxosMainnet {
network: WalletDefaultNetworkConfigurationIds.mainnet;
utxos: (TaprootUtxo & OrdApiInscriptionTxOutput)[];
}
export interface PsbtDecodedUtxosTestnet {
network: WalletDefaultNetworkConfigurationIds.testnet;
utxos: BitcoinTransactionVectorOutput[];
}
export function usePsbtDecodedUtxos(
unsignedInputs: btc.TransactionInputRequired[]
): PsbtDecodedUtxosMainnet | PsbtDecodedUtxosTestnet {
const network = useCurrentNetwork();
const unsignedUtxos = useGetBitcoinTransactionQueries(unsignedInputs)
.map(query => query.data)
.filter(isDefined)
.map((input, i) => input.vout[unsignedInputs[i].index]);
// Mainnet only enabled query
const unsignedUtxosWithInscriptions = useOrdinalsAwareUtxoQueries(unsignedInputs)
.map(query => query.data)
.filter(isDefined);
return network.chain.bitcoin.network === 'mainnet' && unsignedUtxosWithInscriptions.length
? {
network: WalletDefaultNetworkConfigurationIds.mainnet,
utxos: unsignedUtxosWithInscriptions,
}
: { network: WalletDefaultNetworkConfigurationIds.testnet, utxos: unsignedUtxos };
}

View File

@@ -20,8 +20,6 @@ export function usePsbtSigner() {
return useMemo(
() => ({
nativeSegwitSigner,
taprootSigner,
signPsbtAtIndex(allowedSighash: btc.SignatureHash[], idx: number, tx: btc.Transaction) {
try {
nativeSegwitSigner?.signIndex(tx, idx, allowedSighash);
@@ -33,6 +31,17 @@ export function usePsbtSigner() {
}
}
},
signPsbt(tx: btc.Transaction) {
try {
nativeSegwitSigner?.sign(tx);
} catch (e1) {
try {
taprootSigner?.sign(tx);
} catch (e2) {
logger.error('Error signing PSBT', e1, e2);
}
}
},
getPsbtAsTransaction(psbt: string | Uint8Array) {
const bytes = isString(psbt) ? hexToBytes(psbt) : psbt;
return btc.Transaction.fromPSBT(bytes);

View File

@@ -5,7 +5,7 @@ import { useOnOriginTabClose } from '@app/routes/hooks/use-on-tab-closed';
import { PsbtDecodedRequest } from './components/psbt-decoded-request/psbt-decoded-request';
import { PsbtRequestActions } from './components/psbt-request-actions';
import { PsbtRequestHeader } from './components/psbt-request-header';
import { PsbtRequestWarningLabel } from './components/psbt-request-warning-label';
import { PsbtRequestAppWarningLabel } from './components/psbt-request-warning-label';
import { PsbtRequestLayout } from './components/psbt-request.layout';
import { DecodedPsbt } from './hooks/use-psbt-signer';
@@ -26,7 +26,7 @@ export function PsbtSigner(props: PsbtSignerProps) {
<>
<PsbtRequestLayout>
<PsbtRequestHeader origin={appName} />
<PsbtRequestWarningLabel appName={appName} />
<PsbtRequestAppWarningLabel appName={appName} />
<PsbtDecodedRequest psbt={psbt} />
</PsbtRequestLayout>
<PsbtRequestActions isLoading={false} onCancel={onCancel} onSignPsbt={onSignPsbt} />

View File

@@ -57,7 +57,7 @@ function SetPasswordPage() {
const { decodedAuthRequest } = useOnboardingState();
const analytics = useAnalytics();
useRouteHeader(<Header hideActions onClose={() => navigate(RouteUrls.BackUpSecretKey)} />);
useRouteHeader(<Header hideActions onClose={() => navigate(-1)} />);
useEffect(() => {
void analytics.page('view', '/set-password');

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef } from 'react';
import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react';
import toast from 'react-hot-toast';
import { useNavigate } from 'react-router-dom';
@@ -24,6 +24,8 @@ async function simulateShortDelayToAvoidImmediateNavigation() {
export function useSignIn() {
const [error, setError] = useSeedInputErrorState();
const [isKeyMasked, setIsKeyMasked] = useState(true);
const [sanitizedSecretKey, setSanitizedSecretKey] = useState('');
const { isLoading, setIsLoading, setIsIdle } = useLoading('useSignIn');
const navigate = useNavigate();
@@ -97,6 +99,33 @@ export function useSignIn() {
[submitMnemonicForm]
);
const onChange = useCallback(
(
event: ChangeEvent<HTMLInputElement>,
handleFormChange: (e: ChangeEvent<HTMLInputElement>) => void
) => {
const { value } = event.target;
setSanitizedSecretKey(previousSanitizedKey => {
// if value is shorter than sanitized secret key, remove characters
// from sanitized secret key
if (value.length < previousSanitizedKey.length) {
const removedChars = previousSanitizedKey.slice(value.length);
return previousSanitizedKey.slice(0, -removedChars.length);
} else {
// append new characters if value is longer than sanitized secret key
const newChars = value.slice(previousSanitizedKey.length);
return previousSanitizedKey + newChars;
}
});
handleFormChange(event);
},
[]
);
const toggleKeyMask = useCallback(() => {
setIsKeyMasked(prev => !prev);
}, []);
useEffect(
() => () => {
setError(undefined);
@@ -107,5 +136,15 @@ export function useSignIn() {
[setError]
);
return { onPaste, submitMnemonicForm, ref: textAreaRef, error, isLoading };
return {
onPaste,
submitMnemonicForm,
ref: textAreaRef,
error,
isLoading,
onChange,
toggleKeyMask,
isKeyMasked,
sanitizedSecretKey,
};
}

View File

@@ -1,7 +1,9 @@
import { ChangeEvent } from 'react';
import { FiEye, FiEyeOff } from 'react-icons/fi';
import { useNavigate } from 'react-router-dom';
import YourSecretKey from '@assets/images/onboarding/your-secret-key.png';
import { Box, Input, Stack, Text, color, useMediaQuery } from '@stacks/ui';
import { Box, Input, Stack, Text, color, useClipboard, useMediaQuery } from '@stacks/ui';
import { OnboardingSelectors } from '@tests/selectors/onboarding.selectors';
import { Form, Formik } from 'formik';
@@ -15,14 +17,26 @@ import {
DESKTOP_VIEWPORT_MIN_WIDTH,
} from '@app/components/global-styles/full-page-styles';
import { Header } from '@app/components/header';
import { Link } from '@app/components/link';
import { PageTitle } from '@app/components/page-title';
import { PrimaryButton } from '@app/components/primary-button';
import { Title } from '@app/components/typography';
import { useSignIn } from '@app/pages/onboarding/sign-in/hooks/use-sign-in';
export function SignIn() {
const { onPaste, submitMnemonicForm, error, isLoading, ref } = useSignIn();
const {
onPaste,
submitMnemonicForm,
error,
isLoading,
ref,
onChange,
toggleKeyMask,
isKeyMasked,
sanitizedSecretKey,
} = useSignIn();
const navigate = useNavigate();
const { onCopy } = useClipboard(sanitizedSecretKey);
const [desktopViewport] = useMediaQuery(`(min-width: ${DESKTOP_VIEWPORT_MIN_WIDTH})`);
@@ -32,7 +46,7 @@ export function SignIn() {
<CenteredPageContainer>
<Formik
initialValues={{ secretKey: '' }}
onSubmit={values => submitMnemonicForm(values.secretKey)}
onSubmit={_values => submitMnemonicForm(sanitizedSecretKey)}
>
{form => (
<Form>
@@ -65,14 +79,19 @@ export function SignIn() {
borderRadius="10px"
fontSize="16px"
minHeight="168px"
onChange={form.handleChange}
onChange={event => {
onChange(event as ChangeEvent<HTMLInputElement>, form.handleChange);
}}
onKeyDown={e => e.key === 'Enter' && form.submitForm()}
onPaste={onPaste}
onCopy={onCopy}
placeholder="Paste or type your Secret Key"
ref={ref as any}
spellCheck={false}
style={{ resize: 'none' }}
value={form.values.secretKey}
value={
isKeyMasked ? form.values.secretKey.replace(/[^ ]/g, '*') : sanitizedSecretKey
}
width="100%"
/>
{error && (
@@ -89,6 +108,14 @@ export function SignIn() {
</Text>
</ErrorLabel>
)}
<Stack alignItems="center">
<Link fontSize="14px" _hover={{ textDecoration: 'none' }} onClick={toggleKeyMask}>
<Stack alignItems="center" isInline spacing="tight">
{isKeyMasked ? <FiEye /> : <FiEyeOff />}
<Text>{isKeyMasked ? 'Show' : 'Hide'} Secret Key</Text>
</Stack>
</Link>
</Stack>
</Stack>
<PrimaryButton
data-testid={OnboardingSelectors.SignInBtn}

View File

@@ -14,13 +14,7 @@ import { usePsbtRequestSearchParams } from '@app/pages/psbt-request/psbt-request
export function usePsbtRequest() {
const { requestToken, tabId } = usePsbtRequestSearchParams();
const [isLoading, setIsLoading] = useState(false);
const {
signPsbtAtIndex,
getDecodedPsbt,
nativeSegwitSigner,
taprootSigner,
getPsbtAsTransaction,
} = usePsbtSigner();
const { signPsbt, signPsbtAtIndex, getDecodedPsbt, getPsbtAsTransaction } = usePsbtSigner();
const analytics = useAnalytics();
return useMemo(() => {
if (!requestToken) throw new Error('Cannot decode psbt without request token');
@@ -56,15 +50,7 @@ export function usePsbtRequest() {
if (!isUndefined(indexOrIndexes) && !isUndefined(allowedSighash)) {
ensureArray(indexOrIndexes).forEach(idx => signPsbtAtIndex(allowedSighash, idx, tx));
} else {
try {
nativeSegwitSigner?.sign(tx);
} catch (e1) {
try {
taprootSigner?.sign(tx);
} catch (e2) {
logger.error('Error signing tx', e1, e2);
}
}
signPsbt(tx);
}
const psbt = tx.toPSBT();
@@ -85,10 +71,9 @@ export function usePsbtRequest() {
getDecodedPsbt,
getPsbtAsTransaction,
isLoading,
nativeSegwitSigner,
requestToken,
signPsbt,
signPsbtAtIndex,
tabId,
taprootSigner,
]);
}

View File

@@ -10,7 +10,7 @@ import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params';
import { initialSearchParams } from '@app/common/initial-search-params';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountTaprootAddressIndexZeroPayment } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { useCurrentAccountTaprootIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { useAppPermissions } from '@app/store/app-permissions/app-permissions.slice';
@@ -32,14 +32,15 @@ export function useGetAddresses() {
const { tabId, origin, requestId } = useRpcRequestParams();
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const taprootPayment = useCurrentAccountTaprootAddressIndexZeroPayment();
const taprootSigner = useCurrentAccountTaprootIndexZeroSigner();
const stacksAccount = useCurrentStacksAccount();
const taprootAddressResponse: BtcAddress = {
symbol: 'BTC',
type: 'p2tr',
address: taprootPayment.address,
publicKey: bytesToHex(taprootPayment.publicKey),
address: taprootSigner.address,
publicKey: bytesToHex(taprootSigner.publicKey),
derivationPath: taprootSigner.derivationPath,
};
const nativeSegwitAddressResponse: BtcAddress = {
@@ -47,6 +48,7 @@ export function useGetAddresses() {
type: 'p2wpkh',
address: nativeSegwitSigner.address,
publicKey: bytesToHex(nativeSegwitSigner.publicKey),
derivationPath: nativeSegwitSigner.derivationPath,
};
const stacksAddressResponse = {

View File

@@ -5,7 +5,6 @@ import { RpcErrorCode } from '@btckit/types';
import { bytesToHex } from '@noble/hashes/utils';
import * as btc from '@scure/btc-signer';
import { logger } from '@shared/logger';
import { makeRpcErrorResponse, makeRpcSuccessResponse } from '@shared/rpc/rpc-methods';
import { isUndefined } from '@shared/utils';
@@ -40,13 +39,7 @@ function useRpcSignPsbtParams() {
function useRpcSignPsbt() {
const { origin, tabId, requestId, psbtHex, allowedSighash, signAtIndex } = useRpcSignPsbtParams();
const {
signPsbtAtIndex,
getDecodedPsbt,
nativeSegwitSigner,
taprootSigner,
getPsbtAsTransaction,
} = usePsbtSigner();
const { signPsbt, signPsbtAtIndex, getDecodedPsbt, getPsbtAsTransaction } = usePsbtSigner();
const tx = getPsbtAsTransaction(psbtHex);
@@ -61,15 +54,7 @@ function useRpcSignPsbt() {
signPsbtAtIndex(allowedSighash, idx, tx);
});
} else {
try {
nativeSegwitSigner?.sign(tx);
} catch (e1) {
try {
taprootSigner?.sign(tx);
} catch (e2) {
logger.error('Error signing tx', e1, e2);
}
}
signPsbt(tx);
}
const psbt = tx.toPSBT();

View File

@@ -1,7 +1,16 @@
import { useQuery } from '@tanstack/react-query';
import { useCallback, useEffect } from 'react';
import { AppUseQueryConfig } from '@app/query/query-config';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { createNumArrayOfRange } from '@app/common/utils';
import { QueryPrefixes } from '@app/query/query-prefixes';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountTaprootSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
const addressesSimultaneousFetchLimit = 5;
const stopSearchAfterNumberAddressesWithoutBrc20Tokens = 5;
interface Brc20TokenResponse {
available_balance: string;
@@ -53,15 +62,81 @@ async function fetchBrc20TokensByAddress(address: string): Promise<Brc20Token[]>
});
}
type FetchBrc20TokensByAddressResp = Awaited<ReturnType<typeof fetchBrc20TokensByAddress>>;
export function useBrc20TokensQuery() {
const network = useCurrentNetwork();
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const currentBitcoinAddress = nativeSegwitSigner.address;
const createSigner = useCurrentAccountTaprootSigner();
const analytics = useAnalytics();
export function useBrc20TokensByAddressQuery<T extends unknown = FetchBrc20TokensByAddressResp>(
address: string,
options?: AppUseQueryConfig<FetchBrc20TokensByAddressResp, T>
) {
return useQuery({
queryKey: [QueryPrefixes.Brc20TokenBalance, address],
queryFn: () => fetchBrc20TokensByAddress(address),
...options,
if (!createSigner) throw new Error('No signer');
const getNextTaprootAddressBatch = useCallback(
(fromIndex: number, toIndex: number) => {
return createNumArrayOfRange(fromIndex, toIndex - 1).map(num => {
const address = createSigner(num).address;
return address;
});
},
[createSigner]
);
const query = useInfiniteQuery({
queryKey: [QueryPrefixes.Brc20InfiniteQuery, currentBitcoinAddress, network.id],
async queryFn({ pageParam }) {
const fromIndex: number = pageParam?.fromIndex ?? 0;
let addressesWithoutTokens = pageParam?.addressesWithoutTokens ?? 0;
const addressesData = getNextTaprootAddressBatch(
fromIndex,
fromIndex + addressesSimultaneousFetchLimit
);
const brc20TokensPromises = addressesData.map(address => {
return fetchBrc20TokensByAddress(address);
});
const brc20Tokens = await Promise.all(brc20TokensPromises);
addressesWithoutTokens += brc20Tokens.filter(tokens => tokens.length === 0).length;
return {
addressesWithoutTokens,
brc20Tokens,
fromIndex,
};
},
getNextPageParam(prevInscriptionQuery) {
const { fromIndex, brc20Tokens, addressesWithoutTokens } = prevInscriptionQuery;
if (addressesWithoutTokens >= stopSearchAfterNumberAddressesWithoutBrc20Tokens) {
return undefined;
}
return {
fromIndex: fromIndex + addressesSimultaneousFetchLimit,
addressesWithoutTokens,
brc20Tokens,
};
},
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
staleTime: 3 * 60 * 1000,
});
// Auto-trigger next request
useEffect(() => {
void query.fetchNextPage();
}, [query, query.data]);
useEffect(() => {
const brc20AcrossAddressesCount = query.data?.pages.reduce((acc, page) => {
return acc + page.brc20Tokens.flatMap(item => item).length;
}, 0);
if (!query.hasNextPage && brc20AcrossAddressesCount && brc20AcrossAddressesCount > 0) {
void analytics.identify({
brc20_across_addresses_count: brc20AcrossAddressesCount,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [analytics, query.hasNextPage]);
return query;
}

View File

@@ -1,7 +1,7 @@
import { useConfigOrdinalsbot } from '@app/query/common/remote-config/remote-config.query';
import { useAppDispatch } from '@app/store';
import { useCurrentAccountIndex } from '@app/store/accounts/account';
import { useCurrentAccountTaprootAddressIndexZeroPayment } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { useCurrentAccountTaprootIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
import { brc20TransferInitiated } from '@app/store/ordinals/ordinals.slice';
@@ -34,7 +34,7 @@ export function useBrc20Transfers() {
const dispatch = useAppDispatch();
const currentAccountIndex = useCurrentAccountIndex();
const ordinalsbotClient = useOrdinalsbotClient();
const { address } = useCurrentAccountTaprootAddressIndexZeroPayment();
const { address } = useCurrentAccountTaprootIndexZeroSigner();
const { data: fees } = useAverageBitcoinFeeRates();
return {

View File

@@ -7,6 +7,7 @@ import { isTypedArray } from '@shared/utils';
import { Prettify } from '@shared/utils/type-utils';
import { QueryPrefixes } from '@app/query/query-prefixes';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
import { TaprootUtxo } from './use-taproot-address-utxos.query';
@@ -58,11 +59,14 @@ const queryOptions = {
} as const;
export function useOrdinalsAwareUtxoQueries(utxos: TaprootUtxo[] | btc.TransactionInputRequired[]) {
const network = useCurrentNetwork();
return useQueries({
queries: utxos.map(utxo => {
const txId = isTypedArray(utxo.txid) ? bytesToHex(utxo.txid) : utxo.txid;
const txIndex = 'index' in utxo ? utxo.index : utxo.vout;
return {
enabled: network.chain.bitcoin.network === 'mainnet',
queryKey: makeOrdinalsAwareUtxoQueryKey(txId, txIndex),
queryFn: () => fetchOrdinalsAwareUtxo(txId, txIndex),
select: (resp: OrdApiInscriptionTxOutput) =>

View File

@@ -1,4 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import * as btc from '@scure/btc-signer';
import { bytesToHex } from '@stacks/common';
import { UseQueryResult, useQueries, useQuery } from '@tanstack/react-query';
import { BitcoinTransaction } from '@shared/models/transactions/bitcoin-transaction.model';
import { AppUseQueryConfig } from '@app/query/query-config';
import { useBitcoinClient } from '@app/store/common/api-clients.hooks';
@@ -29,3 +33,26 @@ export function useGetBitcoinTransaction<T extends unknown = FetchBitcoinTransac
...options,
});
}
const queryOptions = {
cacheTime: Infinity,
staleTime: 15 * 60 * 1000,
refetchOnWindowFocus: false,
} as const;
export function useGetBitcoinTransactionQueries(
inputs: btc.TransactionInputRequired[]
): UseQueryResult<BitcoinTransaction>[] {
const client = useBitcoinClient();
return useQueries({
queries: inputs.map(input => {
const txId = bytesToHex(input.txid);
return {
queryKey: ['bitcoin-transaction', txId],
queryFn: () => fetchBitcoinTransaction(client)(txId),
...queryOptions,
};
}),
});
}

View File

@@ -7,6 +7,7 @@ import { CryptoCurrencies } from '@shared/models/currencies.model';
import { MarketData, createMarketData, createMarketPair } from '@shared/models/market.model';
import { createMoney, currencyDecimalsMap } from '@shared/models/money.model';
import { createMoneyFromDecimal } from '@shared/models/money.model';
import { isNumber } from '@shared/utils';
import { calculateMeanAverage } from '@app/common/math/calculate-averages';
import { convertAmountToFractionalUnit } from '@app/common/money/calculate-money';
@@ -58,8 +59,11 @@ export function useCalculateBitcoinFiatValue() {
const btcMarketData = useCryptoCurrencyMarketData('BTC');
return useCallback(
(value: string) =>
baseCurrencyAmountInQuote(createMoneyFromDecimal(Number(value), 'BTC'), btcMarketData),
(value: string | number) =>
baseCurrencyAmountInQuote(
createMoneyFromDecimal(isNumber(value) ? value : Number(value), 'BTC'),
btcMarketData
),
[btcMarketData]
);
}

View File

@@ -16,4 +16,5 @@ export enum QueryPrefixes {
StampCollection = 'stamp-collection',
StampsByAddress = 'stamps-by-address',
Brc20InfiniteQuery = 'brc20-infinite-query',
}

View File

@@ -9,11 +9,16 @@ import { getBtcSignerLibNetworkConfigByMode } from '@shared/crypto/bitcoin/bitco
import {
deriveAddressIndexKeychainFromAccount,
deriveAddressIndexZeroFromAccount,
whenPaymentType,
} from '@shared/crypto/bitcoin/bitcoin.utils';
import { deriveTaprootAccountFromRootKeychain } from '@shared/crypto/bitcoin/p2tr-address-gen';
import {
deriveTaprootAccountFromRootKeychain,
getTaprootAddressIndexDerivationPath,
} from '@shared/crypto/bitcoin/p2tr-address-gen';
import {
deriveNativeSegWitAccountKeychain,
getNativeSegWitPaymentFromAddressIndex,
getNativeSegwitAddressIndexDerivationPath,
} from '@shared/crypto/bitcoin/p2wpkh-address-gen';
import { mnemonicToRootNode } from '@app/common/keychain/keychain';
@@ -77,12 +82,13 @@ export function useBitcoinScureLibNetworkConfig() {
}
interface BitcoinSignerFactoryArgs {
accountIndex: number;
accountKeychain: HDKey;
paymentFn(keychain: HDKey, network: BitcoinNetworkModes): any;
network: BitcoinNetworkModes;
}
export function bitcoinSignerFactory<T extends BitcoinSignerFactoryArgs>(args: T) {
const { network, paymentFn, accountKeychain } = args;
const { accountIndex, network, paymentFn, accountKeychain } = args;
return (addressIndex: number) => {
const addressIndexKeychain =
deriveAddressIndexKeychainFromAccount(accountKeychain)(addressIndex);
@@ -96,6 +102,13 @@ export function bitcoinSignerFactory<T extends BitcoinSignerFactoryArgs>(args: T
payment,
addressIndex,
publicKeychain,
derivationPath: whenPaymentType(payment.type)({
p2wpkh: getNativeSegwitAddressIndexDerivationPath(network, accountIndex, addressIndex),
p2tr: getTaprootAddressIndexDerivationPath(network, accountIndex, addressIndex),
'p2wpkh-p2sh': 'Not supported',
p2pkh: 'Not supported',
p2sh: 'Not supported',
}),
get address() {
if (!payment.address) throw new Error('Unable to get address from payment');
return payment.address;
@@ -136,6 +149,7 @@ function createSignersForAllNetworkTypes<T extends CreateSignersForAllNetworkTyp
function makeNetworkSigner(keychain: HDKey, network: BitcoinNetworkModes) {
return bitcoinSignerFactory({
accountIndex,
accountKeychain: keychain,
paymentFn: paymentFn as T['paymentFn'],
network,

View File

@@ -1,3 +1,4 @@
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { bitcoinNetworkModeToCoreNetworkMode } from '@shared/crypto/bitcoin/bitcoin.utils';
@@ -16,18 +17,21 @@ import {
function useNativeSegwitActiveNetworkAccountPrivateKeychain() {
const network = useCurrentNetwork();
return useSelector(
whenNetwork(bitcoinNetworkModeToCoreNetworkMode(network.chain.bitcoin.network))({
mainnet: selectMainnetNativeSegWitKeychain,
testnet: selectTestnetNativeSegWitKeychain,
})
const selector = useMemo(
() =>
whenNetwork(bitcoinNetworkModeToCoreNetworkMode(network.chain.bitcoin.network))({
mainnet: selectMainnetNativeSegWitKeychain,
testnet: selectTestnetNativeSegWitKeychain,
}),
[network.chain.bitcoin.network]
);
return useSelector(selector);
}
export function useNativeSegwitCurrentAccountPrivateKeychain() {
const keychain = useNativeSegwitActiveNetworkAccountPrivateKeychain();
const currentAccountIndex = useCurrentAccountIndex();
return keychain?.(currentAccountIndex);
return useMemo(() => keychain?.(currentAccountIndex), [currentAccountIndex, keychain]);
}
export function useNativeSegwitNetworkSigners() {
@@ -44,13 +48,16 @@ export function useNativeSegwitNetworkSigners() {
function useNativeSegwitSigner(accountIndex: number) {
const network = useCurrentNetwork();
const accountKeychain = useNativeSegwitActiveNetworkAccountPrivateKeychain()?.(accountIndex);
if (!accountKeychain) return;
return bitcoinSignerFactory({
accountKeychain,
paymentFn: getNativeSegWitPaymentFromAddressIndex,
network: network.chain.bitcoin.network,
});
return useMemo(() => {
if (!accountKeychain) return;
return bitcoinSignerFactory({
accountIndex,
accountKeychain,
paymentFn: getNativeSegWitPaymentFromAddressIndex,
network: network.chain.bitcoin.network,
});
}, [accountIndex, accountKeychain, network.chain.bitcoin.network]);
}
export function useCurrentAccountNativeSegwitSigner() {
@@ -60,8 +67,10 @@ export function useCurrentAccountNativeSegwitSigner() {
export function useCurrentAccountNativeSegwitIndexZeroSigner() {
const signer = useCurrentAccountNativeSegwitSigner();
if (!signer) throw new Error('No signer');
return signer(0);
return useMemo(() => {
if (!signer) throw new Error('No signer');
return signer(0);
}, [signer]);
}
/**
@@ -69,7 +78,7 @@ export function useCurrentAccountNativeSegwitIndexZeroSigner() {
*/
export function useCurrentAccountNativeSegwitAddressIndexZero() {
const signer = useCurrentAccountNativeSegwitSigner();
return signer?.(0).payment.address as string;
return useMemo(() => signer?.(0).payment.address, [signer]) as string;
}
/**
@@ -79,11 +88,3 @@ export function useNativeSegwitAccountIndexAddressIndexZero(accountIndex: number
const signer = useNativeSegwitSigner(accountIndex)?.(0);
return signer?.payment.address as string;
}
/**
* @deprecated Use signer.publicKeychain directly instead
*/
export function useCurrentBitcoinNativeSegwitAddressIndexPublicKeychain() {
const signer = useCurrentAccountNativeSegwitSigner();
return signer?.(0).publicKeychain;
}

View File

@@ -1,3 +1,4 @@
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { BitcoinNetworkModes } from '@shared/constants';
@@ -32,8 +33,10 @@ export function useTaprootCurrentAccountPrivateKeychain() {
export function useTaprootAccountKeychain(accountIndex: number) {
const accountKeychain = useTaprootActiveNetworkAccountPrivateKeychain();
if (!accountKeychain) return; // TODO: Revisit this return early
return accountKeychain(accountIndex);
return useMemo(() => {
if (!accountKeychain) return; // TODO: Revisit this return early
return accountKeychain(accountIndex);
}, [accountIndex, accountKeychain]);
}
export function useTaprootNetworkSigners() {
@@ -49,13 +52,24 @@ export function useTaprootNetworkSigners() {
function useTaprootSigner(accountIndex: number, network: BitcoinNetworkModes) {
const accountKeychain = useTaprootAccountKeychain(accountIndex);
if (!accountKeychain) return; // TODO: Revisit this return early
return bitcoinSignerFactory({
accountKeychain,
paymentFn: getTaprootPaymentFromAddressIndex,
network,
});
return useMemo(() => {
if (!accountKeychain) return; // TODO: Revisit this return early
return bitcoinSignerFactory({
accountIndex,
accountKeychain,
paymentFn: getTaprootPaymentFromAddressIndex,
network,
});
}, [accountIndex, accountKeychain, network]);
}
export function useCurrentAccountTaprootIndexZeroSigner() {
const signer = useCurrentAccountTaprootSigner();
return useMemo(() => {
if (!signer) throw new Error('No signer');
return signer(0);
}, [signer]);
}
export function useCurrentAccountTaprootSigner() {
@@ -63,17 +77,3 @@ export function useCurrentAccountTaprootSigner() {
const network = useCurrentNetwork();
return useTaprootSigner(currentAccountIndex, network.chain.bitcoin.network);
}
export function useCurrentAccountTaprootAddressIndexZeroPayment() {
const createSigner = useCurrentAccountTaprootSigner();
const indexZeroSigner = createSigner?.(0);
if (!indexZeroSigner?.payment.address) throw new Error('No address found');
const publicKey = indexZeroSigner.publicKeychain.publicKey;
if (!publicKey) throw new Error('No public key found');
// Creating new object to have known property types
return {
address: indexZeroSigner.payment.address,
publicKey,
type: indexZeroSigner.payment.type,
};
}

View File

@@ -2,6 +2,8 @@ import { useSelector } from 'react-redux';
import { createSelector } from '@reduxjs/toolkit';
import { initialSearchParams } from '@app/common/initial-search-params';
import { initBigNumber } from '@app/common/math/helpers';
import { RootState } from '@app/store';
import { selectStacksChain } from '../chains/stx-chain.selectors';
@@ -19,6 +21,10 @@ export function useCurrentKeyDetails() {
}
export const selectCurrentAccountIndex = createSelector(selectStacksChain, state => {
const customAccountIndex = initialSearchParams.get('accountIndex');
if (customAccountIndex && initBigNumber(customAccountIndex).isInteger()) {
return initBigNumber(customAccountIndex).toNumber();
}
return state[defaultKeyId].currentAccountIndex;
});

View File

@@ -3,7 +3,11 @@ import * as btc from '@scure/btc-signer';
import { hexToBytes } from '@stacks/common';
import { RouteUrls } from '@shared/route-urls';
import { SignPsbtRequest } from '@shared/rpc/methods/sign-psbt';
import {
SignPsbtRequest,
getRpcSignPsbtParamErrors,
validateRpcSignPsbtParams,
} from '@shared/rpc/methods/sign-psbt';
import { makeRpcErrorResponse } from '@shared/rpc/rpc-methods';
import { ensureArray, isDefined } from '@shared/utils';
@@ -25,24 +29,23 @@ function validatePsbt(hex: string) {
}
export async function rpcSignPsbt(message: SignPsbtRequest, port: chrome.runtime.Port) {
if (!message.params || !message.params.hex) {
if (!validateRpcSignPsbtParams(message.params)) {
const errors = getRpcSignPsbtParamErrors(message.params);
chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('signPsbt', {
id: message.id,
error: { code: RpcErrorCode.INVALID_PARAMS, message: 'Invalid parameters' },
error: {
code: RpcErrorCode.INVALID_PARAMS,
message:
'Invalid parameters: ' +
errors.map(e => `Error in path ${e.path}, ${e.message}.`).join(' '),
},
})
);
return;
}
const params: RequestParams = [
['requestId', message.id],
['hex', message.params.hex],
['publicKey', message.params.publicKey],
['network', message.params.network ?? 'mainnet'],
];
if (!validatePsbt(message.params.hex)) {
chrome.tabs.sendMessage(
getTabIdFromPort(port),
@@ -54,17 +57,28 @@ export async function rpcSignPsbt(message: SignPsbtRequest, port: chrome.runtime
return;
}
const requestParams: RequestParams = [
['requestId', message.id],
['hex', message.params.hex],
['publicKey', message.params.publicKey],
['network', message.params.network ?? 'mainnet'],
];
if (isDefined(message.params.account)) {
requestParams.push(['accountIndex', message.params.account.toString()]);
}
if (isDefined(message.params.allowedSighash))
ensureArray(message.params.allowedSighash).forEach(hash =>
params.push(['allowedSighash', hash.toString()])
requestParams.push(['allowedSighash', hash.toString()])
);
if (isDefined(message.params.signAtIndex))
ensureArray(message.params.signAtIndex).forEach(index =>
params.push(['signAtIndex', index.toString()])
requestParams.push(['signAtIndex', index.toString()])
);
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, params);
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, requestParams);
const { id } = await triggerRequestWindowOpen(RouteUrls.RpcSignPsbt, urlParams);
listenForPopupClose({

View File

@@ -25,6 +25,13 @@ export function rpcSupportedMethods(message: SupportedMethodsRequest, port: chro
},
{
name: 'sendTransfer',
docsUrl:
'https://hirowallet.gitbook.io/developers/bitcoin/sign-transactions/sending-bitcoin',
},
{
name: 'signPsbt',
docsUrl:
'https://hirowallet.gitbook.io/developers/bitcoin/sign-transactions/partially-signed-bitcoin-transactions-psbts',
},
],
},

View File

@@ -38,7 +38,9 @@ export enum WalletDefaultNetworkConfigurationIds {
export type DefaultNetworkConfigurations = keyof typeof WalletDefaultNetworkConfigurationIds;
export type NetworkModes = 'mainnet' | 'testnet';
export const networkModes = ['mainnet', 'testnet'] as const;
export type NetworkModes = (typeof networkModes)[number];
type BitcoinTestnetModes = 'testnet' | 'regtest' | 'signet';

View File

@@ -1,3 +1,4 @@
import { PaymentTypes } from '@btckit/types';
import { hexToBytes } from '@noble/hashes/utils';
import { HDKey } from '@scure/bip32';
import * as btc from '@scure/btc-signer';
@@ -84,3 +85,32 @@ export function getAddressFromOutScript(script: Uint8Array, network: BitcoinNetw
logger.error(`Unknown address type=${outScript.type}`);
return '';
}
type BtcSignerLibPaymentTypeIdentifers = 'wpkh' | 'wsh' | 'tr' | 'pkh' | 'sh';
const paymentTypeMap: Record<BtcSignerLibPaymentTypeIdentifers, PaymentTypes> = {
wpkh: 'p2wpkh',
wsh: 'p2wpkh-p2sh',
tr: 'p2tr',
pkh: 'p2pkh',
sh: 'p2sh',
};
function btcSignerLibPaymentTypeToPaymentTypeMap(payment: BtcSignerLibPaymentTypeIdentifers) {
return paymentTypeMap[payment];
}
function isBtcSignerLibPaymentType(payment: string): payment is BtcSignerLibPaymentTypeIdentifers {
return payment in paymentTypeMap;
}
function parseKnownPaymentType(payment: BtcSignerLibPaymentTypeIdentifers | PaymentTypes) {
return isBtcSignerLibPaymentType(payment)
? btcSignerLibPaymentTypeToPaymentTypeMap(payment)
: payment;
}
type PaymentTypeMap<T> = Record<PaymentTypes, T>;
export function whenPaymentType(mode: PaymentTypes | BtcSignerLibPaymentTypeIdentifers) {
return <T>(paymentMap: PaymentTypeMap<T>): T => paymentMap[parseKnownPaymentType(mode)];
}

View File

@@ -11,6 +11,14 @@ function getTaprootAccountDerivationPath(network: BitcoinNetworkModes, accountIn
return `m/86'/${getBitcoinCoinTypeIndexByNetwork(network)}'/${accountIndex}'`;
}
export function getTaprootAddressIndexDerivationPath(
network: BitcoinNetworkModes,
accountIndex: number,
addressIndex: number
) {
return getTaprootAccountDerivationPath(network, accountIndex) + `/0/${addressIndex}`;
}
export function deriveTaprootAccountFromRootKeychain(
keychain: HDKey,
network: BitcoinNetworkModes

View File

@@ -10,13 +10,21 @@ import {
getBitcoinCoinTypeIndexByNetwork,
} from './bitcoin.utils';
function getNativeSegWitAccountDerivationPath(network: BitcoinNetworkModes, accountIndex: number) {
function getNativeSegwitAccountDerivationPath(network: BitcoinNetworkModes, accountIndex: number) {
return `m/84'/${getBitcoinCoinTypeIndexByNetwork(network)}'/${accountIndex}'`;
}
export function getNativeSegwitAddressIndexDerivationPath(
network: BitcoinNetworkModes,
accountIndex: number,
addressIndex: number
) {
return getNativeSegwitAccountDerivationPath(network, accountIndex) + `/0/${addressIndex}`;
}
export function deriveNativeSegWitAccountKeychain(keychain: HDKey, network: NetworkModes) {
if (keychain.depth !== DerivationPathDepth.Root) throw new Error('Keychain passed is not a root');
return (index: number) => keychain.derive(getNativeSegWitAccountDerivationPath(network, index));
return (index: number) => keychain.derive(getNativeSegwitAccountDerivationPath(network, index));
}
export function getNativeSegWitPaymentFromAddressIndex(

View File

@@ -25,7 +25,7 @@ interface BitcoinTransactionStatus {
block_time: number | null;
}
interface BitcoinTransactionVectorOutput {
export interface BitcoinTransactionVectorOutput {
scriptpubkey: string;
scriptpubkey_asm: string;
scriptpubkey_type: string;

View File

@@ -1,14 +1,36 @@
import { DefineRpcMethod, RpcRequest, RpcResponse } from '@btckit/types';
import { SignatureHash } from '@scure/btc-signer';
import * as yup from 'yup';
import { NetworkModes } from '@shared/constants';
import { networkModes } from '@shared/constants';
interface SignPsbtRequestParams {
publicKey: string;
allowedSighash?: SignatureHash[];
hex: string;
signAtIndex?: number | number[];
network?: NetworkModes;
const rpcSignPsbtValidator = yup.object().shape({
publicKey: yup.string().required(),
allowedSighash: yup.array().of(yup.number().required()),
hex: yup.string().required(),
signAtIndex: yup.number().integer().positive(),
network: yup.string().oneOf(networkModes),
account: yup.number().integer().positive(),
});
type SignPsbtRequestParams = yup.InferType<typeof rpcSignPsbtValidator>;
export function validateRpcSignPsbtParams(obj: unknown): obj is SignPsbtRequestParams {
try {
rpcSignPsbtValidator.validateSync(obj, { abortEarly: false });
return true;
} catch (e) {
return false;
}
}
export function getRpcSignPsbtParamErrors(obj: unknown) {
try {
rpcSignPsbtValidator.validateSync(obj, { abortEarly: false });
return [];
} catch (e) {
if (e instanceof yup.ValidationError) return e.inner;
return [];
}
}
export type SignPsbtRequest = RpcRequest<'signPsbt', SignPsbtRequestParams>;

View File

@@ -23,6 +23,10 @@ const bitcoinTestnet: BitcoinNetwork = {
wif: 0xef,
};
// For testing mainnet
const tempHex =
'70736274ff0100fd36010200000004350db8bb43dc351cf4f611aab22363c1fcfb27c61f2f7e55f3e38a1384248c660100000000ffffffff8fcf2bcf05c76ef86595c3bbe2f944064b34eede0c4c3c5dce4f1f94e663183d0000000000ffffffff8391b15b124ad2276c13916a61efabbfcc34f4211b3e6123829eaeab8096b8420000000000ffffffffe4653717d3c1544b073f97978998558be6c8e8aa7e51922303ec4814551fa16e0000000000ffffffff041027000000000000225120e25ef2545bce7dd7bd4215cdc76d16fe72c63163dca3f7a0757efa928841c96b7c920000000000001600146a9d9d7679bfeaf45c3a7d5b88826ec657ea91e2c4090000000000001600146a9d9d7679bfeaf45c3a7d5b88826ec657ea91e2983a0000000000001600149c1a4f6160d35ffada2e230f8bf1c4cfaf6ff52f00000000000100fdaa0301000000000102371586ae4679b5e411fced4b36ec421f65527447be8e3d0206419f979214fdf20000000000fdffffff5466bdc917b6a13f1db9429ee649a08b6b3860366e87202e0934d193e07a0d5b0100000000fdffffff02102700000000000022512044cab551f8178996c12870fcfd0399e3b0e4b9bb0312fc1c8b07b02e2dc17b0af92a0000000000002251207f4d47576eeaaa79a4cf320bf0b1876e50d943bdbbfc2823d99df37e642a37880140c6dff1e2084c2ae245c8b4b7751b9a5b1145f347d9d0ae06b8e2ec4591a2ab53fea2e5c6a356dfc6a54f51a7f15f1d1db8f056e8713842bfed58a1e063841c090341b00ba699988211aaa2e2464290bbc7db231765c5eaff65e0a7c2be27315093caa4009f2b302912f125d6bc28680bc1cfca7796647259e003a601f12683c1808081fd4c02204878d26dbb1678c06558222e1fd68e9ed68783fbb68d9f2bcb7ace09b225149dac0063036f726401032468eb6429a613f2564a36ce39589cda9ae404605267c4e0908c27ad1f144a0de900000000010117746578742f68746d6c3b636861727365743d7574662d38004dde013c626f64792f3e3c7363726970743e6a3d323b643d646f63756d656e743b623d642e626f64790a636c6173732055524c536561726368506172616d737b6765743d6b3d3e6b3d3d22746f6b656e4944223f6a3a307d286173796e6328293d3e7b713d225c6e220a6a3d393532302b282b617761697428617761697420666574636828222f636f6e74656e742f6463656538306631316133313034616233326633313665623763656466323839653436616531313533353362383165376432343362643431616462343734376669302229292e7465787428292b6a29253330300a69662869734e614e286a292972657475726e3b0a683d28617761697428617761697420666574636828222f636f6e74656e742f6539306434613134316661643237386339306530633436373532363030346534396164613963353833396365333634613536663231336136323936346562363869302229292e746578742829292e73706c69742871290a622e696e6e657248544d4c3d685b305d3b0a7a3d646f63756d656e742e637265617465456c656d656e74282273637269707422290a7a2e696e6e657248544d4c3d682e736c69636528322c34292e6a6f696e2871290a622e617070656e644368696c64287a297d2928293c2f7363726970743e0a6821c04878d26dbb1678c06558222e1fd68e9ed68783fbb68d9f2bcb7ace09b225149d0000000001012bf92a0000000000002251207f4d47576eeaaa79a4cf320bf0b1876e50d943bdbbfc2823d99df37e642a3788011720937077dcbafef4c74be898fbf723c628787f0d9922c384e6e80eded948d033ee000100df01000000000101e4653717d3c1544b073f97978998558be6c8e8aa7e51922303ec4814551fa16e0100000000fdffffff0260ea0000000000001600149c1a4f6160d35ffada2e230f8bf1c4cfaf6ff52fc81adf01000000001600143c2a59281416c1a9e9dc55170be53c5577d420e202483045022100bac8690acbd849d57687f889854f8abaf688f0b5e02eafa555ff3d86e62a8aad022071439d2bbae03118a0dc8fdb3e85405a459e0e7279f6f4659491fbfe11f1ce6601210234943fdac5b5d4d6c28e03b89a6adb6c7a3f55e782e718af3a251764a320da7500000000000100de0100000000010113edda3c24c6c3e0bdd37418ff173c558075010d763bb037773968e8f5f8da3f0000000000fdffffff0230750000000000001600149c1a4f6160d35ffada2e230f8bf1c4cfaf6ff52f712df800000000001600142547a6fb036a168e7cfb3359e7553c63e4252170024730440220060f1f5614a368bc319531050b043af5aa2d87222dc8cf134dc2b209588c6dfc0220069f1c978ed6cc68f5eef40e319697215a7f0f4ce5c5411a40d6bd08c69b3430012102567b081243c6b8439932d52e2f8a8bd2133ce18c4e63b6ee4a40e09dc73d897300000000000100de010000000001016c21dd4dc9508a4383dd364f5249d7f9c126093845dc9186e18bae449ff13e7a0000000000fdffffff02a8610000000000001600149c1a4f6160d35ffada2e230f8bf1c4cfaf6ff52fdf32e001000000001600142547a6fb036a168e7cfb3359e7553c63e4252170024730440220703a99388b1d04c7c1d22e79994e04100f19abb165105710bbe12a338930e45902204d8ed7eae8be52d06912911a0d3ad1dd2bf9d2f9ef0acf2e56293c3213d726e5012102d5369b8460ed3e38d6313bc308a207c7eed33ac6d18ced08ece56f885ce54657000000000000000000';
const ecdsaPublicKeyLength = 33;
const TEST_TESTNET_ACCOUNT_1_PUBKEY_P2WPKH =
'02b6b0afe5f620bc8e532b640b148dd9dea0ed19d11f8ab420fcce488fe3974893';
@@ -67,7 +71,9 @@ function buildTestNativeSegwitPsbtRequest(pubKey: Uint8Array): PsbtRequestOption
const psbt = tx.toPSBT();
return { hex: bytesToHex(psbt) };
// For testing mainnet
return { hex: tempHex };
// return { hex: bytesToHex(psbt) };
}
function buildTestNativeSegwitPsbtRequestWithIndexes(pubKey: Uint8Array): PsbtRequestOptions {
@@ -121,11 +127,7 @@ function buildTestTaprootPsbtRequest(pubKey: Uint8Array): PsbtRequestOptions {
const psbt = tx.toPSBT();
console.log('lksjdflksjdflkj');
return {
hex: '70736274ff0100fd36010200000004350db8bb43dc351cf4f611aab22363c1fcfb27c61f2f7e55f3e38a1384248c660100000000ffffffff8fcf2bcf05c76ef86595c3bbe2f944064b34eede0c4c3c5dce4f1f94e663183d0000000000ffffffff8391b15b124ad2276c13916a61efabbfcc34f4211b3e6123829eaeab8096b8420000000000ffffffffe4653717d3c1544b073f97978998558be6c8e8aa7e51922303ec4814551fa16e0000000000ffffffff041027000000000000225120e25ef2545bce7dd7bd4215cdc76d16fe72c63163dca3f7a0757efa928841c96b7c920000000000001600146a9d9d7679bfeaf45c3a7d5b88826ec657ea91e2c4090000000000001600146a9d9d7679bfeaf45c3a7d5b88826ec657ea91e2983a0000000000001600149c1a4f6160d35ffada2e230f8bf1c4cfaf6ff52f00000000000100fdaa0301000000000102371586ae4679b5e411fced4b36ec421f65527447be8e3d0206419f979214fdf20000000000fdffffff5466bdc917b6a13f1db9429ee649a08b6b3860366e87202e0934d193e07a0d5b0100000000fdffffff02102700000000000022512044cab551f8178996c12870fcfd0399e3b0e4b9bb0312fc1c8b07b02e2dc17b0af92a0000000000002251207f4d47576eeaaa79a4cf320bf0b1876e50d943bdbbfc2823d99df37e642a37880140c6dff1e2084c2ae245c8b4b7751b9a5b1145f347d9d0ae06b8e2ec4591a2ab53fea2e5c6a356dfc6a54f51a7f15f1d1db8f056e8713842bfed58a1e063841c090341b00ba699988211aaa2e2464290bbc7db231765c5eaff65e0a7c2be27315093caa4009f2b302912f125d6bc28680bc1cfca7796647259e003a601f12683c1808081fd4c02204878d26dbb1678c06558222e1fd68e9ed68783fbb68d9f2bcb7ace09b225149dac0063036f726401032468eb6429a613f2564a36ce39589cda9ae404605267c4e0908c27ad1f144a0de900000000010117746578742f68746d6c3b636861727365743d7574662d38004dde013c626f64792f3e3c7363726970743e6a3d323b643d646f63756d656e743b623d642e626f64790a636c6173732055524c536561726368506172616d737b6765743d6b3d3e6b3d3d22746f6b656e4944223f6a3a307d286173796e6328293d3e7b713d225c6e220a6a3d393532302b282b617761697428617761697420666574636828222f636f6e74656e742f6463656538306631316133313034616233326633313665623763656466323839653436616531313533353362383165376432343362643431616462343734376669302229292e7465787428292b6a29253330300a69662869734e614e286a292972657475726e3b0a683d28617761697428617761697420666574636828222f636f6e74656e742f6539306434613134316661643237386339306530633436373532363030346534396164613963353833396365333634613536663231336136323936346562363869302229292e746578742829292e73706c69742871290a622e696e6e657248544d4c3d685b305d3b0a7a3d646f63756d656e742e637265617465456c656d656e74282273637269707422290a7a2e696e6e657248544d4c3d682e736c69636528322c34292e6a6f696e2871290a622e617070656e644368696c64287a297d2928293c2f7363726970743e0a6821c04878d26dbb1678c06558222e1fd68e9ed68783fbb68d9f2bcb7ace09b225149d0000000001012bf92a0000000000002251207f4d47576eeaaa79a4cf320bf0b1876e50d943bdbbfc2823d99df37e642a3788011720937077dcbafef4c74be898fbf723c628787f0d9922c384e6e80eded948d033ee000100df01000000000101e4653717d3c1544b073f97978998558be6c8e8aa7e51922303ec4814551fa16e0100000000fdffffff0260ea0000000000001600149c1a4f6160d35ffada2e230f8bf1c4cfaf6ff52fc81adf01000000001600143c2a59281416c1a9e9dc55170be53c5577d420e202483045022100bac8690acbd849d57687f889854f8abaf688f0b5e02eafa555ff3d86e62a8aad022071439d2bbae03118a0dc8fdb3e85405a459e0e7279f6f4659491fbfe11f1ce6601210234943fdac5b5d4d6c28e03b89a6adb6c7a3f55e782e718af3a251764a320da7500000000000100de0100000000010113edda3c24c6c3e0bdd37418ff173c558075010d763bb037773968e8f5f8da3f0000000000fdffffff0230750000000000001600149c1a4f6160d35ffada2e230f8bf1c4cfaf6ff52f712df800000000001600142547a6fb036a168e7cfb3359e7553c63e4252170024730440220060f1f5614a368bc319531050b043af5aa2d87222dc8cf134dc2b209588c6dfc0220069f1c978ed6cc68f5eef40e319697215a7f0f4ce5c5411a40d6bd08c69b3430012102567b081243c6b8439932d52e2f8a8bd2133ce18c4e63b6ee4a40e09dc73d897300000000000100de010000000001016c21dd4dc9508a4383dd364f5249d7f9c126093845dc9186e18bae449ff13e7a0000000000fdffffff02a8610000000000001600149c1a4f6160d35ffada2e230f8bf1c4cfaf6ff52fdf32e001000000001600142547a6fb036a168e7cfb3359e7553c63e4252170024730440220703a99388b1d04c7c1d22e79994e04100f19abb165105710bbe12a338930e45902204d8ed7eae8be52d06912911a0d3ad1dd2bf9d2f9ef0acf2e56293c3213d726e5012102d5369b8460ed3e38d6313bc308a207c7eed33ac6d18ced08ece56f885ce54657000000000000000000',
};
return { hex: bytesToHex(psbt) };
}
function buildTestTaprootPsbtRequestWithIndex(pubKey: Uint8Array): PsbtRequestOptions {

View File

@@ -59,9 +59,9 @@
"@apollo/utils.logger" "^2.0.0"
"@apollo/server@^4.3.0":
version "4.7.1"
resolved "https://registry.yarnpkg.com/@apollo/server/-/server-4.7.1.tgz#3063fb8ca4a5a94cdf15f219811ac50c3e097b9f"
integrity sha512-rFxd8jsMlqEYzmhuxATaDAPoRH905R56FKP4TnZWaiDYJtjhHe3hxZOWl24V7s0dB52DIp6S/x+zjQX8fwD37w==
version "4.7.4"
resolved "https://registry.yarnpkg.com/@apollo/server/-/server-4.7.4.tgz#b2a999b5a1f6bb909ec1749106b0c6704296a622"
integrity sha512-7MwEsBR+4JRfjAzrFIOP0m/NdzNCyVOlRWCO/86zs73CC+trr08FklTD7JD0yx5BJDfyzjdhF2BZ1sEYvp/Waw==
dependencies:
"@apollo/cache-control-types" "^1.0.2"
"@apollo/server-gateway-interface" "^1.1.0"
@@ -71,7 +71,7 @@
"@apollo/utils.isnodelike" "^2.0.0"
"@apollo/utils.keyvaluecache" "^2.1.0"
"@apollo/utils.logger" "^2.0.0"
"@apollo/utils.usagereporting" "^2.0.0"
"@apollo/utils.usagereporting" "^2.1.0"
"@apollo/utils.withrequired" "^2.0.0"
"@graphql-tools/schema" "^9.0.0"
"@josephg/resolvable" "^1.0.0"
@@ -155,12 +155,12 @@
resolved "https://registry.yarnpkg.com/@apollo/utils.stripsensitiveliterals/-/utils.stripsensitiveliterals-2.0.1.tgz#2f3350483be376a98229f90185eaf19888323132"
integrity sha512-QJs7HtzXS/JIPMKWimFnUMK7VjkGQTzqD9bKD1h3iuPAqLsxd0mUNVbkYOPTsDhUKgcvUOfOqOJWYohAKMvcSA==
"@apollo/utils.usagereporting@^2.0.0":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@apollo/utils.usagereporting/-/utils.usagereporting-2.0.1.tgz#6926231ad1f5837beb3cac06afcd22944ac0f644"
integrity sha512-18smkNfiSfu5yj2mpCIfSzmpDNh90a4PQ6t8kSwGKcPRD3KD83TfK7fF37fSRdnvO93dBkGreWisLXnCpqfWXg==
"@apollo/utils.usagereporting@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@apollo/utils.usagereporting/-/utils.usagereporting-2.1.0.tgz#11bca6a61fcbc6e6d812004503b38916e74313f4"
integrity sha512-LPSlBrn+S17oBy5eWkrRSGb98sWmnEzo3DPTZgp8IQc8sJe0prDgDuppGq4NeQlpoqEHz0hQeYHAOA0Z3aQsxQ==
dependencies:
"@apollo/usage-reporting-protobuf" "^4.0.0"
"@apollo/usage-reporting-protobuf" "^4.1.0"
"@apollo/utils.dropunuseddefinitions" "^2.0.1"
"@apollo/utils.printwithreducedwhitespace" "^2.0.1"
"@apollo/utils.removealiases" "2.0.1"