From a11d4b7ecff2913def201effe272e818cfe79e05 Mon Sep 17 00:00:00 2001 From: Pete Watters <2938440+pete-watters@users.noreply.github.com> Date: Thu, 19 Dec 2024 12:01:21 +0000 Subject: [PATCH 1/4] fix: add src20 image types, ref leather-io/mono#724 --- package.json | 4 +- pnpm-lock.yaml | 89 ++++++++++++++----- .../btc-balance-native-segwit.hooks.ts | 4 + 3 files changed, 71 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 14ecdc0e2c..6b2b02310a 100644 --- a/package.json +++ b/package.json @@ -149,8 +149,8 @@ "@leather.io/bitcoin": "0.17.0", "@leather.io/constants": "0.13.5", "@leather.io/crypto": "1.6.14", - "@leather.io/models": "0.22.0", - "@leather.io/query": "2.26.1", + "@leather.io/models": "0.24.0", + "@leather.io/query": "2.26.9", "@leather.io/stacks": "1.4.0", "@leather.io/tokens": "0.12.1", "@leather.io/ui": "1.44.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e334d1d466..927a69b924 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,11 +57,11 @@ importers: specifier: 1.6.14 version: 1.6.14 '@leather.io/models': - specifier: 0.22.0 - version: 0.22.0 + specifier: 0.24.0 + version: 0.24.0 '@leather.io/query': - specifier: 2.26.1 - version: 2.26.1(encoding@0.1.13)(react@18.3.1) + specifier: 2.26.9 + version: 2.26.9(encoding@0.1.13)(react@18.3.1) '@leather.io/stacks': specifier: 1.4.0 version: 1.4.0(encoding@0.1.13) @@ -3018,7 +3018,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {node: '>=0.10.0'} + engines: {'0': node >=0.10.0} '@expo/cli@0.18.28': resolution: {integrity: sha512-fvbVPId6s6etindzP6Nzos/CS1NurMVy4JKozjebArHr63tBid5i/UY5Pp+4wTCAM20gB2SjRdwcwoL6HFC4Iw==} @@ -3281,8 +3281,8 @@ packages: '@leather.io/bitcoin@0.17.0': resolution: {integrity: sha512-Nc4Bl2HWmxWvgIXbpa6Gs8EpqR1UJHDzXPu+R+2TIyOsjxFoGXoIQwLohxySnQa4i0UodrCXBrqDOqOTHtwbMQ==} - '@leather.io/bitcoin@0.19.0': - resolution: {integrity: sha512-fqE1peFL3kgOnHcQgo8s/ClmLTIWRZrOBoFMWjM+8x8vcZQWvBXC5oTWDSGWsrxabzQhh0jKBcj8HXMOhcf3ZA==} + '@leather.io/bitcoin@0.19.8': + resolution: {integrity: sha512-gqTD8Mtp8cYPcvQd0DnzFNz/uXbghpadPvB8rtB467g4fbjswW/52lOKzazrzNrl+CmQXuCKep4NrTBgTBRNpg==} '@leather.io/constants@0.13.5': resolution: {integrity: sha512-FOh/F/g8WepB8HfoTXsMB/BYcm/F6INPEpyEZc3ljzaN0mLwVLO1kwgMTFU9Pq7tQlITvyWiyGHcB7OYovLoUQ==} @@ -3293,11 +3293,14 @@ packages: '@leather.io/constants@0.15.1': resolution: {integrity: sha512-qgkUHOz/10jxTsprhwzBb3Iml9BkYFWtWKeomNh0nK3zVto2zb8mn6PXvcthVs5FYIYkxou6DfIAUDeUO0czDQ==} + '@leather.io/constants@0.15.2': + resolution: {integrity: sha512-QNs1xQ+DdMi9ahpxbQM+LNqgzP9hvcHE6iIepTomzZOU6iEyJkppMk1BhpHA/AgmJEmlZK7nb2EgPj6tvMKNlg==} + '@leather.io/crypto@1.6.14': resolution: {integrity: sha512-D9Z0EgvXhdDSJdaQX+KCm2czqNCRDe3Kj7YJ3Hn1GiHet+wOy68sBJzG6yzESeC0Z7f1UBuDhmN9Edq5u1Zz2g==} - '@leather.io/crypto@1.6.17': - resolution: {integrity: sha512-CctP7QMqL6DvniYTCetG92H1vYzJeJCdbyuDhwe29v2oqmEnjwRJsX5X6ue0o2sEihtjJtvXJG1HPwd+D17XGw==} + '@leather.io/crypto@1.6.25': + resolution: {integrity: sha512-ueYMQi/LQzHiXTVWXkSNyJzBcKaRywl5HRN9FjbbXSp3vAPUOnHkcyDnPT3mbtRpj9yCaDJyWk8b3kQN8ZMOwQ==} '@leather.io/eslint-config@0.7.0': resolution: {integrity: sha512-4K7olfSC+mJnG90TSaLIlytp14yDprGXwe1+oP9TLQbuPFpJai3/+g5Bp/FeUC4NZ23UVbAlGXFCav2amBb77w==} @@ -3311,14 +3314,17 @@ packages: '@leather.io/models@0.24.1': resolution: {integrity: sha512-BRjiX7N/LUlg5MMe3r5mkjUGLGtGRSpd21LPgo0lhUUSO1cBMAdp7rLACGM9LFPw+/dnUw1yMpT2hURC3pe3Gg==} + '@leather.io/models@0.24.2': + resolution: {integrity: sha512-DTxDbcQp5CZyh0ri4aiNurkHxORKbbj05vnyHGWSok33DZSE3j0/hcGHP9IA1sGPNO5OMt96ivINiMuKmRA8Fw==} + '@leather.io/panda-preset@0.8.0': resolution: {integrity: sha512-DnDSxZ5AJPYBdykTNpTeuB4WewwWUGWPoDWQdeX4/Th5gfekUwvnybLM9D3iVCkvmUgv+BZgMgCzHnd5cGO2FA==} '@leather.io/prettier-config@0.6.0': resolution: {integrity: sha512-QBKtLanfxFxXBlR58U/j8a6lBI0xzJzqqi36fXpGVp+9mJoEf6Ro6xrtFrixjW6seY6EOva4OApVnnPBsvOC/w==} - '@leather.io/query@2.26.1': - resolution: {integrity: sha512-/F0ddwk552/XAoP0dN+Yk//7m1qjvkHMZ+v6N4KFkwbNSNDh+1p5ehZ3GNf6AX1zT6pFc+6OhD4LPR7dmKdlFw==} + '@leather.io/query@2.26.9': + resolution: {integrity: sha512-ijmVE6RoiU+kZY5ApvwkZ5IAXWGZFl18AG2/YBveZNc5ExKjlNnunfqp1UFxE62REwkp8sGIWSsnZpysNosAHw==} peerDependencies: react: '*' @@ -3331,6 +3337,9 @@ packages: '@leather.io/rpc@2.4.0': resolution: {integrity: sha512-S9PYtyOnZ9LJL8ZYsEPHUWmVkVL/E7oAfNLunFY7zVI0tJUl45OXJVCOjYRRKDKOqdx2pRpdqzeUSxRZvJdyVA==} + '@leather.io/rpc@2.4.1': + resolution: {integrity: sha512-edpoAkrBXjnQPqKRJrvUJKMxr5MbgNes1gluT3EUZuYYBAj5bzQ3ZhBz+hghX72UqaA4K5c12f7rjYMuYIPb4Q==} + '@leather.io/stacks@1.4.0': resolution: {integrity: sha512-vF3eQljr+dsfg8DhlEFgQKvr9NHn9CKwt8XT51kWnULTtZH6syrABiarHGwhtE/AZz9weg5n5q/+m8b4lN6bGw==} @@ -3349,6 +3358,9 @@ packages: '@leather.io/utils@0.22.4': resolution: {integrity: sha512-7KW+SzpjaFqeB75Y7LaC9CUK0k5rDl1y1fammMUOchaPVdLrsJ+6bzLW2p+7Vh6EnoRAKGk3V4/tML9eHpH7Gg==} + '@leather.io/utils@0.22.5': + resolution: {integrity: sha512-EV39v6ymv77W8txpuhjFKsFSgsD8WDg64X4pgDBkCH6w89q5TCM66SHE+P+eSS9qgmfo30NAKx0bILy82phuLw==} + '@ledgerhq/devices@8.4.2': resolution: {integrity: sha512-oWNTp3jCMaEvRHsXNYE/yo+PFMgXAJGFHLOU1UdE4/fYkniHbD9wdxwyZrZvrxr9hNw4/9wHiThyITwPtMzG7g==} @@ -19112,13 +19124,13 @@ snapshots: transitivePeerDependencies: - encoding - '@leather.io/bitcoin@0.19.0(encoding@0.1.13)': + '@leather.io/bitcoin@0.19.8(encoding@0.1.13)': dependencies: '@bitcoinerlab/secp256k1': 1.0.2 - '@leather.io/constants': 0.14.0 - '@leather.io/crypto': 1.6.17 - '@leather.io/models': 0.24.0 - '@leather.io/utils': 0.21.1 + '@leather.io/constants': 0.15.2 + '@leather.io/crypto': 1.6.25(encoding@0.1.13) + '@leather.io/models': 0.24.2 + '@leather.io/utils': 0.22.5(encoding@0.1.13) '@noble/hashes': 1.5.0 '@noble/secp256k1': 2.1.0 '@scure/base': 1.1.9 @@ -19149,6 +19161,10 @@ snapshots: dependencies: '@leather.io/models': 0.24.1 + '@leather.io/constants@0.15.2': + dependencies: + '@leather.io/models': 0.24.2 + '@leather.io/crypto@1.6.14': dependencies: '@leather.io/utils': 0.20.0 @@ -19156,12 +19172,14 @@ snapshots: '@scure/bip39': 1.4.0 just-memoize: 2.2.0 - '@leather.io/crypto@1.6.17': + '@leather.io/crypto@1.6.25(encoding@0.1.13)': dependencies: - '@leather.io/utils': 0.21.1 + '@leather.io/utils': 0.22.5(encoding@0.1.13) '@scure/bip32': 1.5.0 '@scure/bip39': 1.4.0 just-memoize: 2.2.0 + transitivePeerDependencies: + - encoding '@leather.io/eslint-config@0.7.0(typescript@5.4.5)': dependencies: @@ -19191,6 +19209,12 @@ snapshots: bignumber.js: 9.1.2 zod: 3.23.8 + '@leather.io/models@0.24.2': + dependencies: + '@stacks/stacks-blockchain-api-types': 7.8.2 + bignumber.js: 9.1.2 + zod: 3.23.8 + '@leather.io/panda-preset@0.8.0(jsdom@22.1.0)(typescript@5.4.5)': dependencies: '@pandacss/dev': 0.46.1(jsdom@22.1.0)(typescript@5.4.5) @@ -19206,15 +19230,15 @@ snapshots: - '@vue/compiler-sfc' - supports-color - '@leather.io/query@2.26.1(encoding@0.1.13)(react@18.3.1)': + '@leather.io/query@2.26.9(encoding@0.1.13)(react@18.3.1)': dependencies: '@fungible-systems/zone-file': 2.0.0 '@hirosystems/token-metadata-api-client': 1.2.0(encoding@0.1.13) - '@leather.io/bitcoin': 0.19.0(encoding@0.1.13) - '@leather.io/constants': 0.14.0 - '@leather.io/models': 0.24.0 - '@leather.io/rpc': 2.1.22 - '@leather.io/utils': 0.21.1 + '@leather.io/bitcoin': 0.19.8(encoding@0.1.13) + '@leather.io/constants': 0.15.2 + '@leather.io/models': 0.24.2 + '@leather.io/rpc': 2.4.1(encoding@0.1.13) + '@leather.io/utils': 0.22.5(encoding@0.1.13) '@noble/hashes': 1.5.0 '@scure/base': 1.1.9 '@scure/bip32': 1.5.0 @@ -19257,6 +19281,14 @@ snapshots: transitivePeerDependencies: - encoding + '@leather.io/rpc@2.4.1(encoding@0.1.13)': + dependencies: + '@leather.io/models': 0.24.2 + '@stacks/network': 6.13.0(encoding@0.1.13) + zod: 3.23.8 + transitivePeerDependencies: + - encoding + '@leather.io/stacks@1.4.0(encoding@0.1.13)': dependencies: '@leather.io/constants': 0.13.5 @@ -19357,6 +19389,15 @@ snapshots: transitivePeerDependencies: - encoding + '@leather.io/utils@0.22.5(encoding@0.1.13)': + dependencies: + '@leather.io/constants': 0.15.2 + '@leather.io/models': 0.24.2 + '@leather.io/rpc': 2.4.1(encoding@0.1.13) + bignumber.js: 9.1.2 + transitivePeerDependencies: + - encoding + '@ledgerhq/devices@8.4.2': dependencies: '@ledgerhq/errors': 6.18.0 diff --git a/src/app/query/bitcoin/balance/btc-balance-native-segwit.hooks.ts b/src/app/query/bitcoin/balance/btc-balance-native-segwit.hooks.ts index eaf046d3dc..028f07083e 100644 --- a/src/app/query/bitcoin/balance/btc-balance-native-segwit.hooks.ts +++ b/src/app/query/bitcoin/balance/btc-balance-native-segwit.hooks.ts @@ -13,6 +13,10 @@ function createBtcCryptoAssetBalance(balance: Money): BtcCryptoAssetBalance { availableBalance: balance, protectedBalance: createMoney(0, 'BTC'), uneconomicalBalance: createMoney(0, 'BTC'), + pendingBalance: createMoney(0, 'BTC'), + totalBalance: balance, + inboundBalance: createMoney(0, 'BTC'), + outboundBalance: createMoney(0, 'BTC'), }; } From 7660e935190e5cb019172a1cbdbd966678ffc64f Mon Sep 17 00:00:00 2001 From: Will Cameron Date: Thu, 19 Dec 2024 13:48:03 -0800 Subject: [PATCH 2/4] chore: update platform to extension --- src/shared/utils/analytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/utils/analytics.ts b/src/shared/utils/analytics.ts index c3e6331315..7a071b1167 100644 --- a/src/shared/utils/analytics.ts +++ b/src/shared/utils/analytics.ts @@ -28,7 +28,7 @@ const segmentClient = new AnalyticsBrowser(); export const analytics = configureAnalyticsClient({ client: segmentClient, defaultProperties: { - platform: 'mobile', + platform: 'extension', }, }); From 3b223348a6f3f7f52d74aa8d7a5a45bbb441f03a Mon Sep 17 00:00:00 2001 From: Alex Perry Date: Sat, 21 Dec 2024 12:05:48 +0100 Subject: [PATCH 3/4] fix: broken sbtc supply cap check --- src/app/pages/swap/hooks/use-swap-form.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/pages/swap/hooks/use-swap-form.tsx b/src/app/pages/swap/hooks/use-swap-form.tsx index b985627712..49c77602e1 100644 --- a/src/app/pages/swap/hooks/use-swap-form.tsx +++ b/src/app/pages/swap/hooks/use-swap-form.tsx @@ -30,12 +30,12 @@ export function useSwapForm() { const remainingSbtcPegCapSupply = useMemo(() => { const sBtcPegCap = sBtcLimits?.pegCap; - if (!sBtcPegCap) return; + if (!sBtcPegCap || !supply) return; const currentSupplyValue = supply?.result && cvToValue(hexToCV(supply?.result)); return convertAmountToFractionalUnit( - createMoney(new BigNumber(Number(sBtcPegCap - currentSupplyValue)), 'BTC', BTC_DECIMALS) + createMoney(new BigNumber(Number(sBtcPegCap - currentSupplyValue.value)), 'BTC', BTC_DECIMALS) ); - }, [sBtcLimits?.pegCap, supply?.result]); + }, [sBtcLimits?.pegCap, supply]); const sBtcDepositCapMin = createMoney( new BigNumber(sBtcLimits?.perDepositMinimum ?? defaultSbtcLimits.perDepositMinimum), From 3703202054cb064d1504109f595cd73d13af7c21 Mon Sep 17 00:00:00 2001 From: Fara Woolf Date: Thu, 19 Dec 2024 13:49:09 -0500 Subject: [PATCH 4/4] fix: sbtc swap updates and bugs --- .../sbtc-deposit-status-item.tsx | 48 ++++++++++---- .../features/activity-list/activity-list.tsx | 28 ++++++-- .../activity-list/activity-list.utils.ts | 17 ++++- .../pending-transaction-list.tsx | 8 +-- .../transaction-list-item.tsx | 4 ++ .../transaction-list.model.ts | 18 +++-- .../transaction-list/transaction-list.tsx | 14 +++- .../transaction-list.utils.spec.ts | 27 ++++++-- .../transaction-list.utils.ts | 12 +++- src/app/pages/swap/bitflow-swap.utils.ts | 2 +- .../components/swap-amount-field.tsx | 31 ++++----- .../swap-details/swap-detail.layout.tsx | 8 +++ .../components/swap-details/swap-details.tsx | 8 ++- .../hooks/use-sbtc-deposit-transaction.tsx | 2 +- .../swap/hooks/use-swap-assets-from-route.ts | 4 +- src/app/pages/swap/hooks/use-swap-form.tsx | 13 ++++ src/app/query/sbtc/get-stacks-block.query.ts | 53 +++++++++++++++ src/app/query/sbtc/sbtc-deposits.query.ts | 66 ++++++++++++++++++- 18 files changed, 300 insertions(+), 63 deletions(-) create mode 100644 src/app/query/sbtc/get-stacks-block.query.ts 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 index f89988aace..630713e2a8 100644 --- 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 @@ -1,12 +1,14 @@ import SbtcAvatarIconSrc from '@assets/avatars/sbtc-avatar-icon.png'; +import { HStack } from 'leather-styles/jsx'; -import { Avatar, Caption, Title } from '@leather.io/ui'; +import { Avatar, Caption, Link, 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, SbtcStatus } from '@app/query/sbtc/sbtc-deposits.query'; +import { openInNewTab } from '@app/common/utils/open-in-new-tab'; +import { SbtcDeposit, SbtcStatus } from '@app/query/sbtc/sbtc-deposits.query'; import { TransactionItemLayout } from '../transaction-item/transaction-item.layout'; @@ -17,37 +19,61 @@ function getDepositStatus(status: SbtcStatus) { return 'Pending deposit'; case 'accepted': return 'Pending mint'; - case 'confirmed': - return 'Done'; case 'failed': return 'Failed'; + case 'confirmed': default: return ''; } } +function getDepositStatusTextColor(status: SbtcStatus) { + switch (status) { + case 'pending': + case 'reprocessing': + case 'accepted': + return 'yellow.action-primary-default'; + case 'failed': + return 'red.action-primary-default'; + case 'confirmed': + default: + return ''; + } +} + +const sbtcReclaimUrl = 'https://app.stacks.co/reclaim?depositTxId='; + interface SbtcDepositTransactionItemProps { - deposit: SbtcDepositInfo; + deposit: SbtcDeposit; } export function SbtcDepositTransactionItem({ deposit }: SbtcDepositTransactionItemProps) { const { handleOpenBitcoinTxLink: handleOpenTxLink } = useBitcoinExplorerLink(); + const { bitcoinTxid, status } = deposit; + const depositFailed = status === 'failed'; - const openTxLink = () => { + function openTxLink() { void analytics.track('view_bitcoin_transaction'); - handleOpenTxLink({ txid: deposit.bitcoinTxid }); - }; + handleOpenTxLink({ txid: bitcoinTxid }); + } + + function openReclaimLink() { + return openInNewTab(`${sbtcReclaimUrl}${bitcoinTxid}`); + } return ( {}} + txCaption={truncateMiddle(bitcoinTxid, 4)} txIcon={ } txStatus={ - {getDepositStatus(deposit.status)} + + {getDepositStatus(status)} + {depositFailed && Reclaim} + } txTitle={BTC → sBTC} // Api is only returning 0 right now diff --git a/src/app/features/activity-list/activity-list.tsx b/src/app/features/activity-list/activity-list.tsx index b9c50e337d..4a92a03579 100644 --- a/src/app/features/activity-list/activity-list.tsx +++ b/src/app/features/activity-list/activity-list.tsx @@ -12,14 +12,22 @@ 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 { + useSbtcConfirmedDeposits, + useSbtcFailedDeposits, + 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'; import { useUpdateSubmittedTransactions } from '@app/store/submitted-transactions/submitted-transactions.hooks'; import { useSubmittedTransactions } from '@app/store/submitted-transactions/submitted-transactions.selectors'; -import { convertBitcoinTxsToListType, convertStacksTxsToListType } from './activity-list.utils'; +import { + convertBitcoinTxsToListType, + convertSbtcDepositToListType, + convertStacksTxsToListType, +} from './activity-list.utils'; import { NoAccountActivity } from './components/no-account-activity'; import { PendingTransactionList } from './components/pending-transaction-list/pending-transaction-list'; import { SubmittedTransactionList } from './components/submitted-transaction-list/submitted-transaction-list'; @@ -64,8 +72,12 @@ export function ActivityList() { [nsPendingTxs, trPendingTxs] ); - const { isLoading: isLoadingSbtcDeposits, pendingSbtcDeposits } = + const { isLoading: isLoadingSbtcPendingDeposits, pendingSbtcDeposits } = useSbtcPendingDeposits(stxAddress); + const { isLoading: isLoadingSbtcConfirmedDeposits, confirmedSbtcDeposits } = + useSbtcConfirmedDeposits(stxAddress); + const { isLoading: isLoadingSbtcFailedDeposits, failedSbtcDeposits } = + useSbtcFailedDeposits(stxAddress); const { isLoading: isLoadingStacksTransactions, data: stacksTransactionsWithTransfers } = useGetAccountTransactionsWithTransfersQuery(stxAddress); @@ -85,7 +97,9 @@ export function ActivityList() { isLoadingTrBitcoinTransactions || isLoadingStacksTransactions || isLoadingStacksPendingTransactions || - isLoadingSbtcDeposits; + isLoadingSbtcPendingDeposits || + isLoadingSbtcConfirmedDeposits || + isLoadingSbtcFailedDeposits; const transactionListBitcoinTxs = useMemo(() => { return convertBitcoinTxsToListType( @@ -135,7 +149,7 @@ export function ActivityList() { {hasPendingTransactions && ( )} @@ -143,6 +157,10 @@ export function ActivityList() { )} diff --git a/src/app/features/activity-list/activity-list.utils.ts b/src/app/features/activity-list/activity-list.utils.ts index c4d74fa71c..7815d34b85 100644 --- a/src/app/features/activity-list/activity-list.utils.ts +++ b/src/app/features/activity-list/activity-list.utils.ts @@ -2,8 +2,11 @@ import { AddressTransactionWithTransfers } from '@stacks/stacks-blockchain-api-t import type { BitcoinTx } from '@leather.io/models'; -import { +import type { SbtcDeposit } from '@app/query/sbtc/sbtc-deposits.query'; + +import type { TransactionListBitcoinTx, + TransactionListSbtcDeposit, TransactionListStacksTx, } from './components/transaction-list/transaction-list.model'; @@ -21,6 +24,13 @@ function createStacksTxTypeWrapper(tx: AddressTransactionWithTransfers): Transac }; } +function createSbtcDepositTxTypeWrapper(deposit: SbtcDeposit): TransactionListSbtcDeposit { + return { + blockchain: 'bitcoin-stacks', + deposit, + }; +} + export function convertBitcoinTxsToListType(txs?: BitcoinTx[]) { if (!txs) return []; const confirmedTxs = txs.filter(tx => tx.status.confirmed); @@ -31,3 +41,8 @@ export function convertStacksTxsToListType(txs?: AddressTransactionWithTransfers if (!txs) return []; return txs.map(tx => createStacksTxTypeWrapper(tx)); } + +export function convertSbtcDepositToListType(deposits?: SbtcDeposit[]) { + if (!deposits) return []; + return deposits.map(deposit => createSbtcDepositTxTypeWrapper(deposit)); +} 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 3174080c24..de9bf7ca52 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 @@ -5,18 +5,18 @@ 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 type { SbtcDeposit } from '@app/query/sbtc/sbtc-deposits.query'; import { PendingTransactionListLayout } from './pending-transaction-list.layout'; interface PendingTransactionListProps { bitcoinTxs: BitcoinTx[]; - sBtcDeposits: SbtcDepositInfo[]; + sbtcDeposits: SbtcDeposit[]; stacksTxs: MempoolTransaction[]; } export function PendingTransactionList({ bitcoinTxs, - sBtcDeposits, + sbtcDeposits, stacksTxs, }: PendingTransactionListProps) { return ( @@ -24,7 +24,7 @@ export function PendingTransactionList({ {bitcoinTxs.map(tx => ( ))} - {sBtcDeposits.map(deposit => ( + {sbtcDeposits.map(deposit => ( ))} {stacksTxs.map(tx => ( diff --git a/src/app/features/activity-list/components/transaction-list/transaction-list-item.tsx b/src/app/features/activity-list/components/transaction-list/transaction-list-item.tsx index af3debc6c9..849727f4e6 100644 --- a/src/app/features/activity-list/components/transaction-list/transaction-list-item.tsx +++ b/src/app/features/activity-list/components/transaction-list/transaction-list-item.tsx @@ -1,3 +1,5 @@ +import { SbtcDepositTransactionItem } from '@app/components/sbtc-deposit-status-item/sbtc-deposit-status-item'; + import { BitcoinTransaction } from './bitcoin-transaction/bitcoin-transaction'; import { StacksTransaction } from './stacks-transaction/stacks-transaction'; import { TransactionListTxs } from './transaction-list.model'; @@ -11,6 +13,8 @@ export function TransactionListItem({ tx }: TransactionListItemProps) { return ; case 'stacks': return ; + case 'bitcoin-stacks': + return ; default: return null; } diff --git a/src/app/features/activity-list/components/transaction-list/transaction-list.model.ts b/src/app/features/activity-list/components/transaction-list/transaction-list.model.ts index bf21bcaf30..01dfbc7d7b 100644 --- a/src/app/features/activity-list/components/transaction-list/transaction-list.model.ts +++ b/src/app/features/activity-list/components/transaction-list/transaction-list.model.ts @@ -1,15 +1,25 @@ import { AddressTransactionWithTransfers } from '@stacks/stacks-blockchain-api-types'; -import type { BitcoinTx, Blockchain } from '@leather.io/models'; +import type { BitcoinTx } from '@leather.io/models'; + +import type { SbtcDeposit } from '@app/query/sbtc/sbtc-deposits.query'; export interface TransactionListBitcoinTx { - blockchain: Extract; + blockchain: 'bitcoin'; transaction: BitcoinTx; } export interface TransactionListStacksTx { - blockchain: Extract; + blockchain: 'stacks'; transaction: AddressTransactionWithTransfers; } -export type TransactionListTxs = TransactionListBitcoinTx | TransactionListStacksTx; +export interface TransactionListSbtcDeposit { + blockchain: 'bitcoin-stacks'; + deposit: SbtcDeposit; +} + +export type TransactionListTxs = + | TransactionListBitcoinTx + | TransactionListStacksTx + | TransactionListSbtcDeposit; diff --git a/src/app/features/activity-list/components/transaction-list/transaction-list.tsx b/src/app/features/activity-list/components/transaction-list/transaction-list.tsx index 66fd66605b..d145490bc8 100644 --- a/src/app/features/activity-list/components/transaction-list/transaction-list.tsx +++ b/src/app/features/activity-list/components/transaction-list/transaction-list.tsx @@ -5,19 +5,25 @@ import { Box } from 'leather-styles/jsx'; 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'; +import type { + TransactionListBitcoinTx, + TransactionListSbtcDeposit, + TransactionListStacksTx, +} from './transaction-list.model'; import { createTxDateFormatList, getTransactionId } from './transaction-list.utils'; import { TransactionsByDateLayout } from './transactions-by-date.layout'; interface TransactionListProps { bitcoinTxs: TransactionListBitcoinTx[]; stacksTxs: TransactionListStacksTx[]; + sbtcDeposits: TransactionListSbtcDeposit[]; currentBitcoinAddress: string; } export function TransactionList({ bitcoinTxs, stacksTxs, + sbtcDeposits, currentBitcoinAddress, }: TransactionListProps) { const { intersectionSentinel, visibleTxsNum } = useTransactionListRender({ @@ -25,8 +31,10 @@ export function TransactionList({ }); const txsGroupedByDate = useMemo( () => - bitcoinTxs.length || stacksTxs.length ? createTxDateFormatList(bitcoinTxs, stacksTxs) : [], - [bitcoinTxs, stacksTxs] + bitcoinTxs.length || stacksTxs.length || sbtcDeposits.length + ? createTxDateFormatList(bitcoinTxs, stacksTxs, sbtcDeposits) + : [], + [bitcoinTxs, sbtcDeposits, stacksTxs] ); const groupedByDateTxsLength = useMemo(() => { diff --git a/src/app/features/activity-list/components/transaction-list/transaction-list.utils.spec.ts b/src/app/features/activity-list/components/transaction-list/transaction-list.utils.spec.ts index 015402f3d2..1f6b1c7fd0 100644 --- a/src/app/features/activity-list/components/transaction-list/transaction-list.utils.spec.ts +++ b/src/app/features/activity-list/components/transaction-list/transaction-list.utils.spec.ts @@ -1,7 +1,14 @@ import { AddressTransactionWithTransfers, Transaction } from '@stacks/stacks-blockchain-api-types'; import dayjs from 'dayjs'; -import { TransactionListBitcoinTx, TransactionListStacksTx } from './transaction-list.model'; +import type { StacksBlock } from '@app/query/sbtc/get-stacks-block.query'; +import type { SbtcDeposit } from '@app/query/sbtc/sbtc-deposits.query'; + +import type { + TransactionListBitcoinTx, + TransactionListSbtcDeposit, + TransactionListStacksTx, +} from './transaction-list.model'; import { createTxDateFormatList } from './transaction-list.utils'; function createFakeTx(tx: Partial) { @@ -17,6 +24,13 @@ function createFakeTx(tx: Partial) { } as TransactionListStacksTx; } +function createFakeDeposit(block: Partial) { + return { + blockchain: 'bitcoin-stacks', + deposit: { block } as Partial, + } as TransactionListSbtcDeposit; +} + describe(createTxDateFormatList.name, () => { test('grouping by date', () => { const mockBitcoinTx = { @@ -26,7 +40,7 @@ describe(createTxDateFormatList.name, () => { const mockStacksTx = createFakeTx({ burn_block_time_iso: '1991-02-08T13:48:04.699Z', }); - expect(createTxDateFormatList([mockBitcoinTx], [mockStacksTx])).toEqual([ + expect(createTxDateFormatList([mockBitcoinTx], [mockStacksTx], [])).toEqual([ { date: '1991-02-08', displayDate: 'Feb 8th, 1991', @@ -42,7 +56,8 @@ describe(createTxDateFormatList.name, () => { transaction: { status: { confirmed: true, block_time: dayjs().unix() } }, } as TransactionListBitcoinTx; const mockStacksTx = createFakeTx({ burn_block_time_iso: today }); - const result = createTxDateFormatList([mockBitcoinTx], [mockStacksTx]); + const mockSbtcDeposit = createFakeDeposit({ burn_block_time_iso: today }); + const result = createTxDateFormatList([mockBitcoinTx], [mockStacksTx], [mockSbtcDeposit]); expect(result[0].date).toEqual(today.split('T')[0]); expect(result[0].displayDate).toEqual('Today'); }); @@ -55,7 +70,8 @@ describe(createTxDateFormatList.name, () => { transaction: { status: { confirmed: true, block_time: dayjs().subtract(1, 'day').unix() } }, } as TransactionListBitcoinTx; const mockStacksTx = createFakeTx({ burn_block_time_iso: yesterday.toISOString() }); - const result = createTxDateFormatList([mockBitcoinTx], [mockStacksTx]); + const mockSbtcDeposit = createFakeDeposit({ burn_block_time_iso: yesterday.toISOString() }); + const result = createTxDateFormatList([mockBitcoinTx], [mockStacksTx], [mockSbtcDeposit]); expect(result[0].date).toEqual(yesterday.toISOString().split('T')[0]); expect(result[0].displayDate).toEqual('Yesterday'); }); @@ -65,7 +81,8 @@ describe(createTxDateFormatList.name, () => { date.setFullYear(date.getFullYear()); date.setMonth(6); const mockStacksTx = createFakeTx({ burn_block_time_iso: date.toISOString() }); - const result = createTxDateFormatList([], [mockStacksTx]); + const mockSbtcDeposit = createFakeDeposit({ burn_block_time_iso: date.toISOString() }); + const result = createTxDateFormatList([], [mockStacksTx], [mockSbtcDeposit]); expect(result[0].date).toEqual(date.toISOString().split('T')[0]); expect(result[0].displayDate).not.toContain(new Date().getFullYear().toString()); }); diff --git a/src/app/features/activity-list/components/transaction-list/transaction-list.utils.ts b/src/app/features/activity-list/components/transaction-list/transaction-list.utils.ts index 1f8a2e204b..acaec94568 100644 --- a/src/app/features/activity-list/components/transaction-list/transaction-list.utils.ts +++ b/src/app/features/activity-list/components/transaction-list/transaction-list.utils.ts @@ -3,8 +3,9 @@ import dayjs from 'dayjs'; import { isUndefined } from '@leather.io/utils'; import { displayDate, isoDateToLocalDateSafe, todaysIsoDate } from '@app/common/date-utils'; -import { +import type { TransactionListBitcoinTx, + TransactionListSbtcDeposit, TransactionListStacksTx, TransactionListTxs, } from '@app/features/activity-list/components/transaction-list/transaction-list.model'; @@ -30,6 +31,8 @@ function getTransactionTime(listTx: TransactionListTxs) { listTx.transaction.tx.burn_block_time_iso || listTx.transaction.tx.parent_burn_block_time_iso ); + case 'bitcoin-stacks': + return listTx.deposit.block?.burn_block_time_iso; default: return undefined; } @@ -51,6 +54,8 @@ function getTransactionBlockHeight(listTx: TransactionListTxs) { return listTx.transaction.status.block_height; case 'stacks': return listTx.transaction.tx.block_height; + case 'bitcoin-stacks': + return listTx.deposit.block?.height ?? listTx.deposit.lastUpdateHeight; default: return undefined; } @@ -141,10 +146,11 @@ function sortGroupedTransactions( export function createTxDateFormatList( bitcoinTxs: TransactionListBitcoinTx[], - stacksTxs: TransactionListStacksTx[] + stacksTxs: TransactionListStacksTx[], + sbtcDeposits: TransactionListSbtcDeposit[] ) { const formattedTxs = formatTxDateMapAsList( - groupTxsByDateMap([...bitcoinTxs, ...filterDuplicateStacksTxs(stacksTxs)]) + groupTxsByDateMap([...bitcoinTxs, ...filterDuplicateStacksTxs(stacksTxs), ...sbtcDeposits]) ); return sortGroupedTransactions(formattedTxs); } diff --git a/src/app/pages/swap/bitflow-swap.utils.ts b/src/app/pages/swap/bitflow-swap.utils.ts index 22b7cbf2e3..3a593b4b24 100644 --- a/src/app/pages/swap/bitflow-swap.utils.ts +++ b/src/app/pages/swap/bitflow-swap.utils.ts @@ -57,7 +57,7 @@ export function getCrossChainSwapSubmissionData(values: SwapFormValues): SwapSub feeType: BtcFeeType.Standard, liquidityFee: 0, maxSignerFee: 0, - protocol: 'Bitcoin L2 Labs', + protocol: 'sBTC Protocol', dexPath: [], router: [values.swapAssetBase, values.swapAssetQuote].filter(isDefined), slippage: 0, diff --git a/src/app/pages/swap/components/swap-asset-select/components/swap-amount-field.tsx b/src/app/pages/swap/components/swap-asset-select/components/swap-amount-field.tsx index d87d2907cc..51b6d2b067 100644 --- a/src/app/pages/swap/components/swap-asset-select/components/swap-amount-field.tsx +++ b/src/app/pages/swap/components/swap-asset-select/components/swap-amount-field.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, useEffect } from 'react'; +import { ChangeEvent } from 'react'; import { SwapSelectors } from '@tests/selectors/swap.selectors'; import BigNumber from 'bignumber.js'; @@ -6,8 +6,7 @@ import { useField, useFormikContext } from 'formik'; import { Stack, styled } from 'leather-styles/jsx'; import { - convertAmountToFractionalUnit, - createMoney, + createMoneyFromDecimal, formatMoneyWithoutSymbol, isDefined, isUndefined, @@ -31,37 +30,33 @@ interface SwapAmountFieldProps { name: string; } export function SwapAmountField({ amountAsFiat, isDisabled, name }: SwapAmountFieldProps) { - const { fetchQuoteAmount, isFetchingExchangeRate, onSetIsSendingMax } = useSwapContext(); + const { fetchQuoteAmount, isCrossChainSwap, isFetchingExchangeRate, onSetIsSendingMax } = + useSwapContext(); const { setFieldError, setFieldValue, values } = useFormikContext(); const [field] = useField(name); const showError = useShowFieldError(name) && name === 'swapAmountBase' && values.swapAssetQuote; - useEffect(() => { - // Clear quote amount if quote asset is reset - if (isUndefined(values.swapAssetQuote)) { - void setFieldValue('swapAmountQuote', ''); - } - }, [setFieldValue, values]); - async function onBlur(event: ChangeEvent) { const { swapAssetBase, swapAssetQuote } = values; if (isUndefined(swapAssetBase) || isUndefined(swapAssetQuote)) return; onSetIsSendingMax(false); const value = event.currentTarget.value; const toAmount = await fetchQuoteAmount(swapAssetBase, swapAssetQuote, value); - if (isUndefined(toAmount)) { + const valueLengthAsDecimals = value.length - 1; + if (isUndefined(toAmount) || valueLengthAsDecimals > swapAssetBase.balance.decimals) { await setFieldValue('swapAmountQuote', ''); return; } - const toAmountAsMoney = createMoney( - convertAmountToFractionalUnit( - new BigNumber(toAmount), - values.swapAssetQuote?.balance.decimals - ), + const toAmountAsMoney = createMoneyFromDecimal( + new BigNumber(toAmount), values.swapAssetQuote?.balance.symbol ?? '', values.swapAssetQuote?.balance.decimals ); - await setFieldValue('swapAmountQuote', formatMoneyWithoutSymbol(toAmountAsMoney)); + + await setFieldValue( + 'swapAmountQuote', + isCrossChainSwap ? toAmount : formatMoneyWithoutSymbol(toAmountAsMoney) + ); setFieldError('swapAmountQuote', undefined); } diff --git a/src/app/pages/swap/components/swap-details/swap-detail.layout.tsx b/src/app/pages/swap/components/swap-details/swap-detail.layout.tsx index 02dddfb649..3683acb9d1 100644 --- a/src/app/pages/swap/components/swap-details/swap-detail.layout.tsx +++ b/src/app/pages/swap/components/swap-details/swap-detail.layout.tsx @@ -4,16 +4,19 @@ import { Box, HStack, styled } from 'leather-styles/jsx'; import { InfoCircleIcon } from '@leather.io/ui'; +import { openInNewTab } from '@app/common/utils/open-in-new-tab'; import { BasicTooltip } from '@app/ui/components/tooltip/basic-tooltip'; interface SwapDetailLayoutProps { dataTestId?: string; + moreInfoUrl?: string; title: string; tooltipLabel?: string; value: ReactNode; } export function SwapDetailLayout({ dataTestId, + moreInfoUrl, title, tooltipLabel, value, @@ -31,6 +34,11 @@ export function SwapDetailLayout({ ) : null} + {moreInfoUrl ? ( + openInNewTab(moreInfoUrl)} type="button"> + + + ) : null} {value} diff --git a/src/app/pages/swap/components/swap-details/swap-details.tsx b/src/app/pages/swap/components/swap-details/swap-details.tsx index c7ae3980cb..f90d4fcc81 100644 --- a/src/app/pages/swap/components/swap-details/swap-details.tsx +++ b/src/app/pages/swap/components/swap-details/swap-details.tsx @@ -7,7 +7,7 @@ import { convertAmountToBaseUnit, createMoney, createMoneyFromDecimal, - formatMoneyPadded, + formatMoney, isDefined, isUndefined, satToBtc, @@ -31,11 +31,12 @@ function RouteNames(props: { swapSubmissionData: SwapSubmissionData }) { }); } +const sbtcMoreInfoUrl = 'https://github.com/stacks-network/sbtc-bridge'; const sponsoredFeeLabel = 'Sponsorship may not apply when you have pending transactions. In such cases, if you choose to proceed, the associated costs will be deducted from your balance.'; export function SwapDetails() { - const { swapSubmissionData } = useSwapContext(); + const { isCrossChainSwap, swapSubmissionData } = useSwapContext(); if ( isUndefined(swapSubmissionData) || @@ -46,7 +47,7 @@ export function SwapDetails() { const maxSignerFee = satToBtc(swapSubmissionData.maxSignerFee ?? 0); - const formattedMinToReceive = formatMoneyPadded( + const formattedMinToReceive = formatMoney( createMoneyFromDecimal( new BigNumber(swapSubmissionData.swapAmountQuote) .times(1 - swapSubmissionData.slippage) @@ -69,6 +70,7 @@ export function SwapDetails() { 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 c85c274d51..be95634d0d 100644 --- a/src/app/pages/swap/hooks/use-sbtc-deposit-transaction.tsx +++ b/src/app/pages/swap/hooks/use-sbtc-deposit-transaction.tsx @@ -37,7 +37,7 @@ import type { SwapSubmissionData } from '../swap.context'; // Also set as defaults in sbtc lib const maxSignerFee = 80_000; -const reclaimLockTime = 144; +const reclaimLockTime = 12; interface SbtcDeposit { address: string; diff --git a/src/app/pages/swap/hooks/use-swap-assets-from-route.ts b/src/app/pages/swap/hooks/use-swap-assets-from-route.ts index dfb3c4dedc..8e2080a328 100644 --- a/src/app/pages/swap/hooks/use-swap-assets-from-route.ts +++ b/src/app/pages/swap/hooks/use-swap-assets-from-route.ts @@ -9,7 +9,7 @@ import { RouteUrls } from '@shared/route-urls'; import { useSwapContext } from '../swap.context'; export function useSwapAssetsFromRoute() { - const { swappableAssetsBase, swappableAssetsQuote } = useSwapContext(); + const { onSetIsCrossChainSwap, swappableAssetsBase, swappableAssetsQuote } = useSwapContext(); const { setFieldValue, values, validateForm } = useFormikContext(); const { base, quote } = useParams(); const navigate = useNavigate(); @@ -32,10 +32,12 @@ export function useSwapAssetsFromRoute() { 'swapAssetQuote', swappableAssetsQuote.find(asset => asset.name === quote) ); + if (base === 'BTC') onSetIsCrossChainSwap(true); void validateForm(); }, [ base, navigate, + onSetIsCrossChainSwap, quote, setFieldValue, swappableAssetsBase, diff --git a/src/app/pages/swap/hooks/use-swap-form.tsx b/src/app/pages/swap/hooks/use-swap-form.tsx index 49c77602e1..881c10f172 100644 --- a/src/app/pages/swap/hooks/use-swap-form.tsx +++ b/src/app/pages/swap/hooks/use-swap-form.tsx @@ -78,6 +78,19 @@ export function useSwapForm() { return true; }, }) + .test({ + message: 'Decimal precision not supported', + test(value) { + if (!value || isFetchingExchangeRate) return true; + const { swapAssetBase } = this.parent; + const numStr = value.toString(); + if (numStr.includes('e')) { + const exponent = Math.abs(parseInt(value.toExponential().split('e')[1], 10)); + if (exponent > swapAssetBase.balance.decimals) return false; + } + return true; + }, + }) .test({ message: `Min amount is ${convertAmountToBaseUnit(sBtcDepositCapMin).toString()} BTC`, test(value) { diff --git a/src/app/query/sbtc/get-stacks-block.query.ts b/src/app/query/sbtc/get-stacks-block.query.ts new file mode 100644 index 0000000000..c52652959f --- /dev/null +++ b/src/app/query/sbtc/get-stacks-block.query.ts @@ -0,0 +1,53 @@ +import { useQueries } from '@tanstack/react-query'; +import axios from 'axios'; + +import { getHiroApiRateLimiter } from '@leather.io/query'; + +import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; + +export interface StacksBlock { + canonical: boolean; + height: number; + hash: string; + block_time: number; + block_time_iso: string; + index_block_hash: string; + parent_block_hash: string; + parent_index_block_hash: string; + burn_block_time: number; + burn_block_time_iso: string; + burn_block_hash: string; + burn_block_height: number; + miner_txid: string; + tx_count: number; + execution_cost_read_count: number; + execution_cost_read_length: number; + execution_cost_runtime: number; + execution_cost_write_count: number; + execution_cost_write_length: number; +} + +async function getStacksBlock(basePath: string, block: number): Promise { + const rateLimiter = getHiroApiRateLimiter(basePath); + const resp = await rateLimiter.add(() => axios.get(`${basePath}/extended/v2/blocks/${block}`), { + priority: 3, + throwOnTimeout: true, + }); + return resp.data; +} + +function makeGetStacksBlockQuery(basePath: string, block: number) { + return { + queryKey: ['get-stacks-block', block], + async queryFn() { + return getStacksBlock(basePath, block); + }, + }; +} + +export function useGetStacksBlocks(blocks: number[]) { + const network = useCurrentNetwork(); + return useQueries({ + queries: blocks.map(block => makeGetStacksBlockQuery(network.chain.stacks.url, block)), + }); +} diff --git a/src/app/query/sbtc/sbtc-deposits.query.ts b/src/app/query/sbtc/sbtc-deposits.query.ts index 44599d1816..00fd5f70b9 100644 --- a/src/app/query/sbtc/sbtc-deposits.query.ts +++ b/src/app/query/sbtc/sbtc-deposits.query.ts @@ -3,7 +3,10 @@ import { BytesReader, addressToString, deserializeAddress } from '@stacks/transa import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; +import { isDefined } from '@leather.io/utils'; + import { useConfigSbtc } from '../common/remote-config/remote-config.query'; +import { type StacksBlock, useGetStacksBlocks } from './get-stacks-block.query'; export enum SbtcStatus { Pending = 'pending', @@ -13,7 +16,7 @@ export enum SbtcStatus { Failed = 'failed', } -export interface SbtcDepositInfo { +interface SbtcDepositInfo { amount: number; bitcoinTxOutputIndex: number; bitcoinTxid: string; @@ -54,6 +57,29 @@ function useGetSbtcDeposits(stxAddress: string, status: string) { }); } +export interface SbtcDeposit extends SbtcDepositInfo { + block?: StacksBlock; +} + +function useSbtcDeposits(deposits: SbtcDepositInfo[]) { + const blockResults = useGetStacksBlocks(deposits.map(deposit => deposit.lastUpdateHeight)); + const isLoadingBlocks = blockResults.some(query => query.isLoading); + + return { + isLoadingBlocks, + deposits: deposits.map(deposit => { + const block = blockResults + .map(query => query.data) + .filter(isDefined) + .find(block => block.height === deposit.lastUpdateHeight); + return { + ...deposit, + block, + }; + }), + }; +} + export function useSbtcPendingDeposits(stxAddress: string) { const { data: pendingDeposits = [], isLoading: isLoadingStatusPending } = useGetSbtcDeposits( stxAddress, @@ -66,8 +92,42 @@ export function useSbtcPendingDeposits(stxAddress: string) { 'accepted' ); + const { isLoadingBlocks, deposits } = useSbtcDeposits([ + ...pendingDeposits, + ...reprocessingDeposits, + ...acceptedDeposits, + ]); + + return { + isLoading: + isLoadingStatusPending || + isLoadingStatusReprocessing || + isLoadingStatusAccepted || + isLoadingBlocks, + pendingSbtcDeposits: deposits, + }; +} + +export function useSbtcConfirmedDeposits(stxAddress: string) { + const { data: confirmedSbtcDeposits = [], isLoading: isLoadingStatusConfirmed } = + useGetSbtcDeposits(stxAddress, 'confirmed'); + const { isLoadingBlocks, deposits } = useSbtcDeposits(confirmedSbtcDeposits); + + return { + isLoading: isLoadingStatusConfirmed || isLoadingBlocks, + confirmedSbtcDeposits: deposits, + }; +} + +export function useSbtcFailedDeposits(stxAddress: string) { + const { data: failedSbtcDeposits = [], isLoading: isLoadingStatusFailed } = useGetSbtcDeposits( + stxAddress, + 'failed' + ); + const { isLoadingBlocks, deposits } = useSbtcDeposits(failedSbtcDeposits); + return { - isLoading: isLoadingStatusPending || isLoadingStatusReprocessing || isLoadingStatusAccepted, - pendingSbtcDeposits: [...pendingDeposits, ...reprocessingDeposits, ...acceptedDeposits], + isLoading: isLoadingStatusFailed || isLoadingBlocks, + failedSbtcDeposits: deposits, }; }