diff --git a/config/wallet-config.json b/config/wallet-config.json index b705ddadfa..5a17ca7f55 100644 --- a/config/wallet-config.json +++ b/config/wallet-config.json @@ -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": { diff --git a/package.json b/package.json index d26b244db8..26a35d4ad9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a6ca6e63a..158727b459 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -354,8 +354,8 @@ importers: specifier: 7.8.1 version: 7.8.1 sbtc: - specifier: 0.3.0 - version: 0.3.0(encoding@0.1.13) + specifier: 0.3.1 + version: 0.3.1(encoding@0.1.13) style-loader: specifier: 3.3.4 version: 3.3.4(webpack@5.94.0(@swc/core@1.9.3)(esbuild@0.24.0)(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack-dev-server@4.15.1)(webpack@5.94.0))) @@ -13245,8 +13245,8 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} - sbtc@0.3.0: - resolution: {integrity: sha512-nB6lAEdm+c12RteLeDmdzEqZ57KEB/yPZUiKjeYVTN2VtipXj7wNyudwiT786Qux40LROLUbL2x5yrKYriU0Zg==} + sbtc@0.3.1: + resolution: {integrity: sha512-dAd0hxgIS1qchzb6tWbBTgoYio/v0Rln9pq3L23cX52DBSI4WpoMr2MRsM8z3lORTeqhIPLd3Tn/IdB+jKYdIA==} sc-errors@3.0.0: resolution: {integrity: sha512-rIqv2HTPb9DVreZwK/DV0ytRUqyw2DbDcoB9XTKjEQL7oMEQKsfPA8V8dGGr7p8ZYfmvaRIGZ4Wu5qwvs/hGDA==} @@ -31414,7 +31414,7 @@ snapshots: dependencies: xmlchars: 2.2.0 - sbtc@0.3.0(encoding@0.1.13): + sbtc@0.3.1(encoding@0.1.13): dependencies: '@btc-helpers/rpc': 2.0.0(encoding@0.1.13) '@noble/secp256k1': 2.1.0 diff --git a/src/app/pages/swap/bitflow-swap.utils.ts b/src/app/pages/swap/bitflow-swap.utils.ts index c319215fba..715e2a16ad 100644 --- a/src/app/pages/swap/bitflow-swap.utils.ts +++ b/src/app/pages/swap/bitflow-swap.utils.ts @@ -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: [], diff --git a/src/app/pages/swap/components/swap-details/swap-details.tsx b/src/app/pages/swap/components/swap-details/swap-details.tsx index 90d8fb9f09..bc9399a17a 100644 --- a/src/app/pages/swap/components/swap-details/swap-details.tsx +++ b/src/app/pages/swap/components/swap-details/swap-details.tsx @@ -42,7 +42,6 @@ export function SwapDetails() { ) return null; - const liquidityFee = swapSubmissionData.liquidityFee; const maxSignerFee = satToBtc(swapSubmissionData.maxSignerFee ?? 0); const formattedMinToReceive = formatMoneyPadded( @@ -80,18 +79,18 @@ export function SwapDetails() { } /> + - - {liquidityFee ? ( - - ) : null} + {maxSignerFee ? ( ) : null} - { + 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', @@ -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), diff --git a/src/app/pages/swap/swap.context.ts b/src/app/pages/swap/swap.context.ts index fe67bf0e94..2ff547145e 100644 --- a/src/app/pages/swap/swap.context.ts +++ b/src/app/pages/swap/swap.context.ts @@ -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[]; diff --git a/src/app/query/sbtc/sbtc-limits.query.ts b/src/app/query/sbtc/sbtc-limits.query.ts new file mode 100644 index 0000000000..df0e931af9 --- /dev/null +++ b/src/app/query/sbtc/sbtc-limits.query.ts @@ -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; +} + +async function getSbtcLimits(apiUrl: string): Promise { + 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: [] }, + }), + }); +}