diff --git a/src/app/components/sbtc-deposit-status-item/sbtc-deposit-status-item.tsx b/src/app/components/sbtc-deposit-status-item/sbtc-deposit-status-item.tsx new file mode 100644 index 0000000000..7c54890b33 --- /dev/null +++ b/src/app/components/sbtc-deposit-status-item/sbtc-deposit-status-item.tsx @@ -0,0 +1,40 @@ +import SBtcAvatarIconSrc from '@assets/avatars/sbtc-avatar-icon.png'; + +import { Avatar, Caption, Title } from '@leather.io/ui'; +import { truncateMiddle } from '@leather.io/utils'; + +import { analytics } from '@shared/utils/analytics'; + +import { useBitcoinExplorerLink } from '@app/common/hooks/use-bitcoin-explorer-link'; +import type { SBtcDepositInfo } from '@app/query/sbtc/sbtc-deposits.query'; + +import { TransactionItemLayout } from '../transaction-item/transaction-item.layout'; + +interface SBtcDepositTransactionItemProps { + deposit: SBtcDepositInfo; +} +export function SBtcDepositTransactionItem({ deposit }: SBtcDepositTransactionItemProps) { + const { handleOpenBitcoinTxLink: handleOpenTxLink } = useBitcoinExplorerLink(); + + const openTxLink = () => { + void analytics.track('view_bitcoin_transaction'); + handleOpenTxLink({ txid: deposit.bitcoinTxid }); + }; + + return ( + + + + } + txStatus={Pending} + txTitle={BTC → sBTC} + // Api is only returning 0 right now + txValue={''} // deposit.amount.toString() + /> + ); +} diff --git a/src/app/features/activity-list/activity-list.tsx b/src/app/features/activity-list/activity-list.tsx index be85aceeb2..753edf9759 100644 --- a/src/app/features/activity-list/activity-list.tsx +++ b/src/app/features/activity-list/activity-list.tsx @@ -12,6 +12,7 @@ import { import { LoadingSpinner } from '@app/components/loading-spinner'; import { useConfigBitcoinEnabled } from '@app/query/common/remote-config/remote-config.query'; +import { useSBtcPendingDeposits } from '@app/query/sbtc/sbtc-deposits.query'; import { useZeroIndexTaprootAddress } from '@app/store/accounts/blockchain/bitcoin/bitcoin.hooks'; import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; @@ -63,6 +64,9 @@ export function ActivityList() { [nsPendingTxs, trPendingTxs] ); + const { isLoading: isLoadingSBtcDeposits, pendingSBtcDeposits } = + useSBtcPendingDeposits(stxAddress); + const { isLoading: isLoadingStacksTransactions, data: stacksTransactionsWithTransfers } = useGetAccountTransactionsWithTransfersQuery(stxAddress); const { @@ -80,7 +84,8 @@ export function ActivityList() { isLoadingNsBitcoinTransactions || isLoadingTrBitcoinTransactions || isLoadingStacksTransactions || - isLoadingStacksPendingTransactions; + isLoadingStacksPendingTransactions || + isLoadingSBtcDeposits; const transactionListBitcoinTxs = useMemo(() => { return convertBitcoinTxsToListType( @@ -99,7 +104,9 @@ export function ActivityList() { const hasSubmittedTransactions = submittedTransactions.length > 0; const hasPendingTransactions = - bitcoinPendingTxs.length > 0 || stacksPendingTransactions.length > 0; + bitcoinPendingTxs.length > 0 || + stacksPendingTransactions.length > 0 || + pendingSBtcDeposits.length > 0; const hasTransactions = transactionListBitcoinTxs.length > 0 || transactionListStacksTxs.length > 0; @@ -128,6 +135,7 @@ export function ActivityList() { {hasPendingTransactions && ( )} diff --git a/src/app/features/activity-list/components/pending-transaction-list/pending-transaction-list.tsx b/src/app/features/activity-list/components/pending-transaction-list/pending-transaction-list.tsx index 3b0f276390..22ba28850f 100644 --- a/src/app/features/activity-list/components/pending-transaction-list/pending-transaction-list.tsx +++ b/src/app/features/activity-list/components/pending-transaction-list/pending-transaction-list.tsx @@ -3,20 +3,30 @@ import { MempoolTransaction } from '@stacks/stacks-blockchain-api-types'; import type { BitcoinTx } from '@leather.io/models'; import { BitcoinTransactionItem } from '@app/components/bitcoin-transaction-item/bitcoin-transaction-item'; +import { SBtcDepositTransactionItem } from '@app/components/sbtc-deposit-status-item/sbtc-deposit-status-item'; import { StacksTransactionItem } from '@app/components/stacks-transaction-item/stacks-transaction-item'; +import type { SBtcDepositInfo } from '@app/query/sbtc/sbtc-deposits.query'; import { PendingTransactionListLayout } from './pending-transaction-list.layout'; interface PendingTransactionListProps { bitcoinTxs: BitcoinTx[]; + sBtcDeposits: SBtcDepositInfo[]; stacksTxs: MempoolTransaction[]; } -export function PendingTransactionList({ bitcoinTxs, stacksTxs }: PendingTransactionListProps) { +export function PendingTransactionList({ + bitcoinTxs, + sBtcDeposits, + stacksTxs, +}: PendingTransactionListProps) { return ( {bitcoinTxs.map(tx => ( ))} + {sBtcDeposits.map(deposit => ( + + ))} {stacksTxs.map(tx => ( ))} diff --git a/src/app/pages/swap/bitflow-swap-container.tsx b/src/app/pages/swap/bitflow-swap-container.tsx index 9ac9a7368d..ce9c3c97b1 100644 --- a/src/app/pages/swap/bitflow-swap-container.tsx +++ b/src/app/pages/swap/bitflow-swap-container.tsx @@ -124,7 +124,7 @@ function BitflowSwapContainer() { // TODO: Handle cross-chain swaps if (isCrossChainSwap) { - return await onDepositSBtc(); + return await onDepositSBtc(swapSubmissionData); } try { 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 37c68c7be4..eaf2db1cd2 100644 --- a/src/app/pages/swap/hooks/use-sbtc-deposit-transaction.tsx +++ b/src/app/pages/swap/hooks/use-sbtc-deposit-transaction.tsx @@ -5,7 +5,7 @@ import * as btc from '@scure/btc-signer'; import { REGTEST, SbtcApiClientTestnet, buildSbtcDepositTx } from 'sbtc'; import { useAverageBitcoinFeeRates } from '@leather.io/query'; -import { createMoney } from '@leather.io/utils'; +import { btcToSat, createMoney } from '@leather.io/utils'; import { RouteUrls } from '@shared/route-urls'; @@ -17,6 +17,8 @@ import { useBitcoinScureLibNetworkConfig } from '@app/store/accounts/blockchain/ import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import type { SwapSubmissionData } from '../swap.context'; + const client = new SbtcApiClientTestnet(); export function useSBtcDepositTransaction() { @@ -30,13 +32,13 @@ export function useSBtcDepositTransaction() { const navigate = useNavigate(); return { - async onDepositSBtc() { - if (!stacksAccount) throw new Error('no stacks account'); - if (!utxos) throw new Error('no utxos'); - + async onDepositSBtc(swapSubmissionData: SwapSubmissionData) { + if (!stacksAccount) throw new Error('No stacks account'); + if (!utxos) throw new Error('No utxos'); + console.log('amount', btcToSat(swapSubmissionData.swapAmountQuote).toNumber()); try { const deposit = buildSbtcDepositTx({ - amountSats: 100_000, + amountSats: btcToSat(swapSubmissionData.swapAmountQuote).toNumber(), network: REGTEST, stacksAddress: stacksAccount.address, signersPublicKey: await client.fetchSignersPublicKey(), @@ -92,7 +94,10 @@ export function useSBtcDepositTransaction() { setIsIdle(); navigate(RouteUrls.Activity); } catch (error) { + setIsIdle(); console.error(error); + } finally { + setIsIdle(); } }, }; diff --git a/src/app/pages/test-deposit-sbtc/deposit.tsx b/src/app/pages/test-deposit-sbtc/deposit.tsx index de10370692..999b471b71 100644 --- a/src/app/pages/test-deposit-sbtc/deposit.tsx +++ b/src/app/pages/test-deposit-sbtc/deposit.tsx @@ -93,3 +93,7 @@ export async function notifySbtc({ depositScript: string; }; } + +export async function getDepositStatus(txid: string, index: number) { + return fetch(`${emilyUrl}/${txid}/${index}`).then(res => res.json()); +} diff --git a/src/app/query/sbtc/sbtc-deposits.query.ts b/src/app/query/sbtc/sbtc-deposits.query.ts new file mode 100644 index 0000000000..ecdecf677c --- /dev/null +++ b/src/app/query/sbtc/sbtc-deposits.query.ts @@ -0,0 +1,68 @@ +import { hexToBytes } from '@stacks/common'; +import { BytesReader, addressToString, deserializeAddress } from '@stacks/transactions'; +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; + +export enum SBtcStatus { + Pending = 'pending', + Reprocessing = 'reprocessing', + Accepted = 'accepted', + Confirmed = 'confirmed', + Failed = 'failed', +} + +export interface SBtcDepositInfo { + amount: number; + bitcoinTxOutputIndex: number; + bitcoinTxid: string; + depositScript: string; + lastUpdateBlockHash: string; + lastUpdateHeight: number; + recipient: string; // Stacks address + reclaimScript: string; + status: SBtcStatus; +} + +interface GetSBtcDepositsResponse { + deposits: SBtcDepositInfo[]; + nextToken?: string; +} + +const emilyUrl = 'https://beta.sbtc-emily.com/deposit'; + +async function getSBtcDeposits(status: string): Promise { + const resp = await axios.get(`${emilyUrl}?status=${status}`, { + headers: { + 'Content-Type': 'application/json', + }, + }); + return resp.data; +} + +export function useGetSBtcDeposits(stxAddress: string, status: string) { + return useQuery({ + queryKey: ['get-sbtc-deposits', stxAddress, status], + queryFn: () => getSBtcDeposits(status), + select: resp => + resp.deposits.filter(deposit => { + const recipient = addressToString( + deserializeAddress(new BytesReader(hexToBytes(deposit.recipient.slice(2)))) + ); + return recipient === stxAddress; + }), + }); +} + +export function useSBtcPendingDeposits(stxAddress: string) { + const { data: pendingDeposits = [], isLoading: isLoadingStatusPending } = useGetSBtcDeposits( + stxAddress, + 'pending' + ); + const { data: reprocessingDeposits = [], isLoading: isLoadingStatusReprocessing } = + useGetSBtcDeposits(stxAddress, 'reprocessing'); + + return { + isLoading: isLoadingStatusPending || isLoadingStatusReprocessing, + pendingSBtcDeposits: [...pendingDeposits, ...reprocessingDeposits], + }; +}