mirror of
https://github.com/zhigang1992/wallet.git
synced 2026-01-12 22:53:27 +08:00
feat: add option to broadcast rpc psbt, closes #3895
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ interface InfoCardAssetValueProps extends StackProps {
|
||||
value: number;
|
||||
fiatValue?: string;
|
||||
fiatSymbol?: string;
|
||||
symbol: string;
|
||||
symbol?: string;
|
||||
icon?: React.FC;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
30
src/app/features/psbt-signer/psbt-signer.context.ts
Normal file
30
src/app/features/psbt-signer/psbt-signer.context.ts
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
66
src/app/pages/rpc-sign-psbt/rpc-sign-psbt-summary.tsx
Normal file
66
src/app/pages/rpc-sign-psbt/rpc-sign-psbt-summary.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()])
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user