diff --git a/public/assets/illustrations/sbtc-earn-promo.svg b/public/assets/illustrations/sbtc-earn-promo.svg new file mode 100644 index 00000000000..60ef4c7e85e --- /dev/null +++ b/public/assets/illustrations/sbtc-earn-promo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/assets/illustrations/stack-of-coins-with-hands-coming-out.png b/public/assets/illustrations/stack-of-coins-with-hands-coming-out.png deleted file mode 100644 index 4568e7f84e8..00000000000 Binary files a/public/assets/illustrations/stack-of-coins-with-hands-coming-out.png and /dev/null differ diff --git a/src/app/components/stacks-asset-avatar.tsx b/src/app/components/stacks-asset-avatar.tsx index a6d1d8a47b3..84bd5467fa3 100644 --- a/src/app/components/stacks-asset-avatar.tsx +++ b/src/app/components/stacks-asset-avatar.tsx @@ -3,7 +3,7 @@ import { Box, BoxProps } from 'leather-styles/jsx'; import { Avatar, DynamicColorCircle, StxAvatarIcon, defaultFallbackDelay } from '@leather.io/ui'; interface StacksAssetAvatarProps extends BoxProps { - imageCanonicalUri?: string; + img?: string; gradientString: string; isStx?: boolean; size?: string; @@ -11,7 +11,7 @@ interface StacksAssetAvatarProps extends BoxProps { export function StacksAssetAvatar({ children, gradientString, - imageCanonicalUri, + img, isStx, size = '36', ...props @@ -20,10 +20,10 @@ export function StacksAssetAvatar({ const { color } = props; - if (imageCanonicalUri) + if (img) return ( - + FT ); diff --git a/src/app/components/tx-asset-item.tsx b/src/app/components/tx-asset-item.tsx index 13a06f2f713..be7e9fe051f 100644 --- a/src/app/components/tx-asset-item.tsx +++ b/src/app/components/tx-asset-item.tsx @@ -18,7 +18,7 @@ export function TxAssetItem(props: TxAssetItemProps) { diff --git a/src/app/features/activity-list/components/transaction-list/stacks-transaction/ft-transfer-item.tsx b/src/app/features/activity-list/components/transaction-list/stacks-transaction/ft-transfer-item.tsx index af332960f5f..d68b5f2c3a4 100644 --- a/src/app/features/activity-list/components/transaction-list/stacks-transaction/ft-transfer-item.tsx +++ b/src/app/features/activity-list/components/transaction-list/stacks-transaction/ft-transfer-item.tsx @@ -53,11 +53,7 @@ export function FtTransferItem({ ftTransfer, parentTx }: FtTransferItemProps) { const title = `${assetMetadata.name || 'Token'} Transfer`; const value = `${isOriginator ? '-' : ''}${displayAmount.toFormat()}`; const transferIcon = ftImageCanonicalUri ? ( - + {title} ) : ( diff --git a/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-item.tsx b/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-item.tsx index 125485821af..eb0ff4c5f60 100644 --- a/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-item.tsx +++ b/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-item.tsx @@ -1,3 +1,5 @@ +import SbtcAvatarIconSrc from '@assets/avatars/sbtc-avatar-icon.png'; + import type { CryptoAssetBalance, MarketData, Sip10CryptoAssetInfo } from '@leather.io/models'; import { convertAssetBalanceToFiat } from '@app/common/asset-utils'; @@ -35,13 +37,14 @@ export function Sip10TokenAssetItem({ const { isTokenEnabled } = useManageTokens(); const { contractId, imageCanonicalUri, name, symbol } = info; + const isSbtc = symbol === 'sBTC'; const icon = ( <> {name[0]} diff --git a/src/app/features/sbtc-promo-card/sbtc-promo-card.tsx b/src/app/features/sbtc-promo-card/sbtc-promo-card.tsx index 1212f2ed542..570c2b635a2 100644 --- a/src/app/features/sbtc-promo-card/sbtc-promo-card.tsx +++ b/src/app/features/sbtc-promo-card/sbtc-promo-card.tsx @@ -1,4 +1,3 @@ -import illustration from '@assets/illustrations/stack-of-coins-with-hands-coming-out.png'; import { Box, styled } from 'leather-styles/jsx'; import { Flag, type FlagProps } from '@leather.io/ui'; @@ -15,17 +14,24 @@ function SbtcPromoCardLayout(props: SbtcPromoCardContentProps) { return ( } + img={ + + } background="ink.background-secondary" borderRadius={8} {...props} > - + - Bridge BTC → sBTC + Earn rewards in BTC - And receive yields of 5% + Enroll your sBTC to unlock yields through the protocol. diff --git a/src/app/pages/send/send-crypto-asset-form/form/sip10/sip10-token-send-form-container.tsx b/src/app/pages/send/send-crypto-asset-form/form/sip10/sip10-token-send-form-container.tsx index 97a7059f7ca..19892f6e4b8 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/sip10/sip10-token-send-form-container.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/sip10/sip10-token-send-form-container.tsx @@ -51,10 +51,7 @@ export function Sip10TokenSendFormContainer({ + ) : ( ) diff --git a/src/app/pages/swap/bitflow-swap-container.tsx b/src/app/pages/swap/bitflow-swap-container.tsx index 0c3b6b937b1..b9cbd5f00f3 100644 --- a/src/app/pages/swap/bitflow-swap-container.tsx +++ b/src/app/pages/swap/bitflow-swap-container.tsx @@ -10,7 +10,7 @@ import { serializePostCondition, } from '@stacks/transactions'; -import { isError, isUndefined } from '@leather.io/utils'; +import { isError, isUndefined, satToBtc } from '@leather.io/utils'; import { logger } from '@shared/logger'; import type { SwapFormValues } from '@shared/models/form.model'; @@ -24,7 +24,7 @@ import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/s import { useGenerateStacksContractCallUnsignedTx } from '@app/store/transactions/contract-call.hooks'; import { useSignStacksTransaction } from '@app/store/transactions/transaction.hooks'; -import { getCrossChainSwapSubmissionData, getSwapSubmissionData } from './bitflow-swap.utils'; +import { getCrossChainSwapSubmissionData, getStacksSwapSubmissionData } from './bitflow-swap.utils'; import { SwapForm } from './components/swap-form'; import { generateSwapRoutes } from './generate-swap-routes'; import { useBitflowSwap } from './hooks/use-bitflow-swap'; @@ -45,7 +45,7 @@ function BitflowSwapContainer() { const generateUnsignedTx = useGenerateStacksContractCallUnsignedTx(); const signTx = useSignStacksTransaction(); const broadcastStacksSwap = useStacksBroadcastSwap(); - const { onDepositSbtc } = useSbtcDepositTransaction(); + const { onDepositSbtc, onReviewDepositSbtc } = useSbtcDepositTransaction(); const { fetchRouteQuote, @@ -71,9 +71,14 @@ function BitflowSwapContainer() { return; } - // TODO: Handle cross-chain swaps if (isCrossChainSwap) { - onSetSwapSubmissionData(getCrossChainSwapSubmissionData(values)); + const swapData = getCrossChainSwapSubmissionData(values); + const sBtcDepositData = await onReviewDepositSbtc(swapData); + onSetSwapSubmissionData({ + ...swapData, + fee: satToBtc(sBtcDepositData?.fee ?? 0).toNumber(), + txData: sBtcDepositData?.deposit, + }); swapNavigate(RouteUrls.SwapReview); return; } @@ -87,7 +92,7 @@ function BitflowSwapContainer() { if (!routeQuote) return; onSetSwapSubmissionData( - getSwapSubmissionData({ bitflowSwapAssets, routeQuote, slippage, values }) + getStacksSwapSubmissionData({ bitflowSwapAssets, routeQuote, slippage, values }) ); swapNavigate(RouteUrls.SwapReview); } finally { @@ -98,6 +103,7 @@ function BitflowSwapContainer() { bitflowSwapAssets, fetchRouteQuote, isCrossChainSwap, + onReviewDepositSbtc, onSetSwapSubmissionData, slippage, swapNavigate, @@ -122,7 +128,6 @@ function BitflowSwapContainer() { setIsLoading(); - // TODO: Handle cross-chain swaps if (isCrossChainSwap) { return await onDepositSbtc(swapSubmissionData); } diff --git a/src/app/pages/swap/bitflow-swap.utils.ts b/src/app/pages/swap/bitflow-swap.utils.ts index ccaf1138b8a..8a854269aad 100644 --- a/src/app/pages/swap/bitflow-swap.utils.ts +++ b/src/app/pages/swap/bitflow-swap.utils.ts @@ -5,7 +5,8 @@ import { BtcFeeType, FeeTypes } from '@leather.io/models'; import { type SwapAsset, defaultSwapFee } from '@leather.io/query'; import { capitalize, isDefined, microStxToStx } from '@leather.io/utils'; -import type { SwapFormValues } from './hooks/use-swap-form'; +import type { SwapFormValues } from '@shared/models/form.model'; + import type { SwapSubmissionData } from './swap.context'; function estimateLiquidityFee(dexPath: string[]) { @@ -17,18 +18,18 @@ function formatDexPathItem(dex: string) { return name === 'ALEX' ? name : capitalize(name.toLowerCase()); } -interface GetSwapSubmissionDataArgs { +interface getStacksSwapSubmissionDataArgs { bitflowSwapAssets: SwapAsset[]; routeQuote: RouteQuote; slippage: number; values: SwapFormValues; } -export function getSwapSubmissionData({ +export function getStacksSwapSubmissionData({ bitflowSwapAssets, routeQuote, slippage, values, -}: GetSwapSubmissionDataArgs): SwapSubmissionData { +}: getStacksSwapSubmissionDataArgs): SwapSubmissionData { return { fee: microStxToStx(defaultSwapFee.amount).toNumber(), feeCurrency: 'STX', @@ -55,7 +56,7 @@ export function getCrossChainSwapSubmissionData(values: SwapFormValues): SwapSub feeCurrency: 'BTC', feeType: BtcFeeType.Standard, liquidityFee: 0, - protocol: 'sBTC', + protocol: 'Bitcoin L2 Labs', dexPath: [], router: [values.swapAssetBase, values.swapAssetQuote].filter(isDefined), slippage: 0, 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 834dc06d2dd..74db6a950c1 100644 --- a/src/app/pages/swap/hooks/use-sbtc-deposit-transaction.tsx +++ b/src/app/pages/swap/hooks/use-sbtc-deposit-transaction.tsx @@ -2,23 +2,38 @@ import { useNavigate } from 'react-router-dom'; import * as btc from '@scure/btc-signer'; +import type { P2TROut } from '@scure/btc-signer/payment'; import { REGTEST, SbtcApiClientTestnet, buildSbtcDepositTx } from 'sbtc'; import { useAverageBitcoinFeeRates } from '@leather.io/query'; import { btcToSat, createMoney } from '@leather.io/utils'; +import { logger } from '@shared/logger'; import { RouteUrls } from '@shared/route-urls'; import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading'; import { determineUtxosForSpend } from '@app/common/transactions/bitcoin/coinselect/local-coin-selection'; 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 { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import type { SwapSubmissionData } from '../swap.context'; +const maxSignerFee = 80_000; +const reclaimLockTime = 6_000; + +interface SbtcDeposit { + address: string; + depositScript: string; + reclaimScript: string; + transaction: btc.Transaction; + trOut: P2TROut; +} + +// Check network for correct client const client = new SbtcApiClientTestnet(); export function useSbtcDepositTransaction() { @@ -31,22 +46,24 @@ export function useSbtcDepositTransaction() { const networkMode = useBitcoinScureLibNetworkConfig(); const navigate = useNavigate(); + // Check if the signer is compliant + useBreakOnNonCompliantEntity(); + return { - 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()); + async onReviewDepositSbtc(swapData: SwapSubmissionData) { + if (!stacksAccount || !utxos) return; + try { - const deposit = buildSbtcDepositTx({ - amountSats: btcToSat(swapSubmissionData.swapAmountQuote).toNumber(), - network: REGTEST, + const deposit: SbtcDeposit = buildSbtcDepositTx({ + amountSats: btcToSat(swapData.swapAmountQuote).toNumber(), + network: REGTEST, // TODO: Use current network, should be set by default on client stacksAddress: stacksAccount.address, signersPublicKey: await client.fetchSignersPublicKey(), - maxSignerFee: 80_000, - reclaimLockTime: 6_000, + maxSignerFee, + reclaimLockTime, }); - const { inputs, outputs } = determineUtxosForSpend({ + const { inputs, outputs, fee } = determineUtxosForSpend({ feeRate: feeRates?.halfHourFee.toNumber() ?? 0, recipients: [ { @@ -56,8 +73,7 @@ export function useSbtcDepositTransaction() { ], utxos, }); - console.log('inputs', inputs); - console.log('outputs', outputs); + const p2wpkh = btc.p2wpkh(signer.publicKey, networkMode); for (const input of inputs) { @@ -81,15 +97,26 @@ export function useSbtcDepositTransaction() { } }); - signer.sign(deposit.transaction); - deposit.transaction.finalize(); + return { deposit, fee }; + } catch (error) { + logger.error('Error generating deposit transaction', error); + return null; + } + }, + async onDepositSbtc(swapSubmissionData: SwapSubmissionData) { + if (!stacksAccount) return; + const sBtcDeposit = swapSubmissionData.txData?.deposit as SbtcDeposit; + + try { + signer.sign(sBtcDeposit.transaction); + sBtcDeposit.transaction.finalize(); - console.log('deposit tx', deposit.transaction); - console.log('tx hex', deposit.transaction.hex); + console.log('deposit tx', sBtcDeposit.transaction); + console.log('tx hex', sBtcDeposit.transaction.hex); - const txid = await client.broadcastTx(deposit.transaction); + const txid = await client.broadcastTx(sBtcDeposit.transaction); console.log('broadcasted tx', txid); - await client.notifySbtc(deposit); + await client.notifySbtc(sBtcDeposit); toast.success('Transaction submitted!'); setIsIdle(); navigate(RouteUrls.Activity); diff --git a/src/app/pages/swap/hooks/use-swap-form.tsx b/src/app/pages/swap/hooks/use-swap-form.tsx index d407f68a43b..9a23e348ebf 100644 --- a/src/app/pages/swap/hooks/use-swap-form.tsx +++ b/src/app/pages/swap/hooks/use-swap-form.tsx @@ -5,17 +5,10 @@ import { type SwapAsset } from '@leather.io/query'; import { convertAmountToFractionalUnit, createMoney } from '@leather.io/utils'; import { FormErrorMessages } from '@shared/error-messages'; -import { StacksTransactionFormValues } from '@shared/models/form.model'; +import { type SwapFormValues } from '@shared/models/form.model'; import { useSwapContext } from '../swap.context'; -export interface SwapFormValues extends StacksTransactionFormValues { - swapAmountBase: string; - swapAmountQuote: string; - swapAssetBase?: SwapAsset; - swapAssetQuote?: SwapAsset; -} - export function useSwapForm() { const { isFetchingExchangeRate } = useSwapContext(); diff --git a/src/app/pages/swap/swap.context.ts b/src/app/pages/swap/swap.context.ts index df4da74c5ff..223be30712a 100644 --- a/src/app/pages/swap/swap.context.ts +++ b/src/app/pages/swap/swap.context.ts @@ -12,6 +12,7 @@ export interface SwapSubmissionData extends SwapFormValues { slippage: number; sponsored?: boolean; timestamp: string; + txData?: Record; } export interface SwapContext { diff --git a/src/app/query/common/compliance-checker/compliance-checker.query.ts b/src/app/query/common/compliance-checker/compliance-checker.query.ts index 14bfb3f3e50..03622580460 100644 --- a/src/app/query/common/compliance-checker/compliance-checker.query.ts +++ b/src/app/query/common/compliance-checker/compliance-checker.query.ts @@ -75,7 +75,7 @@ function useCheckAddressComplianceQueries(addresses: string[]) { export const compliantErrorBody = 'Unable to handle request, errorCode: 1398'; -export function useBreakOnNonCompliantEntity(address: string | string[]) { +export function useBreakOnNonCompliantEntity(address: string | string[] = '') { const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSignerNullable(); const complianceReports = useCheckAddressComplianceQueries([