Skip to content

Commit

Permalink
feat: bitflow implemenation, closes leather-io/issues#99
Browse files Browse the repository at this point in the history
  • Loading branch information
alexp3y committed Sep 28, 2024
1 parent fb5a46f commit f070d54
Show file tree
Hide file tree
Showing 21 changed files with 641 additions and 104 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/build-extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ jobs:
SEGMENT_WRITE_KEY: ${{ secrets.SEGMENT_WRITE_KEY_STAGING }}
TRANSAK_API_KEY: ${{ secrets.TRANSAK_API_KEY }}
BESTINSLOT_API_KEY: ${{ secrets.BESTINSLOT_API_KEY }}
BITFLOW_API_HOST: ${{ secrets.BITFLOW_API_HOST }}
BITFLOW_API_KEY: ${{ secrets.BITFLOW_API_KEY }}
BITFLOW_STACKS_API_HOST: ${{ secrets.BITFLOW_STACKS_API_HOST }}
BITFLOW_READONLY_CALL_API_HOST: ${{ secrets.BITFLOW_READONLY_CALL_API_HOST }}
PR_NUMBER: ${{ github.event.number }}
COMMIT_SHA: ${{ needs.sha-hash.outputs.SHORT_SHA }}

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/development-extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ env:
SEGMENT_WRITE_KEY: ${{ secrets.SEGMENT_WRITE_KEY_STAGING }}
TRANSAK_API_KEY: ${{ secrets.TRANSAK_API_KEY }}
BESTINSLOT_API_KEY: ${{ secrets.BESTINSLOT_API_KEY }}
BITFLOW_API_HOST: ${{ secrets.BITFLOW_API_HOST }}
BITFLOW_API_KEY: ${{ secrets.BITFLOW_API_KEY }}
BITFLOW_STACKS_API_HOST: ${{ secrets.BITFLOW_STACKS_API_HOST }}
BITFLOW_READONLY_CALL_API_HOST: ${{ secrets.BITFLOW_READONLY_CALL_API_HOST }}
PREVIEW_RELEASE: true
WALLET_ENVIRONMENT: preview

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ name: Integration tests
env:
CI: true
WALLET_ENVIRONMENT: testing
BITFLOW_API_HOST: ${{ secrets.BITFLOW_API_HOST }}
BITFLOW_API_KEY: ${{ secrets.BITFLOW_API_KEY }}
BITFLOW_STACKS_API_HOST: ${{ secrets.BITFLOW_STACKS_API_HOST }}
BITFLOW_READONLY_CALL_API_HOST: ${{ secrets.BITFLOW_READONLY_CALL_API_HOST }}

on:
push:
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/publish-extensions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ env:
SEGMENT_WRITE_KEY: ${{ secrets.SEGMENT_WRITE_KEY }}
TRANSAK_API_KEY: ${{ secrets.TRANSAK_API_KEY }}
BESTINSLOT_API_KEY: ${{ secrets.BESTINSLOT_API_KEY }}
BITFLOW_API_HOST: ${{ secrets.BITFLOW_API_HOST }}
BITFLOW_API_KEY: ${{ secrets.BITFLOW_API_KEY }}
BITFLOW_STACKS_API_HOST: ${{ secrets.BITFLOW_STACKS_API_HOST }}
BITFLOW_READONLY_CALL_API_HOST: ${{ secrets.BITFLOW_READONLY_CALL_API_HOST }}
WALLET_ENVIRONMENT: production
IS_PUBLISHING: true

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,9 @@
"bignumber.js": "9.1.2",
"bitcoin-address-validation": "2.2.1",
"bitcoinjs-lib": "6.1.5",
"bitflow-sdk": "1.6.1",
"bn.js": "5.2.1",
"browserify-fs": "1.0.0",
"c32check": "2.0.0",
"chroma-js": "2.4.2",
"coinselect": "3.1.13",
Expand All @@ -219,6 +221,7 @@
"micro-packed": "0.3.2",
"object-hash": "3.0.0",
"observable-hooks": "4.2.3",
"os-browserify": "0.3.0",
"p-queue": "8.0.1",
"pino": "8.19.0",
"postcss-preset-env": "9.5.4",
Expand Down
331 changes: 328 additions & 3 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { useState } from 'react';
import { Outlet } from 'react-router-dom';
import { Outlet, useNavigate } from 'react-router-dom';

import { bytesToHex } from '@stacks/common';
import { ContractCallPayload, TransactionTypes } from '@stacks/connect';
import { type ContractCallPayload, TransactionTypes } from '@stacks/connect';
import {
AnchorMode,
PostConditionMode,
serializeCV,
serializePostCondition,
} from '@stacks/transactions';
import BigNumber from 'bignumber.js';

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

import { logger } from '@shared/logger';
import { RouteUrls } from '@shared/route-urls';
import { alex } from '@shared/utils/alex-sdk';
import { bitflow } from '@shared/utils/bitflow-sdk';

import { migratePositiveAssetBalancesToTop } from '@app/common/asset-utils';
import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading';
Expand All @@ -26,54 +25,61 @@ 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 { SwapForm } from './components/swap-form';
import { generateSwapRoutes } from './generate-swap-routes';
import { oneHundredMillion, useAlexSwap } from './hooks/use-alex-swap';
import { useBitflowSwap } from './hooks/use-bitflow-swap';
import { useStacksBroadcastSwap } from './hooks/use-stacks-broadcast-swap';
import { SwapFormValues } from './hooks/use-swap-form';
import { useSwapNavigate } from './hooks/use-swap-navigate';
import { SwapContext, SwapProvider } from './swap.context';

export const alexSwapRoutes = generateSwapRoutes(<AlexSwapContainer />);
export const bitflowSwapRoutes = generateSwapRoutes(<BitflowSwapContainer />);

function AlexSwapContainer() {
function BitflowSwapContainer() {
const [isSendingMax, setIsSendingMax] = useState(false);
const navigate = useSwapNavigate();
const { setIsLoading } = useLoading(LoadingKeys.SUBMIT_SWAP_TRANSACTION);
const navigate = useNavigate();
const swapNavigate = useSwapNavigate();
const { setIsLoading, setIsIdle, isLoading } = useLoading(LoadingKeys.SUBMIT_SWAP_TRANSACTION);
const currentAccount = useCurrentStacksAccount();
const generateUnsignedTx = useGenerateStacksContractCallUnsignedTx();
const signTx = useSignStacksTransaction();

const broadcastStacksSwap = useStacksBroadcastSwap();
const {
fetchRouteQuote,
fetchQuoteAmount,
isFetchingExchangeRate,
onSetIsFetchingExchangeRate,
onSetSwapSubmissionData,
slippage,
swapAssets,
swapSubmissionData,
} = useAlexSwap();
const broadcastStacksSwap = useStacksBroadcastSwap();
} = useBitflowSwap();

async function onSubmitSwapForReview(values: SwapFormValues) {
if (isUndefined(values.swapAssetBase) || isUndefined(values.swapAssetQuote)) {
logger.error('Error submitting swap for review');
return;
}

const [router, lpFee] = await Promise.all([
alex.getRouter(values.swapAssetBase.currency, values.swapAssetQuote.currency),
alex.getFeeRate(values.swapAssetBase.currency, values.swapAssetQuote.currency),
]);
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: new BigNumber(Number(lpFee)).dividedBy(oneHundredMillion).toNumber(),
liquidityFee: estimateLiquidityFee(routeQuote.route.dex_path),
nonce: values.nonce,
protocol: 'ALEX',
router: router.map(x => swapAssets.find(asset => asset.currency === x)).filter(isDefined),
protocol: 'Bitflow',
dexPath: routeQuote.route.dex_path.map(formatDexPathItem),
router: routeQuote.route.token_path
.map(x => swapAssets.find(asset => asset.currency === x))
.filter(isDefined),
slippage,
sponsored: false,
swapAmountBase: values.swapAmountBase,
Expand All @@ -83,10 +89,12 @@ function AlexSwapContainer() {
timestamp: new Date().toISOString(),
});

navigate(RouteUrls.SwapReview);
swapNavigate(RouteUrls.SwapReview);
}

async function onSubmitSwap() {
if (isLoading) return;

if (isUndefined(currentAccount) || isUndefined(swapSubmissionData)) {
logger.error('Error submitting swap data to sign');
return;
Expand All @@ -102,60 +110,65 @@ function AlexSwapContainer() {

setIsLoading();

const fromAmount = BigInt(
new BigNumber(swapSubmissionData.swapAmountBase)
.multipliedBy(oneHundredMillion)
.dp(0)
.toString()
);

const minToAmount = BigInt(
new BigNumber(swapSubmissionData.swapAmountQuote)
.multipliedBy(oneHundredMillion)
.multipliedBy(new BigNumber(1).minus(slippage))
.dp(0)
.toString()
);

const tx = await alex.runSwap(
currentAccount?.address,
swapSubmissionData.swapAssetBase.currency,
swapSubmissionData.swapAssetQuote.currency,
fromAmount,
minToAmount
);

// TODO: Add choose fee step
const tempFormValues = {
fee: swapSubmissionData.fee,
feeCurrency: swapSubmissionData.feeCurrency,
feeType: swapSubmissionData.feeType,
nonce: swapSubmissionData.nonce,
};

const payload: ContractCallPayload = {
anchorMode: AnchorMode.Any,
contractAddress: tx.contractAddress,
contractName: tx.contractName,
functionName: tx.functionName,
functionArgs: tx.functionArgs.map(x => bytesToHex(serializeCV(x))),
postConditionMode: PostConditionMode.Deny,
postConditions: tx.postConditions.map(pc => bytesToHex(serializePostCondition(pc))),
publicKey: currentAccount?.stxPublicKey,
sponsored: swapSubmissionData.sponsored,
txType: TransactionTypes.ContractCall,
};

const unsignedTx = await generateUnsignedTx(payload, tempFormValues);
if (!unsignedTx) return logger.error('Attempted to generate unsigned tx, but tx is undefined');

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

const swapExecutionData = {
route: routeQuote.route,
amount: Number(swapSubmissionData.swapAmountBase),
tokenXDecimals: routeQuote.tokenXDecimals,
tokenYDecimals: routeQuote.tokenYDecimals,
};

const swapParams = await bitflow.getSwapParams(
swapExecutionData,
currentAccount.address,
swapSubmissionData.slippage
);

const tempFormValues = {
fee: swapSubmissionData.fee,
feeCurrency: swapSubmissionData.feeCurrency,
feeType: swapSubmissionData.feeType,
nonce: swapSubmissionData.nonce,
};

const payload: ContractCallPayload = {
anchorMode: AnchorMode.Any,
contractAddress: swapParams.contractAddress,
contractName: swapParams.contractName,
functionName: swapParams.functionName,
functionArgs: swapParams.functionArgs.map(x => bytesToHex(serializeCV(x))),
postConditionMode: PostConditionMode.Deny,
postConditions: swapParams.postConditions.map(pc => bytesToHex(serializePostCondition(pc))),
publicKey: currentAccount?.stxPublicKey,
sponsored: swapSubmissionData.sponsored,
txType: TransactionTypes.ContractCall,
};

const unsignedTx = await generateUnsignedTx(payload, tempFormValues);
if (!unsignedTx)
return logger.error('Attempted to generate unsigned tx, but tx is undefined');

const signedTx = await signTx(unsignedTx);
if (!signedTx)
return logger.error('Attempted to generate raw tx, but signed tx is undefined');

return await broadcastStacksSwap(signedTx);
} catch (error) {}
} catch (e) {
navigate(RouteUrls.SwapError, {
state: {
message: isError(e) ? e.message : '',
title: 'Swap Error',
},
});
} finally {
setIsIdle();
}
}

const swapContextValue: SwapContext = {
Expand Down
12 changes: 12 additions & 0 deletions src/app/pages/swap/bitflow-swap.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import BigNumber from 'bignumber.js';

import { capitalize } from '@leather.io/utils';

export function estimateLiquidityFee(dexPath: string[]) {
return new BigNumber(dexPath.length).times(0.3).toNumber();
}

export function formatDexPathItem(dex: string) {
const name = dex.split('_')[0];
return name === 'ALEX' ? name : capitalize(name.toLowerCase());
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,7 @@ export function SwapAssetList({ assets, type }: SwapAssetList) {
return (
<Stack mb="space.05" p="space.05" width="100%" data-testid={SwapSelectors.SwapAssetList}>
{selectableAssets.map(asset => (
<SwapAssetItem
asset={asset}
key={asset.balance.symbol}
onClick={() => onSelectAsset(asset)}
/>
<SwapAssetItem asset={asset} key={asset.currency} onClick={() => onSelectAsset(asset)} />
))}
</Stack>
);
Expand Down
15 changes: 12 additions & 3 deletions src/app/pages/swap/components/swap-details/swap-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,21 @@ export function SwapDetails() {
)
);

const getFormattedPoweredBy = () => {
const uniqueDexPath = Array.from(new Set(swapSubmissionData.dexPath));
const isOnlySwapProtocol =
uniqueDexPath.length === 1 && uniqueDexPath[0] === swapSubmissionData.protocol;
return isOnlySwapProtocol || !uniqueDexPath.length
? swapSubmissionData.protocol
: `${uniqueDexPath.join(', ')} via ${swapSubmissionData.protocol}`;
};

return (
<SwapDetailsLayout>
<SwapDetailLayout
dataTestId={SwapSelectors.SwapDetailsProtocol}
title="Powered by"
value={swapSubmissionData.protocol}
value={getFormattedPoweredBy()}
/>
<SwapDetailLayout
title="Route"
Expand All @@ -76,8 +85,8 @@ export function SwapDetails() {
/>
<SwapDetailLayout
title="Liquidity provider fee"
tooltipLabel="To receive a share of these fees, become a Liquidity Provider on app.alexlab.co."
value={`${swapSubmissionData.liquidityFee} ${swapSubmissionData.swapAssetBase.name}`}
tooltipLabel="To receive a share of these fees, become a Liquidity Provider on app.bitflow.finance."
value={`${swapSubmissionData.liquidityFee.toFixed(1)}%`}
/>
<SwapDetailLayout
title="Transaction fees"
Expand Down
1 change: 1 addition & 0 deletions src/app/pages/swap/components/swap-review.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function SwapReview() {
footer={
<Button
aria-busy={isLoading}
disabled={isLoading}
data-testid={SwapSelectors.SwapSubmitBtn}
type="button"
onClick={onSubmitSwap}
Expand Down
Loading

0 comments on commit f070d54

Please sign in to comment.