Skip to content

Commit

Permalink
feat: add sbtc deposit to activity
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Dec 9, 2024
1 parent 96fcbfe commit 9951c3d
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -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 (
<TransactionItemLayout
openTxLink={openTxLink}
txCaption={truncateMiddle(deposit.bitcoinTxid, 4)}
txIcon={
// Replace with sBTC avatar icon
<Avatar.Root>
<Avatar.Image alt="ST" src={SBtcAvatarIconSrc} />
</Avatar.Root>
}
txStatus={<Caption color="yellow.action-primary-default">Pending</Caption>}
txTitle={<Title textStyle="label.02">BTC → sBTC</Title>}
// Api is only returning 0 right now
txValue={''} // deposit.amount.toString()
/>
);
}
12 changes: 10 additions & 2 deletions src/app/features/activity-list/activity-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -63,6 +64,9 @@ export function ActivityList() {
[nsPendingTxs, trPendingTxs]
);

const { isLoading: isLoadingSBtcDeposits, pendingSBtcDeposits } =
useSBtcPendingDeposits(stxAddress);

const { isLoading: isLoadingStacksTransactions, data: stacksTransactionsWithTransfers } =
useGetAccountTransactionsWithTransfersQuery(stxAddress);
const {
Expand All @@ -80,7 +84,8 @@ export function ActivityList() {
isLoadingNsBitcoinTransactions ||
isLoadingTrBitcoinTransactions ||
isLoadingStacksTransactions ||
isLoadingStacksPendingTransactions;
isLoadingStacksPendingTransactions ||
isLoadingSBtcDeposits;

const transactionListBitcoinTxs = useMemo(() => {
return convertBitcoinTxsToListType(
Expand All @@ -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;

Expand Down Expand Up @@ -128,6 +135,7 @@ export function ActivityList() {
{hasPendingTransactions && (
<PendingTransactionList
bitcoinTxs={isBitcoinEnabled ? bitcoinPendingTxs : []}
sBtcDeposits={pendingSBtcDeposits}
stacksTxs={stacksPendingTransactions}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<PendingTransactionListLayout>
{bitcoinTxs.map(tx => (
<BitcoinTransactionItem key={tx.txid} transaction={tx} />
))}
{sBtcDeposits.map(deposit => (
<SBtcDepositTransactionItem key={deposit.bitcoinTxid} deposit={deposit} />
))}
{stacksTxs.map(tx => (
<StacksTransactionItem key={tx.tx_id} transaction={tx} />
))}
Expand Down
2 changes: 1 addition & 1 deletion src/app/pages/swap/bitflow-swap-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ function BitflowSwapContainer() {

// TODO: Handle cross-chain swaps
if (isCrossChainSwap) {
return await onDepositSBtc();
return await onDepositSBtc(swapSubmissionData);
}

try {
Expand Down
17 changes: 11 additions & 6 deletions src/app/pages/swap/hooks/use-sbtc-deposit-transaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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() {
Expand All @@ -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(),
Expand Down Expand Up @@ -92,7 +94,10 @@ export function useSBtcDepositTransaction() {
setIsIdle();
navigate(RouteUrls.Activity);
} catch (error) {
setIsIdle();
console.error(error);
} finally {
setIsIdle();
}
},
};
Expand Down
4 changes: 4 additions & 0 deletions src/app/pages/test-deposit-sbtc/deposit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
68 changes: 68 additions & 0 deletions src/app/query/sbtc/sbtc-deposits.query.ts
Original file line number Diff line number Diff line change
@@ -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<GetSBtcDepositsResponse> {
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],
};
}

0 comments on commit 9951c3d

Please sign in to comment.