diff --git a/src/app/components/address-displayer/address-displayer.layout.tsx b/src/app/components/address-displayer/address-displayer.layout.tsx index 1398888f73a..bc6fa4ee9af 100644 --- a/src/app/components/address-displayer/address-displayer.layout.tsx +++ b/src/app/components/address-displayer/address-displayer.layout.tsx @@ -13,6 +13,7 @@ export function AddressDisplayerLayout({ isEven, ...props }: AddressDisplayerLay color={isEven ? figmaTheme.textSubdued : figmaTheme.text} fontFamily="Fira Code" mr="tight" + lineHeight="24px" {...props} /> ); diff --git a/src/app/components/info-card/info-card.tsx b/src/app/components/info-card/info-card.tsx index fec862ff942..34edf2a1649 100644 --- a/src/app/components/info-card/info-card.tsx +++ b/src/app/components/info-card/info-card.tsx @@ -35,6 +35,8 @@ export function InfoCardRow({ title, value, ...props }: InfoCardRowProps) { color={figmaTheme.text} fontWeight="500" data-testid={SharedComponentsSelectors.InfoCardRowValue} + fontVariant="tabular-nums" + letterSpacing="-0.01em" > {value} diff --git a/src/app/pages/psbt-request/hooks/use-psbt-request.tsx b/src/app/pages/psbt-request/hooks/use-psbt-request.tsx index 5aba6275fa8..fc0f25d6c88 100644 --- a/src/app/pages/psbt-request/hooks/use-psbt-request.tsx +++ b/src/app/pages/psbt-request/hooks/use-psbt-request.tsx @@ -11,14 +11,8 @@ import { isNumber, isUndefined } from '@shared/utils'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { useOnMount } from '@app/common/hooks/use-on-mount'; import { getPsbtPayloadFromToken } from '@app/common/psbt/requests'; -import { - useSignBitcoinNativeSegwitInputAtIndex, - useSignBitcoinNativeSegwitTx, -} from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; -import { - useSignBitcoinTaprootInputAtIndex, - useSignBitcoinTaprootTx, -} from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; +import { useCurrentAccountNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; +import { useCurrentAccountTaprootSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; import { usePsbtRequestSearchParams } from '@app/store/psbts/requests.hooks'; export function usePsbtRequest() { @@ -27,10 +21,12 @@ export function usePsbtRequest() { const [tx, setTx] = useState(); const analytics = useAnalytics(); const { requestToken, tabId } = usePsbtRequestSearchParams(); - const signNativeSegwitTxAtIndex = useSignBitcoinNativeSegwitInputAtIndex(); - const signNativeSegwitTx = useSignBitcoinNativeSegwitTx(); - const signTaprootTxAtIndex = useSignBitcoinTaprootInputAtIndex(); - const signTaprootTx = useSignBitcoinTaprootTx(); + + const createNativeSegwitSigner = useCurrentAccountNativeSegwitSigner(); + const createTaprootSigner = useCurrentAccountTaprootSigner(); + + const nativeSegwitSigner = createNativeSegwitSigner?.(0); + const taprootSigner = createTaprootSigner?.(0); useOnMount(() => { if (!requestToken) return; @@ -49,6 +45,7 @@ export function usePsbtRequest() { const getDecodedPsbt = useCallback(() => { if (!psbtPayload || !tx) return; + try { return btc.RawPSBTV0.decode(hexToBytes(psbtPayload.hex)); } catch (e0) { @@ -69,16 +66,16 @@ export function usePsbtRequest() { const signPsbtAtIndex = useCallback( (allowedSighash: btc.SignatureHash[], idx: number, tx: btc.Transaction) => { try { - signNativeSegwitTxAtIndex({ allowedSighash, idx, tx }); + nativeSegwitSigner?.signIndex(tx, idx, allowedSighash); } catch (e1) { try { - signTaprootTxAtIndex({ allowedSighash, idx, tx }); + taprootSigner?.signIndex(tx, idx, allowedSighash); } catch (e2) { logger.error('Error signing tx at provided index', e1, e2); } } }, - [signNativeSegwitTxAtIndex, signTaprootTxAtIndex] + [nativeSegwitSigner, taprootSigner] ); const onSignPsbt = useCallback(() => { @@ -101,10 +98,10 @@ export function usePsbtRequest() { } } else { try { - signNativeSegwitTx(tx); + nativeSegwitSigner?.sign(tx); } catch (e1) { try { - signTaprootTx(tx); + taprootSigner?.sign(tx); } catch (e2) { logger.error('Error signing tx', e1, e2); } @@ -124,13 +121,13 @@ export function usePsbtRequest() { }); }, [ analytics, + nativeSegwitSigner, psbtPayload?.allowedSighash, psbtPayload?.signAtIndex, requestToken, - signNativeSegwitTx, signPsbtAtIndex, - signTaprootTx, tabId, + taprootSigner, tx, ]); diff --git a/src/app/pages/send/send-crypto-asset-form/family/bitcoin/hooks/use-generate-bitcoin-tx.ts b/src/app/pages/send/send-crypto-asset-form/family/bitcoin/hooks/use-generate-bitcoin-tx.ts index ae0fb30e415..c9ba61b8757 100644 --- a/src/app/pages/send/send-crypto-asset-form/family/bitcoin/hooks/use-generate-bitcoin-tx.ts +++ b/src/app/pages/send/send-crypto-asset-form/family/bitcoin/hooks/use-generate-bitcoin-tx.ts @@ -2,6 +2,7 @@ import { useCallback } from 'react'; import * as btc from '@scure/btc-signer'; +import { logger } from '@shared/logger'; import { BitcoinSendFormValues } from '@shared/models/form.model'; import { btcToSat } from '@app/common/money/unit-conversion'; @@ -9,24 +10,27 @@ import { determineUtxosForSpend } from '@app/common/transactions/bitcoin/coinsel import { useGetUtxosByAddressQuery } from '@app/query/bitcoin/address/utxos-by-address.query'; import { useBitcoinLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain'; import { + useCurrentAccountNativeSegwitSigner, useCurrentBitcoinNativeSegwitAddressIndexPublicKeychain, useCurrentBtcNativeSegwitAccountAddressIndexZero, - useSignBitcoinNativeSegwitTx, } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; export function useGenerateSignedBitcoinTx() { const currentAccountBtcAddress = useCurrentBtcNativeSegwitAccountAddressIndexZero(); const { data: utxos } = useGetUtxosByAddressQuery(currentAccountBtcAddress); const currentAddressIndexKeychain = useCurrentBitcoinNativeSegwitAddressIndexPublicKeychain(); - const signTx = useSignBitcoinNativeSegwitTx(); + const createSigner = useCurrentAccountNativeSegwitSigner(); const networkMode = useBitcoinLibNetworkConfig(); return useCallback( (values: BitcoinSendFormValues, feeRate: number) => { if (!utxos) return; if (!feeRate) return; + if (!createSigner) return; try { + const signer = createSigner(0); + const tx = new btc.Transaction(); const { inputs, outputs, fee } = determineUtxosForSpend({ @@ -36,11 +40,10 @@ export function useGenerateSignedBitcoinTx() { feeRate, }); - // eslint-disable-next-line no-console - console.log('coinselect', { inputs, outputs, fee }); + logger.info('coinselect', { inputs, outputs, fee }); - if (!inputs) throw new Error('No inputs to sign'); - if (!outputs) throw new Error('No outputs to sign'); + if (!inputs.length) throw new Error('No inputs to sign'); + if (!outputs.length) throw new Error('No outputs to sign'); if (outputs.length > 2) throw new Error('Address reuse mode: wallet should have max 2 outputs'); @@ -66,8 +69,9 @@ export function useGenerateSignedBitcoinTx() { } tx.addOutputAddress(values.recipient, BigInt(output.value), networkMode); }); - signTx(tx); + signer.sign(tx); tx.finalize(); + return { hex: tx.hex, fee }; } catch (e) { // eslint-disable-next-line no-console @@ -75,6 +79,12 @@ export function useGenerateSignedBitcoinTx() { return null; } }, - [currentAccountBtcAddress, currentAddressIndexKeychain?.publicKey, networkMode, signTx, utxos] + [ + createSigner, + currentAccountBtcAddress, + currentAddressIndexKeychain?.publicKey, + networkMode, + utxos, + ] ); } 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 f4228f9afaa..61867fb8db8 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 @@ -11,7 +11,7 @@ import { RouteUrls } from '@shared/route-urls'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { useRouteHeader } from '@app/common/hooks/use-route-header'; import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money'; -import { formatMoney, formatMoneyPadded, i18nFormatCurrency } from '@app/common/money/format-money'; +import { formatMoneyPadded, i18nFormatCurrency } from '@app/common/money/format-money'; import { satToBtc } from '@app/common/money/unit-conversion'; import { FormAddressDisplayer } from '@app/components/address-displayer/form-address-displayer'; import { @@ -61,10 +61,10 @@ export function BtcSendFormConfirmation() { const txFiatValueSymbol = btcMarketData.price.symbol; const feeInBtc = satToBtc(fee); - const totalSpend = formatMoney( + const totalSpend = formatMoneyPadded( createMoneyFromDecimal(Number(transferAmount) + Number(feeInBtc), symbol) ); - const sendingValue = formatMoney(createMoneyFromDecimal(Number(transferAmount), symbol)); + const sendingValue = formatMoneyPadded(createMoneyFromDecimal(Number(transferAmount), symbol)); const summaryFee = formatMoneyPadded(createMoney(Number(fee), symbol)); async function initiateTransaction() { diff --git a/src/app/store/accounts/blockchain/bitcoin/bitcoin-keychain.ts b/src/app/store/accounts/blockchain/bitcoin/bitcoin-keychain.ts index 95c005611d1..ef4b5905fb2 100644 --- a/src/app/store/accounts/blockchain/bitcoin/bitcoin-keychain.ts +++ b/src/app/store/accounts/blockchain/bitcoin/bitcoin-keychain.ts @@ -1,13 +1,14 @@ import { createSelector } from '@reduxjs/toolkit'; import { HDKey } from '@scure/bip32'; +import * as btc from '@scure/btc-signer'; -import { NetworkModes } from '@shared/constants'; +import { BitcoinNetworkModes, NetworkModes } from '@shared/constants'; import { getBtcSignerLibNetworkConfigByMode } from '@shared/crypto/bitcoin/bitcoin.network'; import { deriveAddressIndexZeroFromAccount } from '@shared/crypto/bitcoin/bitcoin.utils'; import { deriveTaprootAccountFromRootKeychain } from '@shared/crypto/bitcoin/p2tr-address-gen'; import { deriveNativeSegWitAccountKeychain, - getNativeSegWitAddressIndexFromKeychain, + getNativeSegWitPaymentFromKeychain, } from '@shared/crypto/bitcoin/p2wpkh-address-gen'; import { mnemonicToRootNode } from '@app/common/keychain/keychain'; @@ -38,7 +39,7 @@ export function getNativeSegwitMainnetAddressFromMnemonic(secretKey: string) { return (accountIndex: number) => { const rootNode = mnemonicToRootNode(secretKey); const account = deriveNativeSegWitAccountKeychain(rootNode, 'mainnet')(accountIndex); - return getNativeSegWitAddressIndexFromKeychain( + return getNativeSegWitPaymentFromKeychain( deriveAddressIndexZeroFromAccount(account), 'mainnet' ); @@ -69,3 +70,30 @@ export function useBitcoinLibNetworkConfig() { const network = useCurrentNetwork(); return getBtcSignerLibNetworkConfigByMode(network.chain.bitcoin.network); } + +interface BitcoinSignerFactoryArgs { + addressIndexKeychainFn(addressIndex: number): HDKey; + paymentFn(keychain: HDKey, network: BitcoinNetworkModes): unknown; + network: BitcoinNetworkModes; +} +export function bitcoinSignerFactory(args: T) { + const { network, paymentFn, addressIndexKeychainFn } = args; + return (addressIndex: number) => { + const addressIndexKeychain = addressIndexKeychainFn(addressIndex); + return { + payment: paymentFn(addressIndexKeychain, network) as ReturnType, + sign(tx: btc.Transaction) { + if (!addressIndexKeychain.privateKey) + throw new Error('Unable to sign taproot transaction, no private key found'); + + tx.sign(addressIndexKeychain.privateKey); + }, + signIndex(tx: btc.Transaction, index: number, allowedSighash?: btc.SignatureHash[]) { + if (!addressIndexKeychain.privateKey) + throw new Error('Unable to sign taproot transaction, no private key found'); + + tx.signIdx(addressIndexKeychain.privateKey, index, allowedSighash); + }, + }; + }; +} 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 acae4b2a2f3..b0689a8877a 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 @@ -2,7 +2,6 @@ import { useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { HDKey } from '@scure/bip32'; -import * as btc from '@scure/btc-signer'; import { deriveAddressIndexKeychainFromAccount, @@ -10,7 +9,7 @@ import { } from '@shared/crypto/bitcoin/bitcoin.utils'; import { deriveNativeSegWitReceiveAddressIndex, - getNativeSegWitAddressIndexFromKeychain, + getNativeSegWitPaymentFromKeychain, } from '@shared/crypto/bitcoin/p2wpkh-address-gen'; import { isUndefined } from '@shared/utils'; @@ -23,6 +22,7 @@ import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; import { useCurrentAccountIndex } from '../../account'; import { + bitcoinSignerFactory, selectMainnetNativeSegWitKeychain, selectTestnetNativeSegWitKeychain, } from './bitcoin-keychain'; @@ -116,67 +116,16 @@ export function useBtcNativeSegwitAccountIndexAddressIndexZero(accountIndex: num return useDeriveNativeSegWitAccountIndexAddressIndexZero(xpub)?.address as string; } -/** - * @deprecated - * Let's update to use the signer structure, also used in taproot - */ -export function useSignBitcoinNativeSegwitTx() { - const index = useCurrentAccountIndex(); - const keychain = useNativeSegWitCurrentNetworkAccountKeychain()?.(index); - return useCallback( - (tx: btc.Transaction) => { - if (isUndefined(keychain)) return; - tx.sign(deriveAddressIndexZeroFromAccount(keychain).privateKey!); - }, - [keychain] - ); -} - export function useCurrentAccountNativeSegwitSigner() { const network = useCurrentNetwork(); const index = useCurrentAccountIndex(); const accountKeychain = useNativeSegWitCurrentNetworkAccountKeychain()?.(index); if (!accountKeychain) return; - const addressIndexKeychainFn = deriveAddressIndexKeychainFromAccount(accountKeychain); - return (addressIndex: number) => { - const addressIndexKeychain = addressIndexKeychainFn(addressIndex); - return { - payment: getNativeSegWitAddressIndexFromKeychain( - addressIndexKeychain, - network.chain.bitcoin.network - ), - sign(tx: btc.Transaction) { - if (!addressIndexKeychain.privateKey) - throw new Error('Unable to sign taproot transaction, no private key found'); - - tx.sign(addressIndexKeychain.privateKey); - }, - signIndex(tx: btc.Transaction, index: number) { - if (!addressIndexKeychain.privateKey) - throw new Error('Unable to sign taproot transaction, no private key found'); - - tx.signIdx(addressIndexKeychain.privateKey, index); - }, - }; - }; -} - -interface UseSignBitcoinNativeSegwitInputAtIndexArgs { - allowedSighash?: btc.SignatureHash[]; - idx: number; - tx: btc.Transaction; -} -export function useSignBitcoinNativeSegwitInputAtIndex() { - const index = useCurrentAccountIndex(); - const keychain = useNativeSegWitCurrentNetworkAccountKeychain()?.(index); - - return useCallback( - ({ allowedSighash, idx, tx }: UseSignBitcoinNativeSegwitInputAtIndexArgs) => { - if (isUndefined(keychain)) return; - tx.signIdx(deriveAddressIndexZeroFromAccount(keychain).privateKey!, idx, allowedSighash); - }, - [keychain] - ); + return bitcoinSignerFactory({ + addressIndexKeychainFn, + paymentFn: getNativeSegWitPaymentFromKeychain, + network: network.chain.bitcoin.network, + }); } diff --git a/src/app/store/accounts/blockchain/bitcoin/taproot-account.hooks.ts b/src/app/store/accounts/blockchain/bitcoin/taproot-account.hooks.ts index 6a5494aaf2e..e879fa5f561 100644 --- a/src/app/store/accounts/blockchain/bitcoin/taproot-account.hooks.ts +++ b/src/app/store/accounts/blockchain/bitcoin/taproot-account.hooks.ts @@ -1,8 +1,6 @@ import { useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; -import * as btc from '@scure/btc-signer'; - import { deriveAddressIndexKeychainFromAccount, deriveAddressIndexZeroFromAccount, @@ -18,7 +16,11 @@ import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; import { useCurrentAccountIndex } from '../../account'; import { formatBitcoinAccount, tempHardwareAccountForTesting } from './bitcoin-account.models'; -import { selectMainnetTaprootKeychain, selectTestnetTaprootKeychain } from './bitcoin-keychain'; +import { + bitcoinSignerFactory, + selectMainnetTaprootKeychain, + selectTestnetTaprootKeychain, +} from './bitcoin-keychain'; function useTaprootCurrentNetworkAccountPrivateKeychain() { const network = useCurrentNetwork(); @@ -83,37 +85,6 @@ export function useCurrentBtcTaprootAccountAddressIndexZeroPayment() { return { address: payment.address, type: payment.type }; } -export function useSignBitcoinTaprootTx() { - const index = useCurrentAccountIndex(); - const keychain = useTaprootCurrentNetworkAccountPrivateKeychain()?.(index); - - return useCallback( - (tx: btc.Transaction) => { - if (isUndefined(keychain)) return; - tx.sign(deriveAddressIndexZeroFromAccount(keychain).privateKey!); - }, - [keychain] - ); -} - -interface UseSignBitcoinTaprootInputAtIndexArgs { - allowedSighash?: btc.SignatureHash[]; - idx: number; - tx: btc.Transaction; -} -export function useSignBitcoinTaprootInputAtIndex() { - const index = useCurrentAccountIndex(); - const keychain = useTaprootCurrentNetworkAccountPrivateKeychain()?.(index); - - return useCallback( - ({ allowedSighash, idx, tx }: UseSignBitcoinTaprootInputAtIndexArgs) => { - if (isUndefined(keychain)) return; - tx.signIdx(deriveAddressIndexZeroFromAccount(keychain).privateKey!, idx, allowedSighash); - }, - [keychain] - ); -} - // TODO: Address index 0 is hardcoded here bc this is only used to pass the first // taproot address to the app thru the auth response. This is only temporary, it // should be removed once the request address api is in place. @@ -149,25 +120,9 @@ export function useCurrentAccountTaprootSigner() { if (!accountKeychain) return; // TODO: Revisit this return early const addressIndexKeychainFn = deriveAddressIndexKeychainFromAccount(accountKeychain); - return (addressIndex: number) => { - const addressIndexKeychain = addressIndexKeychainFn(addressIndex); - return { - payment: getTaprootPaymentFromAddressIndex( - addressIndexKeychain, - network.chain.bitcoin.network - ), - sign(tx: btc.Transaction) { - if (!addressIndexKeychain.privateKey) - throw new Error('Unable to sign taproot transaction, no private key found'); - - tx.sign(addressIndexKeychain.privateKey); - }, - signIndex(tx: btc.Transaction, index: number) { - if (!addressIndexKeychain.privateKey) - throw new Error('Unable to sign taproot transaction, no private key found'); - - tx.signIdx(addressIndexKeychain.privateKey, index); - }, - }; - }; + return bitcoinSignerFactory({ + addressIndexKeychainFn, + paymentFn: getTaprootPaymentFromAddressIndex, + network: network.chain.bitcoin.network, + }); } diff --git a/src/shared/crypto/bitcoin/p2wpkh-address-gen.ts b/src/shared/crypto/bitcoin/p2wpkh-address-gen.ts index b215b132697..c3758b74c1d 100644 --- a/src/shared/crypto/bitcoin/p2wpkh-address-gen.ts +++ b/src/shared/crypto/bitcoin/p2wpkh-address-gen.ts @@ -19,14 +19,13 @@ export function deriveNativeSegWitAccountKeychain(keychain: HDKey, network: Netw return (index: number) => keychain.derive(getNativeSegWitAccountDerivationPath(network, index)); } -export function getNativeSegWitAddressIndexFromKeychain( - keychain: HDKey, - network: BitcoinNetworkModes -) { +export function getNativeSegWitPaymentFromKeychain(keychain: HDKey, network: BitcoinNetworkModes) { if (keychain.depth !== DerivationPathDepth.AddressIndex) throw new Error('Keychain passed is not an address index'); - return btc.p2wpkh(keychain.publicKey!, getBtcSignerLibNetworkConfigByMode(network)); + if (!keychain.publicKey) throw new Error('Keychain does not have a public key'); + + return btc.p2wpkh(keychain.publicKey, getBtcSignerLibNetworkConfigByMode(network)); } interface DeriveNativeSegWitReceiveAddressIndexArgs { @@ -41,5 +40,5 @@ export function deriveNativeSegWitReceiveAddressIndex({ const keychain = HDKey.fromExtendedKey(xpub); if (!keychain) return; const zeroAddressIndex = deriveAddressIndexZeroFromAccount(keychain); - return getNativeSegWitAddressIndexFromKeychain(zeroAddressIndex, network); + return getNativeSegWitPaymentFromKeychain(zeroAddressIndex, network); }