diff --git a/src/app/common/publish-subscribe.ts b/src/app/common/publish-subscribe.ts index c36e4c472a1..e866c39567d 100644 --- a/src/app/common/publish-subscribe.ts +++ b/src/app/common/publish-subscribe.ts @@ -1,4 +1,6 @@ +import type { Transaction } from '@scure/btc-signer'; import type { StacksTransaction } from '@stacks/transactions'; +import type { Psbt } from 'bitcoinjs-lib'; type PubTypeFn = ( event: Key, @@ -19,7 +21,7 @@ interface PubSubType { subscribe: SubTypeFn; unsubscribe: SubTypeFn; } -function PublishSubscribe(): PubSubType { +function createPublishSubscribe(): PubSubType { const handlers: { [key: string]: MessageFn[] } = {}; return { @@ -42,7 +44,7 @@ function PublishSubscribe(): PubSubType { } // Global app events. Only add events if your feature isn't capable of -//communicating internally. +// communicating internally. export interface GlobalAppEvents { ledgerStacksTxSigned: { unsignedTx: string; @@ -51,6 +53,13 @@ export interface GlobalAppEvents { ledgerStacksTxSigningCancelled: { unsignedTx: string; }; + ledgerBitcoinTxSigned: { + unsignedPsbt: string; + signedPsbt: Transaction; + }; + ledgerBitcoinTxSigningCancelled: { + unsignedPsbt: string; + }; } -export const appEvents = PublishSubscribe(); +export const appEvents = createPublishSubscribe(); diff --git a/src/app/features/increase-fee-drawer/components/increase-stx-fee-form.tsx b/src/app/features/increase-fee-drawer/components/increase-stx-fee-form.tsx index 0233ee3fc0e..0619d5ff8c2 100644 --- a/src/app/features/increase-fee-drawer/components/increase-stx-fee-form.tsx +++ b/src/app/features/increase-fee-drawer/components/increase-stx-fee-form.tsx @@ -63,7 +63,7 @@ export function IncreaseStxFeeForm() { await replaceByFee(rawTx); }, ledger: () => { - ledgerNavigate.toConnectAndSignTransactionStep(rawTx); + ledgerNavigate.toConnectAndSignStacksTransactionStep(rawTx); }, })(); }, diff --git a/src/app/features/ledger/flows/bitcoin-tx-signing/bitcoin-tx-signing-event-listeners.ts b/src/app/features/ledger/flows/bitcoin-tx-signing/bitcoin-tx-signing-event-listeners.ts new file mode 100644 index 00000000000..0c93a3e0fc1 --- /dev/null +++ b/src/app/features/ledger/flows/bitcoin-tx-signing/bitcoin-tx-signing-event-listeners.ts @@ -0,0 +1,25 @@ +import * as btc from '@scure/btc-signer'; + +import { GlobalAppEvents, appEvents } from '@app/common/publish-subscribe'; + +export async function listenForBitcoinTxLedgerSigning(psbt: string): Promise { + return new Promise((resolve, reject) => { + function txSignedHandler(msg: GlobalAppEvents['ledgerBitcoinTxSigned']) { + if (msg.unsignedPsbt === psbt) { + appEvents.unsubscribe('ledgerBitcoinTxSigned', txSignedHandler); + appEvents.unsubscribe('ledgerBitcoinTxSigningCancelled', signingAbortedHandler); + resolve(msg.signedPsbt); + } + } + appEvents.subscribe('ledgerBitcoinTxSigned', txSignedHandler); + + function signingAbortedHandler(msg: GlobalAppEvents['ledgerBitcoinTxSigningCancelled']) { + if (msg.unsignedPsbt === psbt) { + appEvents.unsubscribe('ledgerBitcoinTxSigningCancelled', signingAbortedHandler); + appEvents.unsubscribe('ledgerBitcoinTxSigned', txSignedHandler); + reject(new Error('User cancelled the signing operation')); + } + } + appEvents.subscribe('ledgerBitcoinTxSigningCancelled', signingAbortedHandler); + }); +} diff --git a/src/app/features/ledger/flows/bitcoin-tx-signing/ledger-bitcoin-sign-tx-container.tsx b/src/app/features/ledger/flows/bitcoin-tx-signing/ledger-bitcoin-sign-tx-container.tsx index 1a5623d0aaf..77cd82a512f 100644 --- a/src/app/features/ledger/flows/bitcoin-tx-signing/ledger-bitcoin-sign-tx-container.tsx +++ b/src/app/features/ledger/flows/bitcoin-tx-signing/ledger-bitcoin-sign-tx-container.tsx @@ -2,13 +2,16 @@ import { useEffect, useState } from 'react'; import toast from 'react-hot-toast'; import { Outlet, Route, useLocation, useNavigate } from 'react-router-dom'; +import { bytesToHex } from '@noble/hashes/utils'; import * as btc from '@scure/btc-signer'; import { hexToBytes } from '@stacks/common'; +import { Psbt } from 'bitcoinjs-lib'; import get from 'lodash.get'; import { RouteUrls } from '@shared/route-urls'; import { useScrollLock } from '@app/common/hooks/use-scroll-lock'; +import { appEvents } from '@app/common/publish-subscribe'; import { delay } from '@app/common/utils'; import { BaseDrawer } from '@app/components/drawer/base-drawer'; import { @@ -62,16 +65,19 @@ export function LedgerSignBitcoinTxContainer() { useScrollLock(true); const navigate = useNavigate(); - const { broadcastTx } = useBitcoinBroadcastTransaction(); const canUserCancelAction = useActionCancellableByUser(); + const [unsignedTransactionRaw, setUnsignedTransactionRaw] = useState(null); const [unsignedTransaction, setUnsignedTransaction] = useState(null); const signLedger = useSignLedgerTx(); const sendFormNavigate = useSendFormNavigate(); useEffect(() => { const tx = get(location.state, 'tx'); - if (tx) console.log({ tx, decoded: btc.Transaction.fromPSBT(hexToBytes(tx)) }); - if (tx) setUnsignedTransaction(btc.Transaction.fromPSBT(hexToBytes(tx))); + if (tx) { + setUnsignedTransactionRaw(tx); + console.log({ tx, decoded: btc.Transaction.fromPSBT(hexToBytes(tx)) }); + setUnsignedTransaction(btc.Transaction.fromPSBT(hexToBytes(tx))); + } }, [location.state]); useEffect(() => () => setUnsignedTransaction(null), []); @@ -89,7 +95,6 @@ export function LedgerSignBitcoinTxContainer() { ledgerNavigate.toDeviceBusyStep('Verifying public key on Ledger…'); - // try { ledgerNavigate.toConnectionSuccessStep('bitcoin'); await delay(1000); if (!unsignedTransaction) throw new Error('No unsigned tx'); @@ -97,33 +102,17 @@ export function LedgerSignBitcoinTxContainer() { ledgerNavigate.toAwaitingDeviceOperation({ hasApprovedOperation: false }); try { - const resp = await signLedger(bitcoinApp, unsignedTransaction.toPSBT()); - if (!resp) throw new Error('No tx returned'); - console.log(resp); + const btcTx = await signLedger(bitcoinApp, unsignedTransaction.toPSBT()); + if (!btcTx || !unsignedTransactionRaw) throw new Error('No tx returned'); + console.log('response from ledger', { resp: btcTx }); ledgerNavigate.toAwaitingDeviceOperation({ hasApprovedOperation: true }); await delay(1000); - await broadcastTx({ - tx: resp.hex, - onSuccess(txid) { - console.log(txid); - toast.success('Tx broadcast'); - navigate('/activity', { replace: true }); - }, - onError(e) { - console.log(e); - }, + appEvents.publish('ledgerBitcoinTxSigned', { + signedPsbt: btcTx, + unsignedPsbt: unsignedTransactionRaw, }); - navigate('/activity', { replace: true }); - // sendFormNavigate.toConfirmAndSignBtcTransaction({ - // fee: 1000, - // feeRowValue: '1231', - // recipient: 'anslkdjfs', - // time: 'slkdjfslkdf', - // tx: resp?.toHex(), - // }); } catch (e) { - console.log('error', e); ledgerAnalytics.transactionSignedOnLedgerRejected(); ledgerNavigate.toOperationRejectedStep(); } diff --git a/src/app/features/ledger/hooks/use-ledger-navigate.ts b/src/app/features/ledger/hooks/use-ledger-navigate.ts index 44f74da7709..ffaeeea79b4 100644 --- a/src/app/features/ledger/hooks/use-ledger-navigate.ts +++ b/src/app/features/ledger/hooks/use-ledger-navigate.ts @@ -3,6 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { bytesToHex } from '@stacks/common'; import { ClarityValue, StacksTransaction } from '@stacks/transactions'; +import { Psbt } from 'bitcoinjs-lib'; import { SupportedBlockchains } from '@shared/constants'; import { RouteUrls } from '@shared/route-urls'; @@ -22,7 +23,7 @@ export function useLedgerNavigate() { }); }, - toConnectAndSignTransactionStep(transaction: StacksTransaction) { + toConnectAndSignStacksTransactionStep(transaction: StacksTransaction) { return navigate(RouteUrls.ConnectLedger, { replace: true, relative: 'path', @@ -30,6 +31,14 @@ export function useLedgerNavigate() { }); }, + toConnectAndSignBitcoinTransactionStep(psbt: Psbt) { + return navigate(RouteUrls.ConnectLedger, { + replace: true, + relative: 'path', + state: { tx: psbt.toHex() }, + }); + }, + toConnectAndSignUtf8MessageStep(message: string) { return navigate(RouteUrls.ConnectLedger, { replace: true, diff --git a/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form-confirmation.tsx b/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form-confirmation.tsx index 5687ced6625..6058042d77a 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form-confirmation.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form-confirmation.tsx @@ -1,5 +1,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; +import { hexToBytes } from '@noble/hashes/utils'; +import * as btc from '@scure/btc-signer'; import { Stack } from '@stacks/ui'; import { SendCryptoAssetSelectors } from '@tests/selectors/send.selectors'; import get from 'lodash.get'; @@ -47,16 +49,22 @@ function useBtcSendFormConfirmationState() { export function BtcSendFormConfirmation() { const navigate = useNavigate(); const { tx, recipient, fee, arrivesIn, feeRowValue } = useBtcSendFormConfirmationState(); + console.log({ tx }); const { refetch } = useCurrentNativeSegwitUtxos(); const analytics = useAnalytics(); const btcMarketData = useCryptoCurrencyMarketData('BTC'); const { broadcastTx, isBroadcasting } = useBitcoinBroadcastTransaction(); - const psbt = decodeBitcoinTx(tx); + + const transaction = btc.Transaction.fromRaw(hexToBytes(tx)); + + // const psbt = Psbt.fromHex(transaction.toPSBT()); + + const decodedTx = decodeBitcoinTx(transaction.hex); const nav = useSendFormNavigate(); - const transferAmount = satToBtc(psbt.outputs[0].amount.toString()).toString(); + const transferAmount = satToBtc(decodedTx.outputs[0].amount.toString()).toString(); const txFiatValue = i18nFormatCurrency( baseCurrencyAmountInQuote(createMoneyFromDecimal(Number(transferAmount), symbol), btcMarketData) ); @@ -71,14 +79,14 @@ export function BtcSendFormConfirmation() { async function initiateTransaction() { await broadcastTx({ - tx, + tx: transaction.hex, async onSuccess(txid) { void analytics.track('broadcast_transaction', { symbol: 'btc', amount: transferAmount, fee, - inputs: psbt.inputs.length, - outputs: psbt.inputs.length, + inputs: decodedTx.inputs.length, + outputs: decodedTx.inputs.length, }); await refetch(); navigate(RouteUrls.SentBtcTxSummary.replace(':txId', `${txid}`), { @@ -86,9 +94,10 @@ export function BtcSendFormConfirmation() { }); // invalidate txs query after some time to ensure that the new tx will be shown in the list - setTimeout(() => { - void queryClient.invalidateQueries({ queryKey: ['btc-txs-by-address'] }); - }, 2000); + setTimeout( + () => void queryClient.invalidateQueries({ queryKey: ['btc-txs-by-address'] }), + 2000 + ); }, onError(e) { nav.toErrorPage(e); diff --git a/src/app/pages/send/send-crypto-asset-form/form/btc/use-btc-choose-fee.tsx b/src/app/pages/send/send-crypto-asset-form/form/btc/use-btc-choose-fee.tsx index 0f25964d3c7..23af5b3d59d 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/btc/use-btc-choose-fee.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/btc/use-btc-choose-fee.tsx @@ -1,17 +1,16 @@ import { useNavigate } from 'react-router-dom'; -import { bytesToHex } from '@noble/hashes/utils'; +import { Psbt } from 'bitcoinjs-lib'; import { logger } from '@shared/logger'; import { BtcFeeType } from '@shared/models/fees/bitcoin-fees.model'; import { createMoney } from '@shared/models/money.model'; -import { RouteUrls } from '@shared/route-urls'; import { btcToSat } from '@app/common/money/unit-conversion'; import { formFeeRowValue } from '@app/common/send/utils'; import { useGenerateSignedNativeSegwitTx } from '@app/common/transactions/bitcoin/use-generate-bitcoin-tx'; -import { useWalletType } from '@app/common/use-wallet-type'; import { OnChooseFeeArgs } from '@app/components/bitcoin-fees-list/bitcoin-fees-list'; +import { useSignBitcoinTx } from '@app/store/accounts/blockchain/bitcoin/bitcoin.hooks'; import { useSendBitcoinAssetContextState } from '../../family/bitcoin/components/send-bitcoin-asset-container'; import { useCalculateMaxBitcoinSpend } from '../../family/bitcoin/hooks/use-calculate-max-spend'; @@ -21,13 +20,12 @@ import { useBtcChooseFeeState } from './btc-choose-fee'; export function useBtcChooseFee() { const { isSendingMax, txValues, utxos } = useBtcChooseFeeState(); const navigate = useNavigate(); - const { whenWallet } = useWalletType(); const sendFormNavigate = useSendFormNavigate(); const generateTx = useGenerateSignedNativeSegwitTx(); const { setSelectedFeeType } = useSendBitcoinAssetContextState(); const calcMaxSpend = useCalculateMaxBitcoinSpend(); // const signLedger = useSignNativeSegwitLedgerTx(); - + const signTx = useSignBitcoinTx(); const amountAsMoney = createMoney(btcToSat(txValues.amount).toNumber(), 'BTC'); return { @@ -52,31 +50,19 @@ export function useBtcChooseFee() { const feeRowValue = formFeeRowValue(feeRate, isCustomFee); if (!resp) return logger.error('Attempted to generate raw tx, but no tx exists'); - void whenWallet({ - software: async () => { - sendFormNavigate.toConfirmAndSignBtcTransaction({ - tx: resp.hex, - recipient: txValues.recipient, - fee: feeValue, - feeRowValue, - time, - }); - }, - ledger: async () => { - console.log('opening route with', resp.hex); - // const app = await connectLedgerBitcoinApp(); - navigate(RouteUrls.ConnectLedger, { - replace: true, - state: { - tx: bytesToHex(resp.psbt), - recipient: txValues.recipient, - fee: feeValue, - feeRowValue, - time, - }, - }); - }, - })(); + const signedTx = await signTx(Psbt.fromBuffer(Buffer.from(resp.psbt))); + + if (!signedTx) return logger.error('Attempted to sign tx, but no tx exists'); + + signedTx.finalize(); + + sendFormNavigate.toConfirmAndSignBtcTransaction({ + tx: signedTx.hex, + recipient: txValues.recipient, + fee: feeValue, + feeRowValue, + time, + }); }, }; } diff --git a/src/app/pages/send/send-crypto-asset-form/form/stacks-sip10/use-sip10-send-form.tsx b/src/app/pages/send/send-crypto-asset-form/form/stacks-sip10/use-sip10-send-form.tsx index 022455b3eb0..cb2c0ea067b 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/stacks-sip10/use-sip10-send-form.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/stacks-sip10/use-sip10-send-form.tsx @@ -90,7 +90,7 @@ export function useSip10SendForm({ symbol, contractId }: UseSip10SendFormArgs) { name: assetBalance.asset.name, tx, }), - ledger: () => ledgerNavigate.toConnectAndSignTransactionStep(tx), + ledger: () => ledgerNavigate.toConnectAndSignStacksTransactionStep(tx), })(); }, }; diff --git a/src/app/store/accounts/blockchain/bitcoin/bitcoin.hooks.ts b/src/app/store/accounts/blockchain/bitcoin/bitcoin.hooks.ts index 9c7e4e0c3f5..af9fc10ba86 100644 --- a/src/app/store/accounts/blockchain/bitcoin/bitcoin.hooks.ts +++ b/src/app/store/accounts/blockchain/bitcoin/bitcoin.hooks.ts @@ -1,5 +1,10 @@ +import { Psbt } from 'bitcoinjs-lib'; + import { getTaprootAddress } from '@shared/crypto/bitcoin/bitcoin.utils'; +import { useWalletType } from '@app/common/use-wallet-type'; +import { listenForBitcoinTxLedgerSigning } from '@app/features/ledger/flows/bitcoin-tx-signing/bitcoin-tx-signing-event-listeners'; +import { useLedgerNavigate } from '@app/features/ledger/hooks/use-ledger-navigate'; import { useCurrentAccountIndex } from '@app/store/accounts/account'; import { useTaprootAccount } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; @@ -30,3 +35,19 @@ export function useZeroIndexTaprootAddress(accIndex?: number) { return address; } + +export function useSignBitcoinTx() { + const { whenWallet } = useWalletType(); + const ledgerNavigate = useLedgerNavigate(); + + return (tx: Psbt, inputsToSign?: [number, string]) => + whenWallet({ + async ledger(tx: Psbt) { + ledgerNavigate.toConnectAndSignBitcoinTransactionStep(tx); + return listenForBitcoinTxLedgerSigning(tx.toHex()); + }, + async software(tx: Psbt) { + // return signSoftwareTx(tx); + }, + })(tx); +} diff --git a/src/app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks.ts b/src/app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks.ts index 01179c1969c..6640d3f9a96 100644 --- a/src/app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks.ts +++ b/src/app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks.ts @@ -268,9 +268,7 @@ export function useSignLedgerTx() { } // Turn back into BtcSigner lib format - const btcSignerPsbt = btc.Transaction.fromPSBT(psbt.toBuffer()); - - btcSignerPsbt.finalize(); - return btcSignerPsbt; + // REMINDER: this tx is not finalized + return btc.Transaction.fromPSBT(psbt.toBuffer()); }; } diff --git a/src/app/store/transactions/transaction.hooks.ts b/src/app/store/transactions/transaction.hooks.ts index 21c884e5dba..b1286fb5e06 100644 --- a/src/app/store/transactions/transaction.hooks.ts +++ b/src/app/store/transactions/transaction.hooks.ts @@ -207,7 +207,7 @@ export function useSignStacksTransaction() { return (tx: StacksTransaction) => whenWallet({ async ledger(tx: StacksTransaction) { - ledgerNavigate.toConnectAndSignTransactionStep(tx); + ledgerNavigate.toConnectAndSignStacksTransactionStep(tx); return listenForStacksTxLedgerSigning(bytesToHex(tx.serialize())); }, async software(tx: StacksTransaction) {