mirror of
https://github.com/zhigang1992/wallet.git
synced 2026-01-12 22:53:27 +08:00
feat: add taproot txs in activity list, closes #3249
This commit is contained in:
@@ -166,6 +166,7 @@
|
||||
"@tanstack/react-query-devtools": "4.32.0",
|
||||
"@tanstack/react-query-persist-client": "4.32.0",
|
||||
"@tippyjs/react": "4.2.6",
|
||||
"@types/lodash.uniqby": "4.7.7",
|
||||
"@typescript-eslint/eslint-plugin": "5.60.1",
|
||||
"@vkontakte/vk-qr": "2.0.13",
|
||||
"@zondax/ledger-stacks": "1.0.4",
|
||||
@@ -195,6 +196,7 @@
|
||||
"ledger-bitcoin": "0.2.2",
|
||||
"limiter": "2.1.0",
|
||||
"lodash.get": "4.4.2",
|
||||
"lodash.uniqby": "4.7.0",
|
||||
"mdi-react": "9.2.0",
|
||||
"micro-packed": "0.3.2",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -209,6 +211,7 @@
|
||||
"react-dom": "18.2.0",
|
||||
"react-hot-toast": "2.4.1",
|
||||
"react-icons": "4.10.1",
|
||||
"react-intersection-observer": "9.5.2",
|
||||
"react-lottie": "1.2.3",
|
||||
"react-redux": "8.1.1",
|
||||
"react-router-dom": "6.14.0",
|
||||
|
||||
@@ -1,38 +1,16 @@
|
||||
import { FiArrowDown as IconArrowDown, FiArrowUp as IconArrowUp } from 'react-icons/fi';
|
||||
|
||||
import { Box, BoxProps, Circle, ColorsStringLiteral, Flex, color } from '@stacks/ui';
|
||||
import { Box, BoxProps, Circle, Flex, color } from '@stacks/ui';
|
||||
|
||||
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
|
||||
|
||||
import { isBitcoinTxInbound } from '@app/common/transactions/bitcoin/utils';
|
||||
import { BtcIcon } from '@app/components/icons/btc-icon';
|
||||
|
||||
import { IconForTx, colorFromTx } from './utils';
|
||||
|
||||
interface TransactionIconProps extends BoxProps {
|
||||
transaction: BitcoinTx;
|
||||
btcAddress: string;
|
||||
}
|
||||
|
||||
type BtcTxStatus = 'pending' | 'success';
|
||||
type BtcStatusColorMap = Record<BtcTxStatus, ColorsStringLiteral>;
|
||||
|
||||
const statusFromTx = (tx: BitcoinTx): BtcTxStatus => {
|
||||
if (tx.status.confirmed) return 'success';
|
||||
return 'pending';
|
||||
};
|
||||
|
||||
const colorFromTx = (tx: BitcoinTx): ColorsStringLiteral => {
|
||||
const colorMap: BtcStatusColorMap = {
|
||||
pending: 'feedback-alert',
|
||||
success: 'brand',
|
||||
};
|
||||
|
||||
return colorMap[statusFromTx(tx)] ?? 'feedback-error';
|
||||
};
|
||||
|
||||
function IconForTx(address: string, tx: BitcoinTx) {
|
||||
if (isBitcoinTxInbound(address, tx)) return IconArrowDown;
|
||||
return IconArrowUp;
|
||||
}
|
||||
export function BitcoinTransactionIcon({ transaction, btcAddress, ...rest }: TransactionIconProps) {
|
||||
return (
|
||||
<Flex position="relative">
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Box, Circle, Flex, color } from '@stacks/ui';
|
||||
|
||||
import { SupportedInscription } from '@shared/models/inscription.model';
|
||||
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
|
||||
|
||||
import { OrdinalIcon } from '../icons/ordinal-icon';
|
||||
import { IconForTx, colorFromTx } from './utils';
|
||||
|
||||
interface BitcoinTransactionInscriptionIconProps {
|
||||
inscription: SupportedInscription;
|
||||
transaction: BitcoinTx;
|
||||
btcAddress: string;
|
||||
}
|
||||
|
||||
function InscriptionIcon({ inscription, ...rest }: { inscription: SupportedInscription }) {
|
||||
switch (inscription.type) {
|
||||
case 'image':
|
||||
return (
|
||||
<Circle
|
||||
bg={color('accent')}
|
||||
color={color('bg')}
|
||||
flexShrink={0}
|
||||
position="relative"
|
||||
size="36px"
|
||||
{...rest}
|
||||
>
|
||||
<img
|
||||
src={inscription.src}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
aspectRatio: '1 / 1',
|
||||
objectFit: 'cover',
|
||||
borderRadius: '6px',
|
||||
}}
|
||||
/>
|
||||
</Circle>
|
||||
);
|
||||
default:
|
||||
return <OrdinalIcon />;
|
||||
}
|
||||
}
|
||||
|
||||
export function BitcoinTransactionInscriptionIcon({
|
||||
inscription,
|
||||
transaction,
|
||||
btcAddress,
|
||||
...rest
|
||||
}: BitcoinTransactionInscriptionIconProps) {
|
||||
return (
|
||||
<Flex position="relative">
|
||||
<InscriptionIcon inscription={inscription} />
|
||||
<Circle
|
||||
bottom="-2px"
|
||||
right="-9px"
|
||||
position="absolute"
|
||||
size="21px"
|
||||
bg={color(colorFromTx(transaction))}
|
||||
color={color('bg')}
|
||||
border="2px solid"
|
||||
borderColor={color('bg')}
|
||||
{...rest}
|
||||
>
|
||||
<Box size="13px" as={IconForTx(btcAddress, transaction)} />
|
||||
</Circle>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -14,19 +14,23 @@ import {
|
||||
isBitcoinTxInbound,
|
||||
} from '@app/common/transactions/bitcoin/utils';
|
||||
import { useWalletType } from '@app/common/use-wallet-type';
|
||||
import { openInNewTab } from '@app/common/utils/open-in-new-tab';
|
||||
import { usePressable } from '@app/components/item-hover';
|
||||
import { IncreaseFeeButton } from '@app/components/stacks-transaction-item/increase-fee-button';
|
||||
import { TransactionTitle } from '@app/components/transaction/transaction-title';
|
||||
import { createInscriptionInfoUrl } from '@app/query/bitcoin/ordinals/inscription.hooks';
|
||||
import { useInscriptionByOutput } from '@app/query/bitcoin/ordinals/use-inscription-by-output';
|
||||
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
|
||||
|
||||
import { TransactionItemLayout } from '../transaction-item/transaction-item.layout';
|
||||
import { BitcoinTransactionCaption } from './bitcoin-transaction-caption';
|
||||
import { BitcoinTransactionIcon } from './bitcoin-transaction-icon';
|
||||
import { BitcoinTransactionInscriptionIcon } from './bitcoin-transaction-inscription-icon';
|
||||
import { BitcoinTransactionStatus } from './bitcoin-transaction-status';
|
||||
import { BitcoinTransactionValue } from './bitcoin-transaction-value';
|
||||
|
||||
interface BitcoinTransactionItemProps extends BoxProps {
|
||||
transaction?: BitcoinTx;
|
||||
transaction: BitcoinTx;
|
||||
}
|
||||
export function BitcoinTransactionItem({ transaction, ...rest }: BitcoinTransactionItemProps) {
|
||||
const [component, bind, { isHovered }] = usePressable(true);
|
||||
@@ -34,6 +38,8 @@ export function BitcoinTransactionItem({ transaction, ...rest }: BitcoinTransact
|
||||
const navigate = useNavigate();
|
||||
const { whenWallet } = useWalletType();
|
||||
|
||||
const { data: inscriptionData } = useInscriptionByOutput(transaction);
|
||||
|
||||
const bitcoinAddress = useCurrentAccountNativeSegwitAddressIndexZero();
|
||||
const { handleOpenTxLink } = useExplorerLink();
|
||||
const analytics = useAnalytics();
|
||||
@@ -56,6 +62,10 @@ export function BitcoinTransactionItem({ transaction, ...rest }: BitcoinTransact
|
||||
|
||||
const openTxLink = () => {
|
||||
void analytics.track('view_bitcoin_transaction');
|
||||
if (inscriptionData) {
|
||||
openInNewTab(createInscriptionInfoUrl(inscriptionData.id));
|
||||
return;
|
||||
}
|
||||
handleOpenTxLink({
|
||||
blockchain: 'bitcoin',
|
||||
txid: transaction?.txid || '',
|
||||
@@ -67,7 +77,16 @@ export function BitcoinTransactionItem({ transaction, ...rest }: BitcoinTransact
|
||||
|
||||
const txCaption = <BitcoinTransactionCaption>{caption}</BitcoinTransactionCaption>;
|
||||
const txValue = <BitcoinTransactionValue>{value}</BitcoinTransactionValue>;
|
||||
|
||||
const txIcon = inscriptionData ? (
|
||||
<BitcoinTransactionInscriptionIcon
|
||||
inscription={inscriptionData}
|
||||
transaction={transaction}
|
||||
btcAddress={bitcoinAddress}
|
||||
/>
|
||||
) : (
|
||||
<BitcoinTransactionIcon transaction={transaction} btcAddress={bitcoinAddress} />
|
||||
);
|
||||
const title = inscriptionData ? 'Ordinal inscription' : 'Bitcoin';
|
||||
const increaseFeeButton = (
|
||||
<IncreaseFeeButton
|
||||
isEnabled={isEnabled}
|
||||
@@ -76,13 +95,14 @@ export function BitcoinTransactionItem({ transaction, ...rest }: BitcoinTransact
|
||||
onIncreaseFee={onIncreaseFee}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<TransactionItemLayout
|
||||
openTxLink={openTxLink}
|
||||
txCaption={txCaption}
|
||||
txIcon={<BitcoinTransactionIcon transaction={transaction} btcAddress={bitcoinAddress} />}
|
||||
txIcon={txIcon}
|
||||
txStatus={<BitcoinTransactionStatus transaction={transaction} />}
|
||||
txTitle={<TransactionTitle title="Bitcoin" />}
|
||||
txTitle={<TransactionTitle title={title} />}
|
||||
txValue={txValue}
|
||||
belowCaptionEl={increaseFeeButton}
|
||||
{...bind}
|
||||
|
||||
29
src/app/components/bitcoin-transaction-item/utils.ts
Normal file
29
src/app/components/bitcoin-transaction-item/utils.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { FiArrowDown as IconArrowDown, FiArrowUp as IconArrowUp } from 'react-icons/fi';
|
||||
|
||||
import { ColorsStringLiteral } from '@stacks/ui-theme';
|
||||
|
||||
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
|
||||
|
||||
import { isBitcoinTxInbound } from '@app/common/transactions/bitcoin/utils';
|
||||
|
||||
type BtcTxStatus = 'pending' | 'success';
|
||||
type BtcStatusColorMap = Record<BtcTxStatus, ColorsStringLiteral>;
|
||||
|
||||
const statusFromTx = (tx: BitcoinTx): BtcTxStatus => {
|
||||
if (tx.status.confirmed) return 'success';
|
||||
return 'pending';
|
||||
};
|
||||
|
||||
export const colorFromTx = (tx: BitcoinTx): ColorsStringLiteral => {
|
||||
const colorMap: BtcStatusColorMap = {
|
||||
pending: 'feedback-alert',
|
||||
success: 'brand',
|
||||
};
|
||||
|
||||
return colorMap[statusFromTx(tx)] ?? 'feedback-error';
|
||||
};
|
||||
|
||||
export function IconForTx(address: string, tx: BitcoinTx) {
|
||||
if (isBitcoinTxInbound(address, tx)) return IconArrowDown;
|
||||
return IconArrowUp;
|
||||
}
|
||||
@@ -1,19 +1,29 @@
|
||||
export function OrdinalIcon() {
|
||||
return (
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="18" cy="18" r="18" fill="#0C0C0D" />
|
||||
<circle cx="18" cy="18" r="12.375" fill="white" />
|
||||
<rect x="7.32143" y="7.32143" width="21.3571" height="21.3571" rx="10.6786" fill="white" />
|
||||
<circle cx="18.0001" cy="18" r="4.57143" fill="#0C0C0D" />
|
||||
<rect
|
||||
x="7.32143"
|
||||
y="7.32143"
|
||||
width="21.3571"
|
||||
height="21.3571"
|
||||
rx="10.6786"
|
||||
stroke="#0C0C0D"
|
||||
strokeWidth="1.14286"
|
||||
/>
|
||||
<g clipPath="url(#clip0_1_5)">
|
||||
<path
|
||||
d="M18 36C27.9411 36 36 27.9411 36 18C36 8.05888 27.9411 0 18 0C8.05888 0 0 8.05888 0 18C0 27.9411 8.05888 36 18 36Z"
|
||||
fill="#0C0C0D"
|
||||
/>
|
||||
<path
|
||||
d="M18 31.5C25.4558 31.5 31.5 25.4558 31.5 18C31.5 10.5442 25.4558 4.5 18 4.5C10.5442 4.5 4.5 10.5442 4.5 18C4.5 25.4558 10.5442 31.5 18 31.5Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M18 24.75C21.7279 24.75 24.75 21.7279 24.75 18C24.75 14.2721 21.7279 11.25 18 11.25C14.2721 11.25 11.25 14.2721 11.25 18C11.25 21.7279 14.2721 24.75 18 24.75Z"
|
||||
fill="#0C0C0D"
|
||||
/>
|
||||
<path
|
||||
d="M36 18C36 8.05888 27.9411 0 18 0C8.05888 0 0 8.05888 0 18C0 27.9411 8.05888 36 18 36C27.9411 36 36 27.9411 36 18Z"
|
||||
stroke="white"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1_5">
|
||||
<rect width="36" height="36" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import uniqby from 'lodash.uniqby';
|
||||
|
||||
import { LoadingSpinner } from '@app/components/loading-spinner';
|
||||
import { useBitcoinPendingTransactions } from '@app/query/bitcoin/address/transactions-by-address.hooks';
|
||||
import { useGetBitcoinTransactionsByAddressQuery } from '@app/query/bitcoin/address/transactions-by-address.query';
|
||||
import { useGetBitcoinTransactionsByAddressesQuery } from '@app/query/bitcoin/address/transactions-by-address.query';
|
||||
import { useZeroIndexTaprootAddress } from '@app/query/bitcoin/ordinals/use-zero-index-taproot-address';
|
||||
import { useConfigBitcoinEnabled } from '@app/query/common/remote-config/remote-config.query';
|
||||
import { useStacksPendingTransactions } from '@app/query/stacks/mempool/mempool.hooks';
|
||||
import { useGetAccountTransactionsWithTransfersQuery } from '@app/query/stacks/transactions/transactions-with-transfers.query';
|
||||
@@ -17,7 +20,7 @@ import { TransactionList } from './components/transaction-list/transaction-list'
|
||||
|
||||
// TODO: temporary really ugly fix while we address conditional data problem of
|
||||
// bitcoin sometimes being undefined
|
||||
function useBitcoinAddress() {
|
||||
function useNsBitcoinAddress() {
|
||||
try {
|
||||
return useCurrentAccountNativeSegwitIndexZeroSigner().address;
|
||||
} catch (e) {
|
||||
@@ -25,11 +28,32 @@ function useBitcoinAddress() {
|
||||
}
|
||||
}
|
||||
|
||||
function useTrBitcoinAddress() {
|
||||
try {
|
||||
return useZeroIndexTaprootAddress();
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function ActivityList() {
|
||||
const bitcoinAddress = useBitcoinAddress();
|
||||
const { isInitialLoading: isInitialLoadingBitcoinTransactions, data: bitcoinTransactions } =
|
||||
useGetBitcoinTransactionsByAddressQuery(bitcoinAddress);
|
||||
const { data: bitcoinPendingTxs = [] } = useBitcoinPendingTransactions(bitcoinAddress);
|
||||
const nsBitcoinAddress = useNsBitcoinAddress();
|
||||
const trBitcoinAddress = useTrBitcoinAddress();
|
||||
|
||||
const [
|
||||
{ isInitialLoading: isInitialLoadingNsBitcoinTransactions, data: nsBitcoinTransactions = [] },
|
||||
{ isInitialLoading: isInitialLoadingTrBitcoinTransactions, data: trBitcoinTransactions = [] },
|
||||
] = useGetBitcoinTransactionsByAddressesQuery([nsBitcoinAddress, trBitcoinAddress]);
|
||||
|
||||
const [{ data: nsPendingTxs = [] }, { data: trPendingTxs = [] }] = useBitcoinPendingTransactions([
|
||||
nsBitcoinAddress,
|
||||
trBitcoinAddress,
|
||||
]);
|
||||
const bitcoinPendingTxs = useMemo(
|
||||
() => [...nsPendingTxs, ...trPendingTxs],
|
||||
[nsPendingTxs, trPendingTxs]
|
||||
);
|
||||
|
||||
const {
|
||||
isInitialLoading: isInitialLoadingStacksTransactions,
|
||||
data: stacksTransactionsWithTransfers,
|
||||
@@ -42,14 +66,16 @@ export function ActivityList() {
|
||||
const isBitcoinEnabled = useConfigBitcoinEnabled();
|
||||
|
||||
const isInitialLoading =
|
||||
isInitialLoadingBitcoinTransactions ||
|
||||
isInitialLoadingNsBitcoinTransactions ||
|
||||
isInitialLoadingTrBitcoinTransactions ||
|
||||
isInitialLoadingStacksTransactions ||
|
||||
isInitialLoadingStacksPendingTransactions;
|
||||
|
||||
const transactionListBitcoinTxs = useMemo(
|
||||
() => convertBitcoinTxsToListType(bitcoinTransactions),
|
||||
[bitcoinTransactions]
|
||||
);
|
||||
const transactionListBitcoinTxs = useMemo(() => {
|
||||
return convertBitcoinTxsToListType(
|
||||
uniqby([...nsBitcoinTransactions, ...trBitcoinTransactions], 'txid')
|
||||
);
|
||||
}, [nsBitcoinTransactions, trBitcoinTransactions]);
|
||||
|
||||
const pendingTransactionIds = stacksPendingTransactions.map(tx => tx.tx_id);
|
||||
const transactionListStacksTxs = useMemo(
|
||||
@@ -85,6 +111,7 @@ export function ActivityList() {
|
||||
<TransactionList
|
||||
bitcoinTxs={isBitcoinEnabled ? transactionListBitcoinTxs : []}
|
||||
stacksTxs={transactionListStacksTxs}
|
||||
currentBitcoinAddress={nsBitcoinAddress}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
|
||||
export function useTransactionListRender({
|
||||
currentBitcoinAddress,
|
||||
}: {
|
||||
currentBitcoinAddress: string;
|
||||
}) {
|
||||
const [visibleTxsNum, setVisibleTxsNum] = useState(10);
|
||||
const { ref: intersectionSentinel, inView } = useInView({
|
||||
rootMargin: '0% 0% 20% 0%',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (inView) {
|
||||
setVisibleTxsNum(visibleTxsNum + 10);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [inView]);
|
||||
|
||||
useEffect(() => {
|
||||
setVisibleTxsNum(10);
|
||||
}, [currentBitcoinAddress]);
|
||||
|
||||
return {
|
||||
visibleTxsNum,
|
||||
intersectionSentinel,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Box } from '@stacks/ui';
|
||||
|
||||
import { useTransactionListRender } from './hooks/use-transaction-list-render';
|
||||
import { TransactionListItem } from './transaction-list-item';
|
||||
import { TransactionListLayout } from './transaction-list.layout';
|
||||
import { TransactionListBitcoinTx, TransactionListStacksTx } from './transaction-list.model';
|
||||
@@ -9,23 +12,50 @@ import { TransactionsByDateLayout } from './transactions-by-date.layout';
|
||||
interface TransactionListProps {
|
||||
bitcoinTxs: TransactionListBitcoinTx[];
|
||||
stacksTxs: TransactionListStacksTx[];
|
||||
currentBitcoinAddress: string;
|
||||
}
|
||||
export function TransactionList({ bitcoinTxs, stacksTxs }: TransactionListProps) {
|
||||
|
||||
export function TransactionList({
|
||||
bitcoinTxs,
|
||||
stacksTxs,
|
||||
currentBitcoinAddress,
|
||||
}: TransactionListProps) {
|
||||
const { intersectionSentinel, visibleTxsNum } = useTransactionListRender({
|
||||
currentBitcoinAddress,
|
||||
});
|
||||
const txsGroupedByDate = useMemo(
|
||||
() =>
|
||||
bitcoinTxs.length || stacksTxs.length ? createTxDateFormatList(bitcoinTxs, stacksTxs) : [],
|
||||
[bitcoinTxs, stacksTxs]
|
||||
);
|
||||
|
||||
const groupedByDateTxsLength = useMemo(() => {
|
||||
return txsGroupedByDate.reduce((acc: Record<string, number>, item, index) => {
|
||||
acc[index] = item.txs.length + (acc[index - 1] || 0);
|
||||
return acc;
|
||||
}, {});
|
||||
}, [txsGroupedByDate]);
|
||||
|
||||
return (
|
||||
<TransactionListLayout>
|
||||
{txsGroupedByDate.map(({ date, displayDate, txs }) => (
|
||||
<TransactionsByDateLayout date={date} displayDate={displayDate} key={date}>
|
||||
{txs.map(tx => (
|
||||
<TransactionListItem key={getTransactionId(tx)} tx={tx} />
|
||||
))}
|
||||
</TransactionsByDateLayout>
|
||||
))}
|
||||
{txsGroupedByDate.map(({ date, displayDate, txs }, dateIndex) => {
|
||||
const prevVal = groupedByDateTxsLength[dateIndex - 1] || 0;
|
||||
// hide dates with no visible txs
|
||||
if (prevVal > visibleTxsNum) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TransactionsByDateLayout date={date} displayDate={displayDate} key={date}>
|
||||
{txs.map((tx, txIndex) => {
|
||||
// hide txs that are not visible
|
||||
if (prevVal + txIndex > visibleTxsNum) return null;
|
||||
return <TransactionListItem key={getTransactionId(tx)} tx={tx} />;
|
||||
})}
|
||||
</TransactionsByDateLayout>
|
||||
);
|
||||
})}
|
||||
<Box ref={intersectionSentinel} />
|
||||
</TransactionListLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ export function useGenerateSignedOrdinalTx(trInput: TaprootUtxo) {
|
||||
txid: trInput.txid,
|
||||
index: trInput.vout,
|
||||
tapInternalKey: trSigner.payment.tapInternalKey,
|
||||
sequence: 0,
|
||||
witnessUtxo: {
|
||||
script: trSigner.payment.script,
|
||||
amount: BigInt(trInput.value),
|
||||
@@ -54,6 +55,7 @@ export function useGenerateSignedOrdinalTx(trInput: TaprootUtxo) {
|
||||
tx.addInput({
|
||||
txid: input.txid,
|
||||
index: input.vout,
|
||||
sequence: 0,
|
||||
witnessUtxo: {
|
||||
amount: BigInt(input.value),
|
||||
script: nativeSegwitSigner.payment.script,
|
||||
|
||||
@@ -6,7 +6,10 @@ import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model
|
||||
import { sumNumbers } from '@app/common/math/helpers';
|
||||
|
||||
import { UtxoResponseItem } from '../bitcoin-client';
|
||||
import { useGetBitcoinTransactionsByAddressQuery } from './transactions-by-address.query';
|
||||
import {
|
||||
useGetBitcoinTransactionsByAddressQuery,
|
||||
useGetBitcoinTransactionsByAddressesQuery,
|
||||
} from './transactions-by-address.query';
|
||||
import { useAllSpendableNativeSegwitUtxos } from './utxos-by-address.hooks';
|
||||
|
||||
function useFilterAddressPendingTransactions() {
|
||||
@@ -15,10 +18,10 @@ function useFilterAddressPendingTransactions() {
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function useBitcoinPendingTransactions(address: string) {
|
||||
export function useBitcoinPendingTransactions(addresses: string[]) {
|
||||
const filterPendingTransactions = useFilterAddressPendingTransactions();
|
||||
|
||||
return useGetBitcoinTransactionsByAddressQuery(address, {
|
||||
return useGetBitcoinTransactionsByAddressesQuery(addresses, {
|
||||
select(txs) {
|
||||
return filterPendingTransactions(txs);
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useQueries, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
|
||||
|
||||
@@ -23,3 +23,22 @@ export function useGetBitcoinTransactionsByAddressQuery<T extends unknown = Bitc
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function useGetBitcoinTransactionsByAddressesQuery<T extends unknown = BitcoinTx[]>(
|
||||
addresses: string[],
|
||||
options?: AppUseQueryConfig<BitcoinTx[], T>
|
||||
) {
|
||||
const client = useBitcoinClient();
|
||||
|
||||
return useQueries({
|
||||
queries: addresses.map(address => {
|
||||
return {
|
||||
enabled: !!address,
|
||||
queryKey: ['btc-txs-by-addresses', address],
|
||||
queryFn: () => client.addressApi.getTransactionsByAddress(address),
|
||||
...queryOptions,
|
||||
...options,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
|
||||
import { useGetInscriptionQuery } from './inscription.query';
|
||||
|
||||
function createInfoUrl(id: string) {
|
||||
export function createInscriptionInfoUrl(id: string) {
|
||||
return `https://ordinals.hiro.so/inscription/${id}`;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export function convertInscriptionToSupportedInscriptionType(inscription: Inscri
|
||||
const title = `Inscription ${inscription.number}`;
|
||||
return whenInscriptionType<SupportedInscription>(inscription.content_type, {
|
||||
image: () => ({
|
||||
infoUrl: createInfoUrl(inscription.id),
|
||||
infoUrl: createInscriptionInfoUrl(inscription.id),
|
||||
src: `https://api.hiro.so/ordinals/v1/inscriptions/${inscription.id}/content`,
|
||||
type: 'image',
|
||||
title,
|
||||
@@ -22,13 +22,13 @@ export function convertInscriptionToSupportedInscriptionType(inscription: Inscri
|
||||
}),
|
||||
text: () => ({
|
||||
contentSrc: `https://api.hiro.so/ordinals/v1/inscriptions/${inscription.id}/content`,
|
||||
infoUrl: createInfoUrl(inscription.id),
|
||||
infoUrl: createInscriptionInfoUrl(inscription.id),
|
||||
type: 'text',
|
||||
title,
|
||||
...inscription,
|
||||
}),
|
||||
other: () => ({
|
||||
infoUrl: createInfoUrl(inscription.id),
|
||||
infoUrl: createInscriptionInfoUrl(inscription.id),
|
||||
type: 'other',
|
||||
title,
|
||||
...inscription,
|
||||
|
||||
17
src/app/query/bitcoin/ordinals/use-inscription-by-output.ts
Normal file
17
src/app/query/bitcoin/ordinals/use-inscription-by-output.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
|
||||
|
||||
import { convertInscriptionToSupportedInscriptionType } from './inscription.hooks';
|
||||
import { useGetInscriptionByParamQuery } from './use-inscription-by-param.query';
|
||||
|
||||
export function useInscriptionByOutput(transaction: BitcoinTx) {
|
||||
const inputsLength = transaction.vin.length;
|
||||
const index = inputsLength === 1 ? 0 : inputsLength - 2;
|
||||
|
||||
return useGetInscriptionByParamQuery(`output=${transaction.txid}:${index}`, {
|
||||
select(data) {
|
||||
const inscription = data.results[0];
|
||||
if (!inscription) return;
|
||||
return convertInscriptionToSupportedInscriptionType(inscription);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { Paginated } from '@shared/models/api-types';
|
||||
import { Inscription } from '@shared/models/inscription.model';
|
||||
|
||||
import { AppUseQueryConfig } from '@app/query/query-config';
|
||||
|
||||
const inscriptionQueryOptions = {
|
||||
staleTime: Infinity,
|
||||
cacheTime: Infinity,
|
||||
} as const;
|
||||
|
||||
function fetchInscriptionByParam() {
|
||||
return async (param: string) => {
|
||||
const res = await fetch(`https://api.hiro.so/ordinals/v1/inscriptions?${param}`);
|
||||
if (!res.ok) throw new Error('Error retrieving inscription metadata');
|
||||
const data = await res.json();
|
||||
return data as Paginated<Inscription[]>;
|
||||
};
|
||||
}
|
||||
|
||||
type FetchInscriptionResp = Awaited<ReturnType<ReturnType<typeof fetchInscriptionByParam>>>;
|
||||
|
||||
export function useGetInscriptionByParamQuery<T extends unknown = FetchInscriptionResp>(
|
||||
param: string,
|
||||
options?: AppUseQueryConfig<FetchInscriptionResp, T>
|
||||
) {
|
||||
return useQuery({
|
||||
enabled: !!param,
|
||||
queryKey: ['inscription-by-param', param],
|
||||
queryFn: () => fetchInscriptionByParam()(param),
|
||||
...inscriptionQueryOptions,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
17
yarn.lock
17
yarn.lock
@@ -6245,6 +6245,13 @@
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash.uniqby@4.7.7":
|
||||
version "4.7.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash.uniqby/-/lodash.uniqby-4.7.7.tgz#48dbb652c41cc8fb30aa61a44174368081835ab5"
|
||||
integrity sha512-sv2g6vkCIvEUsK5/Vq17haoZaisfj2EWW8mP7QWlnKi6dByoNmeuHDDXHR7sabuDqwO4gvU7ModIL22MmnOocg==
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash@*", "@types/lodash@^4.14.178", "@types/lodash@^4.14.191":
|
||||
version "4.14.194"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.194.tgz#b71eb6f7a0ff11bff59fc987134a093029258a76"
|
||||
@@ -13738,6 +13745,11 @@ lodash.sortby@^4.7.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==
|
||||
|
||||
lodash.uniqby@4.7.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302"
|
||||
integrity sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==
|
||||
|
||||
lodash@4.17.21, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
@@ -15657,6 +15669,11 @@ react-icons@^4.3.1, react-icons@^4.7.1:
|
||||
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.8.0.tgz#621e900caa23b912f737e41be57f27f6b2bff445"
|
||||
integrity sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==
|
||||
|
||||
react-intersection-observer@9.5.2:
|
||||
version "9.5.2"
|
||||
resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.5.2.tgz#f68363a1ff292323c0808201b58134307a1626d0"
|
||||
integrity sha512-EmoV66/yvksJcGa1rdW0nDNc4I1RifDWkT50gXSFnPLYQ4xUptuDD4V7k+Rj1OgVAlww628KLGcxPXFlOkkU/Q==
|
||||
|
||||
react-is@16.9.0:
|
||||
version "16.9.0"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb"
|
||||
|
||||
Reference in New Issue
Block a user