refactor: psbt ui to inputs and outputs

This commit is contained in:
fbwoolf
2023-06-12 17:00:23 -05:00
committed by Fara Woolf
parent 8b6c325dc3
commit dea361f380
16 changed files with 248 additions and 755 deletions

View File

@@ -9,7 +9,7 @@ import { Tooltip } from '@app/components/tooltip';
interface PsbtDecodedNodeLayoutProps {
hoverLabel?: string;
image?: JSX.Element;
image?: React.JSX.Element;
subtitle?: string;
subValue?: string;
subValueAction?(): void;

View File

@@ -2,7 +2,7 @@ import { Box, Text } from '@stacks/ui';
import { PsbtDecodedNodeLayout } from './psbt-decoded-node.layout';
export function PsbtInputOutputPlaceholder() {
export function PsbtPlaceholderNode() {
return (
<Box background="white" borderRadius="16px" p="loose">
<Text fontWeight={500}>Inputs</Text>

View File

@@ -0,0 +1,46 @@
import { truncateMiddle } from '@stacks/ui-utils';
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-node.layout';
import { PsbtUnsignedInputWithInscription } from './psbt-unsigned-input-with-inscription';
interface PsbtUnsignedInputItemProps {
addressNativeSegwit: string;
addressTaproot: string;
utxo: TaprootUtxo & OrdApiInscriptionTxOutput;
}
export function PsbtUnsignedInputItem({
addressNativeSegwit,
addressTaproot,
utxo,
}: PsbtUnsignedInputItemProps) {
const calculateBitcoinFiatValue = useCalculateBitcoinFiatValue();
const isInputCurrentAddress =
utxo.address === addressNativeSegwit || utxo.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}
/>
) : (
<PsbtDecodedNodeLayout
hoverLabel={utxo.address}
subtitle={truncateMiddle(utxo.address)}
subValue={`${fiatValue} USD`}
value={`${isInputCurrentAddress ? '-' : '+'}${inputValue}`}
/>
);
}

View File

@@ -1,28 +1,49 @@
import { Box } from '@stacks/ui';
import { truncateMiddle } from '@stacks/ui-utils';
import { isUndefined } from '@shared/utils';
import { openInNewTab } from '@app/common/utils/open-in-new-tab';
import { OrdinalIcon } from '@app/components/icons/ordinal-icon';
import { InscriptionPreview } from '@app/components/inscription-preview-card/components/inscription-preview';
import { LoadingSpinner } from '@app/components/loading-spinner';
import { useInscription } from '@app/query/bitcoin/ordinals/inscription.hooks';
import { PsbtDecodedNodeLayout } from './psbt-decoded-node.layout';
import { PsbtDecodedNodeLayout } from '../../psbt-decoded-node.layout';
interface PsbtInputWithInscriptionProps {
interface PsbtUnsignedInputWithInscriptionProps {
address: string;
inputValue: string;
path: string;
}
export function PsbtInputWithInscription({
export function PsbtUnsignedInputWithInscription({
address,
inputValue,
path,
}: PsbtInputWithInscriptionProps) {
}: PsbtUnsignedInputWithInscriptionProps) {
const {
isLoading,
isError,
data: inscription,
} = useInscription(path.replace('/inscription/', ''));
if (isLoading || isError) return null;
if (isLoading)
return (
<Box my="loose">
<LoadingSpinner />
</Box>
);
if (isError || isUndefined(inscription))
return (
<PsbtDecodedNodeLayout
hoverLabel={address}
image={<OrdinalIcon />}
subtitle={truncateMiddle(address)}
subValue="# Unknown"
title="No data"
value={`-${inputValue}`}
/>
);
return (
<PsbtDecodedNodeLayout
@@ -32,7 +53,7 @@ export function PsbtInputWithInscription({
subValue={`#${inscription.number}`}
subValueAction={() => openInNewTab(inscription.infoUrl)}
title="Ordinal inscription"
value={`- ${inputValue}`}
value={`-${inputValue}`}
/>
);
}

View File

@@ -0,0 +1,47 @@
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 { PsbtDecodedNodeLayout } from '../psbt-decoded-node.layout';
import { PsbtPlaceholderNode } from '../psbt-placeholder-node';
import { PsbtUnsignedInputItem } from './components/psbt-unsigned-input-item';
interface PsbtUnsignedInputListProps {
addressNativeSegwit: string;
addressTaproot: string;
inputs: btc.TransactionInputRequired[];
showPlaceholder: boolean;
}
export function PsbtUnsignedInputList({
addressNativeSegwit,
addressTaproot,
inputs,
showPlaceholder,
}: PsbtUnsignedInputListProps) {
const unsignedUtxos = useOrdinalsAwareUtxoQueries(inputs).map(query => query.data);
return (
<Box background="white" borderTopLeftRadius="16px" borderTopRightRadius="16px" p="loose">
<Text fontWeight={500}>Inputs</Text>
{showPlaceholder ? (
<PsbtPlaceholderNode />
) : (
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>
);
}

View File

@@ -0,0 +1,40 @@
import * as btc from '@scure/btc-signer';
import { truncateMiddle } from '@stacks/ui-utils';
import { getAddressFromOutScript } from '@shared/crypto/bitcoin/bitcoin.utils';
import { i18nFormatCurrency } from '@app/common/money/format-money';
import { satToBtc } from '@app/common/money/unit-conversion';
import { useCalculateBitcoinFiatValue } from '@app/query/common/market-data/market-data.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
import { PsbtDecodedNodeLayout } from '../../psbt-decoded-node.layout';
interface PsbtUnsignedOutputItemProps {
addressNativeSegwit: string;
addressTaproot: string;
output: btc.TransactionOutputRequired;
}
export function PsbtUnsignedOutputItem({
addressNativeSegwit,
addressTaproot,
output,
}: PsbtUnsignedOutputItemProps) {
const network = useCurrentNetwork();
const calculateBitcoinFiatValue = useCalculateBitcoinFiatValue();
const addressFromScript = getAddressFromOutScript(output.script, network.chain.bitcoin.network);
const isOutputCurrentAddress =
addressFromScript === addressNativeSegwit || addressFromScript === addressTaproot;
const outputValue = satToBtc(Number(output.amount)).toString();
return (
<PsbtDecodedNodeLayout
hoverLabel={addressFromScript}
subtitle={truncateMiddle(addressFromScript)}
subValue={`${i18nFormatCurrency(calculateBitcoinFiatValue(outputValue))} USD`}
value={`${isOutputCurrentAddress ? '+' : ' '}${outputValue}`}
/>
);
}

View File

@@ -0,0 +1,38 @@
import * as btc from '@scure/btc-signer';
import { Box, Text } from '@stacks/ui';
import { PsbtPlaceholderNode } from '../psbt-placeholder-node';
import { PsbtUnsignedOutputItem } from './components/psbt-unsigned-output-item';
interface PsbtUnsignedOutputListProps {
addressNativeSegwit: string;
addressTaproot: string;
outputs: btc.TransactionOutputRequired[];
showPlaceholder: boolean;
}
export function PsbtUnsignedOutputList({
addressNativeSegwit,
addressTaproot,
outputs,
showPlaceholder,
}: PsbtUnsignedOutputListProps) {
return (
<Box background="white" borderBottomLeftRadius="16px" borderBottomRightRadius="16px" p="loose">
<Text fontWeight={500}>Outputs</Text>
{showPlaceholder ? (
<PsbtPlaceholderNode />
) : (
outputs.map((output, i) => {
return (
<PsbtUnsignedOutputItem
addressNativeSegwit={addressNativeSegwit}
addressTaproot={addressTaproot}
key={i}
output={output}
/>
);
})
)}
</Box>
);
}

View File

@@ -1,36 +1,36 @@
import { InputsOutputPair } from '@app/pages/psbt-request/hooks/match-inputs-and-outputs';
import * as btc from '@scure/btc-signer';
import { PsbtInputOutputPair } from '../psbt-input-output-pair/psbt-input-output-pair';
import { PsbtInputOutputPlaceholder } from '../psbt-input-output-pair/psbt-input-output-placeholder';
import { PsbtUnsignedInputList } from '../psbt-decoded-request-node/psbt-unsigned-input-list/psbt-unsigned-input-list';
import { PsbtUnsignedOutputList } from '../psbt-decoded-request-node/psbt-unsigned-output-list/psbt-unsigned-output-list';
interface PsbtDecodedRequestSimpleProps {
bitcoinAddressNativeSegwit: string;
bitcoinAddressTaproot: string;
inputOutputPairs: InputsOutputPair[];
inputs: btc.TransactionInputRequired[];
outputs: btc.TransactionOutputRequired[];
showPlaceholder: boolean;
}
export function PsbtDecodedRequestSimple({
bitcoinAddressNativeSegwit,
bitcoinAddressTaproot,
inputOutputPairs,
inputs,
outputs,
showPlaceholder,
}: PsbtDecodedRequestSimpleProps) {
if (showPlaceholder) return <PsbtInputOutputPlaceholder />;
return (
<>
{inputOutputPairs.map((pair, i) => {
return (
<PsbtInputOutputPair
addressNativeSegwit={bitcoinAddressNativeSegwit}
addressTaproot={bitcoinAddressTaproot}
inputOutputPair={pair}
isFirstPair={i === 0}
isLastPair={i === inputOutputPairs.length - 1}
key={i}
/>
);
})}
<PsbtUnsignedInputList
addressNativeSegwit={bitcoinAddressNativeSegwit}
addressTaproot={bitcoinAddressTaproot}
inputs={inputs}
showPlaceholder={showPlaceholder}
/>
<PsbtUnsignedOutputList
addressNativeSegwit={bitcoinAddressNativeSegwit}
addressTaproot={bitcoinAddressTaproot}
outputs={outputs}
showPlaceholder={showPlaceholder}
/>
</>
);
}

View File

@@ -1,10 +1,10 @@
import * as btc from '@scure/btc-signer';
import { Stack, color } from '@stacks/ui';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountTaprootAddressIndexZeroPayment } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { PsbtInput, usePsbtDecodedRequest } from '../../hooks/use-psbt-decoded-request';
import { usePsbtDecodedRequest } from '../../hooks/use-psbt-decoded-request';
import { PsbtDecodedRequestAdvanced } from './psbt-decoded-request-views/psbt-decoded-request-advanced';
import { PsbtDecodedRequestSimple } from './psbt-decoded-request-views/psbt-decoded-request-simple';
import { PsbtDecodedRequestViewToggle } from './psbt-decoded-request-views/psbt-decoded-request-view-toggle';
@@ -13,20 +13,17 @@ interface PsbtDecodedRequestProps {
psbt: any;
}
export function PsbtDecodedRequest({ psbt }: PsbtDecodedRequestProps) {
const bitcoinAddressNativeSegwit = useCurrentAccountNativeSegwitAddressIndexZero();
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootAddressIndexZeroPayment();
const psbtInputs: PsbtInput[] = psbt.inputs;
const unsignedInputs: btc.TransactionInputRequired[] = psbt.global.unsignedTx.inputs;
const unsignedOutputs: btc.TransactionOutputRequired[] = psbt.global.unsignedTx.outputs;
const {
inputOutputPairs,
onSetShowAdvancedView,
shouldDefaultToAdvancedView,
shouldShowPlaceholder,
showAdvancedView,
} = usePsbtDecodedRequest({
psbtInputs,
unsignedInputs,
unsignedOutputs,
});
@@ -45,9 +42,10 @@ export function PsbtDecodedRequest({ psbt }: PsbtDecodedRequestProps) {
<PsbtDecodedRequestAdvanced psbt={psbt} />
) : (
<PsbtDecodedRequestSimple
bitcoinAddressNativeSegwit={bitcoinAddressNativeSegwit}
bitcoinAddressNativeSegwit={nativeSegwitSigner.address}
bitcoinAddressTaproot={bitcoinAddressTaproot}
inputOutputPairs={inputOutputPairs}
inputs={unsignedInputs}
outputs={unsignedOutputs}
showPlaceholder={shouldShowPlaceholder}
/>
)}

View File

@@ -1,94 +0,0 @@
import { useCallback } from 'react';
import { Box, Text } from '@stacks/ui';
import { truncateMiddle } from '@stacks/ui-utils';
import { getAddressFromOutScript } from '@shared/crypto/bitcoin/bitcoin.utils';
import { createMoneyFromDecimal } from '@shared/models/money.model';
import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money';
import { i18nFormatCurrency } from '@app/common/money/format-money';
import { satToBtc } from '@app/common/money/unit-conversion';
import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
import { InputsOutputPair } from '../../../hooks/match-inputs-and-outputs';
import { PsbtDecodedNodeLayout } from './psbt-decoded-node.layout';
import { PsbtInputWithInscription } from './psbt-input-with-inscription';
interface PsbtInputOutputPairProps {
addressNativeSegwit: string;
addressTaproot: string;
inputOutputPair: InputsOutputPair;
isFirstPair: boolean;
isLastPair: boolean;
}
export function PsbtInputOutputPair({
addressNativeSegwit,
addressTaproot,
inputOutputPair,
isFirstPair,
isLastPair,
}: PsbtInputOutputPairProps) {
const { inputs, output } = inputOutputPair;
const network = useCurrentNetwork();
const btcMarketData = useCryptoCurrencyMarketData('BTC');
const addressFromScript = getAddressFromOutScript(output.script, network.chain.bitcoin.network);
const getFiatValue = useCallback(
(value: string) =>
i18nFormatCurrency(
baseCurrencyAmountInQuote(createMoneyFromDecimal(Number(value), 'BTC'), btcMarketData)
),
[btcMarketData]
);
const isOutputCurrentAddress =
addressFromScript === addressNativeSegwit || addressFromScript === addressTaproot;
const outputValue = satToBtc(Number(output.amount)).toString();
return (
<Box
background="white"
borderBottomLeftRadius={isLastPair ? '16px' : 'unset'}
borderBottomRightRadius={isLastPair ? '16px' : 'unset'}
borderTopLeftRadius={isFirstPair ? '16px' : 'unset'}
borderTopRightRadius={isFirstPair ? '16px' : 'unset'}
p="loose"
>
<Text fontWeight={500}>Inputs</Text>
{inputs.map(input => {
const isInputCurrentAddress =
input.address === addressNativeSegwit || input.address === addressTaproot;
const inputValue = satToBtc(input.value).toString();
const fiatValue = getFiatValue(inputValue);
const inscription = input.unsignedUtxo?.inscriptions;
return inscription ? (
<PsbtInputWithInscription
address={input.address}
inputValue={inputValue}
path={inscription}
/>
) : (
<PsbtDecodedNodeLayout
hoverLabel={input.address}
subtitle={truncateMiddle(input.address)}
subValue={`${fiatValue} USD`}
value={`${isInputCurrentAddress ? '-' : '+'} ${inputValue}`}
/>
);
})}
<hr />
<Text fontWeight={500} mt="loose">
Output
</Text>
<PsbtDecodedNodeLayout
hoverLabel={addressFromScript}
subtitle={truncateMiddle(addressFromScript)}
subValue={`${getFiatValue(outputValue)} USD`}
value={`${isOutputCurrentAddress ? '+' : ' '} ${outputValue}`}
/>
</Box>
);
}

View File

@@ -1,46 +0,0 @@
import { BitcoinNetworkModes } from '@shared/constants';
import { getAddressFromOutScript } from '@shared/crypto/bitcoin/bitcoin.utils';
import { isDefined } from '@shared/utils';
import { OrdApiInscriptionTxOutput } from '@app/query/bitcoin/ordinals/ordinals-aware-utxo.query';
import { PsbtInput, PsbtInputForUi } from './use-psbt-decoded-request';
function getPsbtInputValue(input: PsbtInput, unsignedUtxo?: OrdApiInscriptionTxOutput) {
if (isDefined(input.witnessUtxo)) return Number(input.witnessUtxo.amount);
if (isDefined(input.nonWitnessUtxo) && isDefined(unsignedUtxo)) return Number(unsignedUtxo.value);
return 0;
}
function getPsbtInputAddress(
input: PsbtInput,
network: BitcoinNetworkModes,
unsignedUtxo?: OrdApiInscriptionTxOutput
) {
if (isDefined(input.witnessUtxo))
return getAddressFromOutScript(input.witnessUtxo.script, network);
if (isDefined(input.nonWitnessUtxo) && isDefined(unsignedUtxo)) return unsignedUtxo.address ?? '';
return '';
}
interface BuildPsbtInputForUiArgs {
network: BitcoinNetworkModes;
psbtInputs: PsbtInput[];
unsignedUtxos: (OrdApiInscriptionTxOutput | undefined)[];
}
export function buildPsbtInputsForUi({
network,
psbtInputs,
unsignedUtxos,
}: BuildPsbtInputForUiArgs): PsbtInputForUi[] {
return psbtInputs.map((input, i) => {
const utxoAddress = getPsbtInputAddress(input, network, unsignedUtxos[i]);
const utxoValue = getPsbtInputValue(input, unsignedUtxos[i]);
return {
...input,
address: utxoAddress,
value: utxoValue,
unsignedUtxo: unsignedUtxos[i],
};
});
}

View File

@@ -1,27 +0,0 @@
import {
mockInputOutputPairs,
mockInputOutputPairsWithNonWitnessOnly,
mockPsbtInputs,
mockPsbtInputsWithNonWitnessOnly,
mockPsbtUnsignedOutputs,
} from '@tests/mocks/mock-psbts';
import { matchInputsOutputs } from './match-inputs-and-outputs';
describe('matching psbt inputs and outputs', () => {
test('that psbt inputs and outputs can be paired correctly when witness data is provided', () => {
const { inputOutputPairs } = matchInputsOutputs({
psbtInputs: mockPsbtInputs,
unsignedOutputs: mockPsbtUnsignedOutputs,
});
expect(inputOutputPairs).toEqual(mockInputOutputPairs);
});
test('that psbt inputs and outputs can be paired correctly when only non-witness data is provided', () => {
const { inputOutputPairs } = matchInputsOutputs({
psbtInputs: mockPsbtInputsWithNonWitnessOnly,
unsignedOutputs: mockPsbtUnsignedOutputs,
});
expect(inputOutputPairs).toEqual(mockInputOutputPairsWithNonWitnessOnly);
});
});

View File

@@ -1,82 +0,0 @@
import * as btc from '@scure/btc-signer';
import { isDefined } from '@shared/utils';
import { PsbtInputForUi } from './use-psbt-decoded-request';
export interface InputsOutputPair {
inputs: PsbtInputForUi[];
output: btc.TransactionOutputRequired;
}
function consolidateInputsWithSameAddress(inputs: PsbtInputForUi[]): PsbtInputForUi[] {
const utxo: Record<string, PsbtInputForUi> = {};
inputs.forEach(({ address, value, ...rest }) => {
if (isDefined(utxo[address])) {
utxo[address] = {
...utxo[address],
value: utxo[address].value + value,
};
} else {
utxo[address] = { address, value, ...rest };
}
});
return Object.values(utxo);
}
interface MatchInputsOutputsArgs {
psbtInputs: PsbtInputForUi[];
unsignedOutputs: btc.TransactionOutputRequired[];
}
export function matchInputsOutputs({ psbtInputs, unsignedOutputs }: MatchInputsOutputsArgs): {
fee: number;
inputOutputPairs: InputsOutputPair[];
} {
const pairs: InputsOutputPair[] = [];
const availableInputs = [...psbtInputs];
// Keep track of the input value available across multiple outputs
let remainingInputValue = 0;
for (const output of unsignedOutputs) {
// Note here that the inputs/output `pair` uses an inputs array so
// multiple inputs can be used to fulfill an output
const inputsOutputPair: InputsOutputPair = {
inputs: [],
output: output,
};
// Calculate the remaining value needed for this output
let remainingOutputValue = Number(output.amount);
// Iterate through the available inputs and add them until the output is fulfilled
for (let i = 0; i < availableInputs.length; i++) {
const input = availableInputs[i];
if (remainingInputValue === 0) remainingInputValue = input.value;
if (input.value <= remainingOutputValue) {
// Use this entire input and subtract from output value
inputsOutputPair.inputs.push(input);
remainingOutputValue -= input.value;
remainingInputValue = 0;
availableInputs.splice(i, 1);
i--;
} else {
// Use this input but keep track of remainder for future outputs
remainingInputValue -= remainingOutputValue;
inputsOutputPair.inputs.push(input);
remainingOutputValue = 0;
}
if (remainingOutputValue === 0) break;
}
pairs.push(inputsOutputPair);
}
const inputOutputPairs: InputsOutputPair[] = pairs.map(pair => {
return { inputs: consolidateInputsWithSameAddress(pair.inputs), output: pair.output };
});
return { fee: remainingInputValue, inputOutputPairs };
}

View File

@@ -1,47 +1,18 @@
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useState } from 'react';
import * as btc from '@scure/btc-signer';
import { BitcoinNetworkModes } from '@shared/constants';
import { getAddressFromOutScript } from '@shared/crypto/bitcoin/bitcoin.utils';
import { logger } from '@shared/logger';
import { isEmpty, isUndefined } from '@shared/utils';
import { isUndefined } from '@shared/utils';
import {
OrdApiInscriptionTxOutput,
useOrdinalsAwareUtxoQueries,
} from '@app/query/bitcoin/ordinals/ordinals-aware-utxo.query';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
import { buildPsbtInputsForUi } from './build-psbt-input-for-ui';
import { matchInputsOutputs } from './match-inputs-and-outputs';
export interface NonWitnessUtxo {
version: number;
segwitFlag: boolean;
inputs: btc.TransactionInputRequired[];
outputs: btc.TransactionOutputRequired[];
witnesses: string[][];
lockTime: number;
}
interface WitnessUtxo {
script: Uint8Array;
amount: bigint;
}
export interface PsbtInput {
nonWitnessUtxo?: NonWitnessUtxo;
witnessUtxo?: WitnessUtxo;
}
export interface PsbtInputForUi extends PsbtInput {
address: string;
unsignedUtxo?: OrdApiInscriptionTxOutput;
value: number;
}
function isPlaceholderTransaction(
address: string,
inputs: (OrdApiInscriptionTxOutput | undefined)[],
@@ -59,67 +30,34 @@ function isPlaceholderTransaction(
}
interface UsePsbtDecodedRequestArgs {
psbtInputs: PsbtInput[];
unsignedInputs: btc.TransactionInputRequired[];
unsignedOutputs: btc.TransactionOutputRequired[];
}
export function usePsbtDecodedRequest({
psbtInputs,
unsignedInputs,
unsignedOutputs,
}: UsePsbtDecodedRequestArgs) {
const [showAdvancedView, setShowAdvancedView] = useState(false);
const network = useCurrentNetwork();
const bitcoinAddressNativeSegwit = useCurrentAccountNativeSegwitAddressIndexZero();
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const unsignedUtxos = useOrdinalsAwareUtxoQueries(unsignedInputs).map(query => query.data);
const inputs = useMemo(() => {
if (isUndefined(unsignedUtxos)) {
logger.error('No UTXOs to sign');
return [];
}
return buildPsbtInputsForUi({
network: network.chain.bitcoin.network,
psbtInputs,
unsignedUtxos,
});
}, [network.chain.bitcoin.network, psbtInputs, unsignedUtxos]);
const { fee, inputOutputPairs } = useMemo(
() =>
matchInputsOutputs({
psbtInputs: inputs,
unsignedOutputs,
}),
[inputs, unsignedOutputs]
);
const defaultToAdvancedView = useCallback(() => {
const pairsWithNoInputs = inputOutputPairs.filter(pair => {
return isUndefined(pair.inputs) || !pair.inputs.length;
});
const pairsWithNoOutputs = inputOutputPairs.filter(pair => {
return isUndefined(pair.output) || isEmpty(pair.output);
});
return !!(
inputOutputPairs.length === 0 ||
pairsWithNoInputs.length ||
pairsWithNoOutputs.length
);
}, [inputOutputPairs]);
const noInputs = isUndefined(unsignedUtxos) || !unsignedUtxos.length;
const noOutputs = isUndefined(unsignedOutputs) || !unsignedOutputs.length;
return noInputs || noOutputs;
}, [unsignedOutputs, unsignedUtxos]);
const showPlaceholder = useCallback(() => {
return isPlaceholderTransaction(
bitcoinAddressNativeSegwit,
nativeSegwitSigner.address,
unsignedUtxos,
unsignedOutputs,
network.chain.bitcoin.network
);
}, [bitcoinAddressNativeSegwit, network.chain.bitcoin.network, unsignedOutputs, unsignedUtxos]);
}, [nativeSegwitSigner.address, network.chain.bitcoin.network, unsignedOutputs, unsignedUtxos]);
return {
fee,
inputOutputPairs,
onSetShowAdvancedView: () => setShowAdvancedView(!showAdvancedView),
shouldDefaultToAdvancedView: defaultToAdvancedView(),
shouldShowPlaceholder: showPlaceholder(),

View File

@@ -1,13 +1,16 @@
import { useMemo } from 'react';
import { useCallback } from 'react';
import BigNumber from 'bignumber.js';
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 { calculateMeanAverage } from '@app/common/math/calculate-averages';
import { convertAmountToFractionalUnit } from '@app/common/money/calculate-money';
import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money';
import {
selectBinanceUsdPrice,
@@ -50,3 +53,13 @@ export function useCryptoCurrencyMarketData(currency: CryptoCurrencies): MarketD
return createMarketData(createMarketPair(currency, 'USD'), createMoney(meanStxPrice, 'USD'));
}, [binance, coincap, coingecko, currency]);
}
export function useCalculateBitcoinFiatValue() {
const btcMarketData = useCryptoCurrencyMarketData('BTC');
return useCallback(
(value: string) =>
baseCurrencyAmountInQuote(createMoneyFromDecimal(Number(value), 'BTC'), btcMarketData),
[btcMarketData]
);
}

View File

@@ -1,399 +0,0 @@
import { hexToBytes } from '@stacks/common';
import { OrdApiInscriptionTxOutput } from '@app/query/bitcoin/ordinals/ordinals-aware-utxo.query';
import {
NonWitnessUtxo,
PsbtInputForUi,
} from '../../src/app/pages/psbt-request/hooks/use-psbt-decoded-request';
export const mockPsbtUnsignedOutputs = [
{
amount: BigInt(1200),
script: hexToBytes('001466124290d2fc62f8cb83c0e15836a548e43dcade'),
},
{
amount: BigInt(10000),
script: hexToBytes('5120286626028a2d352bae8dcdfa750025d04ce7f5eb6649a4ddb9ef98eba6315f47'),
},
{
amount: BigInt(98500),
script: hexToBytes('a914fdb87fb236e8530cd4dd97ad9ebe810c265aa0ef87'),
},
{
amount: BigInt(1500),
script: hexToBytes('0014213e3440d21efb9e8e4000fadb90e9c8ef5e3c8d'),
},
];
const mockPsbtInputNonWitnessUtxo: NonWitnessUtxo = {
version: 2,
segwitFlag: true,
inputs: [
{
txid: hexToBytes('e41ca83d77ab711923f25fc88e13d273e4f3828f2d1d6e2576d4b29bcbb32f38'),
index: 1,
finalScriptSig: hexToBytes(''),
sequence: 4294967295,
},
{
txid: hexToBytes('f177c3d4ba24e18356c479d660bbb584d8a78be344c82a3e86b32aa6abd0195f'),
index: 0,
finalScriptSig: hexToBytes(''),
sequence: 4294967295,
},
{
txid: hexToBytes('80de4905b7de5acf913dd68e9d2953df04231854ee4b5e06e7b0821a991200de'),
index: 0,
finalScriptSig: hexToBytes(''),
sequence: 4294967295,
},
{
txid: hexToBytes('f177c3d4ba24e18356c479d660bbb584d8a78be344c82a3e86b32aa6abd0195f'),
index: 1,
finalScriptSig: hexToBytes(''),
sequence: 4294967295,
},
{
txid: hexToBytes('f177c3d4ba24e18356c479d660bbb584d8a78be344c82a3e86b32aa6abd0195f'),
index: 2,
finalScriptSig: hexToBytes(''),
sequence: 4294967295,
},
],
outputs: [
{
amount: BigInt(600),
script: hexToBytes('001466124290d2fc62f8cb83c0e15836a548e43dcade'),
},
{
amount: BigInt(600),
script: hexToBytes('001466124290d2fc62f8cb83c0e15836a548e43dcade'),
},
{
amount: BigInt(112568),
script: hexToBytes('001466124290d2fc62f8cb83c0e15836a548e43dcade'),
},
{
amount: BigInt(224743),
script: hexToBytes('001466124290d2fc62f8cb83c0e15836a548e43dcade'),
},
],
witnesses: [
[
'30440220437e1fd865a1c7201a54ce4fe9729d2966563681812b2c7f71f78c8344b3931502204503703f957f0701cfc208b05b8deb396a6a6fcaae5b51bbc6d3ec1cb832d55e01',
'039aa0f222ca60f0a4d03ae8047d5972856af82ff186d1b99186fc7b44f0d737c6',
],
[
'3045022100e32250233e40fc4f89a0f1d427d46aaea0a2b1f677f81d6e6542002efb52a21e02206430455c2098f82ec17f347832d022acc4d9bc7f4486582c3bb0b47ab254c9a901',
'039aa0f222ca60f0a4d03ae8047d5972856af82ff186d1b99186fc7b44f0d737c6',
],
[
'304402201dfb85876c7ccc56a2f8691780dd8e0ddb6c117f8cf46441a0a26a4b8f6711ca022033c4d5b59304dbe2828d5498795909ee6143de86ed67aeea928b4e39bf3496f601',
'039aa0f222ca60f0a4d03ae8047d5972856af82ff186d1b99186fc7b44f0d737c6',
],
[
'3044022027c01031326e714276c22c65713a9d0b38f082dbf36afd1050da13f1af44f384022070219f1b496eea8f14cd038c6962e5b4ef6b0a6c1439ffeec7c2c80b540a838a01',
'039aa0f222ca60f0a4d03ae8047d5972856af82ff186d1b99186fc7b44f0d737c6',
],
[
'3045022100acf58365b40c5cafdb7707dacf0921dc80072dc3c81953b234f1de301b597cbf02205776a4a3cc840cf923be5c5579a977607e74f2c00b6c2c099c26810b51f040ae01',
'039aa0f222ca60f0a4d03ae8047d5972856af82ff186d1b99186fc7b44f0d737c6',
],
],
lockTime: 0,
};
const mockPsbtInputNonWitnessUtxoWithInscription: NonWitnessUtxo = {
version: 1,
segwitFlag: true,
inputs: [
{
txid: hexToBytes('c4c9c967162e72f4a0414dfc4f499fe75f423da8c6fbda2645962fe504a76b74'),
index: 0,
finalScriptSig: hexToBytes(''),
sequence: 4294967293,
},
],
outputs: [
{
amount: BigInt(9556),
script: hexToBytes('512078f5386cce73363387d7904d5cb25d52bf42247baaaf92de03094eefbeb7d2fa'),
},
],
witnesses: [
[
'0cf260fbf470c7128f2ce76bdcbf0d00cbfa79cfabf4277d64c0a58f66d1e4027395e9cabf09ff88bf3df3dace1fa6d0e112fa371acfd09144794e28e21ec0ad',
],
],
lockTime: 0,
};
const mockUnsignedUtxoInscription: OrdApiInscriptionTxOutput = {
address: 'bc1p3rfd76c37af87e23g4z6tts0zu52u6frjh92m9uq5evxy0sr7hvslly59y',
all_inscriptions: [],
inscriptions: '/inscription/ff4503ab9048d6d0ff4e23def81b614d5270d341ce993992e93902ceb0d4ed79i0',
script_pubkey:
'OP_PUSHNUM_1 OP_PUSHBYTES_32 88d2df6b11f7527f65514545a5ae0f1728ae692395caad9780a658623e03f5d9',
transaction: '/tx/ff4503ab9048d6d0ff4e23def81b614d5270d341ce993992e93902ceb0d4ed79',
value: '9556',
};
const mockPsbtUnsignedUtxos: OrdApiInscriptionTxOutput[] = [
{
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
all_inscriptions: [],
inscriptions: '',
script_pubkey: 'OP_0 OP_PUSHBYTES_20 66124290d2fc62f8cb83c0e15836a548e43dcade',
transaction: '/tx/f177c3d4ba24e18356c479d660bbb584d8a78be344c82a3e86b32aa6abd0195f',
value: '600',
},
{
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
all_inscriptions: [],
inscriptions: '',
script_pubkey: 'OP_0 OP_PUSHBYTES_20 66124290d2fc62f8cb83c0e15836a548e43dcade',
transaction: '/tx/f177c3d4ba24e18356c479d660bbb584d8a78be344c82a3e86b32aa6abd0195f',
value: '600',
},
mockUnsignedUtxoInscription,
{
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
all_inscriptions: [],
inscriptions: '',
script_pubkey: 'OP_0 OP_PUSHBYTES_20 66124290d2fc62f8cb83c0e15836a548e43dcade',
transaction: '/tx/f177c3d4ba24e18356c479d660bbb584d8a78be344c82a3e86b32aa6abd0195f',
value: '292891',
},
];
export const mockPsbtInputs: PsbtInputForUi[] = [
{
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
nonWitnessUtxo: mockPsbtInputNonWitnessUtxo,
unsignedUtxo: undefined,
value: 600,
witnessUtxo: {
amount: BigInt(600),
script: hexToBytes('001466124290d2fc62f8cb83c0e15836a548e43dcade'),
},
},
{
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
nonWitnessUtxo: mockPsbtInputNonWitnessUtxo,
unsignedUtxo: undefined,
value: 600,
witnessUtxo: {
amount: BigInt(600),
script: hexToBytes('001466124290d2fc62f8cb83c0e15836a548e43dcade'),
},
},
{
address: 'bc1p0r6nsmxwwvmr8p7hjpx4evja22l5yfrm42he9hsrp98wl04h6taq4t0af2',
nonWitnessUtxo: mockPsbtInputNonWitnessUtxoWithInscription,
unsignedUtxo: mockUnsignedUtxoInscription,
value: 9556,
witnessUtxo: {
amount: BigInt(9556),
script: hexToBytes('512078f5386cce73363387d7904d5cb25d52bf42247baaaf92de03094eefbeb7d2fa'),
},
},
{
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
nonWitnessUtxo: mockPsbtInputNonWitnessUtxo,
unsignedUtxo: undefined,
value: 115166,
witnessUtxo: {
amount: BigInt(112568),
script: hexToBytes('001466124290d2fc62f8cb83c0e15836a548e43dcade'),
},
},
];
export const mockInputOutputPairs = [
{
inputs: [
{
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
nonWitnessUtxo: mockPsbtInputNonWitnessUtxo,
witnessUtxo: {
amount: BigInt(600),
script: hexToBytes('001466124290d2fc62f8cb83c0e15836a548e43dcade'),
},
unsignedUtxo: undefined,
value: 1200,
},
],
output: {
amount: BigInt(1200),
script: hexToBytes('001466124290d2fc62f8cb83c0e15836a548e43dcade'),
},
},
{
inputs: [
{
address: 'bc1p0r6nsmxwwvmr8p7hjpx4evja22l5yfrm42he9hsrp98wl04h6taq4t0af2',
nonWitnessUtxo: mockPsbtInputNonWitnessUtxoWithInscription,
witnessUtxo: {
amount: BigInt(9556),
script: hexToBytes(
'512078f5386cce73363387d7904d5cb25d52bf42247baaaf92de03094eefbeb7d2fa'
),
},
unsignedUtxo: mockUnsignedUtxoInscription,
value: 9556,
},
{
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
nonWitnessUtxo: mockPsbtInputNonWitnessUtxo,
witnessUtxo: {
amount: BigInt(112568),
script: hexToBytes('001466124290d2fc62f8cb83c0e15836a548e43dcade'),
},
unsignedUtxo: undefined,
value: 115166,
},
],
output: {
amount: BigInt(10000),
script: hexToBytes('5120286626028a2d352bae8dcdfa750025d04ce7f5eb6649a4ddb9ef98eba6315f47'),
},
},
{
inputs: [
{
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
nonWitnessUtxo: mockPsbtInputNonWitnessUtxo,
witnessUtxo: {
amount: BigInt(112568),
script: hexToBytes('001466124290d2fc62f8cb83c0e15836a548e43dcade'),
},
unsignedUtxo: undefined,
value: 115166,
},
],
output: {
amount: BigInt(98500),
script: hexToBytes('a914fdb87fb236e8530cd4dd97ad9ebe810c265aa0ef87'),
},
},
{
inputs: [
{
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
nonWitnessUtxo: mockPsbtInputNonWitnessUtxo,
witnessUtxo: {
amount: BigInt(112568),
script: hexToBytes('001466124290d2fc62f8cb83c0e15836a548e43dcade'),
},
unsignedUtxo: undefined,
value: 115166,
},
],
output: {
amount: BigInt(1500),
script: hexToBytes('0014213e3440d21efb9e8e4000fadb90e9c8ef5e3c8d'),
},
},
];
export const mockPsbtInputsWithNonWitnessOnly: PsbtInputForUi[] = [
{
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
nonWitnessUtxo: mockPsbtInputNonWitnessUtxo,
unsignedUtxo: mockPsbtUnsignedUtxos[0],
witnessUtxo: undefined,
value: 600,
},
{
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
nonWitnessUtxo: mockPsbtInputNonWitnessUtxo,
unsignedUtxo: mockPsbtUnsignedUtxos[1],
witnessUtxo: undefined,
value: 600,
},
{
address: 'bc1p0r6nsmxwwvmr8p7hjpx4evja22l5yfrm42he9hsrp98wl04h6taq4t0af2',
nonWitnessUtxo: mockPsbtInputNonWitnessUtxoWithInscription,
unsignedUtxo: mockPsbtUnsignedUtxos[2],
witnessUtxo: undefined,
value: 9556,
},
{
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
nonWitnessUtxo: mockPsbtInputNonWitnessUtxo,
unsignedUtxo: mockPsbtUnsignedUtxos[3],
witnessUtxo: undefined,
value: 115166,
},
];
export const mockInputOutputPairsWithNonWitnessOnly = [
{
inputs: [
{
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
nonWitnessUtxo: mockPsbtInputNonWitnessUtxo,
witnessUtxo: undefined,
unsignedUtxo: mockPsbtUnsignedUtxos[0],
value: 1200,
},
],
output: {
amount: BigInt(1200),
script: hexToBytes('001466124290d2fc62f8cb83c0e15836a548e43dcade'),
},
},
{
inputs: [
{
address: 'bc1p0r6nsmxwwvmr8p7hjpx4evja22l5yfrm42he9hsrp98wl04h6taq4t0af2',
nonWitnessUtxo: mockPsbtInputNonWitnessUtxoWithInscription,
witnessUtxo: undefined,
unsignedUtxo: mockUnsignedUtxoInscription,
value: 9556,
},
{
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
nonWitnessUtxo: mockPsbtInputNonWitnessUtxo,
witnessUtxo: undefined,
unsignedUtxo: mockPsbtUnsignedUtxos[3],
value: 115166,
},
],
output: {
amount: BigInt(10000),
script: hexToBytes('5120286626028a2d352bae8dcdfa750025d04ce7f5eb6649a4ddb9ef98eba6315f47'),
},
},
{
inputs: [
{
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
nonWitnessUtxo: mockPsbtInputNonWitnessUtxo,
witnessUtxo: undefined,
unsignedUtxo: mockPsbtUnsignedUtxos[3],
value: 115166,
},
],
output: {
amount: BigInt(98500),
script: hexToBytes('a914fdb87fb236e8530cd4dd97ad9ebe810c265aa0ef87'),
},
},
{
inputs: [
{
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
nonWitnessUtxo: mockPsbtInputNonWitnessUtxo,
witnessUtxo: undefined,
unsignedUtxo: mockPsbtUnsignedUtxos[3],
value: 115166,
},
],
output: {
amount: BigInt(1500),
script: hexToBytes('0014213e3440d21efb9e8e4000fadb90e9c8ef5e3c8d'),
},
},
];