diff --git a/src/app/pages/swap/bitflow-swap-container.tsx b/src/app/pages/swap/bitflow-swap-container.tsx index a017a988867..baf8ec3ad9a 100644 --- a/src/app/pages/swap/bitflow-swap-container.tsx +++ b/src/app/pages/swap/bitflow-swap-container.tsx @@ -1,6 +1,7 @@ import { useCallback, useState } from 'react'; import { Outlet, useNavigate } from 'react-router-dom'; +import type { P2Ret } from '@scure/btc-signer/payment'; import { bytesToHex } from '@stacks/common'; import { type ContractCallPayload, TransactionTypes } from '@stacks/connect'; import { @@ -19,11 +20,13 @@ import { bitflow } from '@shared/utils/bitflow-sdk'; import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading'; import { Content, Page } from '@app/components/layout'; +import { BitcoinNativeSegwitAccountLoader } from '@app/components/loaders/bitcoin-account-loader'; import { PageHeader } from '@app/features/container/headers/page.header'; import type { SbtcSponsorshipEligibility, TransactionBase, } from '@app/query/sbtc/sponsored-transactions.query'; +import type { Signer } from '@app/store/accounts/blockchain/bitcoin/bitcoin-signer'; import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { useGenerateStacksContractCallUnsignedTx } from '@app/store/transactions/contract-call.hooks'; import { useSignStacksTransaction } from '@app/store/transactions/transaction.hooks'; @@ -38,9 +41,17 @@ import { useStacksBroadcastSwap } from './hooks/use-stacks-broadcast-swap'; import { useSwapNavigate } from './hooks/use-swap-navigate'; import { SwapContext, SwapProvider } from './swap.context'; -export const bitflowSwapRoutes = generateSwapRoutes(); +// TODO: Refactor coupled Bitflow and Bitcoin swap containers, they should be separate +export const bitflowSwapRoutes = generateSwapRoutes( + }> + {signer => } + +); -function BitflowSwapContainer() { +interface BitflowSwapContainerProps { + btcSigner?: Signer; +} +function BitflowSwapContainer({ btcSigner }: BitflowSwapContainerProps) { const [unsignedTx, setUnsignedTx] = useState(); const [isSendingMax, setIsSendingMax] = useState(false); const [isPreparingSwapReview, setIsPreparingSwapReview] = useState(false); @@ -51,7 +62,7 @@ function BitflowSwapContainer() { const generateUnsignedTx = useGenerateStacksContractCallUnsignedTx(); const signTx = useSignStacksTransaction(); const broadcastStacksSwap = useStacksBroadcastSwap(); - const { onDepositSbtc, onReviewDepositSbtc } = useSbtcDepositTransaction(); + const { onDepositSbtc, onReviewDepositSbtc } = useSbtcDepositTransaction(btcSigner); const [sponsorshipEligibility, setSponsorshipEligibility] = useState< SbtcSponsorshipEligibility | undefined @@ -72,7 +83,7 @@ function BitflowSwapContainer() { swappableAssetsBase, swappableAssetsQuote, swapSubmissionData, - } = useBitflowSwap(); + } = useBitflowSwap(btcSigner); const onSubmitSwapForReview = useCallback( async (values: SwapFormValues) => { diff --git a/src/app/pages/swap/generate-swap-routes.tsx b/src/app/pages/swap/generate-swap-routes.tsx index 18798311d38..b8084376a95 100644 --- a/src/app/pages/swap/generate-swap-routes.tsx +++ b/src/app/pages/swap/generate-swap-routes.tsx @@ -2,6 +2,7 @@ import { Route } from 'react-router-dom'; import { RouteUrls } from '@shared/route-urls'; +import { ledgerBitcoinTxSigningRoutes } from '@app/features/ledger/flows/bitcoin-tx-signing/ledger-bitcoin-sign-tx-container'; import { ledgerStacksTxSigningRoutes } from '@app/features/ledger/flows/stacks-tx-signing/ledger-sign-stacks-tx-container'; import { AccountGate } from '@app/routes/account-gate'; @@ -20,6 +21,7 @@ export function generateSwapRoutes(container: React.ReactNode) { } /> }> + {ledgerBitcoinTxSigningRoutes} {ledgerStacksTxSigningRoutes} diff --git a/src/app/pages/swap/hooks/use-bitflow-swap.tsx b/src/app/pages/swap/hooks/use-bitflow-swap.tsx index 41643f96e09..1b5f01d3000 100644 --- a/src/app/pages/swap/hooks/use-bitflow-swap.tsx +++ b/src/app/pages/swap/hooks/use-bitflow-swap.tsx @@ -1,5 +1,6 @@ import { useCallback, useMemo, useState } from 'react'; +import type { P2Ret } from '@scure/btc-signer/payment'; import type { RouteQuote } from 'bitflow-sdk'; import { type SwapAsset } from '@leather.io/query'; @@ -9,7 +10,9 @@ import { logger } from '@shared/logger'; import { bitflow } from '@shared/utils/bitflow-sdk'; import { useConfigSbtc } from '@app/query/common/remote-config/remote-config.query'; +import type { Signer } from '@app/store/accounts/blockchain/bitcoin/bitcoin-signer'; import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { useHasLedgerKeys } from '@app/store/ledger/ledger.selectors'; import { SwapSubmissionData } from '../swap.context'; import { useBitflowSwappableAssets } from './use-bitflow-swappable-assets'; @@ -26,7 +29,7 @@ function getBitflowSwappableAssetsWithSbtcAtTop(assets: SwapAsset[]) { ].filter(isDefined); } -export function useBitflowSwap() { +export function useBitflowSwap(btcSigner?: Signer) { const [isCrossChainSwap, setIsCrossChainSwap] = useState(false); const [swapSubmissionData, setSwapSubmissionData] = useState(); const [slippage, _setSlippage] = useState(0.04); @@ -34,14 +37,16 @@ export function useBitflowSwap() { const address = useCurrentStacksAccountAddress(); const { data: bitflowSwapAssets = [] } = useBitflowSwappableAssets(address); const { isSbtcEnabled } = useConfigSbtc(); + const isLedger = useHasLedgerKeys(); - const createBtcAsset = useBtcSwapAsset(); + const createBtcAsset = useBtcSwapAsset(btcSigner); const btcAsset = createBtcAsset(); const swappableAssetsBase = useMemo(() => { - if (!isSbtcEnabled) return migratePositiveAssetBalancesToTop(bitflowSwapAssets); + if (!isSbtcEnabled || !btcSigner || isLedger) + return migratePositiveAssetBalancesToTop(bitflowSwapAssets); return [btcAsset, ...getBitflowSwappableAssetsWithSbtcAtTop(bitflowSwapAssets)]; - }, [bitflowSwapAssets, btcAsset, isSbtcEnabled]); + }, [bitflowSwapAssets, btcAsset, btcSigner, isLedger, isSbtcEnabled]); const swappableAssetsQuote = useMemo(() => { if (!isSbtcEnabled) return bitflowSwapAssets; diff --git a/src/app/pages/swap/hooks/use-btc-bridge-asset.tsx b/src/app/pages/swap/hooks/use-btc-bridge-asset.tsx index 3a10b147b55..ea578dd5a5e 100644 --- a/src/app/pages/swap/hooks/use-btc-bridge-asset.tsx +++ b/src/app/pages/swap/hooks/use-btc-bridge-asset.tsx @@ -1,15 +1,15 @@ import { useCallback } from 'react'; import BtcAvatarIconSrc from '@assets/avatars/btc-avatar-icon.png'; +import type { P2Ret } from '@scure/btc-signer/payment'; import { type SwapAsset, useCryptoCurrencyMarketDataMeanAverage } from '@leather.io/query'; import { useBtcCryptoAssetBalanceNativeSegwit } from '@app/query/bitcoin/balance/btc-balance-native-segwit.hooks'; -import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; +import type { Signer } from '@app/store/accounts/blockchain/bitcoin/bitcoin-signer'; -export function useBtcSwapAsset() { - const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner(); - const currentBitcoinAddress = nativeSegwitSigner.address; +export function useBtcSwapAsset(btcSigner?: Signer) { + const currentBitcoinAddress = btcSigner?.address ?? ''; const { balance } = useBtcCryptoAssetBalanceNativeSegwit(currentBitcoinAddress); const bitcoinMarketData = useCryptoCurrencyMarketDataMeanAverage('BTC'); diff --git a/src/app/pages/swap/hooks/use-sbtc-deposit-transaction.tsx b/src/app/pages/swap/hooks/use-sbtc-deposit-transaction.tsx index 24cb0e8a4cf..c85c274d51c 100644 --- a/src/app/pages/swap/hooks/use-sbtc-deposit-transaction.tsx +++ b/src/app/pages/swap/hooks/use-sbtc-deposit-transaction.tsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { bytesToHex } from '@noble/hashes/utils'; import * as btc from '@scure/btc-signer'; -import type { P2TROut } from '@scure/btc-signer/payment'; +import type { P2Ret, P2TROut } from '@scure/btc-signer/payment'; import { MAINNET, REGTEST, @@ -29,7 +29,7 @@ import { useToast } from '@app/features/toasts/use-toast'; import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks'; import { useBreakOnNonCompliantEntity } from '@app/query/common/compliance-checker/compliance-checker.query'; import { useBitcoinScureLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain'; -import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; +import type { Signer } from '@app/store/accounts/blockchain/bitcoin/bitcoin-signer'; import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; @@ -61,16 +61,17 @@ function getSbtcNetworkConfig(network: BitcoinNetworkModes) { const clientMainnet = new SbtcApiClientMainnet(); const clientTestnet = new SbtcApiClientTestnet(); -export function useSbtcDepositTransaction() { +export function useSbtcDepositTransaction(btcSigner?: Signer) { const toast = useToast(); const { setIsIdle } = useLoading(LoadingKeys.SUBMIT_SWAP_TRANSACTION); const stacksAccount = useCurrentStacksAccount(); const { data: utxos } = useCurrentNativeSegwitUtxos(); const { data: feeRates } = useAverageBitcoinFeeRates(); - const signer = useCurrentAccountNativeSegwitIndexZeroSigner(); const networkMode = useBitcoinScureLibNetworkConfig(); const navigate = useNavigate(); const network = useCurrentNetwork(); + // TODO: Use with Ledger integration + // const sign = useSignBitcoinTx(); const client = useMemo( () => (network.chain.bitcoin.mode === 'mainnet' ? clientMainnet : clientTestnet), @@ -82,7 +83,7 @@ export function useSbtcDepositTransaction() { return { async onReviewDepositSbtc(swapData: SwapSubmissionData, isSendingMax: boolean) { - if (!stacksAccount || !utxos) return; + if (!stacksAccount || !utxos || !btcSigner) return; try { const deposit: SbtcDeposit = buildSbtcDepositTx({ @@ -92,7 +93,7 @@ export function useSbtcDepositTransaction() { signersPublicKey: await client.fetchSignersPublicKey(), maxSignerFee, reclaimLockTime, - reclaimPublicKey: bytesToHex(signer.publicKey).slice(2), + reclaimPublicKey: bytesToHex(btcSigner.publicKey).slice(2), }); const determineUtxosArgs = { @@ -110,7 +111,7 @@ export function useSbtcDepositTransaction() { ? determineUtxosForSpendAll(determineUtxosArgs) : determineUtxosForSpend(determineUtxosArgs); - const p2wpkh = btc.p2wpkh(signer.publicKey, networkMode); + const p2wpkh = btc.p2wpkh(btcSigner.publicKey, networkMode); for (const input of inputs) { deposit.transaction.addInput({ @@ -128,7 +129,11 @@ export function useSbtcDepositTransaction() { outputs.forEach(output => { // Add change output if (!output.address) { - deposit.transaction.addOutputAddress(signer.address, BigInt(output.value), networkMode); + deposit.transaction.addOutputAddress( + btcSigner.address, + BigInt(output.value), + networkMode + ); return; } }); @@ -140,11 +145,11 @@ export function useSbtcDepositTransaction() { } }, async onDepositSbtc(swapSubmissionData: SwapSubmissionData) { - if (!stacksAccount) return; + if (!stacksAccount || !btcSigner) return; const sBtcDeposit = swapSubmissionData.txData?.deposit as SbtcDeposit; try { - signer.sign(sBtcDeposit.transaction); + btcSigner.sign(sBtcDeposit.transaction); sBtcDeposit.transaction.finalize(); logger.info('Deposit', { deposit: sBtcDeposit }); diff --git a/src/app/query/bitcoin/address/utxos-by-address.hooks.ts b/src/app/query/bitcoin/address/utxos-by-address.hooks.ts index 7aaf3a6fd0f..9d1891d1c9b 100644 --- a/src/app/query/bitcoin/address/utxos-by-address.hooks.ts +++ b/src/app/query/bitcoin/address/utxos-by-address.hooks.ts @@ -1,6 +1,6 @@ import { useNativeSegwitUtxosByAddress } from '@leather.io/query'; -import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; +import { useCurrentAccountNativeSegwitIndexZeroSignerNullable } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; const defaultArgs = { filterInscriptionUtxos: true, @@ -15,8 +15,8 @@ const defaultArgs = { export function useCurrentNativeSegwitUtxos(args = defaultArgs) { const { filterInscriptionUtxos, filterPendingTxsUtxos, filterRunesUtxos } = args; - const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner(); - const address = nativeSegwitSigner.address; + const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSignerNullable(); + const address = nativeSegwitSigner?.address ?? ''; return useNativeSegwitUtxosByAddress({ address,