Skip to content

Commit

Permalink
feat: add sbtc limits query
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Dec 17, 2024
1 parent 55387c9 commit 85c4e68
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 39 deletions.
4 changes: 2 additions & 2 deletions config/wallet-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@
"swapsEnabled": true,
"sbtc": {
"enabled": true,
"swapsEnabled": true,
"emilyApiUrl": "https://beta.sbtc-emily.com",
"swapsEnabled": false,
"emilyApiUrl": "https://dev.sbtc-emily-dev.com",
"showPromoLinkOnNetworks": ["testnet", "sbtcTestnet"],
"contracts": {
"mainnet": {
Expand Down
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.3.0",
"sbtc": "0.3.1",
"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.

1 change: 1 addition & 0 deletions src/app/pages/swap/bitflow-swap.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function getCrossChainSwapSubmissionData(values: SwapFormValues): SwapSub
fee: 0,
feeCurrency: 'BTC',
feeType: BtcFeeType.Standard,
liquidityFee: 0,
maxSignerFee: 0,
protocol: 'Bitcoin L2 Labs',
dexPath: [],
Expand Down
11 changes: 5 additions & 6 deletions src/app/pages/swap/components/swap-details/swap-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export function SwapDetails() {
)
return null;

const liquidityFee = swapSubmissionData.liquidityFee;
const maxSignerFee = satToBtc(swapSubmissionData.maxSignerFee ?? 0);

const formattedMinToReceive = formatMoneyPadded(
Expand Down Expand Up @@ -80,18 +79,18 @@ export function SwapDetails() {
}
/>
<SwapDetailLayout title="Min to receive" value={formattedMinToReceive} />

<SwapDetailLayout
title="Slippage tolerance"
value={`${swapSubmissionData.slippage * 100}%`}
/>

{liquidityFee ? (
<SwapDetailLayout title="Liquidity provider fee" value={`${liquidityFee.toFixed(1)}%`} />
) : null}
<SwapDetailLayout
title="Liquidity provider fee"
value={`${swapSubmissionData.liquidityFee.toFixed(1)}%`}
/>
{maxSignerFee ? (
<SwapDetailLayout title="Max signer fee" value={maxSignerFee.toString()} />
) : null}

<SwapDetailLayout
title="Transaction fees"
tooltipLabel={swapSubmissionData.sponsored ? sponsoredFeeLabel : undefined}
Expand Down
27 changes: 5 additions & 22 deletions src/app/pages/swap/hooks/use-sbtc-deposit-transaction.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable */
// TODO: Enable eslint and remove test logs
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';

Expand Down Expand Up @@ -37,7 +35,7 @@ import { useCurrentNetwork } from '@app/store/networks/networks.selectors';

import type { SwapSubmissionData } from '../swap.context';

// Suggested to use as defaults
// Also set as defaults in sbtc lib
const maxSignerFee = 80_000;
const reclaimLockTime = 144;

Expand All @@ -60,20 +58,6 @@ function getSbtcNetworkConfig(network: BitcoinNetworkModes) {
return networkMap[network];
}

// TODO: Remove
// const clientMainnet = new SbtcApiClientMainnet({
// btcApiUrl: 'https://mempool.space/api',
// stxApiUrl: 'https://api.hiro.so',
// sbtcApiUrl: 'https://app.stacks.co',
// sbtcContract: 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4',
// });
// const clientTestnet = new SbtcApiClientTestnet({
// btcApiUrl: 'https://beta.sbtc-mempool.tech/api/proxy',
// stxApiUrl: 'https://api.testnet.hiro.so',
// sbtcApiUrl: 'http://bridge.sbtc-emily-dev.com',
// sbtcContract: 'SNGWPN3XDAQE673MXYXF81016M50NHF5X5PWWM70',
// });

const clientMainnet = new SbtcApiClientMainnet();
const clientTestnet = new SbtcApiClientTestnet();

Expand Down Expand Up @@ -162,19 +146,18 @@ export function useSbtcDepositTransaction() {
try {
signer.sign(sBtcDeposit.transaction);
sBtcDeposit.transaction.finalize();

console.log('deposit tx', sBtcDeposit.transaction);
console.log('tx hex', sBtcDeposit.transaction.hex);
logger.info('Deposit', { deposit: sBtcDeposit });

const txid = await client.broadcastTx(sBtcDeposit.transaction);
console.log('broadcasted tx', txid);
logger.info('Broadcasted tx', txid);

await client.notifySbtc(sBtcDeposit);
toast.success('Transaction submitted!');
setIsIdle();
navigate(RouteUrls.Activity);
} catch (error) {
setIsIdle();
console.error(error);
logger.error(`Deposit error: ${error}`);
} finally {
setIsIdle();
}
Expand Down
87 changes: 85 additions & 2 deletions src/app/pages/swap/hooks/use-swap-form.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,50 @@
import { useMemo } from 'react';

import { cvToValue, hexToCV } from '@stacks/transactions';
import BigNumber from 'bignumber.js';
import * as yup from 'yup';

import { BTC_DECIMALS } from '@leather.io/constants';
import { type SwapAsset } from '@leather.io/query';
import { convertAmountToFractionalUnit, createMoney } from '@leather.io/utils';
import {
convertAmountToBaseUnit,
convertAmountToFractionalUnit,
createMoney,
} from '@leather.io/utils';

import { FormErrorMessages } from '@shared/error-messages';
import { type SwapFormValues } from '@shared/models/form.model';

import {
defaultSbtcLimits,
useGetCurrentSbtcSupply,
useGetSbtcLimits,
} from '@app/query/sbtc/sbtc-limits.query';

import { useSwapContext } from '../swap.context';

export function useSwapForm() {
const { isFetchingExchangeRate } = useSwapContext();
const { isCrossChainSwap, isFetchingExchangeRate } = useSwapContext();
const { data: sBtcLimits } = useGetSbtcLimits();
const { data: supply } = useGetCurrentSbtcSupply();

const remainingSbtcPegCapSupply = useMemo(() => {
const sBtcPegCap = sBtcLimits?.pegCap;
if (!sBtcPegCap) return;
const currentSupplyValue = supply?.result && cvToValue(hexToCV(supply?.result));
return convertAmountToFractionalUnit(
createMoney(new BigNumber(Number(sBtcPegCap - currentSupplyValue)), 'BTC', BTC_DECIMALS)
);
}, [sBtcLimits?.pegCap, supply?.result]);

const sBtcDepositCapMin = createMoney(
new BigNumber(sBtcLimits?.perDepositMinimum ?? defaultSbtcLimits.perDepositMinimum),
'BTC'
);
const sBtcDepositCapMax = createMoney(
new BigNumber(sBtcLimits?.perDepositCap ?? defaultSbtcLimits.perDepositCap),
'BTC'
);

const initialValues: SwapFormValues = {
fee: '0',
Expand Down Expand Up @@ -44,6 +78,55 @@ export function useSwapForm() {
return true;
},
})
.test({
message: `Min amount is ${convertAmountToBaseUnit(sBtcDepositCapMin).toString()} BTC`,
test(value) {
if (!isCrossChainSwap) return true;
const { swapAssetBase } = this.parent;
const valueInFractionalUnit = convertAmountToFractionalUnit(
createMoney(
new BigNumber(Number(value)),
swapAssetBase.balance.symbol,
swapAssetBase.balance.decimals
)
);
if (valueInFractionalUnit.isLessThan(sBtcDepositCapMin.amount)) return false;
return true;
},
})
.test({
message: `Max amount is ${convertAmountToBaseUnit(sBtcDepositCapMax).toString()} BTC`,
test(value) {
if (!isCrossChainSwap) return true;
const { swapAssetBase } = this.parent;
const valueInFractionalUnit = convertAmountToFractionalUnit(
createMoney(
new BigNumber(Number(value)),
swapAssetBase.balance.symbol,
swapAssetBase.balance.decimals
)
);
if (valueInFractionalUnit.isGreaterThan(sBtcDepositCapMax.amount)) return false;
return true;
},
})
.test({
message: 'Amount exceeds capped supply',
test(value) {
if (!isCrossChainSwap) return true;
const { swapAssetBase } = this.parent;
const valueInFractionalUnit = convertAmountToFractionalUnit(
createMoney(
new BigNumber(Number(value)),
swapAssetBase.balance.symbol,
swapAssetBase.balance.decimals
)
);
if (!remainingSbtcPegCapSupply) return true;
if (valueInFractionalUnit.isGreaterThan(remainingSbtcPegCapSupply)) return false;
return true;
},
})
.required(FormErrorMessages.AmountRequired)
.typeError(FormErrorMessages.MustBeNumber)
.positive(FormErrorMessages.MustBePositive),
Expand Down
2 changes: 1 addition & 1 deletion src/app/pages/swap/swap.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { SwapAsset } from '@leather.io/query';
import type { SwapFormValues } from '@shared/models/form.model';

export interface SwapSubmissionData extends SwapFormValues {
liquidityFee?: number;
liquidityFee: number;
maxSignerFee?: number;
protocol: string;
router: SwapAsset[];
Expand Down
60 changes: 60 additions & 0 deletions src/app/query/sbtc/sbtc-limits.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

import { useStacksClient } from '@leather.io/query';
import { getStacksContractIdStringParts } from '@leather.io/stacks';

import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';

import { useConfigSbtc } from '../common/remote-config/remote-config.query';

export const defaultSbtcLimits = {
pegCap: 1000000000000,
perDepositMinimum: 100000,
perDepositCap: 100000000,
perWithdrawalCap: 100000000,
accountCaps: {},
};

interface GetSbtcLimitsResponse {
pegCap: number;
perDepositCap: number;
perWithdrawalCap: number;
perDepositMinimum: number;
accountCaps: Record<any, any>;
}

async function getSbtcLimits(apiUrl: string): Promise<GetSbtcLimitsResponse> {
const resp = await axios.get(`${apiUrl}/limits`, {
headers: {
'Content-Type': 'application/json',
},
});
return resp.data;
}

export function useGetSbtcLimits() {
const { emilyApiUrl } = useConfigSbtc();
return useQuery({
queryKey: ['get-sbtc-limits'],
queryFn: () => getSbtcLimits(emilyApiUrl),
});
}

export function useGetCurrentSbtcSupply() {
const client = useStacksClient();
const { contractId } = useConfigSbtc();
const { contractAddress } = getStacksContractIdStringParts(contractId);
const stxAddress = useCurrentStacksAccountAddress();

return useQuery({
queryKey: ['get-current-sbtc-supply'],
queryFn: () =>
client.callReadOnlyFunction({
contractAddress,
contractName: 'sbtc-token',
functionName: 'get-total-supply',
readOnlyFunctionArgs: { sender: stxAddress, arguments: [] },
}),
});
}

0 comments on commit 85c4e68

Please sign in to comment.