Skip to content

Commit

Permalink
refactor: use btc fee and compliance check
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Dec 10, 2024
1 parent e0cac9a commit 885fc7e
Show file tree
Hide file tree
Showing 14 changed files with 99 additions and 59 deletions.
11 changes: 11 additions & 0 deletions public/assets/illustrations/sbtc-earn-promo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
8 changes: 4 additions & 4 deletions src/app/components/stacks-asset-avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ 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;
}
export function StacksAssetAvatar({
children,
gradientString,
imageCanonicalUri,
img,
isStx,
size = '36',
...props
Expand All @@ -20,10 +20,10 @@ export function StacksAssetAvatar({

const { color } = props;

if (imageCanonicalUri)
if (img)
return (
<Avatar.Root>
<Avatar.Image alt="FT" src={encodeURI(imageCanonicalUri)} />
<Avatar.Image alt="FT" src={encodeURI(img)} />
<Avatar.Fallback delayMs={defaultFallbackDelay}>FT</Avatar.Fallback>
</Avatar.Root>
);
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/tx-asset-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function TxAssetItem(props: TxAssetItemProps) {
<HStack>
<StacksAssetAvatar
gradientString={iconString}
imageCanonicalUri={imageCanonicalUri}
img={imageCanonicalUri}
isStx={iconString === 'STX'}
size="32"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,7 @@ export function FtTransferItem({ ftTransfer, parentTx }: FtTransferItemProps) {
const title = `${assetMetadata.name || 'Token'} Transfer`;
const value = `${isOriginator ? '-' : ''}${displayAmount.toFormat()}`;
const transferIcon = ftImageCanonicalUri ? (
<StacksAssetAvatar
color="ink.background-primary"
gradientString=""
imageCanonicalUri={ftImageCanonicalUri}
>
<StacksAssetAvatar color="ink.background-primary" gradientString="" img={ftImageCanonicalUri}>
{title}
</StacksAssetAvatar>
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -35,13 +37,14 @@ export function Sip10TokenAssetItem({
const { isTokenEnabled } = useManageTokens();

const { contractId, imageCanonicalUri, name, symbol } = info;
const isSbtc = symbol === 'sBTC';

const icon = (
<>
<StacksAssetAvatar
color="white"
gradientString={contractId}
imageCanonicalUri={getSafeImageCanonicalUri(imageCanonicalUri, name)}
img={isSbtc ? SbtcAvatarIconSrc : getSafeImageCanonicalUri(imageCanonicalUri, name)}
>
{name[0]}
</StacksAssetAvatar>
Expand Down
16 changes: 11 additions & 5 deletions src/app/features/sbtc-promo-card/sbtc-promo-card.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -15,17 +14,24 @@ function SbtcPromoCardLayout(props: SbtcPromoCardContentProps) {
return (
<Flag
reverse
img={<styled.img src={illustration} width={100} filter={invertStyle} mr="space.03" />}
img={
<styled.img
src="assets/illustrations/sbtc-earn-promo.svg"
width={100}
filter={invertStyle}
mr="space.03"
/>
}
background="ink.background-secondary"
borderRadius={8}
{...props}
>
<Box px="space.04">
<Box pl="space.04" py="space.04">
<styled.h3 textStyle="heading.05" fontSize="17px" lineHeight={1.4}>
Bridge BTC → sBTC
Earn rewards in BTC
</styled.h3>
<styled.p textStyle="label.03" mt="space.01">
And receive yields of 5%
Enroll your sBTC to unlock yields through the protocol.
</styled.p>
</Box>
</Flag>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,7 @@ export function Sip10TokenSendFormContainer({
<SelectedAssetField
icon={
avatar ? (
<StacksAssetAvatar
gradientString={avatar.avatar}
imageCanonicalUri={avatar.imageCanonicalUri}
/>
<StacksAssetAvatar gradientString={avatar.avatar} img={avatar.imageCanonicalUri} />
) : (
<StxAvatarIcon />
)
Expand Down
19 changes: 12 additions & 7 deletions src/app/pages/swap/bitflow-swap-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -45,7 +45,7 @@ function BitflowSwapContainer() {
const generateUnsignedTx = useGenerateStacksContractCallUnsignedTx();
const signTx = useSignStacksTransaction();
const broadcastStacksSwap = useStacksBroadcastSwap();
const { onDepositSbtc } = useSbtcDepositTransaction();
const { onDepositSbtc, onReviewDepositSbtc } = useSbtcDepositTransaction();

const {
fetchRouteQuote,
Expand All @@ -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;
}
Expand All @@ -87,7 +92,7 @@ function BitflowSwapContainer() {
if (!routeQuote) return;

onSetSwapSubmissionData(
getSwapSubmissionData({ bitflowSwapAssets, routeQuote, slippage, values })
getStacksSwapSubmissionData({ bitflowSwapAssets, routeQuote, slippage, values })
);
swapNavigate(RouteUrls.SwapReview);
} finally {
Expand All @@ -98,6 +103,7 @@ function BitflowSwapContainer() {
bitflowSwapAssets,
fetchRouteQuote,
isCrossChainSwap,
onReviewDepositSbtc,
onSetSwapSubmissionData,
slippage,
swapNavigate,
Expand All @@ -122,7 +128,6 @@ function BitflowSwapContainer() {

setIsLoading();

// TODO: Handle cross-chain swaps
if (isCrossChainSwap) {
return await onDepositSbtc(swapSubmissionData);
}
Expand Down
11 changes: 6 additions & 5 deletions src/app/pages/swap/bitflow-swap.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]) {
Expand All @@ -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',
Expand All @@ -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,
Expand Down
63 changes: 45 additions & 18 deletions src/app/pages/swap/hooks/use-sbtc-deposit-transaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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: [
{
Expand All @@ -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) {
Expand All @@ -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);
Expand Down
9 changes: 1 addition & 8 deletions src/app/pages/swap/hooks/use-swap-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
1 change: 1 addition & 0 deletions src/app/pages/swap/swap.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface SwapSubmissionData extends SwapFormValues {
slippage: number;
sponsored?: boolean;
timestamp: string;
txData?: Record<string, any>;
}

export interface SwapContext {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down

0 comments on commit 885fc7e

Please sign in to comment.