mirror of
https://github.com/zhigang1992/wallet.git
synced 2026-03-21 09:35:24 +08:00
refactor: psbt uxui, closes #3849
This commit is contained in:
@@ -6,10 +6,6 @@ export function formatMoney({ amount, symbol, decimals }: Money) {
|
||||
return `${amount.shiftedBy(-decimals).toString()} ${symbol}`;
|
||||
}
|
||||
|
||||
export function formatMoneyWithoutSymbol({ amount, decimals }: Money) {
|
||||
return `${amount.shiftedBy(-decimals).toString()}`;
|
||||
}
|
||||
|
||||
export function formatMoneyPadded({ amount, symbol, decimals }: Money) {
|
||||
return `${amount.shiftedBy(-decimals).toFormat(decimals)} ${symbol}`;
|
||||
}
|
||||
|
||||
@@ -33,10 +33,8 @@ export function useRpcSignPsbtParams() {
|
||||
const allowedSighash = searchParams.getAll('allowedSighash');
|
||||
const signAtIndex = searchParams.getAll('signAtIndex');
|
||||
|
||||
if (!requestId || !psbtHex || !origin) throw new Error('Invalid params');
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
return useMemo(() => {
|
||||
return {
|
||||
origin,
|
||||
tabId: tabId ?? 0,
|
||||
requestId,
|
||||
@@ -45,7 +43,6 @@ export function useRpcSignPsbtParams() {
|
||||
allowedSighash.map(h => Number(h)) as btc.SignatureHash[]
|
||||
),
|
||||
signAtIndex: undefinedIfLengthZero(signAtIndex.map(h => Number(h))),
|
||||
}),
|
||||
[allowedSighash, origin, psbtHex, requestId, signAtIndex, tabId]
|
||||
);
|
||||
};
|
||||
}, [allowedSighash, origin, psbtHex, requestId, signAtIndex, tabId]);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ export function GenericErrorLayout(props: GenericErrorProps) {
|
||||
lineHeight="1.6"
|
||||
mt="base"
|
||||
textAlign="center"
|
||||
width="100%"
|
||||
wordWrap="break-word"
|
||||
>
|
||||
{body}
|
||||
</Text>
|
||||
|
||||
@@ -5,13 +5,13 @@ import { Header } from '@app/components/header';
|
||||
|
||||
import { GenericErrorLayout } from './generic-error.layout';
|
||||
|
||||
interface ErrorProps {
|
||||
interface GenericErrorProps {
|
||||
body: string;
|
||||
helpTextList: ReactNode[];
|
||||
onClose?(): void;
|
||||
title: string;
|
||||
}
|
||||
export function GenericError(props: ErrorProps) {
|
||||
export function GenericError(props: GenericErrorProps) {
|
||||
const { body, helpTextList, onClose = () => window.close(), title } = props;
|
||||
|
||||
useRouteHeader(<Header hideActions />);
|
||||
|
||||
5
src/app/components/hr.tsx
Normal file
5
src/app/components/hr.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Box, BoxProps } from '@stacks/ui';
|
||||
|
||||
export function Hr(props: BoxProps) {
|
||||
return <Box as="hr" backgroundColor="#DCDDE2" width="100%" {...props} />;
|
||||
}
|
||||
30
src/app/components/json.tsx
Normal file
30
src/app/components/json.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { css } from '@emotion/react';
|
||||
import { bytesToHex } from '@stacks/common';
|
||||
import { Box } from '@stacks/ui';
|
||||
|
||||
import { isTypedArray } from '@shared/utils';
|
||||
|
||||
function parseJsonReadable(value: any) {
|
||||
if (typeof value === 'bigint') return value.toString();
|
||||
if (isTypedArray(value)) return bytesToHex(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
export function Json(value: any) {
|
||||
return (
|
||||
<Box
|
||||
css={css`
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
`}
|
||||
fontSize="14px"
|
||||
lineHeight="1.7"
|
||||
mt="loose"
|
||||
>
|
||||
<pre>{JSON.stringify(value, (_, v) => parseJsonReadable(v), 2)}</pre>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import React, { cloneElement, isValidElement } from 'react';
|
||||
|
||||
import { Box, BoxProps } from '@stacks/ui';
|
||||
import { BoxProps } from '@stacks/ui';
|
||||
|
||||
function Hr(props: BoxProps) {
|
||||
return <Box as="hr" width="100%" backgroundColor="#DCDDE2" {...props} />;
|
||||
}
|
||||
import { Hr } from '../hr';
|
||||
|
||||
interface DividerSeparatorProps extends BoxProps {
|
||||
children: React.ReactNode;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Box, Text } from '@stacks/ui';
|
||||
|
||||
import { PsbtDecodedNodeLayout } from './psbt-decoded-node.layout';
|
||||
|
||||
export function PsbtPlaceholderNode() {
|
||||
return (
|
||||
<Box background="white" borderRadius="16px" p="loose">
|
||||
<Text fontWeight={500}>Inputs</Text>
|
||||
<PsbtDecodedNodeLayout value="No inputs will be spent" />
|
||||
<hr />
|
||||
<Text fontWeight={500} mt="loose">
|
||||
Outputs
|
||||
</Text>
|
||||
<PsbtDecodedNodeLayout value="No outputs will transfer" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { css } from '@emotion/react';
|
||||
import { bytesToHex } from '@stacks/common';
|
||||
import { Box } from '@stacks/ui';
|
||||
|
||||
import { isTypedArray } from '@shared/utils';
|
||||
|
||||
import { DecodedPsbt } from '@app/features/psbt-signer/hooks/use-psbt-signer';
|
||||
|
||||
function parseJsonReadable(value: any) {
|
||||
if (typeof value === 'bigint') return value.toString();
|
||||
if (isTypedArray(value)) return bytesToHex(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
interface PsbtDecodedRequestAdvancedProps {
|
||||
psbt: DecodedPsbt;
|
||||
}
|
||||
export function PsbtDecodedRequestAdvanced({ psbt }: PsbtDecodedRequestAdvancedProps) {
|
||||
return (
|
||||
<Box background="white" borderRadius="16px" p="loose">
|
||||
<Box
|
||||
css={css`
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
`}
|
||||
fontSize="14px"
|
||||
lineHeight="1.7"
|
||||
>
|
||||
<pre>{JSON.stringify(psbt, (_, value) => parseJsonReadable(value), 2)}</pre>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
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';
|
||||
|
||||
interface PsbtDecodedRequestSimpleProps {
|
||||
bitcoinAddressNativeSegwit: string;
|
||||
bitcoinAddressTaproot: string;
|
||||
inputs: PsbtDecodedUtxosMainnet | PsbtDecodedUtxosTestnet;
|
||||
outputs: btc.TransactionOutputRequired[];
|
||||
showPlaceholder: boolean;
|
||||
}
|
||||
|
||||
export function PsbtDecodedRequestSimple({
|
||||
bitcoinAddressNativeSegwit,
|
||||
bitcoinAddressTaproot,
|
||||
outputs,
|
||||
showPlaceholder,
|
||||
inputs,
|
||||
}: PsbtDecodedRequestSimpleProps) {
|
||||
if (showPlaceholder) return <PsbtPlaceholderNode />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PsbtUnsignedInputList
|
||||
addressNativeSegwit={bitcoinAddressNativeSegwit}
|
||||
addressTaproot={bitcoinAddressTaproot}
|
||||
inputs={inputs}
|
||||
/>
|
||||
<PsbtUnsignedOutputList
|
||||
addressNativeSegwit={bitcoinAddressNativeSegwit}
|
||||
addressTaproot={bitcoinAddressTaproot}
|
||||
outputs={outputs}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Flex, Text, color } from '@stacks/ui';
|
||||
|
||||
interface PsbtDecodedRequestViewToggleProps {
|
||||
onSetShowAdvancedView(): void;
|
||||
shouldDefaultToAdvancedView: boolean;
|
||||
showAdvancedView: boolean;
|
||||
}
|
||||
export function PsbtDecodedRequestViewToggle({
|
||||
onSetShowAdvancedView,
|
||||
shouldDefaultToAdvancedView,
|
||||
showAdvancedView,
|
||||
}: PsbtDecodedRequestViewToggleProps) {
|
||||
return (
|
||||
<Flex
|
||||
_hover={{ cursor: 'pointer' }}
|
||||
justifyContent="flex-end"
|
||||
as="button"
|
||||
onClick={onSetShowAdvancedView}
|
||||
pt="tight"
|
||||
px="loose"
|
||||
width="100%"
|
||||
>
|
||||
<Text
|
||||
color={color('accent')}
|
||||
display={shouldDefaultToAdvancedView ? 'none' : 'block'}
|
||||
fontSize={1}
|
||||
py="tight"
|
||||
>
|
||||
{showAdvancedView ? 'Hide advanced view' : 'Show advanced view'}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
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 { 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';
|
||||
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';
|
||||
|
||||
interface PsbtDecodedRequestProps {
|
||||
psbt: DecodedPsbt;
|
||||
}
|
||||
export function PsbtDecodedRequest({ psbt }: PsbtDecodedRequestProps) {
|
||||
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
|
||||
const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner();
|
||||
const unsignedInputs: btc.TransactionInputRequired[] = psbt.global.unsignedTx?.inputs ?? [];
|
||||
const unsignedOutputs: btc.TransactionOutputRequired[] = psbt.global.unsignedTx?.outputs ?? [];
|
||||
|
||||
const {
|
||||
onSetShowAdvancedView,
|
||||
shouldDefaultToAdvancedView,
|
||||
shouldShowPlaceholder,
|
||||
showAdvancedView,
|
||||
unsignedUtxos,
|
||||
} = usePsbtDecodedRequest({
|
||||
unsignedInputs,
|
||||
unsignedOutputs,
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack
|
||||
backgroundColor={color('border')}
|
||||
border="4px solid"
|
||||
borderColor={color('border')}
|
||||
borderRadius="20px"
|
||||
paddingBottom="tight"
|
||||
spacing="extra-tight"
|
||||
width="100%"
|
||||
>
|
||||
{showAdvancedView || shouldDefaultToAdvancedView ? (
|
||||
<PsbtDecodedRequestAdvanced psbt={psbt} />
|
||||
) : (
|
||||
<PsbtDecodedRequestSimple
|
||||
bitcoinAddressNativeSegwit={nativeSegwitSigner.address}
|
||||
bitcoinAddressTaproot={bitcoinAddressTaproot}
|
||||
inputs={unsignedUtxos}
|
||||
outputs={unsignedOutputs}
|
||||
showPlaceholder={shouldShowPlaceholder}
|
||||
/>
|
||||
)}
|
||||
<PsbtDecodedRequestViewToggle
|
||||
onSetShowAdvancedView={onSetShowAdvancedView}
|
||||
shouldDefaultToAdvancedView={shouldDefaultToAdvancedView}
|
||||
showAdvancedView={showAdvancedView}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { truncateMiddle } from '@stacks/ui-utils';
|
||||
|
||||
import { logger } from '@shared/logger';
|
||||
import { createMoney } from '@shared/models/money.model';
|
||||
|
||||
import { formatMoneyWithoutSymbol, i18nFormatCurrency } from '@app/common/money/format-money';
|
||||
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 inputValueAsMoney = createMoney(utxo.value, 'BTC');
|
||||
|
||||
if (!utxo.address) {
|
||||
logger.error('UTXO does not have an address');
|
||||
return null;
|
||||
}
|
||||
|
||||
return utxo.inscriptions ? (
|
||||
<PsbtUnsignedInputWithInscription
|
||||
address={utxo.address}
|
||||
path={utxo.inscriptions}
|
||||
value={inputValueAsMoney}
|
||||
/>
|
||||
) : (
|
||||
<PsbtDecodedNodeLayout
|
||||
hoverLabel={utxo.address}
|
||||
subtitle={truncateMiddle(utxo.address)}
|
||||
subValue={i18nFormatCurrency(calculateBitcoinFiatValue(inputValueAsMoney))}
|
||||
value={`${isInputCurrentAddress ? '-' : '+'}${formatMoneyWithoutSymbol(inputValueAsMoney)}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { truncateMiddle } from '@stacks/ui-utils';
|
||||
|
||||
import { createMoney } from '@shared/models/money.model';
|
||||
import { BitcoinTransactionVectorOutput } from '@shared/models/transactions/bitcoin-transaction.model';
|
||||
|
||||
import { formatMoneyWithoutSymbol, i18nFormatCurrency } from '@app/common/money/format-money';
|
||||
import { useCalculateBitcoinFiatValue } from '@app/query/common/market-data/market-data.hooks';
|
||||
|
||||
import { PsbtDecodedNodeLayout } from '../../psbt-decoded-request-node/psbt-decoded-node.layout';
|
||||
|
||||
interface PsbtUnsignedInputItemProps {
|
||||
addressNativeSegwit: string;
|
||||
addressTaproot: string;
|
||||
utxo: BitcoinTransactionVectorOutput;
|
||||
}
|
||||
export function PsbtUnsignedInputItem({
|
||||
addressNativeSegwit,
|
||||
addressTaproot,
|
||||
utxo,
|
||||
}: PsbtUnsignedInputItemProps) {
|
||||
const calculateBitcoinFiatValue = useCalculateBitcoinFiatValue();
|
||||
|
||||
const isInputCurrentAddress =
|
||||
utxo.scriptpubkey_address === addressNativeSegwit ||
|
||||
utxo.scriptpubkey_address === addressTaproot;
|
||||
const inputValueAsMoney = createMoney(utxo.value, 'BTC');
|
||||
|
||||
return (
|
||||
<PsbtDecodedNodeLayout
|
||||
hoverLabel={utxo.scriptpubkey_address}
|
||||
subtitle={truncateMiddle(utxo.scriptpubkey_address)}
|
||||
subValue={i18nFormatCurrency(calculateBitcoinFiatValue(inputValueAsMoney))}
|
||||
value={`${isInputCurrentAddress ? '-' : '+'}${formatMoneyWithoutSymbol(inputValueAsMoney)}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Box } from '@stacks/ui';
|
||||
import { truncateMiddle } from '@stacks/ui-utils';
|
||||
|
||||
import { Money } from '@shared/models/money.model';
|
||||
import { isUndefined } from '@shared/utils';
|
||||
|
||||
import { formatMoneyWithoutSymbol } from '@app/common/money/format-money';
|
||||
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-request-node/psbt-decoded-node.layout';
|
||||
|
||||
interface PsbtUnsignedInputWithInscriptionProps {
|
||||
address: string;
|
||||
path: string;
|
||||
value: Money;
|
||||
}
|
||||
export function PsbtUnsignedInputWithInscription({
|
||||
address,
|
||||
value,
|
||||
path,
|
||||
}: PsbtUnsignedInputWithInscriptionProps) {
|
||||
const {
|
||||
isLoading,
|
||||
isError,
|
||||
data: inscription,
|
||||
} = useInscription(path.replace('/inscription/', ''));
|
||||
|
||||
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={`-${formatMoneyWithoutSymbol(value)}`}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<PsbtDecodedNodeLayout
|
||||
hoverLabel={address}
|
||||
image={<InscriptionPreview inscription={inscription} height="40px" width="40px" />}
|
||||
subtitle={truncateMiddle(address)}
|
||||
subValue={`#${inscription.number}`}
|
||||
subValueAction={() => openInNewTab(inscription.infoUrl)}
|
||||
title="Ordinal inscription"
|
||||
value={`-${formatMoneyWithoutSymbol(value)}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
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: PsbtDecodedUtxosMainnet | PsbtDecodedUtxosTestnet;
|
||||
}
|
||||
export function PsbtUnsignedInputList({
|
||||
addressNativeSegwit,
|
||||
addressTaproot,
|
||||
inputs,
|
||||
}: PsbtUnsignedInputListProps) {
|
||||
if (!inputs.utxos.length)
|
||||
return (
|
||||
<PsbtUnsignedInputListLayout>
|
||||
<PsbtDecodedNodeLayout value="No inputs found" />
|
||||
</PsbtUnsignedInputListLayout>
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import * as btc from '@scure/btc-signer';
|
||||
import { truncateMiddle } from '@stacks/ui-utils';
|
||||
|
||||
import { getAddressFromOutScript } from '@shared/crypto/bitcoin/bitcoin.utils';
|
||||
import { createMoney } from '@shared/models/money.model';
|
||||
|
||||
import { formatMoneyWithoutSymbol, i18nFormatCurrency } from '@app/common/money/format-money';
|
||||
import { useCalculateBitcoinFiatValue } from '@app/query/common/market-data/market-data.hooks';
|
||||
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
|
||||
|
||||
import { PsbtDecodedNodeLayout } from '../../psbt-decoded-request-node/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 outputValueAsMoney = createMoney(output.amount, 'BTC');
|
||||
|
||||
return (
|
||||
<PsbtDecodedNodeLayout
|
||||
hoverLabel={addressFromScript}
|
||||
subtitle={truncateMiddle(addressFromScript)}
|
||||
subValue={i18nFormatCurrency(calculateBitcoinFiatValue(outputValueAsMoney))}
|
||||
value={`${isOutputCurrentAddress ? '+' : ' '}${formatMoneyWithoutSymbol(outputValueAsMoney)}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import * as btc from '@scure/btc-signer';
|
||||
|
||||
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;
|
||||
addressTaproot: string;
|
||||
outputs: btc.TransactionOutputRequired[];
|
||||
}
|
||||
export function PsbtUnsignedOutputList({
|
||||
addressNativeSegwit,
|
||||
addressTaproot,
|
||||
outputs,
|
||||
}: PsbtUnsignedOutputListProps) {
|
||||
if (!outputs.length)
|
||||
return (
|
||||
<PsbtUnsignedOutputListLayout>
|
||||
<PsbtDecodedNodeLayout value="No outputs found" />
|
||||
</PsbtUnsignedOutputListLayout>
|
||||
);
|
||||
|
||||
return (
|
||||
<PsbtUnsignedOutputListLayout>
|
||||
{outputs.map((output, i) => {
|
||||
return (
|
||||
<PsbtUnsignedOutputItem
|
||||
addressNativeSegwit={addressNativeSegwit}
|
||||
addressTaproot={addressTaproot}
|
||||
key={i}
|
||||
output={output}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</PsbtUnsignedOutputListLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { truncateMiddle } from '@stacks/ui-utils';
|
||||
|
||||
import { createMoney } from '@shared/models/money.model';
|
||||
|
||||
import { formatMoney } from '@app/common/money/format-money';
|
||||
import { PsbtInput } from '@app/features/psbt-signer/hooks/use-parsed-inputs';
|
||||
|
||||
import { PsbtInputOutputItemLayout } from '../../psbt-input-output-item.layout';
|
||||
|
||||
const pillHoverLabel = 'Your approval is needed to complete this transaction.';
|
||||
|
||||
export function PsbtInputItem(props: { utxo: PsbtInput }) {
|
||||
const { utxo } = props;
|
||||
|
||||
return (
|
||||
<PsbtInputOutputItemLayout
|
||||
address={truncateMiddle(utxo.address)}
|
||||
addressHoverLabel={utxo.address}
|
||||
amount={formatMoney(createMoney(utxo.value, 'BTC'))}
|
||||
pillHoverLabel={utxo.sign ? pillHoverLabel : undefined}
|
||||
pillLabel={utxo.sign ? <>Approve</> : undefined}
|
||||
txId={truncateMiddle(utxo.txid)}
|
||||
txIdHoverLabel={utxo.txid}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Box } from '@stacks/ui';
|
||||
|
||||
import { HasChildren } from '@app/common/has-children';
|
||||
|
||||
export function PsbtInputListLayout({ children }: HasChildren) {
|
||||
return (
|
||||
<Box pb="extra-loose">
|
||||
<Box borderLeft="1px solid #DCDDE2">{children}</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { PsbtInput } from '@app/features/psbt-signer/hooks/use-parsed-inputs';
|
||||
|
||||
import { PsbtInputItem } from './components/psbt-input-item';
|
||||
import { PsbtInputListLayout } from './components/psbt-input-list.layout';
|
||||
|
||||
export function PsbtInputList({ inputs }: { inputs: PsbtInput[] }) {
|
||||
return (
|
||||
<PsbtInputListLayout>
|
||||
{inputs.map((input, i) => (
|
||||
<PsbtInputItem key={i} utxo={input} />
|
||||
))}
|
||||
</PsbtInputListLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { FiCopy } from 'react-icons/fi';
|
||||
|
||||
import { Box, Flex, Text, color, useClipboard } from '@stacks/ui';
|
||||
|
||||
import { useExplorerLink } from '@app/common/hooks/use-explorer-link';
|
||||
import { Flag } from '@app/components/layout/flag';
|
||||
import { SpaceBetween } from '@app/components/layout/space-between';
|
||||
import { Link } from '@app/components/link';
|
||||
import { Tooltip } from '@app/components/tooltip';
|
||||
|
||||
interface PsbtInputOutputItemLayoutProps {
|
||||
address: string;
|
||||
addressHoverLabel?: string;
|
||||
amount: string;
|
||||
pillHoverLabel?: string;
|
||||
pillLabel?: React.JSX.Element;
|
||||
txId?: string;
|
||||
txIdHoverLabel?: string;
|
||||
}
|
||||
export function PsbtInputOutputItemLayout({
|
||||
address,
|
||||
addressHoverLabel,
|
||||
amount,
|
||||
pillHoverLabel,
|
||||
pillLabel,
|
||||
txId,
|
||||
txIdHoverLabel,
|
||||
}: PsbtInputOutputItemLayoutProps) {
|
||||
const { onCopy, hasCopied } = useClipboard(addressHoverLabel ?? '');
|
||||
const { handleOpenTxLink } = useExplorerLink();
|
||||
|
||||
return (
|
||||
<Flag align="middle" img={<></>} mt="loose" spacing="base">
|
||||
<SpaceBetween>
|
||||
<Flex alignItems="center">
|
||||
<Text color={color('text-caption')} fontSize={1} mr="extra-tight">
|
||||
{address}
|
||||
</Text>
|
||||
<Tooltip
|
||||
disabled={!addressHoverLabel}
|
||||
hideOnClick={false}
|
||||
label={hasCopied ? 'Copied!' : addressHoverLabel}
|
||||
labelProps={{ wordWrap: 'break-word' }}
|
||||
maxWidth="230px"
|
||||
placement="bottom"
|
||||
>
|
||||
<Box
|
||||
as="button"
|
||||
color={color('text-caption')}
|
||||
display="flex"
|
||||
onClick={onCopy}
|
||||
type="button"
|
||||
>
|
||||
{addressHoverLabel ? <FiCopy size="16px" /> : null}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
{pillLabel ? (
|
||||
<Tooltip label={pillHoverLabel} maxWidth="200px" placement="bottom">
|
||||
<Box
|
||||
border="1px solid #DCDDE2"
|
||||
borderRadius="24px"
|
||||
lineHeight="16px"
|
||||
ml="base-tight"
|
||||
px="tight"
|
||||
>
|
||||
<Text color={color('text-caption')} fontSize={0}>
|
||||
{pillLabel}
|
||||
</Text>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Flex>
|
||||
<Text color={color('text-caption')} fontSize={1}>
|
||||
{amount}
|
||||
</Text>
|
||||
</SpaceBetween>
|
||||
<Box mt="tight">
|
||||
{txId && txIdHoverLabel ? (
|
||||
<Link
|
||||
_hover={{ textDecoration: 'none' }}
|
||||
fontSize="14px"
|
||||
mr="4px !important"
|
||||
onClick={() =>
|
||||
handleOpenTxLink({
|
||||
blockchain: 'bitcoin',
|
||||
txid: txIdHoverLabel ?? '',
|
||||
})
|
||||
}
|
||||
>
|
||||
<Tooltip
|
||||
disabled={!txIdHoverLabel}
|
||||
hideOnClick={false}
|
||||
label={txIdHoverLabel}
|
||||
labelProps={{ wordWrap: 'break-word' }}
|
||||
maxWidth="230px"
|
||||
placement="bottom"
|
||||
>
|
||||
<Text color={color('brand')} fontSize={1} mr="extra-tight">
|
||||
{txId}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
) : null}
|
||||
</Box>
|
||||
</Flag>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { truncateMiddle } from '@stacks/ui-utils';
|
||||
|
||||
import { createMoney } from '@shared/models/money.model';
|
||||
|
||||
import { formatMoney } from '@app/common/money/format-money';
|
||||
import { PsbtOutput } from '@app/features/psbt-signer/hooks/use-parsed-outputs';
|
||||
|
||||
import { PsbtInputOutputItemLayout } from '../../psbt-input-output-item.layout';
|
||||
|
||||
const pillHoverLabel = 'Value you’ll receive after this transaction is complete.';
|
||||
|
||||
interface PsbtOutputItemProps {
|
||||
addressNativeSegwit: string;
|
||||
addressTaproot: string;
|
||||
output: PsbtOutput;
|
||||
}
|
||||
export function PsbtOutputItem({
|
||||
addressNativeSegwit,
|
||||
addressTaproot,
|
||||
output,
|
||||
}: PsbtOutputItemProps) {
|
||||
const isOutputCurrentAddress =
|
||||
output.address === addressNativeSegwit || output.address === addressTaproot;
|
||||
|
||||
return (
|
||||
<PsbtInputOutputItemLayout
|
||||
address={truncateMiddle(output.address)}
|
||||
addressHoverLabel={output.address}
|
||||
amount={formatMoney(createMoney(Number(output.value), 'BTC'))}
|
||||
pillHoverLabel={isOutputCurrentAddress ? pillHoverLabel : undefined}
|
||||
pillLabel={isOutputCurrentAddress ? <>You</> : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Box } from '@stacks/ui';
|
||||
|
||||
import { HasChildren } from '@app/common/has-children';
|
||||
|
||||
export function PsbtOutputListLayout({ children }: HasChildren) {
|
||||
return (
|
||||
<Box>
|
||||
<Box borderLeft="1px solid #DCDDE2">{children}</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { PsbtOutput } from '@app/features/psbt-signer/hooks/use-parsed-outputs';
|
||||
|
||||
import { PsbtOutputItem } from './components/psbt-output-item';
|
||||
import { PsbtOutputListLayout } from './components/psbt-output-list.layout';
|
||||
|
||||
interface PsbtOutputListProps {
|
||||
addressNativeSegwit: string;
|
||||
addressTaproot: string;
|
||||
outputs: PsbtOutput[];
|
||||
}
|
||||
export function PsbtOutputList({
|
||||
addressNativeSegwit,
|
||||
addressTaproot,
|
||||
outputs,
|
||||
}: PsbtOutputListProps) {
|
||||
return (
|
||||
<PsbtOutputListLayout>
|
||||
{outputs.map((output, i) => (
|
||||
<PsbtOutputItem
|
||||
addressNativeSegwit={addressNativeSegwit}
|
||||
addressTaproot={addressTaproot}
|
||||
key={i}
|
||||
output={output}
|
||||
/>
|
||||
))}
|
||||
</PsbtOutputListLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { PsbtInput } from '@app/features/psbt-signer/hooks/use-parsed-inputs';
|
||||
import { PsbtOutput } from '@app/features/psbt-signer/hooks/use-parsed-outputs';
|
||||
|
||||
import { PsbtRequestDetailsSectionHeader } from '../psbt-request-details-section-header';
|
||||
import { PsbtRequestDetailsSectionLayout } from '../psbt-request-details-section.layout';
|
||||
import { PsbtInputList } from './components/psbt-input-list/psbt-input-list';
|
||||
import { PsbtOutputList } from './components/psbt-output-list/psbt-output-list';
|
||||
|
||||
interface PsbtInputsAndOutputsProps {
|
||||
addressNativeSegwit: string;
|
||||
addressTaproot: string;
|
||||
inputs: PsbtInput[];
|
||||
outputs: PsbtOutput[];
|
||||
}
|
||||
|
||||
export function PsbtInputsAndOutputs({
|
||||
addressNativeSegwit,
|
||||
addressTaproot,
|
||||
outputs,
|
||||
inputs,
|
||||
}: PsbtInputsAndOutputsProps) {
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
if (!inputs.length || !outputs.length) return null;
|
||||
|
||||
return (
|
||||
<PsbtRequestDetailsSectionLayout>
|
||||
<PsbtRequestDetailsSectionHeader
|
||||
hasDetails
|
||||
onSetShowDetails={(value: boolean) => setShowDetails(value)}
|
||||
showDetails={showDetails}
|
||||
title={showDetails ? 'Inputs' : 'Inputs and Outputs'}
|
||||
/>
|
||||
{showDetails ? (
|
||||
<>
|
||||
<PsbtInputList inputs={inputs} />
|
||||
<PsbtRequestDetailsSectionHeader title="Outputs" />
|
||||
<PsbtOutputList
|
||||
addressNativeSegwit={addressNativeSegwit}
|
||||
addressTaproot={addressTaproot}
|
||||
outputs={outputs}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</PsbtRequestDetailsSectionLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +1,43 @@
|
||||
import { FiArrowUpRight, FiCopy } from 'react-icons/fi';
|
||||
import { FiCopy } from 'react-icons/fi';
|
||||
|
||||
import { Box, Stack, Text, color, useClipboard } from '@stacks/ui';
|
||||
import { Box, Text, color, useClipboard } from '@stacks/ui';
|
||||
|
||||
import { BtcIcon } from '@app/components/icons/btc-icon';
|
||||
import { Flag } from '@app/components/layout/flag';
|
||||
import { SpaceBetween } from '@app/components/layout/space-between';
|
||||
import { Tooltip } from '@app/components/tooltip';
|
||||
|
||||
interface PsbtDecodedNodeLayoutProps {
|
||||
interface PsbtAddressTotalItemProps {
|
||||
hoverLabel?: string;
|
||||
image?: React.JSX.Element;
|
||||
subtitle?: string;
|
||||
subValue?: string;
|
||||
subValueAction?(): void;
|
||||
title?: string;
|
||||
value: string;
|
||||
valueAction?(): void;
|
||||
}
|
||||
export function PsbtDecodedNodeLayout({
|
||||
export function PsbtAddressTotalItem({
|
||||
hoverLabel,
|
||||
image,
|
||||
subtitle,
|
||||
subValue,
|
||||
subValueAction,
|
||||
title,
|
||||
value,
|
||||
}: PsbtDecodedNodeLayoutProps) {
|
||||
valueAction,
|
||||
}: PsbtAddressTotalItemProps) {
|
||||
const { onCopy, hasCopied } = useClipboard(hoverLabel ?? '');
|
||||
|
||||
return (
|
||||
<Flag align="middle" img={image ? image : <BtcIcon />} my="loose" spacing="base">
|
||||
<Flag align="middle" img={image ? image : <BtcIcon />} mt="loose" spacing="base">
|
||||
<SpaceBetween>
|
||||
<Text fontSize={2} fontWeight="500">
|
||||
{title ? title : 'BTC'}
|
||||
</Text>
|
||||
<Text fontSize={2} fontWeight="500">
|
||||
{value}
|
||||
{title ? title : 'Bitcoin'}
|
||||
</Text>
|
||||
<Box as="button" onClick={valueAction} type="button">
|
||||
<Text color={valueAction ? color('accent') : 'unset'} fontSize={2} fontWeight={500}>
|
||||
{value}
|
||||
</Text>
|
||||
</Box>
|
||||
</SpaceBetween>
|
||||
<SpaceBetween mt="tight">
|
||||
{subtitle ? (
|
||||
@@ -48,7 +50,6 @@ export function PsbtDecodedNodeLayout({
|
||||
placement="bottom"
|
||||
>
|
||||
<Box
|
||||
_hover={{ cursor: 'pointer' }}
|
||||
as="button"
|
||||
color={color('text-caption')}
|
||||
display="flex"
|
||||
@@ -63,12 +64,9 @@ export function PsbtDecodedNodeLayout({
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{subValue ? (
|
||||
<Stack as="button" isInline onClick={subValueAction} spacing="extra-tight" type="button">
|
||||
<Text color={subValueAction ? color('accent') : color('text-caption')} fontSize={1}>
|
||||
{subValue}
|
||||
</Text>
|
||||
{subValueAction ? <FiArrowUpRight color={color('accent')} /> : null}
|
||||
</Stack>
|
||||
<Text color={color('text-caption')} fontSize={1}>
|
||||
{subValue}
|
||||
</Text>
|
||||
) : null}
|
||||
</SpaceBetween>
|
||||
</Flag>
|
||||
@@ -0,0 +1,64 @@
|
||||
import { truncateMiddle } from '@stacks/ui-utils';
|
||||
|
||||
import { Money } from '@shared/models/money.model';
|
||||
|
||||
import { formatMoney, i18nFormatCurrency } from '@app/common/money/format-money';
|
||||
import { useCalculateBitcoinFiatValue } from '@app/query/common/market-data/market-data.hooks';
|
||||
|
||||
import { PsbtAddressTotalItem } from './psbt-address-total-item';
|
||||
import { PsbtInscription } from './psbt-inscription';
|
||||
|
||||
interface PsbtAddressTotalsProps {
|
||||
accountInscriptionsBeingTransferred?: string[];
|
||||
accountInscriptionsBeingReceived?: string[];
|
||||
addressNativeSegwit: string;
|
||||
addressTaproot: string;
|
||||
addressNativeSegwitTotal: Money;
|
||||
addressTaprootTotal: Money;
|
||||
showNativeSegwitTotal: boolean;
|
||||
showTaprootTotal: boolean;
|
||||
}
|
||||
export function PsbtAddressTotals({
|
||||
accountInscriptionsBeingTransferred,
|
||||
accountInscriptionsBeingReceived,
|
||||
addressNativeSegwit,
|
||||
addressTaproot,
|
||||
addressNativeSegwitTotal,
|
||||
addressTaprootTotal,
|
||||
showNativeSegwitTotal,
|
||||
showTaprootTotal,
|
||||
}: PsbtAddressTotalsProps) {
|
||||
const calculateBitcoinFiatValue = useCalculateBitcoinFiatValue();
|
||||
|
||||
const isTransferringInscriptions = accountInscriptionsBeingTransferred?.length;
|
||||
const isReceivingInscriptions = accountInscriptionsBeingReceived?.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showNativeSegwitTotal ? (
|
||||
<PsbtAddressTotalItem
|
||||
hoverLabel={addressNativeSegwit}
|
||||
subtitle={truncateMiddle(addressNativeSegwit)}
|
||||
subValue={i18nFormatCurrency(calculateBitcoinFiatValue(addressNativeSegwitTotal))}
|
||||
value={formatMoney(addressNativeSegwitTotal)}
|
||||
/>
|
||||
) : null}
|
||||
{isTransferringInscriptions
|
||||
? accountInscriptionsBeingTransferred.map(path => (
|
||||
<PsbtInscription key={path} path={path} />
|
||||
))
|
||||
: null}
|
||||
{!isReceivingInscriptions && showTaprootTotal ? (
|
||||
<PsbtAddressTotalItem
|
||||
hoverLabel={addressTaproot}
|
||||
subtitle={truncateMiddle(addressTaproot)}
|
||||
subValue={i18nFormatCurrency(calculateBitcoinFiatValue(addressTaprootTotal))}
|
||||
value={formatMoney(addressTaprootTotal)}
|
||||
/>
|
||||
) : null}
|
||||
{isReceivingInscriptions
|
||||
? accountInscriptionsBeingReceived.map(path => <PsbtInscription key={path} path={path} />)
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Box } from '@stacks/ui';
|
||||
|
||||
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 { PsbtAddressTotalItem } from './psbt-address-total-item';
|
||||
|
||||
interface PsbtInscriptionProps {
|
||||
path: string;
|
||||
}
|
||||
export function PsbtInscription({ path }: PsbtInscriptionProps) {
|
||||
const {
|
||||
isLoading,
|
||||
isError,
|
||||
data: inscription,
|
||||
} = useInscription(path.replace('/inscription/', ''));
|
||||
|
||||
if (isLoading)
|
||||
return (
|
||||
<Box my="loose">
|
||||
<LoadingSpinner />
|
||||
</Box>
|
||||
);
|
||||
if (isError || isUndefined(inscription))
|
||||
return (
|
||||
<PsbtAddressTotalItem
|
||||
image={<OrdinalIcon />}
|
||||
title="Inscription not found"
|
||||
value="# Unknown"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<PsbtAddressTotalItem
|
||||
image={<InscriptionPreview inscription={inscription} height="40px" width="40px" />}
|
||||
title="Inscription"
|
||||
value={`#${inscription.number}`}
|
||||
valueAction={() => openInNewTab(inscription.infoUrl)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Box } from '@stacks/ui';
|
||||
import { color } from '@stacks/ui-utils';
|
||||
|
||||
import { Money } from '@shared/models/money.model';
|
||||
|
||||
import { Hr } from '@app/components/hr';
|
||||
|
||||
import { PsbtRequestDetailsSectionHeader } from '../psbt-request-details-section-header';
|
||||
import { PsbtRequestDetailsSectionLayout } from '../psbt-request-details-section.layout';
|
||||
import { PsbtAddressTotals } from './components/psbt-address-totals';
|
||||
|
||||
interface PsbtInputsOutputsTotalsProps {
|
||||
accountInscriptionsBeingTransferred: string[];
|
||||
accountInscriptionsBeingReceived: string[];
|
||||
addressNativeSegwit: string;
|
||||
addressTaproot: string;
|
||||
addressNativeSegwitTotal: Money;
|
||||
addressTaprootTotal: Money;
|
||||
}
|
||||
export function PsbtInputsOutputsTotals({
|
||||
accountInscriptionsBeingTransferred,
|
||||
accountInscriptionsBeingReceived,
|
||||
addressNativeSegwit,
|
||||
addressTaproot,
|
||||
addressNativeSegwitTotal,
|
||||
addressTaprootTotal,
|
||||
}: PsbtInputsOutputsTotalsProps) {
|
||||
// Transferring (+)
|
||||
const isNativeSegwitTotalGreaterThanZero = addressNativeSegwitTotal.amount.isGreaterThan(0);
|
||||
const isTaprootTotalGreaterThanZero = addressTaprootTotal.amount.isGreaterThan(0);
|
||||
// Receiving (-)
|
||||
const isNativeSegwitTotalLessThanZero = addressNativeSegwitTotal.amount.isLessThan(0);
|
||||
const isTaprootTotalLessThanZero = addressTaprootTotal.amount.isLessThan(0);
|
||||
const isTotalZero =
|
||||
addressNativeSegwitTotal.amount.isEqualTo(0) && addressTaprootTotal.amount.isEqualTo(0);
|
||||
|
||||
const isTransferring = isNativeSegwitTotalGreaterThanZero || isTaprootTotalGreaterThanZero;
|
||||
const isReceiving = isNativeSegwitTotalLessThanZero || isTaprootTotalLessThanZero;
|
||||
const showDivider = isTransferring && isReceiving;
|
||||
|
||||
if (isTotalZero) return null;
|
||||
|
||||
return (
|
||||
<PsbtRequestDetailsSectionLayout p="unset">
|
||||
{isTransferring ? (
|
||||
<Box p="loose">
|
||||
<PsbtRequestDetailsSectionHeader title="You'll transfer" />
|
||||
<PsbtAddressTotals
|
||||
accountInscriptionsBeingTransferred={accountInscriptionsBeingTransferred}
|
||||
addressNativeSegwit={addressNativeSegwit}
|
||||
addressTaproot={addressTaproot}
|
||||
addressNativeSegwitTotal={addressNativeSegwitTotal}
|
||||
addressTaprootTotal={addressTaprootTotal}
|
||||
showNativeSegwitTotal={isNativeSegwitTotalGreaterThanZero}
|
||||
showTaprootTotal={isTaprootTotalGreaterThanZero}
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
{showDivider ? <Hr backgroundColor={color('border')} height="3px" /> : null}
|
||||
{isReceiving ? (
|
||||
<Box p="loose">
|
||||
<PsbtRequestDetailsSectionHeader title="You'll receive" />
|
||||
<PsbtAddressTotals
|
||||
accountInscriptionsBeingReceived={accountInscriptionsBeingReceived}
|
||||
addressNativeSegwit={addressNativeSegwit}
|
||||
addressTaproot={addressTaproot}
|
||||
addressNativeSegwitTotal={addressNativeSegwitTotal}
|
||||
addressTaprootTotal={addressTaprootTotal}
|
||||
showNativeSegwitTotal={isNativeSegwitTotalLessThanZero}
|
||||
showTaprootTotal={isTaprootTotalLessThanZero}
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
</PsbtRequestDetailsSectionLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { FiAlertCircle, FiLock } from 'react-icons/fi';
|
||||
|
||||
import { Box, Stack, Text, color } from '@stacks/ui';
|
||||
|
||||
import { Tooltip } from '@app/components/tooltip';
|
||||
import { Title } from '@app/components/typography';
|
||||
|
||||
const immutableLabel =
|
||||
'Any modification to the transaction, including the fee amount or other inputs/outputs, will invalidate the signature.';
|
||||
const uncertainLabel =
|
||||
'The transaction details can be altered by other participants. This means the final outcome of the transaction might be different than initially agreed upon.';
|
||||
|
||||
export function PsbtRequestDetailsHeader(props: { isPsbtMutable: boolean }) {
|
||||
const { isPsbtMutable } = props;
|
||||
const labelColor = isPsbtMutable ? color('feedback-alert') : color('text-caption');
|
||||
|
||||
return (
|
||||
<Stack alignItems="center" isInline spacing="tight">
|
||||
<Title fontSize={3} fontWeight={500}>
|
||||
Transaction
|
||||
</Title>
|
||||
<Tooltip
|
||||
label={isPsbtMutable ? immutableLabel : uncertainLabel}
|
||||
maxWidth="230px"
|
||||
placement="bottom"
|
||||
>
|
||||
<Stack
|
||||
alignItems="center"
|
||||
border="1px solid"
|
||||
borderColor={labelColor}
|
||||
borderRadius="24px"
|
||||
isInline
|
||||
px="tight"
|
||||
py="extra-tight"
|
||||
spacing="extra-tight"
|
||||
>
|
||||
<Box size="12px">
|
||||
{isPsbtMutable ? (
|
||||
<FiAlertCircle color={labelColor} size="12px" />
|
||||
) : (
|
||||
<FiLock color={labelColor} size="12px" />
|
||||
)}
|
||||
</Box>
|
||||
<Text color={labelColor} fontSize={0}>
|
||||
{isPsbtMutable ? 'Uncertain' : 'Certain'}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { FiArrowUp } from 'react-icons/fi';
|
||||
|
||||
import { Stack, Text } from '@stacks/ui';
|
||||
|
||||
import { SpaceBetween } from '@app/components/layout/space-between';
|
||||
|
||||
interface PsbtRequestDetailsSectionHeaderProps {
|
||||
hasDetails?: boolean;
|
||||
onSetShowDetails?(value: boolean): void;
|
||||
showDetails?: boolean;
|
||||
title: string;
|
||||
}
|
||||
export function PsbtRequestDetailsSectionHeader({
|
||||
hasDetails,
|
||||
onSetShowDetails,
|
||||
showDetails,
|
||||
title,
|
||||
}: PsbtRequestDetailsSectionHeaderProps) {
|
||||
return (
|
||||
<SpaceBetween>
|
||||
<Text fontWeight={500}>{title}</Text>
|
||||
{hasDetails && onSetShowDetails ? (
|
||||
<Stack alignItems="center" isInline spacing="extra-tight">
|
||||
<Text
|
||||
as="button"
|
||||
fontSize={2}
|
||||
fontWeight={500}
|
||||
onClick={() => onSetShowDetails(!showDetails)}
|
||||
type="button"
|
||||
>
|
||||
{showDetails ? 'See less' : 'See details'}
|
||||
</Text>
|
||||
{showDetails ? <FiArrowUp /> : <></>}
|
||||
</Stack>
|
||||
) : null}
|
||||
</SpaceBetween>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Stack, StackProps, color } from '@stacks/ui';
|
||||
|
||||
import { HasChildren } from '@app/common/has-children';
|
||||
|
||||
export function PsbtRequestDetailsSectionLayout({ children, ...props }: HasChildren & StackProps) {
|
||||
return (
|
||||
<Stack
|
||||
border="4px solid"
|
||||
borderColor={color('border')}
|
||||
borderRadius="16px"
|
||||
p="loose"
|
||||
spacing="extra-tight"
|
||||
width="100%"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Stack } from '@stacks/ui';
|
||||
|
||||
import { HasChildren } from '@app/common/has-children';
|
||||
|
||||
export function PsbtRequestDetailsLayout({ children }: HasChildren) {
|
||||
return (
|
||||
<Stack spacing="loose" width="100%">
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Stack, Text, color } from '@stacks/ui';
|
||||
|
||||
import { Money } from '@shared/models/money.model';
|
||||
|
||||
import { formatMoney, i18nFormatCurrency } from '@app/common/money/format-money';
|
||||
import { SpaceBetween } from '@app/components/layout/space-between';
|
||||
import { useCalculateBitcoinFiatValue } from '@app/query/common/market-data/market-data.hooks';
|
||||
|
||||
import { PsbtRequestDetailsSectionLayout } from './psbt-request-details-section.layout';
|
||||
|
||||
export function PsbtRequestFee(props: { fee: Money }) {
|
||||
const { fee } = props;
|
||||
const calculateBitcoinFiatValue = useCalculateBitcoinFiatValue();
|
||||
|
||||
return (
|
||||
<PsbtRequestDetailsSectionLayout>
|
||||
<SpaceBetween>
|
||||
<Text fontSize={2} fontWeight="500">
|
||||
Transaction fee
|
||||
</Text>
|
||||
<Stack alignItems="flex-end" spacing="extra-tight">
|
||||
<Text fontSize={2} fontWeight="500">
|
||||
{formatMoney(fee)}
|
||||
</Text>
|
||||
<Text color={color('text-caption')} fontSize={1}>
|
||||
{i18nFormatCurrency(calculateBitcoinFiatValue(fee))}
|
||||
</Text>
|
||||
</Stack>
|
||||
</SpaceBetween>
|
||||
</PsbtRequestDetailsSectionLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Json } from '@app/components/json';
|
||||
import { RawPsbt } from '@app/features/psbt-signer/hooks/use-psbt-signer';
|
||||
|
||||
import { PsbtRequestDetailsSectionHeader } from './psbt-request-details-section-header';
|
||||
import { PsbtRequestDetailsSectionLayout } from './psbt-request-details-section.layout';
|
||||
|
||||
export function PsbtRequestRaw({ psbt }: { psbt: RawPsbt }) {
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
return (
|
||||
<PsbtRequestDetailsSectionLayout>
|
||||
<PsbtRequestDetailsSectionHeader
|
||||
hasDetails
|
||||
onSetShowDetails={(value: boolean) => setShowDetails(value)}
|
||||
showDetails={showDetails}
|
||||
title="Raw transaction"
|
||||
/>
|
||||
{showDetails ? <Json value={psbt} /> : null}
|
||||
</PsbtRequestDetailsSectionLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import * as btc from '@scure/btc-signer';
|
||||
|
||||
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
|
||||
import { useCurrentAccountTaprootIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
|
||||
|
||||
import { useParsedPsbt } from '../../hooks/use-parsed-psbt';
|
||||
import { RawPsbt } from '../../hooks/use-psbt-signer';
|
||||
import { PsbtRequestSighashWarningLabel } from '../psbt-request-sighash-warning-label';
|
||||
import { PsbtInputsAndOutputs } from './components/psbt-inputs-and-outputs/psbt-inputs-and-outputs';
|
||||
import { PsbtInputsOutputsTotals } from './components/psbt-inputs-outputs-totals/psbt-inputs-outputs-totals';
|
||||
import { PsbtRequestDetailsHeader } from './components/psbt-request-details-header';
|
||||
import { PsbtRequestDetailsLayout } from './components/psbt-request-details.layout';
|
||||
import { PsbtRequestFee } from './components/psbt-request-fee';
|
||||
import { PsbtRequestRaw } from './components/psbt-request-raw';
|
||||
|
||||
function getPsbtTxInputs(psbtTx: btc.Transaction) {
|
||||
const inputsLength = psbtTx.inputsLength;
|
||||
const inputs: btc.TransactionInput[] = [];
|
||||
if (inputsLength === 0) return inputs;
|
||||
for (let i = 0; i < inputsLength; i++) {
|
||||
inputs.push(psbtTx.getInput(i));
|
||||
}
|
||||
return inputs;
|
||||
}
|
||||
|
||||
function getPsbtTxOutputs(psbtTx: btc.Transaction) {
|
||||
const outputsLength = psbtTx.outputsLength;
|
||||
const outputs: btc.TransactionOutput[] = [];
|
||||
if (outputsLength === 0) return outputs;
|
||||
for (let i = 0; i < outputsLength; i++) {
|
||||
outputs.push(psbtTx.getOutput(i));
|
||||
}
|
||||
return outputs;
|
||||
}
|
||||
|
||||
interface PsbtDecodedRequestProps {
|
||||
allowedSighashes?: btc.SignatureHash[];
|
||||
inputsToSign?: number | number[];
|
||||
psbtRaw: RawPsbt;
|
||||
psbtTx: btc.Transaction;
|
||||
}
|
||||
export function PsbtRequestDetails({
|
||||
allowedSighashes,
|
||||
inputsToSign,
|
||||
psbtRaw,
|
||||
psbtTx,
|
||||
}: PsbtDecodedRequestProps) {
|
||||
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
|
||||
const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner();
|
||||
const inputs = getPsbtTxInputs(psbtTx);
|
||||
const outputs = getPsbtTxOutputs(psbtTx);
|
||||
|
||||
const {
|
||||
accountInscriptionsBeingTransferred,
|
||||
accountInscriptionsBeingReceived,
|
||||
addressNativeSegwitTotal,
|
||||
addressTaprootTotal,
|
||||
fee,
|
||||
isPsbtMutable,
|
||||
psbtInputs,
|
||||
psbtOutputs,
|
||||
shouldDefaultToAdvancedView,
|
||||
} = useParsedPsbt({ allowedSighashes, inputs, inputsToSign, outputs });
|
||||
if (shouldDefaultToAdvancedView) return <PsbtRequestRaw psbt={psbtRaw} />;
|
||||
|
||||
return (
|
||||
<PsbtRequestDetailsLayout>
|
||||
{isPsbtMutable ? <PsbtRequestSighashWarningLabel /> : null}
|
||||
<PsbtRequestDetailsHeader isPsbtMutable={isPsbtMutable} />
|
||||
<PsbtInputsOutputsTotals
|
||||
accountInscriptionsBeingTransferred={accountInscriptionsBeingTransferred}
|
||||
accountInscriptionsBeingReceived={accountInscriptionsBeingReceived}
|
||||
addressNativeSegwit={nativeSegwitSigner.address}
|
||||
addressTaproot={bitcoinAddressTaproot}
|
||||
addressNativeSegwitTotal={addressNativeSegwitTotal}
|
||||
addressTaprootTotal={addressTaprootTotal}
|
||||
/>
|
||||
<PsbtInputsAndOutputs
|
||||
addressNativeSegwit={nativeSegwitSigner.address}
|
||||
addressTaproot={bitcoinAddressTaproot}
|
||||
inputs={psbtInputs}
|
||||
outputs={psbtOutputs}
|
||||
/>
|
||||
<PsbtRequestRaw psbt={psbtRaw} />
|
||||
<PsbtRequestFee fee={fee} />
|
||||
</PsbtRequestDetailsLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +1,38 @@
|
||||
import { Flex } from '@stacks/ui';
|
||||
import { Flex, Text } from '@stacks/ui';
|
||||
|
||||
import { addPortSuffix, getUrlHostname } from '@app/common/utils';
|
||||
import { Favicon } from '@app/components/favicon';
|
||||
import { Flag } from '@app/components/layout/flag';
|
||||
import { Caption, Title } from '@app/components/typography';
|
||||
import { useCurrentNetworkState } from '@app/store/networks/networks.hooks';
|
||||
|
||||
interface PsbtRequestHeaderProps {
|
||||
name?: string;
|
||||
origin: string;
|
||||
}
|
||||
export function PsbtRequestHeader({ origin }: PsbtRequestHeaderProps) {
|
||||
const caption = `${origin} is requesting you sign this PSBT`;
|
||||
export function PsbtRequestHeader({ name, origin }: PsbtRequestHeaderProps) {
|
||||
const { chain, isTestnet } = useCurrentNetworkState();
|
||||
|
||||
const originAddition = origin ? ` (${getUrlHostname(origin)})` : '';
|
||||
const testnetAddition = isTestnet
|
||||
? ` using ${getUrlHostname(chain.stacks.url)}${addPortSuffix(chain.stacks.url)}`
|
||||
: '';
|
||||
|
||||
const displayName = name ?? origin;
|
||||
|
||||
const caption = displayName
|
||||
? `Requested by ${displayName}${originAddition}${testnetAddition}`
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column" my="loose" width="100%">
|
||||
<Title fontSize={3} fontWeight={500} mb="base">
|
||||
Sign transaction
|
||||
<Title fontSize={4} fontWeight={500} mb="base">
|
||||
Approve transaction
|
||||
</Title>
|
||||
<Text lineHeight="24px" mb="base">
|
||||
Please review the recipient address, amount, and associated fees. Authorize only
|
||||
transactions you fully understand.
|
||||
</Text>
|
||||
{caption && (
|
||||
<Flag align="top" img={<Favicon origin={origin} />} pl="tight">
|
||||
<Caption wordBreak="break-word" lineHeight={1.3}>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Text } from '@stacks/ui';
|
||||
|
||||
import { WarningLabel } from '@app/components/warning-label';
|
||||
|
||||
export function PsbtRequestSighashWarningLabel() {
|
||||
return (
|
||||
<WarningLabel title="Be careful with this transaction" width="100%">
|
||||
The details you see here are not guaranteed. Be sure to fully trust your counterparty, who can
|
||||
later modify this transaction to send or receive other assets from your account, and possibly
|
||||
even drain it.
|
||||
<Text display="inline" pl="extra-tight" textDecoration="underline">
|
||||
Learn more↗
|
||||
</Text>
|
||||
</WarningLabel>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { WarningLabel } from '@app/components/warning-label';
|
||||
|
||||
export function PsbtRequestAppWarningLabel(props: { appName?: string }) {
|
||||
const { appName } = props;
|
||||
const title = `Do not proceed unless you trust ${appName ?? 'Unknown'}!`;
|
||||
|
||||
return (
|
||||
<WarningLabel title={title} width="100%">
|
||||
Signing this PSBT can have dangerous side effects. Only sign if the PSBT is from a site you
|
||||
trust.
|
||||
</WarningLabel>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Stack } from '@stacks/ui';
|
||||
|
||||
interface PsbtRequestLayoutProps {
|
||||
interface PsbtSignerLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
export function PsbtRequestLayout({ children }: PsbtRequestLayoutProps) {
|
||||
export function PsbtSignerLayout({ children }: PsbtSignerLayoutProps) {
|
||||
return (
|
||||
<Stack
|
||||
alignItems="center"
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
mockInscriptions1,
|
||||
mockInscriptions2,
|
||||
mockPsbtInputs1,
|
||||
mockPsbtInputs2,
|
||||
mockPsbtOutputs1,
|
||||
mockPsbtOutputs2,
|
||||
} from '@tests/mocks/mock-psbts';
|
||||
|
||||
import { findOutputsReceivingInscriptions } from './find-outputs-receiving-inscriptions';
|
||||
|
||||
describe('find outputs receiving inscriptions', () => {
|
||||
test('that the correct output receives the inscription (scenario 1)', () => {
|
||||
const outputsReceivingInscriptions = findOutputsReceivingInscriptions({
|
||||
inscriptions: mockInscriptions1,
|
||||
psbtInputs: mockPsbtInputs1,
|
||||
psbtOutputs: mockPsbtOutputs1,
|
||||
});
|
||||
expect(outputsReceivingInscriptions[0]).toEqual({
|
||||
address: 'bc1p9pnzvq52956jht5deha82qp96pxw0a0tvey6fhdea7vwhf33tarskqq3nr',
|
||||
inscription:
|
||||
'/inscription/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0',
|
||||
});
|
||||
});
|
||||
|
||||
test('that the correct output receives the inscription (scenario 2)', () => {
|
||||
const outputsReceivingInscriptions = findOutputsReceivingInscriptions({
|
||||
inscriptions: mockInscriptions2,
|
||||
psbtInputs: mockPsbtInputs2,
|
||||
psbtOutputs: mockPsbtOutputs2,
|
||||
});
|
||||
expect(outputsReceivingInscriptions[0]).toEqual({
|
||||
address: 'bc1p9pnzvq52956jht5deha82qp96pxw0a0tvey6fhdea7vwhf33tarskqq3nr',
|
||||
inscription:
|
||||
'/inscription/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
import { Inscription } from '@shared/models/inscription.model';
|
||||
import { isDefined } from '@shared/utils';
|
||||
|
||||
import { PsbtInput } from './use-parsed-inputs';
|
||||
import { PsbtOutput } from './use-parsed-outputs';
|
||||
|
||||
interface FindOutputsReceivingInscriptionsArgs {
|
||||
inscriptions: Inscription[];
|
||||
psbtInputs: PsbtInput[];
|
||||
psbtOutputs: PsbtOutput[];
|
||||
}
|
||||
export function findOutputsReceivingInscriptions({
|
||||
inscriptions,
|
||||
psbtInputs,
|
||||
psbtOutputs,
|
||||
}: FindOutputsReceivingInscriptionsArgs) {
|
||||
let inputsSatsTotal = new BigNumber(0);
|
||||
|
||||
return psbtInputs
|
||||
.flatMap(input => {
|
||||
if (input.inscription) {
|
||||
const inscription = inscriptions.find(inscription =>
|
||||
input.inscription?.includes(inscription.id)
|
||||
);
|
||||
|
||||
// Offset is zero indexed, so 1 is added here to match the sats total
|
||||
const inscriptionTotalOffset = inputsSatsTotal.plus(Number(inscription?.offset) + 1);
|
||||
|
||||
let outputsSatsTotal = new BigNumber(0);
|
||||
|
||||
for (let output = 0; output < psbtOutputs.length; output++) {
|
||||
outputsSatsTotal = outputsSatsTotal.plus(psbtOutputs[output].value);
|
||||
if (inscriptionTotalOffset.isLessThanOrEqualTo(outputsSatsTotal))
|
||||
return { address: psbtOutputs[output].address, inscription: input.inscription };
|
||||
}
|
||||
}
|
||||
inputsSatsTotal = inputsSatsTotal.plus(input.value);
|
||||
return;
|
||||
})
|
||||
.filter(isDefined);
|
||||
}
|
||||
107
src/app/features/psbt-signer/hooks/use-parsed-inputs.tsx
Normal file
107
src/app/features/psbt-signer/hooks/use-parsed-inputs.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import * as btc from '@scure/btc-signer';
|
||||
import { bytesToHex } from '@stacks/common';
|
||||
|
||||
import {
|
||||
BtcSignerNetwork,
|
||||
getBtcSignerLibNetworkConfigByMode,
|
||||
} from '@shared/crypto/bitcoin/bitcoin.network';
|
||||
import { getAddressFromOutScript } from '@shared/crypto/bitcoin/bitcoin.utils';
|
||||
import { ensureArray, isDefined, isUndefined } from '@shared/utils';
|
||||
|
||||
import { useOrdinalsAwareUtxoQueries } from '@app/query/bitcoin/ordinals/ordinals-aware-utxo.query';
|
||||
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
|
||||
import { useCurrentAccountTaprootIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
|
||||
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
|
||||
|
||||
export interface PsbtInput {
|
||||
address: string;
|
||||
index?: number;
|
||||
inscription?: string;
|
||||
mutable: boolean;
|
||||
sign: boolean;
|
||||
txid: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
function getInputAddress(
|
||||
index: number,
|
||||
input: btc.TransactionInput,
|
||||
bitcoinNetwork: BtcSignerNetwork
|
||||
) {
|
||||
if (isDefined(input.witnessUtxo))
|
||||
return getAddressFromOutScript(input.witnessUtxo.script, bitcoinNetwork);
|
||||
if (isDefined(input.nonWitnessUtxo))
|
||||
return getAddressFromOutScript(input.nonWitnessUtxo.outputs[index]?.script, bitcoinNetwork);
|
||||
return '';
|
||||
}
|
||||
|
||||
function getInputValue(index: number, input: btc.TransactionInput) {
|
||||
if (isDefined(input.witnessUtxo)) return Number(input.witnessUtxo.amount);
|
||||
if (isDefined(input.nonWitnessUtxo)) return Number(input.nonWitnessUtxo.outputs[index]?.amount);
|
||||
return 0;
|
||||
}
|
||||
|
||||
interface UseParsedInputsArgs {
|
||||
allowedSighashes?: btc.SignatureHash[];
|
||||
inputs: btc.TransactionInput[];
|
||||
inputsToSign?: number | number[];
|
||||
}
|
||||
export function useParsedInputs({ allowedSighashes, inputs, inputsToSign }: UseParsedInputsArgs) {
|
||||
const network = useCurrentNetwork();
|
||||
const bitcoinNetwork = getBtcSignerLibNetworkConfigByMode(network.chain.bitcoin.network);
|
||||
const bitcoinAddressNativeSegwit = useCurrentAccountNativeSegwitIndexZeroSigner().address;
|
||||
const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner();
|
||||
const utxosWithInscriptions = useOrdinalsAwareUtxoQueries(inputs).map(query => query.data);
|
||||
const signAll = isUndefined(inputsToSign);
|
||||
|
||||
const psbtInputs = useMemo(
|
||||
() =>
|
||||
inputs.map((input, i) => {
|
||||
const inputAddress = isDefined(input.index)
|
||||
? getInputAddress(input.index, input, bitcoinNetwork)
|
||||
: '';
|
||||
const isCurrentAddress =
|
||||
inputAddress === bitcoinAddressNativeSegwit || inputAddress === bitcoinAddressTaproot;
|
||||
// Flags when not signing ALL inputs/outputs (NONE, SINGLE, and ANYONECANPAY)
|
||||
const canChange =
|
||||
isCurrentAddress &&
|
||||
!(!input.sighashType || input.sighashType === 0 || input.sighashType === 1);
|
||||
// Checks if the sighashType is allowed by the PSBT
|
||||
const isAllowedToSign =
|
||||
isUndefined(allowedSighashes) ||
|
||||
ensureArray(allowedSighashes).some(
|
||||
type => !input.sighashType || type === input.sighashType
|
||||
);
|
||||
// Should we check the sighashType here before it gets to the signing lib?
|
||||
const toSignAll = isCurrentAddress && signAll;
|
||||
const toSignIndex =
|
||||
isCurrentAddress && ensureArray(inputsToSign).some(inputIndex => inputIndex === i);
|
||||
|
||||
return {
|
||||
address: inputAddress,
|
||||
index: input.index,
|
||||
inscription: utxosWithInscriptions[i]?.inscriptions,
|
||||
mutable: canChange,
|
||||
sign: isAllowedToSign && (toSignAll || toSignIndex),
|
||||
txid: input.txid ? bytesToHex(input.txid) : '',
|
||||
value: isDefined(input.index) ? getInputValue(input.index, input) : 0,
|
||||
};
|
||||
}),
|
||||
[
|
||||
allowedSighashes,
|
||||
bitcoinAddressNativeSegwit,
|
||||
bitcoinAddressTaproot,
|
||||
bitcoinNetwork,
|
||||
inputs,
|
||||
inputsToSign,
|
||||
signAll,
|
||||
utxosWithInscriptions,
|
||||
]
|
||||
);
|
||||
|
||||
const isPsbtMutable = useMemo(() => psbtInputs.some(input => input.mutable), [psbtInputs]);
|
||||
|
||||
return { isPsbtMutable, parsedInputs: psbtInputs };
|
||||
}
|
||||
54
src/app/features/psbt-signer/hooks/use-parsed-outputs.tsx
Normal file
54
src/app/features/psbt-signer/hooks/use-parsed-outputs.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import * as btc from '@scure/btc-signer';
|
||||
|
||||
import { NetworkConfiguration } from '@shared/constants';
|
||||
import { getBtcSignerLibNetworkConfigByMode } from '@shared/crypto/bitcoin/bitcoin.network';
|
||||
import { getAddressFromOutScript } from '@shared/crypto/bitcoin/bitcoin.utils';
|
||||
import { logger } from '@shared/logger';
|
||||
import { isDefined, isUndefined } from '@shared/utils';
|
||||
|
||||
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
|
||||
import { useCurrentAccountTaprootIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
|
||||
|
||||
export interface PsbtOutput {
|
||||
address: string;
|
||||
mutable: boolean;
|
||||
sign: boolean;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface UseParsedOutputsArgs {
|
||||
isPsbtMutable: boolean;
|
||||
outputs: btc.TransactionOutput[];
|
||||
network: NetworkConfiguration;
|
||||
}
|
||||
export function useParsedOutputs({ isPsbtMutable, outputs, network }: UseParsedOutputsArgs) {
|
||||
const bitcoinAddressNativeSegwit = useCurrentAccountNativeSegwitIndexZeroSigner().address;
|
||||
const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner();
|
||||
const bitcoinNetwork = getBtcSignerLibNetworkConfigByMode(network.chain.bitcoin.network);
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
outputs
|
||||
.map(output => {
|
||||
if (isUndefined(output.script)) {
|
||||
logger.error('Output has no script');
|
||||
return;
|
||||
}
|
||||
const outputAddress = getAddressFromOutScript(output.script, bitcoinNetwork);
|
||||
|
||||
const isCurrentAddress =
|
||||
outputAddress === bitcoinAddressNativeSegwit || outputAddress === bitcoinAddressTaproot;
|
||||
|
||||
return {
|
||||
address: outputAddress,
|
||||
mutable: isPsbtMutable,
|
||||
sign: isCurrentAddress,
|
||||
value: Number(output.amount),
|
||||
};
|
||||
})
|
||||
.filter(isDefined),
|
||||
[bitcoinAddressNativeSegwit, bitcoinAddressTaproot, bitcoinNetwork, isPsbtMutable, outputs]
|
||||
);
|
||||
}
|
||||
71
src/app/features/psbt-signer/hooks/use-parsed-psbt.tsx
Normal file
71
src/app/features/psbt-signer/hooks/use-parsed-psbt.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import * as btc from '@scure/btc-signer';
|
||||
|
||||
import { subtractMoney } from '@app/common/money/calculate-money';
|
||||
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
|
||||
import { useCurrentAccountTaprootIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
|
||||
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
|
||||
|
||||
import { useParsedInputs } from './use-parsed-inputs';
|
||||
import { useParsedOutputs } from './use-parsed-outputs';
|
||||
import { usePsbtInscriptions } from './use-psbt-inscriptions';
|
||||
import { usePsbtTotals } from './use-psbt-totals';
|
||||
|
||||
interface UseParsedPsbtArgs {
|
||||
allowedSighashes?: btc.SignatureHash[];
|
||||
inputs: btc.TransactionInput[];
|
||||
inputsToSign?: number | number[];
|
||||
outputs: btc.TransactionOutput[];
|
||||
}
|
||||
export function useParsedPsbt({
|
||||
allowedSighashes,
|
||||
inputs,
|
||||
inputsToSign,
|
||||
outputs,
|
||||
}: UseParsedPsbtArgs) {
|
||||
const network = useCurrentNetwork();
|
||||
const bitcoinAddressNativeSegwit = useCurrentAccountNativeSegwitIndexZeroSigner().address;
|
||||
const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner();
|
||||
const { isPsbtMutable, parsedInputs } = useParsedInputs({
|
||||
allowedSighashes,
|
||||
inputs,
|
||||
inputsToSign,
|
||||
});
|
||||
const parsedOutputs = useParsedOutputs({ isPsbtMutable, outputs, network });
|
||||
|
||||
const {
|
||||
inputsTotalNativeSegwit,
|
||||
inputsTotalTaproot,
|
||||
outputsTotalNativeSegwit,
|
||||
outputsTotalTaproot,
|
||||
psbtInputsTotal,
|
||||
psbtOutputsTotal,
|
||||
} = usePsbtTotals({
|
||||
bitcoinAddressNativeSegwit,
|
||||
bitcoinAddressTaproot,
|
||||
parsedInputs,
|
||||
parsedOutputs,
|
||||
});
|
||||
|
||||
const { accountInscriptionsBeingTransferred, accountInscriptionsBeingReceived } =
|
||||
usePsbtInscriptions(parsedInputs, parsedOutputs);
|
||||
|
||||
const defaultToAdvancedView = useCallback(() => {
|
||||
const noInputs = !inputs.length;
|
||||
const noOutputs = !outputs.length;
|
||||
return noInputs || noOutputs;
|
||||
}, [inputs.length, outputs.length]);
|
||||
|
||||
return {
|
||||
accountInscriptionsBeingTransferred,
|
||||
accountInscriptionsBeingReceived,
|
||||
addressNativeSegwitTotal: subtractMoney(inputsTotalNativeSegwit, outputsTotalNativeSegwit),
|
||||
addressTaprootTotal: subtractMoney(inputsTotalTaproot, outputsTotalTaproot),
|
||||
fee: subtractMoney(psbtInputsTotal, psbtOutputsTotal),
|
||||
isPsbtMutable,
|
||||
psbtInputs: parsedInputs,
|
||||
psbtOutputs: parsedOutputs,
|
||||
shouldDefaultToAdvancedView: defaultToAdvancedView(),
|
||||
};
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
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 { 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,
|
||||
network: BitcoinNetworkModes,
|
||||
unsignedOutputs: btc.TransactionOutputRequired[],
|
||||
unsignedUtxos: PsbtDecodedUtxosMainnet | PsbtDecodedUtxosTestnet
|
||||
) {
|
||||
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 utxosNotFromCurrentAddress.length === 0 && outputsNotToCurrentAddress.length === 0;
|
||||
}
|
||||
|
||||
interface UsePsbtDecodedRequestArgs {
|
||||
unsignedInputs: btc.TransactionInputRequired[];
|
||||
unsignedOutputs: btc.TransactionOutputRequired[];
|
||||
}
|
||||
export function usePsbtDecodedRequest({
|
||||
unsignedInputs,
|
||||
unsignedOutputs,
|
||||
}: UsePsbtDecodedRequestArgs) {
|
||||
const [showAdvancedView, setShowAdvancedView] = useState(false);
|
||||
const network = useCurrentNetwork();
|
||||
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
|
||||
const unsignedUtxos = usePsbtDecodedUtxos(unsignedInputs);
|
||||
|
||||
const defaultToAdvancedView = useCallback(() => {
|
||||
const noInputs = !unsignedInputs.length;
|
||||
const noOutputs = !unsignedOutputs.length;
|
||||
return noInputs || noOutputs;
|
||||
}, [unsignedInputs.length, unsignedOutputs.length]);
|
||||
|
||||
const showPlaceholder = useCallback(() => {
|
||||
return isPlaceholderTransaction(
|
||||
nativeSegwitSigner.address,
|
||||
network.chain.bitcoin.network,
|
||||
unsignedOutputs,
|
||||
unsignedUtxos
|
||||
);
|
||||
}, [nativeSegwitSigner.address, network.chain.bitcoin.network, unsignedOutputs, unsignedUtxos]);
|
||||
|
||||
return {
|
||||
onSetShowAdvancedView: () => setShowAdvancedView(!showAdvancedView),
|
||||
shouldDefaultToAdvancedView: defaultToAdvancedView(),
|
||||
shouldShowPlaceholder: showPlaceholder(),
|
||||
showAdvancedView,
|
||||
unsignedUtxos,
|
||||
};
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
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 };
|
||||
}
|
||||
52
src/app/features/psbt-signer/hooks/use-psbt-inscriptions.tsx
Normal file
52
src/app/features/psbt-signer/hooks/use-psbt-inscriptions.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { isDefined } from '@shared/utils';
|
||||
|
||||
import { useGetInscriptionQueries } from '@app/query/bitcoin/ordinals/inscription.query';
|
||||
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
|
||||
import { useCurrentAccountTaprootIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
|
||||
|
||||
import { findOutputsReceivingInscriptions } from './find-outputs-receiving-inscriptions';
|
||||
import { PsbtInput } from './use-parsed-inputs';
|
||||
import { PsbtOutput } from './use-parsed-outputs';
|
||||
|
||||
export function usePsbtInscriptions(psbtInputs: PsbtInput[], psbtOutputs: PsbtOutput[]) {
|
||||
const bitcoinAddressNativeSegwit = useCurrentAccountNativeSegwitIndexZeroSigner().address;
|
||||
const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner();
|
||||
const inscriptions = useGetInscriptionQueries(
|
||||
psbtInputs.map(utxo => utxo.inscription?.replace('/inscription/', '') ?? '')
|
||||
)
|
||||
.map(query => query.data)
|
||||
.filter(isDefined);
|
||||
|
||||
const outputsReceivingInscriptions = useMemo(
|
||||
() =>
|
||||
findOutputsReceivingInscriptions({
|
||||
inscriptions,
|
||||
psbtInputs,
|
||||
psbtOutputs,
|
||||
}),
|
||||
[inscriptions, psbtInputs, psbtOutputs]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
accountInscriptionsBeingTransferred: psbtInputs
|
||||
.filter(
|
||||
input =>
|
||||
input.address === bitcoinAddressNativeSegwit || input.address === bitcoinAddressTaproot
|
||||
)
|
||||
.map(input => input.inscription)
|
||||
.filter(isDefined),
|
||||
accountInscriptionsBeingReceived: outputsReceivingInscriptions
|
||||
.filter(
|
||||
outputWithInscription =>
|
||||
outputWithInscription.address === bitcoinAddressNativeSegwit ||
|
||||
outputWithInscription.address === bitcoinAddressTaproot
|
||||
)
|
||||
.map(input => input.inscription)
|
||||
.filter(isDefined),
|
||||
}),
|
||||
[bitcoinAddressNativeSegwit, bitcoinAddressTaproot, outputsReceivingInscriptions, psbtInputs]
|
||||
);
|
||||
}
|
||||
@@ -9,12 +9,9 @@ import { isString } from '@shared/utils';
|
||||
import { useCurrentAccountNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
|
||||
import { useCurrentAccountTaprootSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
|
||||
|
||||
import { useSignPsbtError } from './use-sign-psbt-error';
|
||||
|
||||
export type DecodedPsbt = ReturnType<typeof btc.RawPSBTV0.decode>;
|
||||
export type RawPsbt = ReturnType<typeof btc.RawPSBTV0.decode>;
|
||||
|
||||
export function usePsbtSigner() {
|
||||
const signPsbtError = useSignPsbtError();
|
||||
const createNativeSegwitSigner = useCurrentAccountNativeSegwitSigner();
|
||||
const createTaprootSigner = useCurrentAccountTaprootSigner();
|
||||
|
||||
@@ -30,7 +27,7 @@ export function usePsbtSigner() {
|
||||
try {
|
||||
taprootSigner?.signIndex(tx, idx, allowedSighash);
|
||||
} catch (e2) {
|
||||
signPsbtError(`Error signing PSBT at provided index, ${e1}, ${e2}`);
|
||||
throw new Error(`Unable to sign PSBT at provided index, ${e1 ?? e2}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -41,9 +38,10 @@ export function usePsbtSigner() {
|
||||
try {
|
||||
taprootSigner?.sign(tx);
|
||||
} catch (e2) {
|
||||
signPsbtError(`Error signing PSBT, ${e1}, ${e2}`);
|
||||
throw new Error(`Unable to sign PSBT, ${e1 ?? e2}`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
},
|
||||
getPsbtAsTransaction(psbt: string | Uint8Array) {
|
||||
const bytes = isString(psbt) ? hexToBytes(psbt) : psbt;
|
||||
@@ -53,17 +51,16 @@ export function usePsbtSigner() {
|
||||
const bytes = isString(psbt) ? hexToBytes(psbt) : psbt;
|
||||
try {
|
||||
return btc.RawPSBTV0.decode(bytes);
|
||||
} catch (e0) {
|
||||
logger.error(`Failed to decode as PSBT v0, trying v2, ${e0}`);
|
||||
} catch (e1) {
|
||||
logger.error(`Unable to decode PSBT as v0, trying v2, ${e1}`);
|
||||
try {
|
||||
return btc.RawPSBTV2.decode(bytes);
|
||||
} catch (e2) {
|
||||
signPsbtError(`Failed to decode PSBT as v0 and v2, ${e0}, ${e2}`);
|
||||
throw new Error(`Unable to decode PSBT, ${e1 ?? e2}`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
},
|
||||
}),
|
||||
[nativeSegwitSigner, signPsbtError, taprootSigner]
|
||||
[nativeSegwitSigner, taprootSigner]
|
||||
);
|
||||
}
|
||||
|
||||
63
src/app/features/psbt-signer/hooks/use-psbt-totals.tsx
Normal file
63
src/app/features/psbt-signer/hooks/use-psbt-totals.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createMoney } from '@shared/models/money.model';
|
||||
|
||||
import { sumNumbers } from '@app/common/math/helpers';
|
||||
|
||||
import { PsbtInput } from './use-parsed-inputs';
|
||||
import { PsbtOutput } from './use-parsed-outputs';
|
||||
|
||||
function calculateAddressInputsTotal(address: string, inputs: PsbtInput[]) {
|
||||
return createMoney(
|
||||
sumNumbers(inputs.filter(input => input.address === address).map(input => input.value)),
|
||||
'BTC'
|
||||
);
|
||||
}
|
||||
|
||||
function calculateAddressOutputsTotal(address: string, outputs: PsbtOutput[]) {
|
||||
return createMoney(
|
||||
sumNumbers(
|
||||
outputs.filter(output => output.address === address).map(output => Number(output.value))
|
||||
),
|
||||
'BTC'
|
||||
);
|
||||
}
|
||||
|
||||
function calculatePsbtInputsTotal(inputs: PsbtInput[]) {
|
||||
return createMoney(sumNumbers(inputs.map(input => input.value)), 'BTC');
|
||||
}
|
||||
|
||||
function calculatePsbtOutputsTotal(outputs: PsbtOutput[]) {
|
||||
return createMoney(sumNumbers(outputs.map(output => output.value)), 'BTC');
|
||||
}
|
||||
|
||||
interface UsePsbtTotalsProps {
|
||||
bitcoinAddressNativeSegwit: string;
|
||||
bitcoinAddressTaproot: string;
|
||||
parsedInputs: PsbtInput[];
|
||||
parsedOutputs: PsbtOutput[];
|
||||
}
|
||||
export function usePsbtTotals({
|
||||
bitcoinAddressNativeSegwit,
|
||||
bitcoinAddressTaproot,
|
||||
parsedInputs,
|
||||
parsedOutputs,
|
||||
}: UsePsbtTotalsProps) {
|
||||
return useMemo(
|
||||
() => ({
|
||||
inputsTotalNativeSegwit: calculateAddressInputsTotal(
|
||||
bitcoinAddressNativeSegwit,
|
||||
parsedInputs
|
||||
),
|
||||
inputsTotalTaproot: calculateAddressInputsTotal(bitcoinAddressTaproot, parsedInputs),
|
||||
outputsTotalNativeSegwit: calculateAddressOutputsTotal(
|
||||
bitcoinAddressNativeSegwit,
|
||||
parsedOutputs
|
||||
),
|
||||
outputsTotalTaproot: calculateAddressOutputsTotal(bitcoinAddressTaproot, parsedOutputs),
|
||||
psbtInputsTotal: calculatePsbtInputsTotal(parsedInputs),
|
||||
psbtOutputsTotal: calculatePsbtOutputsTotal(parsedOutputs),
|
||||
}),
|
||||
[bitcoinAddressNativeSegwit, bitcoinAddressTaproot, parsedInputs, parsedOutputs]
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { RpcErrorCode } from '@btckit/types';
|
||||
|
||||
import { finalizePsbt } from '@shared/actions/finalize-psbt';
|
||||
import { makeRpcErrorResponse } from '@shared/rpc/rpc-methods';
|
||||
|
||||
import {
|
||||
usePsbtRequestSearchParams,
|
||||
useRpcSignPsbtParams,
|
||||
} from '@app/common/psbt/use-psbt-request-params';
|
||||
|
||||
export function useSignPsbtError() {
|
||||
const { requestToken, tabId } = usePsbtRequestSearchParams();
|
||||
const { requestId, tabId: rpcTabId } = useRpcSignPsbtParams();
|
||||
|
||||
return useCallback(
|
||||
(errorMsg: string) => {
|
||||
if (requestToken)
|
||||
finalizePsbt({
|
||||
requestPayload: requestToken,
|
||||
tabId,
|
||||
data: errorMsg,
|
||||
});
|
||||
chrome.tabs.sendMessage(
|
||||
rpcTabId,
|
||||
makeRpcErrorResponse('signPsbt', {
|
||||
id: requestId,
|
||||
error: {
|
||||
message: errorMsg,
|
||||
code: RpcErrorCode.INTERNAL_ERROR,
|
||||
},
|
||||
})
|
||||
);
|
||||
window.close();
|
||||
},
|
||||
[requestId, requestToken, rpcTabId, tabId]
|
||||
);
|
||||
}
|
||||
@@ -1,22 +1,28 @@
|
||||
import * as btc from '@scure/btc-signer';
|
||||
|
||||
import { useRouteHeader } from '@app/common/hooks/use-route-header';
|
||||
import { PopupHeader } from '@app/features/current-account/popup-header';
|
||||
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 { PsbtRequestDetails } from './components/psbt-request-details/psbt-request-details';
|
||||
import { PsbtRequestHeader } from './components/psbt-request-header';
|
||||
import { PsbtRequestAppWarningLabel } from './components/psbt-request-warning-label';
|
||||
import { PsbtRequestLayout } from './components/psbt-request.layout';
|
||||
import { DecodedPsbt } from './hooks/use-psbt-signer';
|
||||
import { PsbtSignerLayout } from './components/psbt-signer.layout';
|
||||
import { RawPsbt } from './hooks/use-psbt-signer';
|
||||
|
||||
interface PsbtSignerProps {
|
||||
appName: string;
|
||||
psbt: DecodedPsbt;
|
||||
allowedSighashes?: btc.SignatureHash[];
|
||||
inputsToSign?: number | number[];
|
||||
name?: string;
|
||||
origin: string;
|
||||
onCancel(): void;
|
||||
onSignPsbt(): void;
|
||||
psbtRaw: RawPsbt;
|
||||
psbtTx: btc.Transaction;
|
||||
}
|
||||
export function PsbtSigner(props: PsbtSignerProps) {
|
||||
const { appName, psbt, onCancel, onSignPsbt } = props;
|
||||
const { allowedSighashes, inputsToSign, name, origin, onCancel, onSignPsbt, psbtRaw, psbtTx } =
|
||||
props;
|
||||
|
||||
useRouteHeader(<PopupHeader displayAddresssBalanceOf="all" />);
|
||||
|
||||
@@ -24,11 +30,15 @@ export function PsbtSigner(props: PsbtSignerProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PsbtRequestLayout>
|
||||
<PsbtRequestHeader origin={appName} />
|
||||
<PsbtRequestAppWarningLabel appName={appName} />
|
||||
<PsbtDecodedRequest psbt={psbt} />
|
||||
</PsbtRequestLayout>
|
||||
<PsbtSignerLayout>
|
||||
<PsbtRequestHeader name={name} origin={origin} />
|
||||
<PsbtRequestDetails
|
||||
allowedSighashes={allowedSighashes}
|
||||
inputsToSign={inputsToSign}
|
||||
psbtRaw={psbtRaw}
|
||||
psbtTx={psbtTx}
|
||||
/>
|
||||
</PsbtSignerLayout>
|
||||
<PsbtRequestActions isLoading={false} onCancel={onCancel} onSignPsbt={onSignPsbt} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -4,16 +4,29 @@ import { PsbtSigner } from '@app/features/psbt-signer/psbt-signer';
|
||||
import { usePsbtRequest } from './use-psbt-request';
|
||||
|
||||
export function PsbtRequest() {
|
||||
const { appName, isLoading, decodedPsbt, onSignPsbt, onDenyPsbtSigning } = usePsbtRequest();
|
||||
const {
|
||||
appName,
|
||||
isLoading,
|
||||
decodedPsbt,
|
||||
onSignPsbt,
|
||||
onDenyPsbtSigning,
|
||||
origin,
|
||||
psbtPayload,
|
||||
tx,
|
||||
} = usePsbtRequest();
|
||||
|
||||
if (isLoading || !decodedPsbt) return <LoadingSpinner height="600px" />;
|
||||
|
||||
return (
|
||||
<PsbtSigner
|
||||
appName={appName ?? ''}
|
||||
psbt={decodedPsbt}
|
||||
allowedSighashes={psbtPayload.allowedSighash}
|
||||
name={appName ?? ''}
|
||||
inputsToSign={psbtPayload.signAtIndex}
|
||||
origin={origin ?? ''}
|
||||
onSignPsbt={onSignPsbt}
|
||||
onCancel={onDenyPsbtSigning}
|
||||
psbtRaw={decodedPsbt}
|
||||
psbtTx={tx}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
||||
|
||||
import { finalizePsbt } from '@shared/actions/finalize-psbt';
|
||||
import { RouteUrls } from '@shared/route-urls';
|
||||
import { ensureArray, isUndefined } from '@shared/utils';
|
||||
|
||||
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
|
||||
@@ -11,10 +13,14 @@ import { usePsbtRequestSearchParams } from '@app/common/psbt/use-psbt-request-pa
|
||||
import { usePsbtSigner } from '@app/features/psbt-signer/hooks/use-psbt-signer';
|
||||
|
||||
export function usePsbtRequest() {
|
||||
const { requestToken, tabId } = usePsbtRequestSearchParams();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { origin, requestToken, tabId } = usePsbtRequestSearchParams();
|
||||
|
||||
const { signPsbt, signPsbtAtIndex, getDecodedPsbt, getPsbtAsTransaction } = usePsbtSigner();
|
||||
|
||||
const analytics = useAnalytics();
|
||||
|
||||
return useMemo(() => {
|
||||
if (!requestToken) throw new Error('Cannot decode psbt without request token');
|
||||
|
||||
@@ -25,16 +31,27 @@ export function usePsbtRequest() {
|
||||
return {
|
||||
appName,
|
||||
isLoading,
|
||||
psbtPayload: payload,
|
||||
getDecodedPsbt,
|
||||
origin,
|
||||
psbtPayload: payload,
|
||||
get decodedPsbt() {
|
||||
return getDecodedPsbt(payload.hex);
|
||||
try {
|
||||
return getDecodedPsbt(payload.hex);
|
||||
} catch (e) {
|
||||
return navigate(RouteUrls.RequestError, {
|
||||
state: { message: e instanceof Error ? e.message : '', title: 'Failed request' },
|
||||
});
|
||||
}
|
||||
},
|
||||
tx: getPsbtAsTransaction(payload.hex),
|
||||
payloadTxBytes,
|
||||
onDenyPsbtSigning() {
|
||||
void analytics.track('request_psbt_cancel');
|
||||
finalizePsbt({ requestPayload: requestToken, tabId, data: 'PSBT request was canceled' });
|
||||
finalizePsbt({
|
||||
data: 'PSBT request was canceled',
|
||||
requestPayload: requestToken,
|
||||
tabId,
|
||||
});
|
||||
},
|
||||
onSignPsbt() {
|
||||
setIsLoading(true);
|
||||
@@ -45,22 +62,26 @@ export function usePsbtRequest() {
|
||||
const indexOrIndexes = payload?.signAtIndex;
|
||||
const allowedSighash = payload?.allowedSighash;
|
||||
|
||||
if (!isUndefined(indexOrIndexes)) {
|
||||
ensureArray(indexOrIndexes).forEach(idx => signPsbtAtIndex(idx, tx, allowedSighash));
|
||||
} else {
|
||||
signPsbt(tx);
|
||||
try {
|
||||
if (!isUndefined(indexOrIndexes)) {
|
||||
ensureArray(indexOrIndexes).forEach(idx => signPsbtAtIndex(idx, tx, allowedSighash));
|
||||
} else {
|
||||
signPsbt(tx);
|
||||
}
|
||||
} catch (e) {
|
||||
return navigate(RouteUrls.RequestError, {
|
||||
state: { message: e instanceof Error ? e.message : '', title: 'Failed to sign' },
|
||||
});
|
||||
}
|
||||
|
||||
const psbt = tx.toPSBT();
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
if (!requestToken) return;
|
||||
|
||||
finalizePsbt({
|
||||
data: { hex: bytesToHex(psbt) },
|
||||
requestPayload: requestToken,
|
||||
tabId,
|
||||
data: { hex: bytesToHex(psbt) },
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -69,6 +90,8 @@ export function usePsbtRequest() {
|
||||
getDecodedPsbt,
|
||||
getPsbtAsTransaction,
|
||||
isLoading,
|
||||
navigate,
|
||||
origin,
|
||||
requestToken,
|
||||
signPsbt,
|
||||
signPsbtAtIndex,
|
||||
|
||||
26
src/app/pages/request-error/request-error.tsx
Normal file
26
src/app/pages/request-error/request-error.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { Box, Text } from '@stacks/ui';
|
||||
import get from 'lodash.get';
|
||||
|
||||
import { GenericError } from '@app/components/generic-error/generic-error';
|
||||
|
||||
const helpTextList = [
|
||||
<Box as="li" mt="base" key={1}>
|
||||
<Text>Please report issue to requesting app</Text>
|
||||
</Box>,
|
||||
];
|
||||
|
||||
function useRequestErrorState() {
|
||||
const location = useLocation();
|
||||
const message = get(location.state, 'message') as string;
|
||||
const title = get(location.state, 'title') as string;
|
||||
|
||||
return { message, title };
|
||||
}
|
||||
|
||||
export function RequestError() {
|
||||
const { message, title } = useRequestErrorState();
|
||||
|
||||
return <GenericError body={message} helpTextList={helpTextList} title={title} />;
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { RpcErrorCode } from '@btckit/types';
|
||||
import { bytesToHex } from '@noble/hashes/utils';
|
||||
|
||||
import { RouteUrls } from '@shared/route-urls';
|
||||
import { makeRpcErrorResponse, makeRpcSuccessResponse } from '@shared/rpc/rpc-methods';
|
||||
import { isDefined } from '@shared/utils';
|
||||
|
||||
@@ -9,15 +12,27 @@ import { usePsbtSigner } from '@app/features/psbt-signer/hooks/use-psbt-signer';
|
||||
import { PsbtSigner } from '@app/features/psbt-signer/psbt-signer';
|
||||
|
||||
function useRpcSignPsbt() {
|
||||
const navigate = useNavigate();
|
||||
const { origin, tabId, requestId, psbtHex, allowedSighash, signAtIndex } = useRpcSignPsbtParams();
|
||||
const { signPsbt, signPsbtAtIndex, getDecodedPsbt, getPsbtAsTransaction } = usePsbtSigner();
|
||||
|
||||
if (!requestId || !psbtHex || !origin) throw new Error('Invalid params');
|
||||
|
||||
const tx = getPsbtAsTransaction(psbtHex);
|
||||
|
||||
return {
|
||||
allowedSighashes: allowedSighash,
|
||||
inputsToSign: signAtIndex,
|
||||
origin,
|
||||
tx,
|
||||
get decodedPsbt() {
|
||||
return getDecodedPsbt(psbtHex);
|
||||
try {
|
||||
return getDecodedPsbt(psbtHex);
|
||||
} catch (e) {
|
||||
return navigate(RouteUrls.RequestError, {
|
||||
state: { message: e instanceof Error ? e.message : '', title: 'Failed request' },
|
||||
});
|
||||
}
|
||||
},
|
||||
onSignPsbt() {
|
||||
if (isDefined(signAtIndex)) {
|
||||
@@ -52,9 +67,18 @@ function useRpcSignPsbt() {
|
||||
}
|
||||
|
||||
export function RpcSignPsbt() {
|
||||
const { origin, decodedPsbt, onSignPsbt, onCancel } = useRpcSignPsbt();
|
||||
const { allowedSighashes, origin, decodedPsbt, inputsToSign, onSignPsbt, onCancel, tx } =
|
||||
useRpcSignPsbt();
|
||||
if (!decodedPsbt) return null;
|
||||
return (
|
||||
<PsbtSigner appName={origin} onSignPsbt={onSignPsbt} onCancel={onCancel} psbt={decodedPsbt} />
|
||||
<PsbtSigner
|
||||
allowedSighashes={allowedSighashes}
|
||||
inputsToSign={inputsToSign}
|
||||
origin={origin}
|
||||
onSignPsbt={onSignPsbt}
|
||||
onCancel={onCancel}
|
||||
psbtRaw={decodedPsbt}
|
||||
psbtTx={tx}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useQueries, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { Inscription } from '@shared/models/inscription.model';
|
||||
|
||||
@@ -36,3 +36,16 @@ export function useGetInscriptionQuery<T extends unknown = FetchInscriptionResp>
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function useGetInscriptionQueries(ids: string[]) {
|
||||
return useQueries({
|
||||
queries: ids.map(id => {
|
||||
return {
|
||||
enabled: !!id,
|
||||
queryKey: [QueryPrefixes.InscriptionMetadata, id],
|
||||
queryFn: () => fetchInscription()(id),
|
||||
...inscriptionQueryOptions,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { bytesToHex } from '@stacks/common';
|
||||
import { useQueries } from '@tanstack/react-query';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import { isTypedArray } from '@shared/utils';
|
||||
import { isDefined, isTypedArray } from '@shared/utils';
|
||||
import { Prettify } from '@shared/utils/type-utils';
|
||||
|
||||
import { QueryPrefixes } from '@app/query/query-prefixes';
|
||||
@@ -25,7 +25,7 @@ const ordApiGetTransactionOutput = yup
|
||||
})
|
||||
.required();
|
||||
|
||||
export type OrdApiInscriptionTxOutput = Prettify<yup.InferType<typeof ordApiGetTransactionOutput>>;
|
||||
type OrdApiInscriptionTxOutput = Prettify<yup.InferType<typeof ordApiGetTransactionOutput>>;
|
||||
|
||||
export async function getNumberOfInscriptionOnUtxo(id: string, index: number) {
|
||||
const resp = await fetchOrdinalsAwareUtxo(id, index);
|
||||
@@ -34,6 +34,12 @@ export async function getNumberOfInscriptionOnUtxo(id: string, index: number) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getQueryArgsWithDefaults(utxo: TaprootUtxo | btc.TransactionInput) {
|
||||
const txId = isTypedArray(utxo.txid) ? bytesToHex(utxo.txid) : utxo.txid ?? '';
|
||||
const txIndex = 'vout' in utxo ? utxo.vout : utxo.index ?? 0;
|
||||
return { txId, txIndex };
|
||||
}
|
||||
|
||||
async function fetchOrdinalsAwareUtxo(
|
||||
txid: string,
|
||||
index: number
|
||||
@@ -48,7 +54,7 @@ async function fetchOrdinalsAwareUtxo(
|
||||
return ordApiGetTransactionOutput.validate(data);
|
||||
}
|
||||
|
||||
function makeOrdinalsAwareUtxoQueryKey(txId: string, txIndex: number) {
|
||||
function makeOrdinalsAwareUtxoQueryKey(txId: string, txIndex?: number) {
|
||||
return [QueryPrefixes.InscriptionFromTxid, txId, txIndex] as const;
|
||||
}
|
||||
|
||||
@@ -58,12 +64,12 @@ const queryOptions = {
|
||||
refetchOnWindowFocus: false,
|
||||
} as const;
|
||||
|
||||
export function useOrdinalsAwareUtxoQueries(utxos: TaprootUtxo[] | btc.TransactionInputRequired[]) {
|
||||
export function useOrdinalsAwareUtxoQueries(utxos: TaprootUtxo[] | btc.TransactionInput[]) {
|
||||
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;
|
||||
const { txId, txIndex } = getQueryArgsWithDefaults(utxo);
|
||||
return {
|
||||
enable: txId !== '' && isDefined(txIndex),
|
||||
queryKey: makeOrdinalsAwareUtxoQueryKey(txId, txIndex),
|
||||
queryFn: () => fetchOrdinalsAwareUtxo(txId, txIndex),
|
||||
select: (resp: OrdApiInscriptionTxOutput) =>
|
||||
|
||||
@@ -40,14 +40,15 @@ const queryOptions = {
|
||||
refetchOnWindowFocus: false,
|
||||
} as const;
|
||||
|
||||
// ts-unused-exports:disable-next-line
|
||||
export function useGetBitcoinTransactionQueries(
|
||||
inputs: btc.TransactionInputRequired[]
|
||||
inputs: btc.TransactionInput[]
|
||||
): UseQueryResult<BitcoinTx>[] {
|
||||
const client = useBitcoinClient();
|
||||
|
||||
return useQueries({
|
||||
queries: inputs.map(input => {
|
||||
const txId = bytesToHex(input.txid);
|
||||
const txId = input.txid ? bytesToHex(input.txid) : '';
|
||||
return {
|
||||
queryKey: ['bitcoin-transaction', txId],
|
||||
queryFn: () => fetchBitcoinTransaction(client)(txId),
|
||||
|
||||
@@ -40,6 +40,7 @@ import { ReceiveModal } from '@app/pages/receive-tokens/receive-modal';
|
||||
import { ReceiveStxModal } from '@app/pages/receive-tokens/receive-stx';
|
||||
import { ReceiveCollectibleModal } from '@app/pages/receive/receive-collectible/receive-collectible-modal';
|
||||
import { ReceiveCollectibleOrdinal } from '@app/pages/receive/receive-collectible/receive-collectible-oridinal';
|
||||
import { RequestError } from '@app/pages/request-error/request-error';
|
||||
import { RpcGetAddresses } from '@app/pages/rpc-get-addresses/rpc-get-addresses';
|
||||
import { rpcSendTransferRoutes } from '@app/pages/rpc-send-transfer/rpc-send-transfer.routes';
|
||||
import { RpcSignPsbt } from '@app/pages/rpc-sign-psbt/rpc-sign-psbt';
|
||||
@@ -90,6 +91,88 @@ function useAppRoutes() {
|
||||
</Route>
|
||||
);
|
||||
|
||||
const legacyRequestRoutes = (
|
||||
<>
|
||||
<Route
|
||||
path={RouteUrls.TransactionRequest}
|
||||
element={
|
||||
<AccountGate>
|
||||
<Suspense fallback={<LoadingSpinner height="600px" />}>
|
||||
<TransactionRequest />
|
||||
</Suspense>
|
||||
</AccountGate>
|
||||
}
|
||||
>
|
||||
{ledgerStacksTxSigningRoutes}
|
||||
<Route path={RouteUrls.EditNonce} element={<EditNonceDrawer />} />
|
||||
<Route path={RouteUrls.TransactionBroadcastError} element={<BroadcastErrorDrawer />} />
|
||||
</Route>
|
||||
<Route
|
||||
path={RouteUrls.SignatureRequest}
|
||||
element={
|
||||
<AccountGate>
|
||||
<Suspense fallback={<LoadingSpinner height="600px" />}>
|
||||
<StacksMessageSigningRequest />
|
||||
</Suspense>
|
||||
</AccountGate>
|
||||
}
|
||||
>
|
||||
{ledgerStacksMessageSigningRoutes}
|
||||
</Route>
|
||||
<Route
|
||||
path={RouteUrls.ProfileUpdateRequest}
|
||||
element={
|
||||
<AccountGate>
|
||||
<Suspense fallback={<LoadingSpinner height="600px" />}>
|
||||
<ProfileUpdateRequest />
|
||||
</Suspense>
|
||||
</AccountGate>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={RouteUrls.PsbtRequest}
|
||||
element={
|
||||
<AccountGate>
|
||||
<Suspense fallback={<LoadingSpinner height="600px" />}>
|
||||
<PsbtRequest />
|
||||
</Suspense>
|
||||
</AccountGate>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const rpcRequestRoutes = (
|
||||
<>
|
||||
<Route
|
||||
path={RouteUrls.RpcGetAddresses}
|
||||
element={
|
||||
<AccountGate>
|
||||
<RpcGetAddresses />
|
||||
</AccountGate>
|
||||
}
|
||||
/>
|
||||
{rpcSendTransferRoutes}
|
||||
<Route
|
||||
path={RouteUrls.RpcSignBip322Message}
|
||||
lazy={async () => {
|
||||
const { RpcSignBip322MessageRoute } = await import(
|
||||
'@app/pages/rpc-sign-bip322-message/rpc-sign-bip322-message'
|
||||
);
|
||||
return { Component: RpcSignBip322MessageRoute };
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path={RouteUrls.RpcSignPsbt}
|
||||
element={
|
||||
<AccountGate>
|
||||
<RpcSignPsbt />
|
||||
</AccountGate>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return createHashRouter(
|
||||
createRoutesFromElements(
|
||||
<Route path={RouteUrls.Container} element={<Container />}>
|
||||
@@ -228,55 +311,6 @@ function useAppRoutes() {
|
||||
|
||||
{sendCryptoAssetFormRoutes}
|
||||
|
||||
<Route
|
||||
path={RouteUrls.TransactionRequest}
|
||||
element={
|
||||
<AccountGate>
|
||||
<Suspense fallback={<LoadingSpinner height="600px" />}>
|
||||
<TransactionRequest />
|
||||
</Suspense>
|
||||
</AccountGate>
|
||||
}
|
||||
>
|
||||
{ledgerStacksTxSigningRoutes}
|
||||
<Route path={RouteUrls.EditNonce} element={<EditNonceDrawer />} />
|
||||
<Route path={RouteUrls.TransactionBroadcastError} element={<BroadcastErrorDrawer />} />
|
||||
</Route>
|
||||
<Route path={RouteUrls.UnauthorizedRequest} element={<UnauthorizedRequest />} />
|
||||
|
||||
<Route
|
||||
path={RouteUrls.SignatureRequest}
|
||||
element={
|
||||
<AccountGate>
|
||||
<Suspense fallback={<LoadingSpinner height="600px" />}>
|
||||
<StacksMessageSigningRequest />
|
||||
</Suspense>
|
||||
</AccountGate>
|
||||
}
|
||||
>
|
||||
{ledgerStacksMessageSigningRoutes}
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path={RouteUrls.ProfileUpdateRequest}
|
||||
element={
|
||||
<AccountGate>
|
||||
<Suspense fallback={<LoadingSpinner height="600px" />}>
|
||||
<ProfileUpdateRequest />
|
||||
</Suspense>
|
||||
</AccountGate>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={RouteUrls.PsbtRequest}
|
||||
element={
|
||||
<AccountGate>
|
||||
<Suspense fallback={<LoadingSpinner height="600px" />}>
|
||||
<PsbtRequest />
|
||||
</Suspense>
|
||||
</AccountGate>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={RouteUrls.ViewSecretKey}
|
||||
element={
|
||||
@@ -291,34 +325,17 @@ function useAppRoutes() {
|
||||
{settingsModalRoutes}
|
||||
</Route>
|
||||
|
||||
{legacyRequestRoutes}
|
||||
{rpcRequestRoutes}
|
||||
<Route path={RouteUrls.UnauthorizedRequest} element={<UnauthorizedRequest />} />
|
||||
<Route
|
||||
path={RouteUrls.RpcGetAddresses}
|
||||
path={RouteUrls.RequestError}
|
||||
element={
|
||||
<AccountGate>
|
||||
<RpcGetAddresses />
|
||||
<RequestError />
|
||||
</AccountGate>
|
||||
}
|
||||
/>
|
||||
{rpcSendTransferRoutes}
|
||||
|
||||
<Route
|
||||
path={RouteUrls.RpcSignPsbt}
|
||||
element={
|
||||
<AccountGate>
|
||||
<RpcSignPsbt />
|
||||
</AccountGate>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={RouteUrls.RpcSignBip322Message}
|
||||
lazy={async () => {
|
||||
const { RpcSignBip322MessageRoute } = await import(
|
||||
'@app/pages/rpc-sign-bip322-message/rpc-sign-bip322-message'
|
||||
);
|
||||
return { Component: RpcSignBip322MessageRoute };
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Catch-all route redirects to onboarding */}
|
||||
<Route path="*" element={<Navigate replace to={RouteUrls.Onboarding} />} />
|
||||
|
||||
@@ -85,13 +85,13 @@ export function bitcoinAddressIndexSignerFactory<T extends BitcoinAddressIndexSi
|
||||
paymentFn,
|
||||
signFn(tx: btc.Transaction) {
|
||||
if (!addressIndexKeychain.privateKey)
|
||||
throw new Error('Unable to sign taproot transaction, no private key found');
|
||||
throw new Error('Unable to sign transaction, no private key found');
|
||||
|
||||
tx.sign(addressIndexKeychain.privateKey);
|
||||
},
|
||||
signAtIndexFn(tx: btc.Transaction, index: number, allowedSighash?: btc.SignatureHash[]) {
|
||||
if (!addressIndexKeychain.privateKey)
|
||||
throw new Error('Unable to sign taproot transaction, no private key found');
|
||||
throw new Error('Unable to sign transaction, no private key found');
|
||||
|
||||
tx.signIdx(addressIndexKeychain.privateKey, index, allowedSighash);
|
||||
},
|
||||
|
||||
@@ -21,11 +21,11 @@ export function formatPsbtResponse({
|
||||
}
|
||||
|
||||
interface FinalizePsbtArgs {
|
||||
data: PsbtData | string;
|
||||
requestPayload: string;
|
||||
tabId: number;
|
||||
data: PsbtData | string;
|
||||
}
|
||||
export function finalizePsbt({ requestPayload, data, tabId }: FinalizePsbtArgs) {
|
||||
export function finalizePsbt({ data, requestPayload, tabId }: FinalizePsbtArgs) {
|
||||
const responseMessage = formatPsbtResponse({ request: requestPayload, response: data });
|
||||
chrome.tabs.sendMessage(tabId, responseMessage);
|
||||
window.close();
|
||||
|
||||
@@ -2,14 +2,12 @@ import { PaymentTypes } from '@btckit/types';
|
||||
import { hexToBytes } from '@noble/hashes/utils';
|
||||
import { HDKey, Versions } from '@scure/bip32';
|
||||
import * as btc from '@scure/btc-signer';
|
||||
import * as P from 'micro-packed';
|
||||
|
||||
import { BitcoinNetworkModes, NetworkModes } from '@shared/constants';
|
||||
import { logger } from '@shared/logger';
|
||||
import { whenNetwork } from '@shared/utils';
|
||||
|
||||
import { DerivationPathDepth } from '../derivation-path.utils';
|
||||
import { BtcSignerNetwork, getBtcSignerLibNetworkConfigByMode } from './bitcoin.network';
|
||||
import { BtcSignerNetwork } from './bitcoin.network';
|
||||
|
||||
export interface BitcoinAccount {
|
||||
type: PaymentTypes;
|
||||
@@ -56,43 +54,45 @@ export function ecdsaPublicKeyToSchnorr(pubKey: Uint8Array) {
|
||||
return pubKey.slice(1);
|
||||
}
|
||||
|
||||
// basically same as above, to remov
|
||||
// Basically same as above, to remove
|
||||
export const toXOnly = (pubKey: Buffer) => (pubKey.length === 32 ? pubKey : pubKey.slice(1, 33));
|
||||
|
||||
export function decodeBitcoinTx(tx: string) {
|
||||
return btc.RawTx.decode(hexToBytes(tx));
|
||||
}
|
||||
|
||||
const concat = P.concatBytes;
|
||||
export function getAddressFromOutScript(script: Uint8Array, bitcoinNetwork: BtcSignerNetwork) {
|
||||
const outputScript = btc.OutScript.decode(script);
|
||||
|
||||
function formatKey(hashed: Uint8Array, prefix: number[]): string {
|
||||
return btc.base58check.encode(concat(Uint8Array.from(prefix), hashed));
|
||||
}
|
||||
|
||||
function getAddressFromWshOutScript(script: Uint8Array, network: BtcSignerNetwork) {
|
||||
return btc.programToWitness(0, script.slice(2), network);
|
||||
}
|
||||
|
||||
function getAddressFromWpkhOutScript(script: Uint8Array, network: BtcSignerNetwork) {
|
||||
return btc.programToWitness(0, script.slice(2), network);
|
||||
}
|
||||
|
||||
function getAddressFromTrOutScript(script: Uint8Array, network: BtcSignerNetwork) {
|
||||
return btc.programToWitness(1, script.slice(2), network);
|
||||
}
|
||||
|
||||
export function getAddressFromOutScript(script: Uint8Array, network: BitcoinNetworkModes) {
|
||||
const outScript = btc.OutScript.decode(script);
|
||||
// This appears to be undefined at times?
|
||||
const bitcoinNetwork = getBtcSignerLibNetworkConfigByMode(network);
|
||||
|
||||
if (outScript.type === 'wsh') return getAddressFromWshOutScript(script, bitcoinNetwork);
|
||||
else if (outScript.type === 'wpkh') return getAddressFromWpkhOutScript(script, bitcoinNetwork);
|
||||
else if (outScript.type === 'tr') return getAddressFromTrOutScript(script, bitcoinNetwork);
|
||||
else if (outScript.type === 'pkh') return formatKey(script, [bitcoinNetwork?.pubKeyHash]);
|
||||
else if (outScript.type === 'sh') return formatKey(script, [bitcoinNetwork?.scriptHash]);
|
||||
logger.error(`Unknown address type=${outScript.type}`);
|
||||
return '';
|
||||
if (outputScript.type === 'pk' || outputScript.type === 'tr') {
|
||||
return btc.Address(bitcoinNetwork).encode({
|
||||
type: outputScript.type,
|
||||
pubkey: outputScript.pubkey,
|
||||
});
|
||||
}
|
||||
if (outputScript.type === 'ms' || outputScript.type === 'tr_ms') {
|
||||
return btc.Address(bitcoinNetwork).encode({
|
||||
type: outputScript.type,
|
||||
pubkeys: outputScript.pubkeys,
|
||||
m: outputScript.m,
|
||||
});
|
||||
}
|
||||
if (outputScript.type === 'tr_ns') {
|
||||
return btc.Address(bitcoinNetwork).encode({
|
||||
type: outputScript.type,
|
||||
pubkeys: outputScript.pubkeys,
|
||||
});
|
||||
}
|
||||
if (outputScript.type === 'unknown') {
|
||||
return btc.Address(bitcoinNetwork).encode({
|
||||
type: outputScript.type,
|
||||
script,
|
||||
});
|
||||
}
|
||||
return btc.Address(bitcoinNetwork).encode({
|
||||
type: outputScript.type,
|
||||
hash: outputScript.hash,
|
||||
});
|
||||
}
|
||||
|
||||
type BtcSignerLibPaymentTypeIdentifers = 'wpkh' | 'wsh' | 'tr' | 'pkh' | 'sh';
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
export interface InscriptionResponseItem {
|
||||
id: string;
|
||||
number: number;
|
||||
address: string;
|
||||
genesis_address: string;
|
||||
genesis_block_height: number;
|
||||
genesis_block_hash: string;
|
||||
genesis_tx_id: string;
|
||||
content: string;
|
||||
content_length: string;
|
||||
content_type: string;
|
||||
genesis_fee: string;
|
||||
genesis_timestamp: number;
|
||||
tx_id: string;
|
||||
genesis_height: string;
|
||||
genesis_transaction: string;
|
||||
id: string;
|
||||
inscription_number: number;
|
||||
location: string;
|
||||
output: string;
|
||||
value: string;
|
||||
offset: string;
|
||||
output: string;
|
||||
output_value: string;
|
||||
preview: string;
|
||||
sat: string;
|
||||
timestamp: string;
|
||||
// Outdated props?
|
||||
genesis_address: string;
|
||||
genesis_block_hash: string;
|
||||
genesis_block_height: number;
|
||||
genesis_timestamp: number;
|
||||
genesis_tx_id: string;
|
||||
mime_type: string;
|
||||
number: number;
|
||||
sat_coinbase_height: number;
|
||||
sat_ordinal: string;
|
||||
sat_rarity: string;
|
||||
sat_coinbase_height: number;
|
||||
mime_type: string;
|
||||
content_type: string;
|
||||
content_length: number;
|
||||
timestamp: number;
|
||||
tx_id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Inscription extends InscriptionResponseItem {
|
||||
|
||||
@@ -25,7 +25,7 @@ interface BitcoinTransactionStatus {
|
||||
block_time?: number | null;
|
||||
}
|
||||
|
||||
export interface BitcoinTransactionVectorOutput {
|
||||
interface BitcoinTransactionVectorOutput {
|
||||
scriptpubkey: string;
|
||||
scriptpubkey_asm: string;
|
||||
scriptpubkey_type: string;
|
||||
|
||||
@@ -44,14 +44,6 @@ export enum RouteUrls {
|
||||
Send = '/send-transaction',
|
||||
ViewSecretKey = '/view-secret-key',
|
||||
|
||||
// App requests (legacy)
|
||||
ProfileUpdateRequest = '/update-profile',
|
||||
PsbtRequest = '/psbt',
|
||||
SignatureRequest = '/signature',
|
||||
TransactionRequest = '/transaction',
|
||||
TransactionBroadcastError = 'broadcast-error',
|
||||
UnauthorizedRequest = '/unauthorized-request',
|
||||
|
||||
// Locked wallet route
|
||||
Unlock = '/unlock',
|
||||
|
||||
@@ -93,7 +85,14 @@ export enum RouteUrls {
|
||||
SendOrdinalInscriptionSent = '/send/ordinal-inscription/sent',
|
||||
SendOrdinalInscriptionError = '/send/ordinal-inscription/error',
|
||||
|
||||
// Request routes
|
||||
// Legacy request routes
|
||||
ProfileUpdateRequest = '/update-profile',
|
||||
PsbtRequest = '/psbt',
|
||||
SignatureRequest = '/signature',
|
||||
TransactionRequest = '/transaction',
|
||||
TransactionBroadcastError = 'broadcast-error',
|
||||
|
||||
// Rpc request routes
|
||||
RpcGetAddresses = '/get-addresses',
|
||||
RpcSignPsbt = '/sign-psbt',
|
||||
RpcSendTransfer = '/send-transfer',
|
||||
@@ -102,4 +101,8 @@ export enum RouteUrls {
|
||||
RpcSendTransferSummary = '/send-transfer/summary',
|
||||
RpcReceiveBitcoinContractOffer = '/bitcoin-contract-offer/:bitcoinContractOffer/:counterpartyWalletURL',
|
||||
RpcSignBip322Message = '/sign-bip322-message',
|
||||
|
||||
// Shared legacy and rpc request routes
|
||||
RequestError = '/request-error',
|
||||
UnauthorizedRequest = '/unauthorized-request',
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ function buildTestNativeSegwitPsbtRequestWithIndexes(pubKey: Uint8Array): PsbtRe
|
||||
tx.addInput({
|
||||
index: 0,
|
||||
txid: '5be910a6557bae29b8ff2dbf4607dbf783eaf82802896d13f61d975c133ccce7',
|
||||
sighashType: 1,
|
||||
witnessUtxo: {
|
||||
amount: BigInt(1268294),
|
||||
script: p2wpkh.script,
|
||||
@@ -104,7 +105,7 @@ function buildTestNativeSegwitPsbtRequestWithIndexes(pubKey: Uint8Array): PsbtRe
|
||||
|
||||
const psbt = tx.toPSBT();
|
||||
|
||||
return { signAtIndex: [0, 1], hex: bytesToHex(psbt) };
|
||||
return { signAtIndex: [0, 1], hex: bytesToHex(psbt), allowedSighash: [2] };
|
||||
}
|
||||
|
||||
function buildTestTaprootPsbtRequest(pubKey: Uint8Array): PsbtRequestOptions {
|
||||
|
||||
169
tests/mocks/mock-psbts.ts
Normal file
169
tests/mocks/mock-psbts.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
export const mockInscriptions1 = [
|
||||
{
|
||||
address: 'bc1pwrmewwprc8k8l2k63x4advg0nx0jk50xzqnee996lm87mcuza7kq6drg2k',
|
||||
addressIndex: 0,
|
||||
content: '/content/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0',
|
||||
content_length: '519 bytes',
|
||||
content_type: 'image/png',
|
||||
genesis_fee: '11130',
|
||||
genesis_height: '/block/792967',
|
||||
genesis_transaction: '/tx/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202',
|
||||
id: 'ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0',
|
||||
inscription_number: 10875335,
|
||||
location: 'ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202:0:0',
|
||||
offset: '200',
|
||||
output: '/output/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202:0',
|
||||
output_value: '10000',
|
||||
preview: '/preview/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0',
|
||||
sat: '/sat/516510429895340',
|
||||
timestamp: '2023-06-05 13:28:12 UTC',
|
||||
title: 'Inscription 10875335',
|
||||
// Outdated props?
|
||||
genesis_address: '',
|
||||
genesis_block_hash: '',
|
||||
genesis_block_height: 0,
|
||||
genesis_timestamp: 0,
|
||||
genesis_tx_id: '',
|
||||
mime_type: '',
|
||||
number: 0,
|
||||
sat_coinbase_height: 0,
|
||||
sat_ordinal: '',
|
||||
sat_rarity: '',
|
||||
tx_id: '',
|
||||
value: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const mockPsbtInputs1 = [
|
||||
{
|
||||
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
|
||||
index: 0,
|
||||
mutable: false,
|
||||
sign: true,
|
||||
txid: 'c7bb536fc3a645c8c9bad70a651f997b10c99ac8bf8c8c34229cd3895bc96f3f',
|
||||
value: 600,
|
||||
},
|
||||
{
|
||||
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
|
||||
index: 1,
|
||||
mutable: false,
|
||||
sign: true,
|
||||
txid: 'c7bb536fc3a645c8c9bad70a651f997b10c99ac8bf8c8c34229cd3895bc96f3f',
|
||||
value: 600,
|
||||
},
|
||||
{
|
||||
address: 'bc1pwrmewwprc8k8l2k63x4advg0nx0jk50xzqnee996lm87mcuza7kq6drg2k',
|
||||
index: 0,
|
||||
inscription: '/inscription/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0',
|
||||
mutable: true,
|
||||
sign: false,
|
||||
txid: 'ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202',
|
||||
value: 10000,
|
||||
},
|
||||
{
|
||||
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
|
||||
index: 2,
|
||||
mutable: false,
|
||||
sign: true,
|
||||
txid: 'c7bb536fc3a645c8c9bad70a651f997b10c99ac8bf8c8c34229cd3895bc96f3f',
|
||||
value: 96794,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockPsbtOutputs1 = [
|
||||
{
|
||||
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
|
||||
mutable: true,
|
||||
sign: true,
|
||||
value: 1200,
|
||||
},
|
||||
{
|
||||
address: 'bc1p9pnzvq52956jht5deha82qp96pxw0a0tvey6fhdea7vwhf33tarskqq3nr',
|
||||
mutable: true,
|
||||
sign: true,
|
||||
value: 10000,
|
||||
},
|
||||
{
|
||||
address: '3NLVhyKWGey73pVbFAZ28nNVZB22upWZWq',
|
||||
mutable: true,
|
||||
sign: false,
|
||||
value: 87220,
|
||||
},
|
||||
{
|
||||
address: 'bc1qyylrgsxjrmaearjqqradhy8ferh4u0ydw4yuze',
|
||||
mutable: true,
|
||||
sign: false,
|
||||
value: 1780,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockInscriptions2 = [
|
||||
{
|
||||
address: 'bc1pwrmewwprc8k8l2k63x4advg0nx0jk50xzqnee996lm87mcuza7kq6drg2k',
|
||||
addressIndex: 0,
|
||||
content: '/content/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0',
|
||||
content_length: '519 bytes',
|
||||
content_type: 'image/png',
|
||||
genesis_fee: '11130',
|
||||
genesis_height: '/block/792967',
|
||||
genesis_transaction: '/tx/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202',
|
||||
id: 'ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0',
|
||||
inscription_number: 10875335,
|
||||
location: 'ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202:0:0',
|
||||
offset: '600',
|
||||
output: '/output/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202:0',
|
||||
output_value: '10000',
|
||||
preview: '/preview/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0',
|
||||
sat: '/sat/516510429895340',
|
||||
timestamp: '2023-06-05 13:28:12 UTC',
|
||||
title: 'Inscription 10875335',
|
||||
// Outdated props?
|
||||
genesis_address: '',
|
||||
genesis_block_hash: '',
|
||||
genesis_block_height: 0,
|
||||
genesis_timestamp: 0,
|
||||
genesis_tx_id: '',
|
||||
mime_type: '',
|
||||
number: 0,
|
||||
sat_coinbase_height: 0,
|
||||
sat_ordinal: '',
|
||||
sat_rarity: '',
|
||||
tx_id: '',
|
||||
value: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const mockPsbtInputs2 = [
|
||||
{
|
||||
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
|
||||
index: 0,
|
||||
mutable: false,
|
||||
sign: true,
|
||||
txid: 'c7bb536fc3a645c8c9bad70a651f997b10c99ac8bf8c8c34229cd3895bc96f3f',
|
||||
value: 100,
|
||||
},
|
||||
{
|
||||
address: 'bc1pwrmewwprc8k8l2k63x4advg0nx0jk50xzqnee996lm87mcuza7kq6drg2k',
|
||||
index: 0,
|
||||
inscription: '/inscription/ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202i0',
|
||||
mutable: true,
|
||||
sign: false,
|
||||
txid: 'ba39f922074c0d338a13ac10e770a5da47ce09df8310c8d3cfaec13a347e8202',
|
||||
value: 1000,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockPsbtOutputs2 = [
|
||||
{
|
||||
address: 'bc1qvcfy9yxjl3303jurcrs4sd49frjrmjk7x045r6',
|
||||
mutable: true,
|
||||
sign: true,
|
||||
value: 400,
|
||||
},
|
||||
{
|
||||
address: 'bc1p9pnzvq52956jht5deha82qp96pxw0a0tvey6fhdea7vwhf33tarskqq3nr',
|
||||
mutable: true,
|
||||
sign: true,
|
||||
value: 500,
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user