Skip to content

Commit

Permalink
feat: cross chain swap review
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Dec 7, 2024
1 parent d2b2611 commit 69a66fe
Show file tree
Hide file tree
Showing 22 changed files with 588 additions and 252 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@
"redux-persist": "6.0.0",
"remark-gfm": "4.0.0",
"rxjs": "7.8.1",
"sbtc": "0.2.2",
"sbtc": "0.2.4",
"style-loader": "3.3.4",
"ts-debounce": "4.0.0",
"url": "0.11.3",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added public/assets/avatars/btc-avatar-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/avatars/sbtc-avatar-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion src/app/components/nonce-setter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import { useNextNonce } from '@leather.io/query';

import { StacksSendFormValues, StacksTransactionFormValues } from '@shared/models/form.model';

import type { SwapFormValues } from '@app/pages/swap/hooks/use-swap-form';
import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';

export function NonceSetter() {
const { setFieldValue, touched, values } = useFormikContext<
StacksSendFormValues | StacksTransactionFormValues
StacksSendFormValues | StacksTransactionFormValues | SwapFormValues
>();
const stxAddress = useCurrentStacksAccountAddress();
const { data: nextNonce } = useNextNonce(stxAddress);
Expand Down
130 changes: 74 additions & 56 deletions src/app/pages/swap/bitflow-swap-container.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useCallback, useState } from 'react';
import { Outlet, useNavigate } from 'react-router-dom';

import { bytesToHex } from '@stacks/common';
Expand All @@ -10,13 +10,7 @@ import {
serializePostCondition,
} from '@stacks/transactions';

import { defaultSwapFee } from '@leather.io/query';
import {
isDefined,
isError,
isUndefined,
migratePositiveAssetBalancesToTop,
} from '@leather.io/utils';
import { isError, isUndefined } from '@leather.io/utils';

import { logger } from '@shared/logger';
import { RouteUrls } from '@shared/route-urls';
Expand All @@ -29,11 +23,11 @@ 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 { estimateLiquidityFee, formatDexPathItem } from './bitflow-swap.utils';
import { getCrossChainSwapSubmissionData, getSwapSubmissionData } from './bitflow-swap.utils';
import { SwapForm } from './components/swap-form';
import { generateSwapRoutes } from './generate-swap-routes';
import { useBitflowSwap } from './hooks/use-bitflow-swap';
import { useBtcSwapAsset, useSBtcSwapAsset } from './hooks/use-sbtc-bridge-assets';
import { useSBtcDepositTransaction } from './hooks/use-sbtc-deposit-transaction';
import { useStacksBroadcastSwap } from './hooks/use-stacks-broadcast-swap';
import { SwapFormValues } from './hooks/use-swap-form';
import { useSwapNavigate } from './hooks/use-swap-navigate';
Expand All @@ -51,63 +45,66 @@ function BitflowSwapContainer() {
const generateUnsignedTx = useGenerateStacksContractCallUnsignedTx();
const signTx = useSignStacksTransaction();
const broadcastStacksSwap = useStacksBroadcastSwap();

// Bridge assets
const btcAsset = useBtcSwapAsset();
const sBtcAsset = useSBtcSwapAsset();
const { onDepositSBtc } = useSBtcDepositTransaction();

const {
fetchRouteQuote,
fetchQuoteAmount,
isCrossChainSwap,
isFetchingExchangeRate,
onSetIsCrossChainSwap,
onSetIsFetchingExchangeRate,
onSetSwapSubmissionData,
slippage,
bitflowSwapAssets,
swappableAssetsBase,
swappableAssetsQuote,
swapSubmissionData,
} = useBitflowSwap();

async function onSubmitSwapForReview(values: SwapFormValues) {
try {
setIsPreparingSwapReview(true);
if (isUndefined(values.swapAssetBase) || isUndefined(values.swapAssetQuote)) {
logger.error('Error submitting swap for review');
return;
const onSubmitSwapForReview = useCallback(
async (values: SwapFormValues) => {
try {
setIsPreparingSwapReview(true);
if (isUndefined(values.swapAssetBase) || isUndefined(values.swapAssetQuote)) {
logger.error('Error submitting swap for review');
return;
}

// TODO: Handle cross-chain swaps
if (isCrossChainSwap) {
onSetSwapSubmissionData(getCrossChainSwapSubmissionData(values));
swapNavigate(RouteUrls.SwapReview);
return;
}

const routeQuote = await fetchRouteQuote(
values.swapAssetBase,
values.swapAssetQuote,
values.swapAmountBase
);

if (!routeQuote) return;

onSetSwapSubmissionData(
getSwapSubmissionData({ bitflowSwapAssets, routeQuote, slippage, values })
);
swapNavigate(RouteUrls.SwapReview);
} finally {
setIsPreparingSwapReview(false);
}
},
[
bitflowSwapAssets,
fetchRouteQuote,
isCrossChainSwap,
onSetSwapSubmissionData,
slippage,
swapNavigate,
]
);

const routeQuote = await fetchRouteQuote(
values.swapAssetBase,
values.swapAssetQuote,
values.swapAmountBase
);
if (!routeQuote) return;

onSetSwapSubmissionData({
fee: defaultSwapFee.amount.toString(),
feeCurrency: values.feeCurrency,
feeType: values.feeType,
liquidityFee: estimateLiquidityFee(routeQuote.route.dex_path),
nonce: values.nonce,
protocol: 'Bitflow',
dexPath: routeQuote.route.dex_path.map(formatDexPathItem),
router: routeQuote.route.token_path
.map(x => bitflowSwapAssets.find(asset => asset.tokenId === x))
.filter(isDefined),
slippage,
sponsored: false,
swapAmountBase: values.swapAmountBase,
swapAmountQuote: values.swapAmountQuote,
swapAssetBase: values.swapAssetBase,
swapAssetQuote: values.swapAssetQuote,
timestamp: new Date().toISOString(),
});
swapNavigate(RouteUrls.SwapReview);
} finally {
setIsPreparingSwapReview(false);
}
}

async function onSubmitSwap() {
const onSubmitSwap = useCallback(async () => {
if (isLoading) return;

if (isUndefined(currentAccount) || isUndefined(swapSubmissionData)) {
Expand All @@ -125,12 +122,18 @@ function BitflowSwapContainer() {

setIsLoading();

// TODO: Handle cross-chain swaps
if (isCrossChainSwap) {
return await onDepositSBtc();
}

try {
const routeQuote = await fetchRouteQuote(
swapSubmissionData.swapAssetBase,
swapSubmissionData.swapAssetQuote,
swapSubmissionData.swapAmountBase
);

if (!routeQuote) return;

const swapExecutionData = {
Expand Down Expand Up @@ -184,19 +187,34 @@ function BitflowSwapContainer() {
} finally {
setIsIdle();
}
}
}, [
broadcastStacksSwap,
currentAccount,
fetchRouteQuote,
generateUnsignedTx,
isCrossChainSwap,
isLoading,
navigate,
onDepositSBtc,
setIsIdle,
setIsLoading,
signTx,
swapSubmissionData,
]);

const swapContextValue: SwapContext = {
fetchQuoteAmount,
isCrossChainSwap,
isFetchingExchangeRate,
isSendingMax,
isPreparingSwapReview,
onSetIsCrossChainSwap,
onSetIsFetchingExchangeRate,
onSetIsSendingMax: value => setIsSendingMax(value),
onSubmitSwapForReview,
onSubmitSwap,
swappableAssetsBase: [...[btcAsset], ...migratePositiveAssetBalancesToTop(bitflowSwapAssets)],
swappableAssetsQuote: [...[sBtcAsset], ...bitflowSwapAssets],
swappableAssetsBase,
swappableAssetsQuote,
swapSubmissionData,
};

Expand Down
59 changes: 58 additions & 1 deletion src/app/pages/swap/bitflow-swap.utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import BigNumber from 'bignumber.js';
import type { RouteQuote } from 'bitflow-sdk';
import { P } from 'pino';

Check failure on line 3 in src/app/pages/swap/bitflow-swap.utils.ts

View workflow job for this annotation

GitHub Actions / typecheck

'P' is declared but its value is never read.

import { capitalize } from '@leather.io/utils';
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 { SwapSubmissionData } from './swap.context';

export function estimateLiquidityFee(dexPath: string[]) {
return new BigNumber(dexPath.length).times(0.3).toNumber();
Expand All @@ -10,3 +17,53 @@ export function formatDexPathItem(dex: string) {
const name = dex.split('_')[0];
return name === 'ALEX' ? name : capitalize(name.toLowerCase());
}

interface GetSwapSubmissionDataArgs {
bitflowSwapAssets: SwapAsset[];
routeQuote: RouteQuote;
slippage: number;
values: SwapFormValues;
}
export function getSwapSubmissionData({
bitflowSwapAssets,
routeQuote,
slippage,
values,
}: GetSwapSubmissionDataArgs): SwapSubmissionData {
return {
fee: microStxToStx(defaultSwapFee.amount).toNumber(),
feeCurrency: 'STX',
feeType: FeeTypes[FeeTypes.Middle],
liquidityFee: estimateLiquidityFee(routeQuote.route.dex_path),
nonce: values.nonce,
protocol: 'Bitflow',
dexPath: routeQuote.route.dex_path.map(formatDexPathItem),
router: routeQuote.route.token_path
.map(x => bitflowSwapAssets.find(asset => asset.tokenId === x))
.filter(isDefined),
slippage,
swapAmountBase: values.swapAmountBase,
swapAmountQuote: values.swapAmountQuote,
swapAssetBase: values.swapAssetBase,
swapAssetQuote: values.swapAssetQuote,
timestamp: new Date().toISOString(),
};
}

export function getCrossChainSwapSubmissionData(values: SwapFormValues): SwapSubmissionData {
return {
fee: 0,
feeCurrency: 'BTC',
feeType: BtcFeeType.Standard,
liquidityFee: 0,
protocol: 'sBTC',
dexPath: [],
router: [values.swapAssetBase, values.swapAssetQuote].filter(isDefined),
slippage: 0,
swapAmountBase: values.swapAmountBase,
swapAmountQuote: values.swapAmountQuote,
swapAssetBase: values.swapAssetBase,
swapAssetQuote: values.swapAssetQuote,
timestamp: new Date().toISOString(),
};
}
Loading

0 comments on commit 69a66fe

Please sign in to comment.