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([