feat: add option to broadcast rpc psbt, closes #3895

This commit is contained in:
fbwoolf
2023-07-31 12:53:15 -05:00
committed by Fara Woolf
parent 281e1386a0
commit da7b51ba97
34 changed files with 326 additions and 162 deletions

View File

@@ -1,8 +1,17 @@
import * as btc from '@scure/btc-signer';
import { PsbtPayload } from '@stacks/connect';
import { decodeToken } from 'jsontokens';
import { Money } from '@shared/models/money.model';
import { isString } from '@shared/utils';
export interface SignPsbtArgs {
addressNativeSegwitTotal?: Money;
addressTaprootTotal?: Money;
fee?: Money;
inputs: btc.TransactionInput[];
}
export function getPsbtPayloadFromToken(requestToken: string): PsbtPayload {
const token = decodeToken(requestToken);
if (isString(token.payload)) throw new Error('Error decoding json token');

View File

@@ -41,21 +41,23 @@ export function useRpcSignPsbtParams() {
const [searchParams] = useSearchParams();
const { origin, tabId } = useDefaultRequestParams();
const requestId = searchParams.get('requestId');
const psbtHex = searchParams.get('hex');
const allowedSighash = searchParams.getAll('allowedSighash');
const broadcast = searchParams.get('broadcast');
const psbtHex = searchParams.get('hex');
const requestId = searchParams.get('requestId');
const signAtIndex = searchParams.getAll('signAtIndex');
return useMemo(() => {
return {
origin,
tabId: tabId ?? 0,
requestId,
psbtHex,
allowedSighash: undefinedIfLengthZero(
allowedSighash.map(h => Number(h)) as AllowedSighashTypes[]
),
broadcast,
origin,
psbtHex,
requestId,
signAtIndex: undefinedIfLengthZero(ensureArray(signAtIndex).map(h => Number(h))),
tabId: tabId ?? 0,
};
}, [allowedSighash, origin, psbtHex, requestId, signAtIndex, tabId]);
}, [allowedSighash, broadcast, origin, psbtHex, requestId, signAtIndex, tabId]);
}

View File

@@ -65,7 +65,7 @@ interface InfoCardAssetValueProps extends StackProps {
value: number;
fiatValue?: string;
fiatSymbol?: string;
symbol: string;
symbol?: string;
icon?: React.FC;
}

View File

@@ -1,21 +1,17 @@
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 { usePsbtSignerContext } from '@app/features/psbt-signer/psbt-signer.context';
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 {
inputs: PsbtInput[];
outputs: PsbtOutput[];
}
export function PsbtInputsAndOutputs({ outputs, inputs }: PsbtInputsAndOutputsProps) {
export function PsbtInputsAndOutputs() {
const { psbtInputs, psbtOutputs } = usePsbtSignerContext();
const [showDetails, setShowDetails] = useState(false);
if (!inputs.length || !outputs.length) return null;
if (!psbtInputs.length || !psbtOutputs.length) return null;
return (
<PsbtRequestDetailsSectionLayout>
@@ -27,9 +23,9 @@ export function PsbtInputsAndOutputs({ outputs, inputs }: PsbtInputsAndOutputsPr
/>
{showDetails ? (
<>
<PsbtInputList inputs={inputs} />
<PsbtInputList inputs={psbtInputs} />
<PsbtRequestDetailsSectionHeader title="Outputs" />
<PsbtOutputList outputs={outputs} />
<PsbtOutputList outputs={psbtOutputs} />
</>
) : null}
</PsbtRequestDetailsSectionLayout>

View File

@@ -1,33 +1,28 @@
import { truncateMiddle } from '@stacks/ui-utils';
import { Money } from '@shared/models/money.model';
import { formatMoney, i18nFormatCurrency } from '@app/common/money/format-money';
import { usePsbtSignerContext } from '@app/features/psbt-signer/psbt-signer.context';
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 {
accountInscriptionsBeingTransferred,
accountInscriptionsBeingReceived,
addressNativeSegwit,
addressTaproot,
addressNativeSegwitTotal,
addressTaprootTotal,
} = usePsbtSignerContext();
const calculateBitcoinFiatValue = useCalculateBitcoinFiatValue();
const isTransferringInscriptions = accountInscriptionsBeingTransferred?.length;

View File

@@ -1,30 +1,15 @@
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 { usePsbtSignerContext } from '@app/features/psbt-signer/psbt-signer.context';
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) {
export function PsbtInputsOutputsTotals() {
const { addressNativeSegwitTotal, addressTaprootTotal } = usePsbtSignerContext();
// Transferring (+)
const isNativeSegwitTotalGreaterThanZero = addressNativeSegwitTotal.amount.isGreaterThan(0);
const isTaprootTotalGreaterThanZero = addressTaprootTotal.amount.isGreaterThan(0);
@@ -46,11 +31,6 @@ export function PsbtInputsOutputsTotals({
<Box p="loose">
<PsbtRequestDetailsSectionHeader title="You'll transfer" />
<PsbtAddressTotals
accountInscriptionsBeingTransferred={accountInscriptionsBeingTransferred}
addressNativeSegwit={addressNativeSegwit}
addressTaproot={addressTaproot}
addressNativeSegwitTotal={addressNativeSegwitTotal}
addressTaprootTotal={addressTaprootTotal}
showNativeSegwitTotal={isNativeSegwitTotalGreaterThanZero}
showTaprootTotal={isTaprootTotalGreaterThanZero}
/>
@@ -61,11 +41,6 @@ export function PsbtInputsOutputsTotals({
<Box p="loose">
<PsbtRequestDetailsSectionHeader title="You'll receive" />
<PsbtAddressTotals
accountInscriptionsBeingReceived={accountInscriptionsBeingReceived}
addressNativeSegwit={addressNativeSegwit}
addressTaproot={addressTaproot}
addressNativeSegwitTotal={addressNativeSegwitTotal}
addressTaprootTotal={addressTaprootTotal}
showNativeSegwitTotal={isNativeSegwitTotalLessThanZero}
showTaprootTotal={isTaprootTotalLessThanZero}
/>

View File

@@ -3,7 +3,7 @@ import { Box, Button, Stack, color } from '@stacks/ui';
import { PrimaryButton } from '@app/components/primary-button';
interface PsbtRequestActionsProps {
isLoading: boolean;
isLoading?: boolean;
onCancel(): void;
onSignPsbt(): void;
}

View File

@@ -4,14 +4,15 @@ import { Box, Stack, Text, color } from '@stacks/ui';
import { Tooltip } from '@app/components/tooltip';
import { Title } from '@app/components/typography';
import { usePsbtSignerContext } from '@app/features/psbt-signer/psbt-signer.context';
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;
export function PsbtRequestDetailsHeader() {
const { isPsbtMutable } = usePsbtSignerContext();
const labelColor = isPsbtMutable ? color('feedback-alert') : color('text-caption');
return (

View File

@@ -1,70 +0,0 @@
import * as btc from '@scure/btc-signer';
import { AllowedSighashTypes } from '@shared/rpc/methods/sign-psbt';
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';
interface PsbtRequestDetailsProps {
allowedSighash?: AllowedSighashTypes[];
indexesToSign?: number[];
psbtRaw?: RawPsbt;
psbtTxInputs: btc.TransactionInput[];
psbtTxOutputs: btc.TransactionOutput[];
}
export function PsbtRequestDetails({
allowedSighash,
indexesToSign,
psbtRaw,
psbtTxInputs,
psbtTxOutputs,
}: PsbtRequestDetailsProps) {
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner();
const {
accountInscriptionsBeingTransferred,
accountInscriptionsBeingReceived,
addressNativeSegwitTotal,
addressTaprootTotal,
fee,
isPsbtMutable,
psbtInputs,
psbtOutputs,
shouldDefaultToAdvancedView,
} = useParsedPsbt({
allowedSighash,
inputs: psbtTxInputs,
indexesToSign,
outputs: psbtTxOutputs,
});
if (shouldDefaultToAdvancedView && psbtRaw) 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 inputs={psbtInputs} outputs={psbtOutputs} />
{psbtRaw ? <PsbtRequestRaw psbt={psbtRaw} /> : null}
<PsbtRequestFee fee={fee} />
</PsbtRequestDetailsLayout>
);
}

View File

@@ -1,9 +1,8 @@
import { Stack } from '@stacks/ui';
interface PsbtSignerLayoutProps {
children: React.ReactNode;
}
export function PsbtSignerLayout({ children }: PsbtSignerLayoutProps) {
import { HasChildren } from '@app/common/has-children';
export function PsbtSignerLayout({ children }: HasChildren) {
return (
<Stack
alignItems="center"

View File

@@ -0,0 +1,30 @@
import { createContext, useContext } from 'react';
import { Money } from '@shared/models/money.model';
import { PsbtInput } from './hooks/use-parsed-inputs';
import { PsbtOutput } from './hooks/use-parsed-outputs';
export interface PsbtSignerContext {
accountInscriptionsBeingTransferred: string[];
accountInscriptionsBeingReceived: string[];
addressNativeSegwit: string;
addressTaproot: string;
addressNativeSegwitTotal: Money;
addressTaprootTotal: Money;
fee: Money;
isPsbtMutable: boolean;
psbtInputs: PsbtInput[];
psbtOutputs: PsbtOutput[];
shouldDefaultToAdvancedView: boolean;
}
const psbtSignerContext = createContext<PsbtSignerContext | null>(null);
export function usePsbtSignerContext() {
const context = useContext(psbtSignerContext);
if (!context) throw new Error('No PsbtSignerContext found');
return context;
}
export const PsbtSignerProvider = psbtSignerContext.Provider;

View File

@@ -7,14 +7,25 @@ import { RouteUrls } from '@shared/route-urls';
import { AllowedSighashTypes } from '@shared/rpc/methods/sign-psbt';
import { useRouteHeader } from '@app/common/hooks/use-route-header';
import { SignPsbtArgs } from '@app/common/psbt/requests';
import { PopupHeader } from '@app/features/current-account/popup-header';
import { useOnOriginTabClose } from '@app/routes/hooks/use-on-tab-closed';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountTaprootIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
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 { PsbtRequestActions } from './components/psbt-request-actions';
import { PsbtRequestDetails } from './components/psbt-request-details/psbt-request-details';
import { PsbtRequestDetailsHeader } from './components/psbt-request-details-header';
import { PsbtRequestDetailsLayout } from './components/psbt-request-details.layout';
import { PsbtRequestFee } from './components/psbt-request-fee';
import { PsbtRequestHeader } from './components/psbt-request-header';
import { PsbtRequestRaw } from './components/psbt-request-raw';
import { PsbtRequestSighashWarningLabel } from './components/psbt-request-sighash-warning-label';
import { PsbtSignerLayout } from './components/psbt-signer.layout';
import { useParsedPsbt } from './hooks/use-parsed-psbt';
import { usePsbtSigner } from './hooks/use-psbt-signer';
import { PsbtSignerContext, PsbtSignerProvider } from './psbt-signer.context';
function getPsbtTxInputs(psbtTx: btc.Transaction) {
const inputsLength = psbtTx.inputsLength;
@@ -39,15 +50,27 @@ function getPsbtTxOutputs(psbtTx: btc.Transaction) {
interface PsbtSignerProps {
allowedSighash?: AllowedSighashTypes[];
indexesToSign?: number[];
isBroadcasting?: boolean;
name?: string;
origin: string;
onCancel(): void;
onSignPsbt(inputs: btc.TransactionInput[]): void;
onSignPsbt({ addressNativeSegwitTotal, addressTaprootTotal, fee, inputs }: SignPsbtArgs): void;
psbtHex: string;
}
export function PsbtSigner(props: PsbtSignerProps) {
const { allowedSighash, indexesToSign, name, origin, onCancel, onSignPsbt, psbtHex } = props;
const {
allowedSighash,
indexesToSign,
isBroadcasting,
name,
origin,
onCancel,
onSignPsbt,
psbtHex,
} = props;
const navigate = useNavigate();
const { address: addressNativeSegwit } = useCurrentAccountNativeSegwitIndexZeroSigner();
const { address: addressTaproot } = useCurrentAccountTaprootIndexZeroSigner();
const { getRawPsbt, getPsbtAsTransaction } = usePsbtSigner();
useRouteHeader(<PopupHeader displayAddresssBalanceOf="all" />);
@@ -69,23 +92,59 @@ export function PsbtSigner(props: PsbtSignerProps) {
const psbtTxInputs = useMemo(() => getPsbtTxInputs(psbtTx), [psbtTx]);
const psbtTxOutputs = useMemo(() => getPsbtTxOutputs(psbtTx), [psbtTx]);
const {
accountInscriptionsBeingTransferred,
accountInscriptionsBeingReceived,
addressNativeSegwitTotal,
addressTaprootTotal,
fee,
isPsbtMutable,
psbtInputs,
psbtOutputs,
shouldDefaultToAdvancedView,
} = useParsedPsbt({
allowedSighash,
inputs: psbtTxInputs,
indexesToSign,
outputs: psbtTxOutputs,
});
const psbtSignerContext: PsbtSignerContext = {
accountInscriptionsBeingTransferred,
accountInscriptionsBeingReceived,
addressNativeSegwit,
addressTaproot,
addressNativeSegwitTotal,
addressTaprootTotal,
fee,
isPsbtMutable,
psbtInputs,
psbtOutputs,
shouldDefaultToAdvancedView,
};
if (shouldDefaultToAdvancedView && psbtRaw) return <PsbtRequestRaw psbt={psbtRaw} />;
return (
<>
<PsbtSignerProvider value={psbtSignerContext}>
<PsbtSignerLayout>
<PsbtRequestHeader name={name} origin={origin} />
<PsbtRequestDetails
allowedSighash={allowedSighash}
indexesToSign={indexesToSign}
psbtRaw={psbtRaw}
psbtTxInputs={psbtTxInputs}
psbtTxOutputs={psbtTxOutputs}
/>
<PsbtRequestDetailsLayout>
{isPsbtMutable ? <PsbtRequestSighashWarningLabel /> : null}
<PsbtRequestDetailsHeader />
<PsbtInputsOutputsTotals />
<PsbtInputsAndOutputs />
{psbtRaw ? <PsbtRequestRaw psbt={psbtRaw} /> : null}
<PsbtRequestFee fee={fee} />
</PsbtRequestDetailsLayout>
</PsbtSignerLayout>
<PsbtRequestActions
isLoading={false}
isLoading={isBroadcasting}
onCancel={onCancel}
onSignPsbt={() => onSignPsbt(psbtTxInputs)}
onSignPsbt={() =>
onSignPsbt({ addressNativeSegwitTotal, addressTaprootTotal, fee, inputs: psbtTxInputs })
}
/>
</>
</PsbtSignerProvider>
);
}

View File

@@ -2,12 +2,12 @@ import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { bytesToHex } from '@noble/hashes/utils';
import * as btc from '@scure/btc-signer';
import { finalizePsbt } from '@shared/actions/finalize-psbt';
import { RouteUrls } from '@shared/route-urls';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { SignPsbtArgs } from '@app/common/psbt/requests';
import { usePsbtRequestSearchParams } from '@app/common/psbt/use-psbt-request-params';
import { usePsbtSigner } from '@app/features/psbt-signer/hooks/use-psbt-signer';
@@ -36,7 +36,7 @@ export function usePsbtRequest() {
tabId,
});
},
onSignPsbt(inputs: btc.TransactionInput[]) {
onSignPsbt({ inputs }: SignPsbtArgs) {
setIsLoading(true);
void analytics.track('request_sign_psbt_submit');

View File

@@ -0,0 +1,66 @@
import toast from 'react-hot-toast';
import { FiCheck, FiCopy, FiExternalLink } from 'react-icons/fi';
import { useLocation } from 'react-router-dom';
import { Flex, Stack, useClipboard } from '@stacks/ui';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { useExplorerLink } from '@app/common/hooks/use-explorer-link';
import {
InfoCard,
InfoCardAssetValue,
InfoCardBtn,
InfoCardFooter,
InfoCardRow,
} from '@app/components/info-card/info-card';
export function RpcSignPsbtSummary() {
const { state } = useLocation();
const { handleOpenTxLink } = useExplorerLink();
const analytics = useAnalytics();
const { fee, sendingValue, totalSpend, txId, txFiatValue, txFiatValueSymbol, txLink, txValue } =
state;
const { onCopy } = useClipboard(txId);
// TODO: Force close window?
// useOnMount(() => {
// setTimeout(() => window.close(), timeOut);
// });
function onClickLink() {
void analytics.track('view_rpc_sign_and_broadcast_psbt_confirmation', { symbol: 'BTC' });
handleOpenTxLink(txLink);
}
function onClickCopy() {
onCopy();
toast.success('ID copied!');
}
return (
<Flex alignItems="center" flexDirection="column" p="loose" width="100%">
<InfoCard>
<InfoCardAssetValue
value={txValue}
fiatValue={txFiatValue}
fiatSymbol={txFiatValueSymbol}
icon={FiCheck}
mb="loose"
/>
<Stack pb="extra-loose" width="100%">
<InfoCardRow title="Total spend" value={totalSpend} />
<InfoCardRow title="Sending" value={sendingValue} />
<InfoCardRow title="Fee" value={fee} />
</Stack>
<InfoCardFooter>
<Stack isInline spacing="base" width="100%">
<InfoCardBtn icon={FiExternalLink} label="View Details" onClick={onClickLink} />
<InfoCardBtn icon={FiCopy} label="Copy ID" onClick={onClickCopy} />
</Stack>
</InfoCardFooter>
</InfoCard>
</Flex>
);
}

View File

@@ -3,12 +3,14 @@ import { PsbtSigner } from '@app/features/psbt-signer/psbt-signer';
import { useRpcSignPsbt } from './use-rpc-sign-psbt';
export function RpcSignPsbt() {
const { allowedSighash, indexesToSign, onSignPsbt, onCancel, origin, psbtHex } = useRpcSignPsbt();
const { allowedSighash, indexesToSign, isBroadcasting, onSignPsbt, onCancel, origin, psbtHex } =
useRpcSignPsbt();
return (
<PsbtSigner
allowedSighash={allowedSighash}
indexesToSign={indexesToSign}
isBroadcasting={isBroadcasting}
origin={origin}
onSignPsbt={onSignPsbt}
onCancel={onCancel}

View File

@@ -1,28 +1,95 @@
import { useNavigate } from 'react-router-dom';
import { RpcErrorCode } from '@btckit/types';
import * as btc from '@scure/btc-signer';
import { bytesToHex } from '@stacks/common';
import { Money } from '@shared/models/money.model';
import { RouteUrls } from '@shared/route-urls';
import { makeRpcErrorResponse, makeRpcSuccessResponse } from '@shared/rpc/rpc-methods';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { sumMoney } from '@app/common/money/calculate-money';
import { formatMoney, formatMoneyPadded, i18nFormatCurrency } from '@app/common/money/format-money';
import { SignPsbtArgs } from '@app/common/psbt/requests';
import { useRpcSignPsbtParams } from '@app/common/psbt/use-psbt-request-params';
import { usePsbtSigner } from '@app/features/psbt-signer/hooks/use-psbt-signer';
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks';
import { useBitcoinBroadcastTransaction } from '@app/query/bitcoin/transaction/use-bitcoin-broadcast-transaction';
import {
useCalculateBitcoinFiatValue,
useCryptoCurrencyMarketData,
} from '@app/query/common/market-data/market-data.hooks';
interface BroadcastSignedPsbtTxArgs {
addressNativeSegwitTotal: Money;
addressTaprootTotal: Money;
fee: Money;
tx: string;
}
export function useRpcSignPsbt() {
const analytics = useAnalytics();
const navigate = useNavigate();
const { origin, tabId, requestId, psbtHex, allowedSighash, signAtIndex } = useRpcSignPsbtParams();
// const { addressNativeSegwitTotal, addressTaprootTotal, fee } = usePsbtSignerContext();
const { allowedSighash, broadcast, origin, psbtHex, requestId, signAtIndex, tabId } =
useRpcSignPsbtParams();
const { signPsbt, getPsbtAsTransaction } = usePsbtSigner();
const { broadcastTx, isBroadcasting } = useBitcoinBroadcastTransaction();
const { refetch } = useCurrentNativeSegwitUtxos();
const btcMarketData = useCryptoCurrencyMarketData('BTC');
const calculateBitcoinFiatValue = useCalculateBitcoinFiatValue();
if (!requestId || !psbtHex || !origin) throw new Error('Invalid params');
async function broadcastSignedPsbtTx({
addressNativeSegwitTotal,
addressTaprootTotal,
fee,
tx,
}: BroadcastSignedPsbtTxArgs) {
void analytics.track('user_approved_sign_and_broadcast_psbt', { origin });
const transferTotalAsMoney = sumMoney([addressNativeSegwitTotal, addressTaprootTotal]);
await broadcastTx({
tx,
async onSuccess(txid) {
await refetch();
const psbtTxSummaryState = {
fee: formatMoneyPadded(fee),
hasHeaderTitle: true,
sendingValue: formatMoney(transferTotalAsMoney),
totalSpend: formatMoney(sumMoney([transferTotalAsMoney, fee])),
txFiatValue: i18nFormatCurrency(calculateBitcoinFiatValue(transferTotalAsMoney)),
txFiatValueSymbol: btcMarketData.price.symbol,
txId: txid,
txLink: {
blockchain: 'bitcoin',
txid: txid || '',
},
txValue: formatMoney(transferTotalAsMoney),
};
navigate(RouteUrls.RpcSignPsbtSummary, {
state: psbtTxSummaryState,
});
},
onError(e) {
navigate(RouteUrls.RequestError, {
state: { message: e instanceof Error ? e.message : '', title: 'Failed to broadcast' },
});
},
});
}
return {
allowedSighash,
indexesToSign: signAtIndex,
isBroadcasting,
origin,
psbtHex,
onSignPsbt(inputs: btc.TransactionInput[]) {
async onSignPsbt({ addressNativeSegwitTotal, addressTaprootTotal, fee, inputs }: SignPsbtArgs) {
const tx = getPsbtAsTransaction(psbtHex);
try {
@@ -39,6 +106,20 @@ export function useRpcSignPsbt() {
tabId,
makeRpcSuccessResponse('signPsbt', { id: requestId, result: { hex: bytesToHex(psbt) } })
);
// Optional args are handle bc we support two request apis,
// but only support broadcasting using the rpc request method
if (broadcast && addressNativeSegwitTotal && addressTaprootTotal && fee) {
tx.finalize();
await broadcastSignedPsbtTx({
addressNativeSegwitTotal,
addressTaprootTotal,
fee,
tx: tx.hex,
});
return;
}
window.close();
},
onCancel() {

View File

@@ -44,6 +44,7 @@ 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';
import { RpcSignPsbtSummary } from '@app/pages/rpc-sign-psbt/rpc-sign-psbt-summary';
import { SelectNetwork } from '@app/pages/select-network/select-network';
import { BroadcastError } from '@app/pages/send/broadcast-error/broadcast-error';
import { LockBitcoinSummary } from '@app/pages/send/locked-bitcoin-summary/locked-bitcoin-summary';
@@ -65,6 +66,10 @@ import { useHasUserRespondedToAnalyticsConsent } from '@app/store/settings/setti
import { OnboardingGate } from './onboarding-gate';
function SuspenseLoadingSpinner() {
return <LoadingSpinner height="600px" />;
}
export function AppRoutes() {
const routes = useAppRoutes();
return <RouterProvider router={routes} />;
@@ -97,7 +102,7 @@ function useAppRoutes() {
path={RouteUrls.TransactionRequest}
element={
<AccountGate>
<Suspense fallback={<LoadingSpinner height="600px" />}>
<Suspense fallback={<SuspenseLoadingSpinner />}>
<TransactionRequest />
</Suspense>
</AccountGate>
@@ -111,7 +116,7 @@ function useAppRoutes() {
path={RouteUrls.SignatureRequest}
element={
<AccountGate>
<Suspense fallback={<LoadingSpinner height="600px" />}>
<Suspense fallback={<SuspenseLoadingSpinner />}>
<StacksMessageSigningRequest />
</Suspense>
</AccountGate>
@@ -123,7 +128,7 @@ function useAppRoutes() {
path={RouteUrls.ProfileUpdateRequest}
element={
<AccountGate>
<Suspense fallback={<LoadingSpinner height="600px" />}>
<Suspense fallback={<SuspenseLoadingSpinner />}>
<ProfileUpdateRequest />
</Suspense>
</AccountGate>
@@ -133,7 +138,7 @@ function useAppRoutes() {
path={RouteUrls.PsbtRequest}
element={
<AccountGate>
<Suspense fallback={<LoadingSpinner height="600px" />}>
<Suspense fallback={<SuspenseLoadingSpinner />}>
<PsbtRequest />
</Suspense>
</AccountGate>
@@ -170,6 +175,14 @@ function useAppRoutes() {
</AccountGate>
}
/>
<Route
path={RouteUrls.RpcSignPsbtSummary}
element={
<AccountGate>
<RpcSignPsbtSummary />
</AccountGate>
}
/>
</>
);
@@ -232,7 +245,7 @@ function useAppRoutes() {
path={RouteUrls.RpcReceiveBitcoinContractOffer}
element={
<AccountGate>
<Suspense fallback={<LoadingSpinner height="600px" />}>
<Suspense fallback={<SuspenseLoadingSpinner />}>
<BitcoinContractRequest />
</Suspense>
</AccountGate>

View File

@@ -80,6 +80,10 @@ export async function rpcSignPsbt(message: SignPsbtRequest, port: chrome.runtime
requestParams.push(['allowedSighash', hash.toString()])
);
if (isDefined(message.params.broadcast)) {
requestParams.push(['broadcast', message.params.broadcast.toString()]);
}
if (isDefined(message.params.signAtIndex))
ensureArray(message.params.signAtIndex).forEach(index =>
requestParams.push(['signAtIndex', index.toString()])

View File

@@ -95,6 +95,7 @@ export enum RouteUrls {
// Rpc request routes
RpcGetAddresses = '/get-addresses',
RpcSignPsbt = '/sign-psbt',
RpcSignPsbtSummary = '/sign-psbt/summary',
RpcSendTransfer = '/send-transfer',
RpcSendTransferChooseFee = '/send-transfer/choose-fee',
RpcSendTransferConfirmation = '/send-transfer/confirm',

View File

@@ -27,6 +27,7 @@ const rpcSignPsbtParamsSchema = yup.object().shape({
...Object.values(btc.SignatureHash).filter(Number.isInteger),
])
),
broadcast: yup.boolean(),
hex: yup.string().required(),
network: yup.string().oneOf(Object.values(WalletDefaultNetworkConfigurationIds)),
signAtIndex: yup.mixed<number | number[]>().test(testIsNumberOrArrayOfNumbers),