From 34d8e5e27d2194495e19e7b36266a82c7fe775f9 Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Fri, 7 Jun 2024 00:28:52 +1000 Subject: [PATCH 01/56] fix: fix unbonding fee --- src/app/components/Delegations/Delegations.tsx | 4 +--- src/app/page.tsx | 3 --- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/app/components/Delegations/Delegations.tsx b/src/app/components/Delegations/Delegations.tsx index db507e4..5b32f52 100644 --- a/src/app/components/Delegations/Delegations.tsx +++ b/src/app/components/Delegations/Delegations.tsx @@ -43,7 +43,6 @@ interface DelegationsProps { delegationsLocalStorage: DelegationInterface[]; globalParamsVersion: GlobalParamsVersion; publicKeyNoCoord: string; - unbondingFeeSat: number; btcWalletNetwork: networks.Network; address: string; signPsbtTx: SignPsbtTransaction; @@ -58,7 +57,6 @@ export const Delegations: React.FC = ({ delegationsLocalStorage, globalParamsVersion, publicKeyNoCoord, - unbondingFeeSat, btcWalletNetwork, address, signPsbtTx, @@ -131,7 +129,7 @@ export const Delegations: React.FC = ({ const { psbt: unsignedUnbondingTx } = unbondingTransaction( scripts, Transaction.fromHex(delegation.stakingTx.txHex), - unbondingFeeSat, + globalParamsWhenStaking.unbondingFeeSat, btcWalletNetwork, delegation.stakingTx.outputIndex, ); diff --git a/src/app/page.tsx b/src/app/page.tsx index 2bc8de2..932802d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -397,9 +397,6 @@ const Home: React.FC = () => { delegationsLocalStorage={delegationsLocalStorage} globalParamsVersion={paramWithContext.currentVersion} publicKeyNoCoord={publicKeyNoCoord} - unbondingFeeSat={ - paramWithContext.currentVersion.unbondingFeeSat - } btcWalletNetwork={btcWalletNetwork} address={address} signPsbtTx={signPsbtTransaction(btcWallet)} From 0fba95fb291b9934294a475511078f0e30861f5d Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Fri, 7 Jun 2024 00:33:30 +1000 Subject: [PATCH 02/56] v0.1.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f1940dd..0f14b0a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-staking", - "version": "0.1.0", + "version": "0.1.3", "private": true, "scripts": { "dev": "next dev", From 0808daed1fc384e374e2c90968c480c7add1a07f Mon Sep 17 00:00:00 2001 From: Govard Barkhatov Date: Thu, 6 Jun 2024 22:26:33 +0200 Subject: [PATCH 03/56] stats overlapping layout flow --- src/app/components/Stats/Stats.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/Stats/Stats.tsx b/src/app/components/Stats/Stats.tsx index 422cee3..aa908c6 100644 --- a/src/app/components/Stats/Stats.tsx +++ b/src/app/components/Stats/Stats.tsx @@ -85,7 +85,7 @@ export const Stats: React.FC = ({ stakingStats, isLoading }) => { ]; return ( -
+
{sections.map((section, index) => (
= ({ stakingStats, isLoading }) => { > {section.map((subSection, subIndex) => ( -
+
{subSection.title}
From 7363d387dc2d24b91270f8945bb4696c86d06c5f Mon Sep 17 00:00:00 2001 From: Govard Barkhatov Date: Thu, 6 Jun 2024 22:37:36 +0200 Subject: [PATCH 04/56] diligence --- src/app/components/TestingInfo/TestingInfo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/TestingInfo/TestingInfo.tsx b/src/app/components/TestingInfo/TestingInfo.tsx index d46dc10..f72b274 100644 --- a/src/app/components/TestingInfo/TestingInfo.tsx +++ b/src/app/components/TestingInfo/TestingInfo.tsx @@ -13,7 +13,7 @@ export const TestingInfo: React.FC = () => {

- Please do your due dilligence. Tokens are for testing only and do not + Please do your due diligence. Tokens are for testing only and do not carry any monetary value.

From b82af45d56d86ae887dff289fbf48d3551ea8054 Mon Sep 17 00:00:00 2001 From: Govard Barkhatov Date: Fri, 7 Jun 2024 09:16:18 +0200 Subject: [PATCH 05/56] note change --- src/app/components/TestingInfo/TestingInfo.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/components/TestingInfo/TestingInfo.tsx b/src/app/components/TestingInfo/TestingInfo.tsx index f72b274..e23bd2a 100644 --- a/src/app/components/TestingInfo/TestingInfo.tsx +++ b/src/app/components/TestingInfo/TestingInfo.tsx @@ -13,8 +13,9 @@ export const TestingInfo: React.FC = () => {

- Please do your due diligence. Tokens are for testing only and do not - carry any monetary value. + The app may contain bugs. Use it after conducting your own research + and making an informed decision. Tokens are for testing only and do + not carry any monetary value.

From 4865fba31ed38507afd01ecc15bf39dd122bc39b Mon Sep 17 00:00:00 2001 From: Filippos Malandrakis <35352222+filippos47@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:44:44 +0300 Subject: [PATCH 06/56] chore: CI: Update image registry name (#232) --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b16d4e5..4ccfccf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -36,7 +36,7 @@ jobs: path: ./ build-path: ./ tag: "$CIRCLE_SHA1,$CIRCLE_TAG" - repo: "$CIRCLE_PROJECT_REPONAME" + repo: "simple-staking" - run: name: Save Docker image to export it to workspace command: | @@ -64,7 +64,7 @@ jobs: - aws-ecr/push-image: registry-id: AWS_ECR_REGISTRY_ID region: "$AWS_REGION" - repo: "$CIRCLE_PROJECT_REPONAME" + repo: "simple-staking" tag: "$CIRCLE_SHA1,$CIRCLE_TAG" deploy_staging: From e1b17912c0b7436b284af6b8ab681c6a587a17ac Mon Sep 17 00:00:00 2001 From: Govard Barkhatov Date: Thu, 6 Jun 2024 22:10:57 +0200 Subject: [PATCH 07/56] pending stored in the local storage tooltip --- src/utils/getState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/getState.ts b/src/utils/getState.ts index 57341d3..f151fa7 100644 --- a/src/utils/getState.ts +++ b/src/utils/getState.ts @@ -43,7 +43,7 @@ export const getStateTooltip = (state: string, confirmationDepth?: number) => { case DelegationState.WITHDRAWN: return "Stake has been withdrawn"; case DelegationState.PENDING: - return `Stake is pending ${confirmationDepth || 10} Bitcoin confirmations`; + return `Stake that is pending ${confirmationDepth || 10} Bitcoin confirmations will only be visible from this device`; case DelegationState.OVERFLOW: return "Stake is over the staking cap"; case DelegationState.EXPIRED: From c75c4cbd2f70cc8c3e98c3c466b5c2e9ea219657 Mon Sep 17 00:00:00 2001 From: Crypto Minion <154598612+jrwbabylonchain@users.noreply.github.com> Date: Wed, 12 Jun 2024 12:03:17 +1000 Subject: [PATCH 08/56] feat: work with height based cap (#243) * feat: work with height based cap --- src/app/api/getGlobalParams.ts | 2 + src/app/common/constants.ts | 1 + src/app/components/Staking/Staking.tsx | 170 ++++++++++++++----- src/app/components/Stats/Stats.tsx | 119 ++++++++++--- src/app/context/api/GlobalParamsProvider.tsx | 19 ++- src/app/context/api/StakingStatsProvider.tsx | 69 ++++++++ src/app/page.tsx | 60 +------ src/app/providers.tsx | 9 +- src/app/types/globalParams.ts | 1 + src/utils/globalParams.ts | 22 ++- 10 files changed, 347 insertions(+), 125 deletions(-) create mode 100644 src/app/context/api/StakingStatsProvider.tsx diff --git a/src/app/api/getGlobalParams.ts b/src/app/api/getGlobalParams.ts index 16634e3..97c180c 100644 --- a/src/app/api/getGlobalParams.ts +++ b/src/app/api/getGlobalParams.ts @@ -9,6 +9,7 @@ interface GlobalParamsDataResponse { version: number; activation_height: number; staking_cap: number; + cap_height: number; tag: string; covenant_pks: string[]; covenant_quorum: number; @@ -35,6 +36,7 @@ export const getGlobalParams = async (): Promise => { version: v.version, activationHeight: v.activation_height, stakingCapSat: v.staking_cap, + stakingCapHeight: v.cap_height, tag: v.tag, covenantPks: v.covenant_pks, covenantQuorum: v.covenant_quorum, diff --git a/src/app/common/constants.ts b/src/app/common/constants.ts index 129263b..0482911 100644 --- a/src/app/common/constants.ts +++ b/src/app/common/constants.ts @@ -1 +1,2 @@ export const OVERFLOW_TVL_WARNING_THRESHOLD = 0.8; +export const OVERFLOW_HEIGHT_WARNING_THRESHOLD = 3; diff --git a/src/app/components/Staking/Staking.tsx b/src/app/components/Staking/Staking.tsx index 2203a3a..216c46a 100644 --- a/src/app/components/Staking/Staking.tsx +++ b/src/app/components/Staking/Staking.tsx @@ -1,15 +1,21 @@ import { Transaction, networks } from "bitcoinjs-lib"; -import { Dispatch, SetStateAction, useState } from "react"; +import { Dispatch, SetStateAction, useMemo, useState } from "react"; import { useLocalStorage } from "usehooks-ts"; +import { OVERFLOW_HEIGHT_WARNING_THRESHOLD } from "@/app/common/constants"; import { LoadingView } from "@/app/components/Loading/Loading"; import { useError } from "@/app/context/Error/ErrorContext"; +import { useGlobalParams } from "@/app/context/api/GlobalParamsProvider"; +import { useStakingStats } from "@/app/context/api/StakingStatsProvider"; import { Delegation } from "@/app/types/delegations"; import { ErrorState } from "@/app/types/errors"; import { FinalityProvider as FinalityProviderInterface } from "@/app/types/finalityProviders"; -import { GlobalParamsVersion } from "@/app/types/globalParams"; import { getNetworkConfig } from "@/config/network.config"; import { getStakingTerm } from "@/utils/getStakingTerm"; +import { + ParamsWithContext, + getCurrentGlobalParamsVersion, +} from "@/utils/globalParams"; import { isStakingSignReady } from "@/utils/isStakingSignReady"; import { toLocalStorageDelegation } from "@/utils/local_storage/toLocalStorageDelegation"; import { signForm } from "@/utils/signForm"; @@ -28,14 +34,16 @@ import stakingNotStarted from "./Form/States/staking-not-started.svg"; import stakingUpgrading from "./Form/States/staking-upgrading.svg"; interface OverflowProperties { - isOverTheCap: boolean; - isApprochingCap: boolean; + isHeightCap: boolean; + overTheCapRange: boolean; + approchingCapRange: boolean; } + interface StakingProps { + btcHeight: number | undefined; finalityProviders: FinalityProviderInterface[] | undefined; isWalletConnected: boolean; isLoading: boolean; - overflow: OverflowProperties; onConnect: () => void; finalityProvidersFetchNext: () => void; finalityProvidersHasNext: boolean; @@ -46,26 +54,17 @@ interface StakingProps { address: string | undefined; publicKeyNoCoord: string; setDelegationsLocalStorage: Dispatch>; - paramWithContext: - | { - height: number | undefined; - firstActivationHeight: number | undefined; - currentVersion: GlobalParamsVersion | undefined; - isApprochingNextVersion: boolean | undefined; - } - | undefined; } export const Staking: React.FC = ({ + btcHeight, finalityProviders, isWalletConnected, - overflow, onConnect, finalityProvidersFetchNext, finalityProvidersHasNext, finalityProvidersIsFetchingMore, isLoading, - paramWithContext, btcWallet, btcWalletNetwork, address, @@ -88,15 +87,74 @@ export const Staking: React.FC = ({ useLocalStorage("bbn-staking-successFeedbackModalOpened", false); const [cancelFeedbackModalOpened, setCancelFeedbackModalOpened] = useLocalStorage("bbn-staking-cancelFeedbackModalOpened ", false); + const [paramWithCtx, setParamWithCtx] = useState< + ParamsWithContext | undefined + >(); + const [overflow, setOverflow] = useState({ + isHeightCap: false, + overTheCapRange: false, + approchingCapRange: false, + }); + + const stakingStats = useStakingStats(); + + // load global params and calculate the current staking params + const globalParams = useGlobalParams(); + useMemo(() => { + if (!btcHeight || !globalParams.data) { + return; + } + const paramCtx = getCurrentGlobalParamsVersion( + btcHeight + 1, + globalParams.data, + ); + setParamWithCtx(paramCtx); + }, [btcHeight, globalParams]); + + // Calculate the overflow properties + useMemo(() => { + if (!paramWithCtx || !paramWithCtx.currentVersion || !btcHeight) { + return; + } + const nextBlockHeight = btcHeight + 1; + const { stakingCapHeight, stakingCapSat, confirmationDepth } = + paramWithCtx.currentVersion; + // Use height based cap than value based cap if it is set + if (stakingCapHeight) { + setOverflow({ + isHeightCap: true, + overTheCapRange: + nextBlockHeight >= stakingCapHeight + confirmationDepth - 1, + /* + When btc height is approching the staking cap height, + there is higher chance of overflow due to tx not being included in the next few blocks on time + We also don't take the confirmation depth into account here as majority + of the delegation will be overflow after the cap is reached, unless btc fork happens but it's unlikely + */ + approchingCapRange: + nextBlockHeight >= + stakingCapHeight - OVERFLOW_HEIGHT_WARNING_THRESHOLD, + }); + } else if (stakingCapSat && stakingStats.data) { + const { activeTVLSat, unconfirmedTVLSat } = stakingStats.data; + setOverflow({ + isHeightCap: false, + overTheCapRange: stakingCapSat <= activeTVLSat, + approchingCapRange: + stakingCapSat * OVERFLOW_HEIGHT_WARNING_THRESHOLD < unconfirmedTVLSat, + }); + } + }, [paramWithCtx, btcHeight, stakingStats]); const { coinName } = getNetworkConfig(); - const stakingParams = paramWithContext?.currentVersion; - const firstActivationHeight = paramWithContext?.firstActivationHeight; - const height = paramWithContext?.height; - const isUpgrading = paramWithContext?.isApprochingNextVersion; + const stakingParams = paramWithCtx?.currentVersion; + const firstActivationHeight = paramWithCtx?.firstActivationHeight; + const isUpgrading = paramWithCtx?.isApprochingNextVersion; const isBlockHeightUnderActivation = !stakingParams || - (height && firstActivationHeight && height < firstActivationHeight); + (btcHeight && + firstActivationHeight && + btcHeight + 1 < firstActivationHeight); const { showError } = useError(); const handleResetState = () => { @@ -115,12 +173,12 @@ export const Staking: React.FC = ({ throw new Error("Address is not set"); } else if (!btcWalletNetwork) { throw new Error("Wallet network not connected"); - } else if (!paramWithContext || !paramWithContext.currentVersion) { + } else if (!paramWithCtx || !paramWithCtx.currentVersion) { throw new Error("Global params not loaded"); } else if (!finalityProvider) { throw new Error("Finality provider not selected"); } - const { currentVersion: globalParamsVersion } = paramWithContext; + const { currentVersion: globalParamsVersion } = paramWithCtx; const stakingTerm = getStakingTerm( globalParamsVersion, stakingTimeBlocks, @@ -214,6 +272,34 @@ export const Staking: React.FC = ({ } }; + const showOverflowWarning = (overflow: OverflowProperties) => { + if (overflow.isHeightCap) { + return ( + + ); + } else { + return ( + + ); + } + }; + const handleCloseFeedbackModal = () => { if (feedbackModal.type === "success") { setSuccessFeedbackModalOpened(true); @@ -223,6 +309,24 @@ export const Staking: React.FC = ({ setFeedbackModal({ type: null, isOpen: false }); }; + const showApproachingCapWarning = () => { + if (!overflow.approchingCapRange) { + return; + } + if (overflow.isHeightCap) { + return ( +

+ Staking window is closing. Your stake may overflow! +

+ ); + } + return ( +

+ Staking cap is filling up. Your stake may overflow! +

+ ); + }; + const renderStakingForm = () => { // States of the staking form: // 1. Wallet is not connected @@ -239,7 +343,7 @@ export const Staking: React.FC = ({ @@ -258,18 +362,8 @@ export const Staking: React.FC = ({ ); } // 5. Staking cap reached - else if (overflow.isOverTheCap) { - return ( - - ); + else if (overflow.overTheCapRange) { + return showOverflowWarning(overflow); } // 6. Staking form else { @@ -318,11 +412,7 @@ export const Staking: React.FC = ({ reset={resetFormInputs} />
- {overflow.isApprochingCap && ( -

- Staking cap is filling up. Your stake may overflow! -

- )} + {showApproachingCapWarning()} +
+ ); + }; + + const selectedModeRender = () => { + // If fee is below the fastest fee, show a warning + const showLowFeesWarning = + selectedFeeRate && mempoolFeeRates && selectedFeeRate < defaultFeeRate; + + return ( +
+
+

+ Selected fee rate:{" "} + {selectedFeeRate || defaultFeeRate} sat/vB +

+

+ Transaction fee amount:{" "} + + {satoshiToBtc(stakingFeeSat)} {coinName} + +

+
+
+ +
+ {minFeeRate} sat/vB + {showLowFeesWarning ? ( +

+ Fees are low, inclusion is not guaranteed +

+ ) : null} + {maxFeeRate} sat/vB +
+
+
+ ); + }; + + // fetched fee rates and staking fee sat + const customModeReady = customMode && mempoolFeeRates && stakingFeeSat; + + return ( +
+ {customModeReady ? selectedModeRender() : defaultModeRender()} +
+ ); +}; diff --git a/src/app/components/Staking/Staking.tsx b/src/app/components/Staking/Staking.tsx index f9488ae..330ec24 100644 --- a/src/app/components/Staking/Staking.tsx +++ b/src/app/components/Staking/Staking.tsx @@ -1,5 +1,6 @@ +import { useQuery } from "@tanstack/react-query"; import { Transaction, networks } from "bitcoinjs-lib"; -import { Dispatch, SetStateAction, useMemo, useState } from "react"; +import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react"; import { useLocalStorage } from "usehooks-ts"; import { OVERFLOW_HEIGHT_WARNING_THRESHOLD } from "@/app/common/constants"; @@ -8,10 +9,14 @@ import { useError } from "@/app/context/Error/ErrorContext"; import { useGlobalParams } from "@/app/context/api/GlobalParamsProvider"; import { useStakingStats } from "@/app/context/api/StakingStatsProvider"; import { Delegation } from "@/app/types/delegations"; -import { ErrorState } from "@/app/types/errors"; +import { ErrorHandlerParam, ErrorState } from "@/app/types/errors"; import { FinalityProvider as FinalityProviderInterface } from "@/app/types/finalityProviders"; import { getNetworkConfig } from "@/config/network.config"; -import { signStakingTx } from "@/utils/delegations/signStakingTx"; +import { + createStakingTx, + signStakingTx, +} from "@/utils/delegations/signStakingTx"; +import { getFeeRateFromMempool } from "@/utils/getFeeRateFromMempool"; import { ParamsWithContext, getCurrentGlobalParamsVersion, @@ -25,6 +30,7 @@ import { PreviewModal } from "../Modals/PreviewModal"; import { FinalityProviders } from "./FinalityProviders/FinalityProviders"; import { StakingAmount } from "./Form/StakingAmount"; +import { StakingFee } from "./Form/StakingFee"; import { StakingTime } from "./Form/StakingTime"; import { Message } from "./Form/States/Message"; import { WalletNotConnected } from "./Form/States/WalletNotConnected"; @@ -76,6 +82,8 @@ export const Staking: React.FC = ({ const [stakingTimeBlocks, setStakingTimeBlocks] = useState(0); const [finalityProvider, setFinalityProvider] = useState(); + // Selected fee rate, comes from the user input + const [selectedFeeRate, setSelectedFeeRate] = useState(0); const [previewModalOpen, setPreviewModalOpen] = useState(false); const [resetFormInputs, setResetFormInputs] = useState(false); const [feedbackModal, setFeedbackModal] = useState<{ @@ -95,6 +103,47 @@ export const Staking: React.FC = ({ approchingCapRange: false, }); + // Mempool fee rates, comes from the network + // Fetch fee rates, sat/vB + const { + data: mempoolFeeRates, + error: mempoolFeeRatesError, + isError: hasMempoolFeeRatesError, + refetch: refetchMempoolFeeRates, + } = useQuery({ + queryKey: ["mempool fee rates"], + queryFn: async () => { + if (btcWallet?.getNetworkFees) { + return await btcWallet.getNetworkFees(); + } + }, + enabled: !!btcWallet?.getNetworkFees, + refetchInterval: 60000, // 1 minute + retry: (failureCount) => { + return !isErrorOpen && failureCount <= 3; + }, + }); + + // Fetch all UTXOs + const { + data: availableUTXOs, + error: availableUTXOsError, + isError: hasAvailableUTXOsError, + refetch: refetchAvailableUTXOs, + } = useQuery({ + queryKey: ["available UTXOs", address], + queryFn: async () => { + if (btcWallet?.getUtxos && address) { + return await btcWallet.getUtxos(address); + } + }, + enabled: !!(btcWallet?.getUtxos && address), + refetchInterval: 60000 * 5, // 5 minutes + retry: (failureCount) => { + return !isErrorOpen && failureCount <= 3; + }, + }); + const stakingStats = useStakingStats(); // load global params and calculate the current staking params @@ -154,32 +203,91 @@ export const Staking: React.FC = ({ (btcHeight && firstActivationHeight && btcHeight + 1 < firstActivationHeight); - const { showError } = useError(); + + const { isErrorOpen, showError } = useError(); + + useEffect(() => { + const handleError = ({ + error, + hasError, + errorState, + refetchFunction, + }: ErrorHandlerParam) => { + if (hasError && error) { + showError({ + error: { + message: error.message, + errorState, + errorTime: new Date(), + }, + retryAction: refetchFunction, + }); + } + }; + + handleError({ + error: mempoolFeeRatesError, + hasError: hasMempoolFeeRatesError, + errorState: ErrorState.SERVER_ERROR, + refetchFunction: refetchMempoolFeeRates, + }); + handleError({ + error: availableUTXOsError, + hasError: hasAvailableUTXOsError, + errorState: ErrorState.SERVER_ERROR, + refetchFunction: refetchAvailableUTXOs, + }); + }, [ + availableUTXOsError, + mempoolFeeRatesError, + hasMempoolFeeRatesError, + hasAvailableUTXOsError, + refetchMempoolFeeRates, + refetchAvailableUTXOs, + showError, + ]); const handleResetState = () => { setFinalityProvider(undefined); setStakingAmountSat(0); setStakingTimeBlocks(0); + setSelectedFeeRate(0); setPreviewModalOpen(false); setResetFormInputs(!resetFormInputs); }; + const { minFeeRate, defaultFeeRate } = getFeeRateFromMempool(mempoolFeeRates); + + // Either use the selected fee rate or the fastest fee rate + const feeRate = selectedFeeRate || defaultFeeRate; + const handleSign = async () => { try { - if (!paramWithCtx || !paramWithCtx.currentVersion) { + // Initial validation + if (!btcWallet) throw new Error("Wallet is not connected"); + if (!address) throw new Error("Address is not set"); + if (!btcWalletNetwork) throw new Error("Wallet network is not connected"); + if (!finalityProvider) + throw new Error("Finality provider is not selected"); + if (!paramWithCtx || !paramWithCtx.currentVersion) throw new Error("Global params not loaded"); - } + if (!feeRate) throw new Error("Fee rates not loaded"); + if (!availableUTXOs || availableUTXOs.length === 0) + throw new Error("No available balance"); + const { currentVersion: globalParamsVersion } = paramWithCtx; // Sign the staking transaction const { stakingTxHex, stakingTerm } = await signStakingTx( + btcWallet, globalParamsVersion, + stakingAmountSat, stakingTimeBlocks, - btcWallet, finalityProvider, btcWalletNetwork, - stakingAmountSat, address, publicKeyNoCoord, + feeRate, + availableUTXOs, ); // UI handleFeedbackModal("success"); @@ -215,6 +323,69 @@ export const Staking: React.FC = ({ ]); }; + // Memoize the staking fee calculation + const stakingFeeSat = useMemo(() => { + if ( + btcWalletNetwork && + address && + publicKeyNoCoord && + stakingAmountSat && + finalityProvider && + paramWithCtx?.currentVersion && + mempoolFeeRates && + availableUTXOs + ) { + try { + // check that selected Fee rate (if present) is bigger than the min fee + if (selectedFeeRate && selectedFeeRate < minFeeRate) { + throw new Error("Selected fee rate is lower than the hour fee"); + } + const memoizedFeeRate = selectedFeeRate || defaultFeeRate; + // Calculate the staking fee + const { stakingFeeSat } = createStakingTx( + paramWithCtx.currentVersion, + stakingAmountSat, + stakingTimeBlocks, + finalityProvider, + btcWalletNetwork, + address, + publicKeyNoCoord, + memoizedFeeRate, + availableUTXOs, + ); + return stakingFeeSat; + } catch (error: Error | any) { + // fees + staking amount can be more than the balance + showError({ + error: { + message: error.message, + errorState: ErrorState.STAKING, + errorTime: new Date(), + }, + retryAction: () => setSelectedFeeRate(0), + }); + setSelectedFeeRate(0); + return 0; + } + } else { + return 0; + } + }, [ + btcWalletNetwork, + address, + publicKeyNoCoord, + stakingAmountSat, + stakingTimeBlocks, + finalityProvider, + paramWithCtx, + mempoolFeeRates, + selectedFeeRate, + availableUTXOs, + showError, + defaultFeeRate, + minFeeRate, + ]); + // Select the finality provider from the list const handleChooseFinalityProvider = (btcPkHex: string) => { if (!finalityProviders) { @@ -376,6 +547,9 @@ export const Staking: React.FC = ({ !!finalityProvider, ); + const previewReady = + signReady && feeRate && availableUTXOs && stakingAmountSat; + return ( <>

Set up staking terms

@@ -394,23 +568,36 @@ export const Staking: React.FC = ({ onStakingAmountSatChange={handleStakingAmountSatChange} reset={resetFormInputs} /> + {signReady && ( + + )} {showApproachingCapWarning()} - + {previewReady && ( + + )} ); diff --git a/src/utils/delegations/signStakingTx.ts b/src/utils/delegations/signStakingTx.ts index 6ff8d48..72d874b 100644 --- a/src/utils/delegations/signStakingTx.ts +++ b/src/utils/delegations/signStakingTx.ts @@ -6,30 +6,25 @@ import { FinalityProvider } from "@/app/types/finalityProviders"; import { GlobalParamsVersion } from "@/app/types/globalParams"; import { apiDataToStakingScripts } from "@/utils/apiDataToStakingScripts"; import { isTaproot } from "@/utils/wallet"; -import { WalletProvider } from "@/utils/wallet/wallet_provider"; +import { UTXO, WalletProvider } from "@/utils/wallet/wallet_provider"; import { getStakingTerm } from "../getStakingTerm"; -// Sign a staking transaction // Returns: -// - stakingTxHex: the signed staking transaction +// - unsignedStakingPsbt: the unsigned staking transaction // - stakingTerm: the staking term -export const signStakingTx = async ( +// - stakingFee: the staking fee +export const createStakingTx = ( globalParamsVersion: GlobalParamsVersion, - stakingTimeBlocks: number, - btcWallet: WalletProvider | undefined, - finalityProvider: FinalityProvider | undefined, - btcWalletNetwork: networks.Network | undefined, stakingAmountSat: number, - address: string | undefined, + stakingTimeBlocks: number, + finalityProvider: FinalityProvider, + btcWalletNetwork: networks.Network, + address: string, publicKeyNoCoord: string, -): Promise<{ stakingTxHex: string; stakingTerm: number }> => { - // Initial validation - if (!btcWallet) throw new Error("Wallet is not connected"); - if (!address) throw new Error("Address is not set"); - if (!btcWalletNetwork) throw new Error("Wallet network is not connected"); - if (!finalityProvider) throw new Error("Finality provider is not selected"); - + feeRate: number, + inputUTXOs: UTXO[], +) => { // Data extraction const stakingTerm = getStakingTerm(globalParamsVersion, stakingTimeBlocks); @@ -43,17 +38,14 @@ export const signStakingTx = async ( throw new Error("Invalid staking data"); } - // Get the UTXOs - let inputUTXOs = []; - try { - inputUTXOs = await btcWallet.getUtxos(address); - } catch (error: Error | any) { - throw new Error(error?.message || "UTXOs error"); - } if (inputUTXOs.length == 0) { throw new Error("Not enough usable balance"); } + if (feeRate <= 0) { + throw new Error("Invalid fee rate"); + } + // Create the staking scripts let scripts; try { @@ -67,19 +59,11 @@ export const signStakingTx = async ( throw new Error(error?.message || "Cannot build staking scripts"); } - // Get the network fees - let feeRate: number; - try { - const netWorkFee = await btcWallet.getNetworkFees(); - feeRate = netWorkFee.fastestFee; - } catch (error) { - throw new Error("Cannot get network fees"); - } - // Create the staking transaction let unsignedStakingPsbt; + let stakingFeeSat; try { - const { psbt } = stakingTransaction( + const { psbt, fee } = stakingTransaction( scripts, stakingAmountSat, address, @@ -94,12 +78,45 @@ export const signStakingTx = async ( globalParamsVersion.activationHeight - 1, ); unsignedStakingPsbt = psbt; + stakingFeeSat = fee; } catch (error: Error | any) { throw new Error( error?.message || "Cannot build unsigned staking transaction", ); } + return { unsignedStakingPsbt, stakingTerm, stakingFeeSat }; +}; + +// Sign a staking transaction +// Returns: +// - stakingTxHex: the signed staking transaction +// - stakingTerm: the staking term +export const signStakingTx = async ( + btcWallet: WalletProvider, + globalParamsVersion: GlobalParamsVersion, + stakingAmountSat: number, + stakingTimeBlocks: number, + finalityProvider: FinalityProvider, + btcWalletNetwork: networks.Network, + address: string, + publicKeyNoCoord: string, + feeRate: number, + inputUTXOs: UTXO[], +): Promise<{ stakingTxHex: string; stakingTerm: number }> => { + // Create the staking transaction + let { unsignedStakingPsbt, stakingTerm } = createStakingTx( + globalParamsVersion, + stakingAmountSat, + stakingTimeBlocks, + finalityProvider, + btcWalletNetwork, + address, + publicKeyNoCoord, + feeRate, + inputUTXOs, + ); + // Sign the staking transaction let stakingTx: Transaction; try { diff --git a/src/utils/delegations/signWithdrawalTx.ts b/src/utils/delegations/signWithdrawalTx.ts index 94b4d8b..923fb32 100644 --- a/src/utils/delegations/signWithdrawalTx.ts +++ b/src/utils/delegations/signWithdrawalTx.ts @@ -100,7 +100,7 @@ export const signWithdrawalTx = async ( address, btcWalletNetwork, fees.fastestFee, - delegation.stakingTx.outputIndex, + 0, // unbonding always has a single output ); } diff --git a/src/utils/getFeeRateFromMempool.ts b/src/utils/getFeeRateFromMempool.ts new file mode 100644 index 0000000..6bc337e --- /dev/null +++ b/src/utils/getFeeRateFromMempool.ts @@ -0,0 +1,19 @@ +import { nextPowerOfTwo } from "./nextPowerOfTwo"; +import { Fees } from "./wallet/wallet_provider"; + +// Returns min, default and max fee rate from mempool +export const getFeeRateFromMempool = (mempoolFeeRates?: Fees) => { + if (mempoolFeeRates) { + return { + minFeeRate: mempoolFeeRates.hourFee, + defaultFeeRate: mempoolFeeRates.fastestFee, + maxFeeRate: nextPowerOfTwo(mempoolFeeRates?.fastestFee! * 2), + }; + } else { + return { + minFeeRate: 0, + defaultFeeRate: 0, + maxFeeRate: 0, + }; + } +}; diff --git a/src/utils/nextPowerOfTwo.ts b/src/utils/nextPowerOfTwo.ts new file mode 100644 index 0000000..fa21595 --- /dev/null +++ b/src/utils/nextPowerOfTwo.ts @@ -0,0 +1,3 @@ +export const nextPowerOfTwo = (x: number) => { + return Math.pow(2, Math.ceil(Math.log2(x))); +}; diff --git a/src/utils/wallet/providers/okx_wallet.ts b/src/utils/wallet/providers/okx_wallet.ts index 4fab950..8f40a29 100644 --- a/src/utils/wallet/providers/okx_wallet.ts +++ b/src/utils/wallet/providers/okx_wallet.ts @@ -111,8 +111,8 @@ export class OKXWallet extends WalletProvider { if (!this.okxWalletInfo) { throw new Error("OKX Wallet not connected"); } - // sign the PSBT - return (await this.signPsbts([psbtHex]))[0]; + // Use signPsbt since it shows the fees + return await this.bitcoinNetworkProvider.signPsbt(psbtHex); }; signPsbts = async (psbtsHexes: string[]): Promise => { From ca2fef656bfdce0a89568cdfcb1800def397660a Mon Sep 17 00:00:00 2001 From: Crypto Minion <154598612+jrwbabylonchain@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:41:42 +1000 Subject: [PATCH 16/56] Add tooltip msg to preview button (#255) * fix: add tooltip msg to preview button to hint on what's not ready --- src/app/components/Staking/Staking.tsx | 40 ++++++++++++++++---------- src/utils/isStakingSignReady.ts | 27 +++++++++++++---- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/src/app/components/Staking/Staking.tsx b/src/app/components/Staking/Staking.tsx index 330ec24..074d7e8 100644 --- a/src/app/components/Staking/Staking.tsx +++ b/src/app/components/Staking/Staking.tsx @@ -1,6 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import { Transaction, networks } from "bitcoinjs-lib"; import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react"; +import { Tooltip } from "react-tooltip"; import { useLocalStorage } from "usehooks-ts"; import { OVERFLOW_HEIGHT_WARNING_THRESHOLD } from "@/app/common/constants"; @@ -537,15 +538,16 @@ export const Staking: React.FC = ({ : stakingTimeBlocks; // Check if the staking transaction is ready to be signed - const signReady = isStakingSignReady( - minStakingAmountSat, - maxStakingAmountSat, - minStakingTimeBlocks, - maxStakingTimeBlocks, - stakingAmountSat, - stakingTimeBlocksWithFixed, - !!finalityProvider, - ); + const { isReady: signReady, reason: signNotReadyReason } = + isStakingSignReady( + minStakingAmountSat, + maxStakingAmountSat, + minStakingTimeBlocks, + maxStakingTimeBlocks, + stakingAmountSat, + stakingTimeBlocksWithFixed, + !!finalityProvider, + ); const previewReady = signReady && feeRate && availableUTXOs && stakingAmountSat; @@ -579,13 +581,21 @@ export const Staking: React.FC = ({ )} {showApproachingCapWarning()} - + + + {previewReady && ( { - if (!itemSelected) return false; + fpSelected: boolean, +): { isReady: boolean; reason: string } => { + if (!fpSelected) + return { + isReady: false, + reason: "Please select a finality provider", + }; // Amount parameters are ready const amountParamatersReady = minAmount && maxAmount; @@ -25,6 +29,19 @@ export const isStakingSignReady = ( const timeValuesReady = time >= minTime && time <= maxTime; // Staking time is ready const timeIsReady = timeParametersReady && timeValuesReady; - - return amountIsReady && timeIsReady && itemSelected; + if (!amountIsReady) { + return { + isReady: false, + reason: "Please enter a valid stake amount", + }; + } else if (!timeIsReady) { + return { + isReady: false, + reason: "Please enter a valid staking period", + }; + } + return { + isReady: true, + reason: "", + }; }; From f33fd0390eb71768a77e134800b74a78ce83bfe3 Mon Sep 17 00:00:00 2001 From: Tong Li Date: Tue, 18 Jun 2024 14:17:31 +1000 Subject: [PATCH 17/56] Set up unit tests that works with staking ts lib (#253) * chore: add unit tests for apiDataToStakingScripts and set up jest --------- Co-authored-by: wjrjerome --- babel.config.js | 25 + jest.config.js | 21 - jest.config.ts | 225 ++ jest.setup.ts | 2 + next.config.mjs | 5 +- package-lock.json | 2215 +++++++++++++++++- package.json | 16 +- src/utils/wallet/providers/bitget_wallet.ts | 8 + src/utils/wallet/providers/keystone/index.ts | 7 + src/utils/wallet/providers/okx_wallet.ts | 11 +- src/utils/wallet/providers/onekey_wallet.ts | 5 + src/utils/wallet/providers/tomo_wallet.ts | 7 +- src/utils/wallet/wallet_provider.ts | 8 - tests/helper/index.ts | 144 ++ tests/utils/apiDataToStakingScripts.test.ts | 22 + tsconfig.json | 3 +- tsconfig.test.json | 7 - 17 files changed, 2597 insertions(+), 134 deletions(-) create mode 100644 babel.config.js delete mode 100644 jest.config.js create mode 100644 jest.config.ts create mode 100644 jest.setup.ts create mode 100644 tests/helper/index.ts create mode 100644 tests/utils/apiDataToStakingScripts.test.ts delete mode 100644 tsconfig.test.json diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..9e38595 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,25 @@ +module.exports = function (api) { + const isTest = api.env("test") || process.env.JEST_ENV === "true"; + if (!isTest) return {}; // Don't need to do anything for non-test environments + const presets = [ + ["@babel/preset-env", { targets: { node: "current" } }], + ["@babel/preset-react", { runtime: "automatic" }], + "@babel/preset-typescript", + ]; + const plugins = [ + [ + "module-resolver", + { + root: ["./"], + alias: { + "@": "./src", + }, + }, + ], + ]; + + return { + presets, + plugins, + }; +}; diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 19f7015..0000000 --- a/jest.config.js +++ /dev/null @@ -1,21 +0,0 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} */ -module.exports = { - preset: "ts-jest", - testEnvironment: "jest-environment-jsdom", - moduleNameMapper: { - "^@/(.*)$": "/src/$1", - }, - transform: { - "^.+\\.tsx?$": [ - "ts-jest", - { - tsconfig: "tsconfig.test.json", - }, - ], - }, - moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], - collectCoverage: false, - collectCoverageFrom: ["src/**/*.{ts,tsx}", "!src/**/*.d.ts"], - coverageDirectory: "coverage", - testMatch: ["**/tests/**/*.test.ts", "**/tests/**/*.test.tsx"], -}; diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000..b3231f5 --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,225 @@ +/** + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ + +import type { Config } from "jest"; +import nextJest from "next/jest.js"; + +const createJestConfig = nextJest({ + // Provide the path to your Next.js app to load next.config.js and .env files in your test environment + dir: "./", +}); + +const config: Config = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/_v/y679fjfj4k745109cs4v3bf40000gn/T/jest_dx", + + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + // collectCoverage: false, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: undefined, + + // The directory where Jest should output its coverage files + // coverageDirectory: undefined, + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "v8", + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, + + // A path to a custom dependency extractor + // dependencyExtractor: undefined, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // The default configuration for fake timers + // fakeTimers: { + // "enableGlobally": false + // }, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, + + // A set of global variables that need to be available in all test environments + // globals: {}, + globals: { + "process.env.JEST_ENV": "true", + }, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "mjs", + // "cjs", + // "jsx", + // "ts", + // "tsx", + // "json", + // "node" + // ], + + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + moduleNameMapper: { + "^@/(.*)$": "/src/$1", + }, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + // preset: undefined, + + // Run tests from one or more projects + // projects: undefined, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state before every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: undefined, + + // Automatically restore mock state and implementation before every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + setupFiles: ["./jest.setup.ts"], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + testEnvironment: "jsdom", + projects: [ + { + displayName: "node", + testEnvironment: "node", + testMatch: ["/tests/**/*.test.ts"], + }, + { + displayName: "jsdom", + testEnvironment: "jsdom", + testMatch: ["/tests/**/*.test.tsx"], + }, + ], + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, + + // This option allows use of a custom test runner + // testRunner: "jest-circus/runner", + + // A map from regular expressions to paths to transformers + // transform: undefined, + transform: { + "^.+\\.(js|jsx|ts|tsx)$": "babel-jest", + }, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/", + // "\\.pnp\\.[^\\/]+$" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: undefined, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; + +export default createJestConfig(config); diff --git a/jest.setup.ts b/jest.setup.ts new file mode 100644 index 0000000..0946543 --- /dev/null +++ b/jest.setup.ts @@ -0,0 +1,2 @@ +global.TextEncoder = require("text-encoding").TextEncoder; +global.TextDecoder = require("text-encoding").TextDecoder; diff --git a/next.config.mjs b/next.config.mjs index 8440804..fadee3e 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,7 +1,10 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - output: "standalone", reactStrictMode: true, + output: "standalone", + experimental: { + forceSwcTransforms: true, + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 8b5ea8e..f339ae2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@tanstack/react-query-next-experimental": "^5.28.14", "axios": "^1.6.8", "bitcoinjs-lib": "^6.1.5", - "btc-staking-ts": "^0.2.0", + "btc-staking-ts": "^0.2.1", "date-fns": "^3.6.0", "framer-motion": "^11.1.9", "next": "14.1.3", @@ -34,16 +34,23 @@ }, "devDependencies": { "@babel/core": "^7.24.7", + "@babel/preset-env": "^7.24.7", + "@babel/preset-react": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", + "@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3", "@tanstack/eslint-plugin-query": "^5.28.11", "@tanstack/react-query-devtools": "^5.28.14", - "@testing-library/jest-dom": "^6.4.5", + "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.0", "@types/jest": "^29.5.12", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", + "babel-jest": "^29.7.0", + "babel-plugin-module-resolver": "^5.0.2", "daisyui": "^4.11.1", + "ecpair": "^2.1.0", "eslint": "^8.57.0", "eslint-config-next": "14.1.3", "eslint-config-prettier": "^9.1.0", @@ -56,7 +63,9 @@ "prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-tailwindcss": "^0.5.14", "tailwindcss": "3.4.3", + "text-encoding": "^0.7.0", "ts-jest": "^29.1.4", + "ts-node": "^10.9.2", "typescript": "^5" }, "engines": { @@ -190,6 +199,33 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", + "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", @@ -227,6 +263,85 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", + "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz", + "integrity": "sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/@babel/helper-environment-visitor": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", @@ -267,6 +382,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", + "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", @@ -301,6 +430,19 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-plugin-utils": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", @@ -311,6 +453,42 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz", + "integrity": "sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-wrap-function": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", + "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/helper-simple-access": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", @@ -325,6 +503,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-split-export-declaration": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", @@ -368,6 +560,22 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz", + "integrity": "sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-function-name": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helpers": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", @@ -482,86 +690,1234 @@ "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "dev": true, "license": "MIT", - "bin": { - "parser": "bin/babel-parser.js" + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz", + "integrity": "sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz", + "integrity": "sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", + "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz", + "integrity": "sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", + "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", + "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz", + "integrity": "sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", + "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", + "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz", + "integrity": "sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", + "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", + "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz", + "integrity": "sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", + "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/template": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", + "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", + "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", + "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", + "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", + "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", + "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", + "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz", + "integrity": "sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", + "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz", + "integrity": "sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", + "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", + "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", + "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", + "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz", + "integrity": "sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", + "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", + "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", + "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", + "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", + "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", + "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", + "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", + "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz", + "integrity": "sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", + "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", + "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", + "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", + "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz", + "integrity": "sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz", + "integrity": "sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz", + "integrity": "sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz", + "integrity": "sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", + "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", + "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { - "node": ">=6.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", + "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "node_modules/@babel/plugin-transform-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", + "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", + "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", + "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz", + "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-jsx": { + "node_modules/@babel/plugin-transform-typescript": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", - "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz", + "integrity": "sha512-iLD3UNkgx2n/HrjBesVbYX6j0yqn/sJktvbtKKgcaLIQ4bTTQ8obAypc1VpyHPD2y4Phh9zHOaAt8e/L14wCpw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-typescript": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -570,92 +1926,207 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", + "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", + "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", + "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", + "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "node_modules/@babel/preset-env": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.7.tgz", + "integrity": "sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/compat-data": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.24.7", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.24.7", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.24.7", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.7", + "@babel/plugin-transform-dotall-regex": "^7.24.7", + "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-dynamic-import": "^7.24.7", + "@babel/plugin-transform-exponentiation-operator": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.24.7", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.24.7", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.24.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-member-expression-literals": "^7.24.7", + "@babel/plugin-transform-modules-amd": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-modules-systemjs": "^7.24.7", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-new-target": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-object-super": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-property-literals": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-reserved-words": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-template-literals": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.7", + "@babel/plugin-transform-unicode-escapes": "^7.24.7", + "@babel/plugin-transform-unicode-property-regex": "^7.24.7", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "node_modules/@babel/preset-react": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.7.tgz", + "integrity": "sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.24.7", + "@babel/plugin-transform-react-jsx-development": "^7.24.7", + "@babel/plugin-transform-react-pure-annotations": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -664,14 +2135,18 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-typescript": { + "node_modules/@babel/preset-typescript": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", - "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz", + "integrity": "sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -680,6 +2155,13 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/runtime": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.6.tgz", @@ -806,6 +2288,30 @@ "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.9.0.tgz", "integrity": "sha512-W7gp8Q/v1NlCZLsv8pQ3Y0uCu/SHgXOVFK+eUluUKWXmsb6VHkpNx0apdOWWcDbB9sJoKeP8uPrjmehJz6xETQ==" }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@emnapi/runtime": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", @@ -3200,13 +4706,13 @@ "peer": true }, "node_modules/@testing-library/jest-dom": { - "version": "6.4.5", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.5.tgz", - "integrity": "sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.6.tgz", + "integrity": "sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w==", "dev": true, "license": "MIT", "dependencies": { - "@adobe/css-tools": "^4.3.2", + "@adobe/css-tools": "^4.4.0", "@babel/runtime": "^7.9.2", "aria-query": "^5.0.0", "chalk": "^3.0.0", @@ -3304,6 +4810,34 @@ "node": ">= 10" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -4230,13 +5764,124 @@ "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-module-resolver": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-5.0.2.tgz", + "integrity": "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-babel-config": "^2.1.1", + "glob": "^9.3.3", + "pkg-up": "^3.1.0", + "reselect": "^4.1.7", + "resolve": "^1.22.8" + } + }, + "node_modules/babel-plugin-module-resolver/node_modules/glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/babel-plugin-module-resolver/node_modules/minimatch": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", + "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/babel-plugin-module-resolver/node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", + "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2" }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-preset-current-node-syntax": { @@ -4515,9 +6160,9 @@ } }, "node_modules/btc-staking-ts": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/btc-staking-ts/-/btc-staking-ts-0.2.0.tgz", - "integrity": "sha512-HG8Zr6vvWBBPEBqURAshu2g7XG3zfuZYORZK84GPOrfqg9Mh0Ka//0DOO1SVZAVNDyNvx4CyW70L1x6x1ltDhw==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/btc-staking-ts/-/btc-staking-ts-0.2.2.tgz", + "integrity": "sha512-h+/AeD5ei/q7LioipROGJExDvnIJ1Oa/1wH4ARt+JglZ019IPcLiKrDhDUk0CNaCL/nWR/U7o2XCW4TckPi2EQ==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3", @@ -4887,6 +6532,20 @@ "dev": true, "license": "MIT" }, + "node_modules/core-js-compat": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", + "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/crc": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", @@ -4942,6 +6601,13 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5253,6 +6919,16 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -5321,6 +6997,21 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecpair": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ecpair/-/ecpair-2.1.0.tgz", + "integrity": "sha512-cL/mh3MtJutFOvFc27GPZE2pWL3a3k4YvzUWEOvilnfZVlH3Jwgx/7d6tlD7/75tNk8TG2m+7Kgtz0SI1tWcqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "randombytes": "^2.1.0", + "typeforce": "^1.18.0", + "wif": "^2.0.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.783", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.783.tgz", @@ -6408,6 +8099,30 @@ "node": ">=8" } }, + "node_modules/find-babel-config": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-2.1.1.tgz", + "integrity": "sha512-5Ji+EAysHGe1OipH7GN4qDjok5Z1uw5KAwDCbicU/4wyTZY7CqOCzcWbG7J5ad9mazq67k89fXlbc1MuIfl9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.3", + "path-exists": "^4.0.0" + } + }, + "node_modules/find-babel-config/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -9493,6 +11208,85 @@ "node": ">=8" } }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -10078,12 +11872,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "dev": true }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -10102,6 +11926,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -10119,6 +11983,13 @@ "dev": true, "license": "MIT" }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -11131,6 +13002,14 @@ "node": "*" } }, + "node_modules/text-encoding": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", + "integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==", + "deprecated": "no longer maintained", + "dev": true, + "license": "(Unlicense OR Apache-2.0)" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -11311,6 +13190,57 @@ "node": ">=6" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -11482,6 +13412,50 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -11590,6 +13564,13 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", @@ -11784,6 +13765,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wif": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", + "integrity": "sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs58check": "<3.0.0" + } + }, + "node_modules/wif/node_modules/base-x": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/wif/node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/wif/node_modules/bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -11915,9 +13938,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "license": "MIT", "engines": { @@ -12033,6 +14056,16 @@ "node": ">=8" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 5823e4f..aefd388 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "clean-docker": "docker rmi babylonchain/simple-staking 2>/dev/null; true", "prepare": "husky", "sort-imports": "eslint --fix .", - "test": "jest" + "test": "jest", + "test:watch": "jest --watch" }, "engines": { "node": ">=22.0.0" @@ -29,7 +30,7 @@ "@tanstack/react-query-next-experimental": "^5.28.14", "axios": "^1.6.8", "bitcoinjs-lib": "^6.1.5", - "btc-staking-ts": "^0.2.0", + "btc-staking-ts": "^0.2.1", "date-fns": "^3.6.0", "framer-motion": "^11.1.9", "next": "14.1.3", @@ -46,16 +47,23 @@ }, "devDependencies": { "@babel/core": "^7.24.7", + "@babel/preset-env": "^7.24.7", + "@babel/preset-react": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", + "@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3", "@tanstack/eslint-plugin-query": "^5.28.11", "@tanstack/react-query-devtools": "^5.28.14", - "@testing-library/jest-dom": "^6.4.5", + "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.0", "@types/jest": "^29.5.12", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", + "babel-jest": "^29.7.0", + "babel-plugin-module-resolver": "^5.0.2", "daisyui": "^4.11.1", + "ecpair": "^2.1.0", "eslint": "^8.57.0", "eslint-config-next": "14.1.3", "eslint-config-prettier": "^9.1.0", @@ -68,7 +76,9 @@ "prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-tailwindcss": "^0.5.14", "tailwindcss": "3.4.3", + "text-encoding": "^0.7.0", "ts-jest": "^29.1.4", + "ts-node": "^10.9.2", "typescript": "^5" } } diff --git a/src/utils/wallet/providers/bitget_wallet.ts b/src/utils/wallet/providers/bitget_wallet.ts index 385693f..3a497a1 100644 --- a/src/utils/wallet/providers/bitget_wallet.ts +++ b/src/utils/wallet/providers/bitget_wallet.ts @@ -1,5 +1,7 @@ import { Psbt } from "bitcoinjs-lib"; +import { getNetworkConfig } from "@/config/network.config"; + import { getAddressBalance, getFundingUTXOs, @@ -21,6 +23,7 @@ const INTERNAL_NETWORK_NAMES = { export class BitgetWallet extends WalletProvider { private bitcoinNetworkProvider: any; + private networkEnv: Network | undefined; constructor() { super(); @@ -29,11 +32,16 @@ export class BitgetWallet extends WalletProvider { if (!window[bitgetWalletProvider]?.unisat) { throw new Error("Bitget Wallet extension not found"); } + this.networkEnv = getNetworkConfig().network; this.bitcoinNetworkProvider = window[bitgetWalletProvider].unisat; } connectWallet = async (): Promise => { + if (!this.networkEnv) { + throw new Error("Network not found"); + } + try { await this.bitcoinNetworkProvider.switchNetwork( INTERNAL_NETWORK_NAMES[this.networkEnv], diff --git a/src/utils/wallet/providers/keystone/index.ts b/src/utils/wallet/providers/keystone/index.ts index b5add9d..3ee0e5e 100644 --- a/src/utils/wallet/providers/keystone/index.ts +++ b/src/utils/wallet/providers/keystone/index.ts @@ -19,6 +19,8 @@ import { tapleafHash } from "bitcoinjs-lib/src/payments/bip341"; import { toXOnly } from "bitcoinjs-lib/src/psbt/bip371"; import { pubkeyInScript } from "bitcoinjs-lib/src/psbt/psbtutils"; +import { getNetworkConfig } from "@/config/network.config"; + import { toNetwork } from "../.."; import { getAddressBalance, @@ -44,6 +46,7 @@ export class KeystoneWallet extends WalletProvider { private keystoneWaleltInfo: KeystoneWalletInfo | undefined; private viewSdk: typeof sdk; private dataSdk: KeystoneSDK; + private networkEnv: Network | undefined; constructor() { super(); @@ -52,6 +55,7 @@ export class KeystoneWallet extends WalletProvider { this.dataSdk = new KeystoneSDK({ origin: "babylon staking app", }); + this.networkEnv = getNetworkConfig().network; } /** @@ -158,6 +162,9 @@ export class KeystoneWallet extends WalletProvider { }; getNetwork = async (): Promise => { + if (!this.networkEnv) { + throw new Error("Network not set"); + } return this.networkEnv; }; diff --git a/src/utils/wallet/providers/okx_wallet.ts b/src/utils/wallet/providers/okx_wallet.ts index 8f40a29..c282336 100644 --- a/src/utils/wallet/providers/okx_wallet.ts +++ b/src/utils/wallet/providers/okx_wallet.ts @@ -1,4 +1,8 @@ -import { network, validateAddress } from "@/config/network.config"; +import { + getNetworkConfig, + network, + validateAddress, +} from "@/config/network.config"; import { getAddressBalance, @@ -22,6 +26,7 @@ export class OKXWallet extends WalletProvider { private okxWalletInfo: WalletInfo | undefined; private okxWallet: any; private bitcoinNetworkProvider: any; + private networkEnv: Network | undefined; constructor() { super(); @@ -32,6 +37,7 @@ export class OKXWallet extends WalletProvider { } this.okxWallet = window[okxProvider]; + this.networkEnv = getNetworkConfig().network; // OKX uses different providers for different networks switch (this.networkEnv) { @@ -136,6 +142,9 @@ export class OKXWallet extends WalletProvider { getNetwork = async (): Promise => { // OKX does not provide a way to get the network for Signet and Testnet // So we pass the check on connection and return the environment network + if (!this.networkEnv) { + throw new Error("Network not set"); + } return this.networkEnv; }; diff --git a/src/utils/wallet/providers/onekey_wallet.ts b/src/utils/wallet/providers/onekey_wallet.ts index 7632d4d..63c5d3d 100644 --- a/src/utils/wallet/providers/onekey_wallet.ts +++ b/src/utils/wallet/providers/onekey_wallet.ts @@ -1,3 +1,5 @@ +import { getNetworkConfig } from "@/config/network.config"; + import { getAddressBalance, getFundingUTXOs, @@ -25,6 +27,7 @@ export class OneKeyWallet extends WalletProvider { private oneKeyWalletInfo: WalletInfo | undefined; private oneKeyWallet: any; private bitcoinNetworkProvider: any; + private networkEnv: Network | undefined; constructor() { super(); @@ -38,6 +41,8 @@ export class OneKeyWallet extends WalletProvider { // OneKey provider stays the same for all networks this.bitcoinNetworkProvider = this.oneKeyWallet.btcwallet; + + this.networkEnv = getNetworkConfig().network; } async connectWallet(): Promise { diff --git a/src/utils/wallet/providers/tomo_wallet.ts b/src/utils/wallet/providers/tomo_wallet.ts index 9f84e83..6a560bd 100644 --- a/src/utils/wallet/providers/tomo_wallet.ts +++ b/src/utils/wallet/providers/tomo_wallet.ts @@ -1,3 +1,4 @@ +import { getNetworkConfig } from "@/config/network.config"; import { getAddressBalance, getFundingUTXOs, @@ -26,6 +27,7 @@ const INTERNAL_NETWORK_NAMES = { export class TomoWallet extends WalletProvider { private tomoWalletInfo: WalletInfo | undefined; private bitcoinNetworkProvider: any; + private networkEnv: Network | undefined; constructor() { super(); @@ -34,6 +36,7 @@ export class TomoWallet extends WalletProvider { if (!window[tomoProvider]) { throw new Error("Tomo Wallet extension not found"); } + this.networkEnv = getNetworkConfig().network; this.bitcoinNetworkProvider = window[tomoProvider]; } @@ -43,7 +46,9 @@ export class TomoWallet extends WalletProvider { if (!this.bitcoinNetworkProvider) { throw new Error("Tomo Wallet extension not found"); } - + if (!this.networkEnv) { + throw new Error("Network not found"); + } if (this.bitcoinNetworkProvider.getVersion) { const version = await this.bitcoinNetworkProvider.getVersion(); if (version < workingVersion) { diff --git a/src/utils/wallet/wallet_provider.ts b/src/utils/wallet/wallet_provider.ts index b323fe2..04541ca 100644 --- a/src/utils/wallet/wallet_provider.ts +++ b/src/utils/wallet/wallet_provider.ts @@ -1,5 +1,3 @@ -import { getNetworkConfig } from "@/config/network.config"; - export type Fees = { // fee for inclusion in the next block fastestFee: number; @@ -44,12 +42,6 @@ export type WalletInfo = { */ export abstract class WalletProvider { - protected networkEnv: Network; - - constructor() { - this.networkEnv = getNetworkConfig().network; - } - /** * Connects to the wallet and returns the instance of the wallet provider. * Currently only supports "native segwit" and "taproot" address types. diff --git a/tests/helper/index.ts b/tests/helper/index.ts new file mode 100644 index 0000000..12be2df --- /dev/null +++ b/tests/helper/index.ts @@ -0,0 +1,144 @@ +import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs"; +import * as bitcoin from "bitcoinjs-lib"; +import ECPairFactory from "ecpair"; + +import { UTXO } from "@/utils/wallet/wallet_provider"; +const ECPair = ECPairFactory(ecc); + +export class DataGenerator { + private network: bitcoin.networks.Network; + + constructor(network: bitcoin.networks.Network) { + this.network = network; + } + + generateRandomTxId = () => { + const randomBuffer = Buffer.alloc(32); + for (let i = 0; i < 32; i++) { + randomBuffer[i] = Math.floor(Math.random() * 256); + } + return randomBuffer.toString("hex"); + }; + + generateRandomKeyPairs = (isNoCoordPk = false) => { + const keyPair = ECPair.makeRandom({ network: this.network }); + const { privateKey, publicKey } = keyPair; + if (!privateKey || !publicKey) { + throw new Error("Failed to generate random key pair"); + } + let pk = publicKey.toString("hex"); + + pk = isNoCoordPk ? pk.slice(2) : pk; + + return { + privateKey: privateKey.toString("hex"), + publicKey: pk, + }; + }; + + // Generate a random staking term (number of blocks to stake) + // ranged from 1 to 65535 + generateRandomStakingTerm = () => { + return Math.floor(Math.random() * 65535) + 1; + }; + + generateRandomFeeRates = () => { + return Math.floor(Math.random() * 1000) + 1; + }; + + // Convenant quorums are a list of public keys that are used to sign a covenant + generateRandomCovenantCommittee = (size: number): Buffer[] => { + const quorum: Buffer[] = []; + for (let i = 0; i < size; i++) { + const keyPair = this.generateRandomKeyPairs(true); + quorum.push(Buffer.from(keyPair.publicKey, "hex")); + } + return quorum; + }; + + generateRandomTag = () => { + const buffer = Buffer.alloc(4); + for (let i = 0; i < 4; i++) { + buffer[i] = Math.floor(Math.random() * 256); + } + return buffer; + }; + + generateRandomGlobalParams = (isFixedTimelock = false) => { + // the commitee size is assunmed to be between 5 and 20 + const committeeSize = Math.floor(Math.random() * 20) + 5; + const covenantPks = this.generateRandomCovenantCommittee(committeeSize).map( + (buffer) => buffer.toString("hex"), + ); + // the covenantQuorum should always be around 2/3 of the committee size + const covenantQuorum = Math.floor((committeeSize * 2) / 3); + let maxStakingTimeBlocks = Math.floor(Math.random() * 100); + let minStakingTimeBlocks = Math.floor(Math.random() * 100); + if (isFixedTimelock) { + maxStakingTimeBlocks = minStakingTimeBlocks; + } + + const minStakingAmountSat = Math.floor(Math.random() * 100); + const maxStakingAmountSat = + minStakingAmountSat + Math.floor(Math.random() * 1000); + + return { + version: Math.floor(Math.random() * 100), + activationHeight: Math.floor(Math.random() * 100), + stakingCapSat: Math.floor(Math.random() * 100), + stakingCapHeight: Math.floor(Math.random() * 100), + tag: this.generateRandomTag().toString("hex"), + covenantPks, + covenantQuorum, + unbondingTime: this.generateRandomStakingTerm(), + unbondingFeeSat: Math.floor(Math.random() * 1000), + maxStakingAmountSat, + minStakingAmountSat, + maxStakingTimeBlocks, + minStakingTimeBlocks, + confirmationDepth: Math.floor(Math.random() * 20), + }; + }; + + getTaprootAddress = (publicKey: string) => { + const internalPubkey = Buffer.from(publicKey, "hex"); + const { address } = bitcoin.payments.p2tr({ + internalPubkey, + network: this.network, + }); + if (!address) { + throw new Error("Failed to generate taproot address from public key"); + } + return address; + }; + + getNativeSegwitAddress = (publicKey: string) => { + const internalPubkey = Buffer.from(publicKey, "hex"); + const { address } = bitcoin.payments.p2wpkh({ + pubkey: internalPubkey, + network: this.network, + }); + if (!address) { + throw new Error( + "Failed to generate native segwit address from public key", + ); + } + return address; + }; + + getNetwork = () => { + return this.network; + }; + + generateRandomUTXOs = ( + dataGenerator: DataGenerator, + numUTXOs: number, + ): UTXO[] => { + return Array.from({ length: numUTXOs }, () => ({ + txid: dataGenerator.generateRandomTxId(), + vout: Math.floor(Math.random() * 10), + scriptPubKey: this.generateRandomKeyPairs().publicKey, + value: Math.floor(Math.random() * 9000) + 1000, + })); + }; +} diff --git a/tests/utils/apiDataToStakingScripts.test.ts b/tests/utils/apiDataToStakingScripts.test.ts new file mode 100644 index 0000000..6edc35f --- /dev/null +++ b/tests/utils/apiDataToStakingScripts.test.ts @@ -0,0 +1,22 @@ +import * as bitcoin from "bitcoinjs-lib"; + +import { apiDataToStakingScripts } from "@/utils/apiDataToStakingScripts"; + +import { DataGenerator } from "../helper"; + +describe("apiDataToStakingScripts", () => { + const dataGen = new DataGenerator(bitcoin.networks.testnet); + it("should throw an error if the publicKeyNoCoord is not set", () => { + const { publicKey: finalityProviderPk } = dataGen.generateRandomKeyPairs(); + const stakingTxTimelock = dataGen.generateRandomStakingTerm(); + const globalParams = dataGen.generateRandomGlobalParams(); + expect(() => { + apiDataToStakingScripts( + finalityProviderPk, + stakingTxTimelock, + globalParams, + "", + ); + }).toThrow("Invalid data"); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 9da5000..5b32a25 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,7 +26,8 @@ "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", - "node_modules/@testing-library/jest-dom" + "node_modules/@testing-library/jest-dom", + "tests" ], "exclude": ["node_modules"] } diff --git a/tsconfig.test.json b/tsconfig.test.json deleted file mode 100644 index 4fe6e8b..0000000 --- a/tsconfig.test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "jsx": "react-jsx" - }, - "include": ["**/*.test.ts", "**/*.test.tsx", "**/__tests__/*"] -} From acc6f4e21dc70a16e7f3f5b9a46aa146413f67b0 Mon Sep 17 00:00:00 2001 From: Crypto Minion <154598612+jrwbabylonchain@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:23:37 +1000 Subject: [PATCH 18/56] fix: should not show error if user decide to cancel the keystone connection (#256) --- src/app/page.tsx | 8 ++++++++ src/utils/wallet/errors.ts | 16 ++++++++++++++++ src/utils/wallet/providers/keystone/index.ts | 10 ++++++++-- 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 src/utils/wallet/errors.ts diff --git a/src/app/page.tsx b/src/app/page.tsx index 76caa0f..8247cf4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -9,6 +9,7 @@ import { useLocalStorage } from "usehooks-ts"; import { network } from "@/config/network.config"; import { getCurrentGlobalParamsVersion } from "@/utils/globalParams"; import { getDelegationsLocalStorageKey } from "@/utils/local_storage/getDelegationsLocalStorageKey"; +import { WalletError, WalletErrorType } from "@/utils/wallet/errors"; import { getPublicKeyNoCoord, isSupportedAddressType, @@ -243,6 +244,13 @@ const Home: React.FC = () => { setAddress(address); setPublicKeyNoCoord(publicKeyNoCoord.toString("hex")); } catch (error: Error | any) { + if ( + error instanceof WalletError && + error.getType() === WalletErrorType.ConnectionCancelled + ) { + // User cancelled the connection, hence do nothing + return; + } showError({ error: { message: error.message, diff --git a/src/utils/wallet/errors.ts b/src/utils/wallet/errors.ts new file mode 100644 index 0000000..b252d6e --- /dev/null +++ b/src/utils/wallet/errors.ts @@ -0,0 +1,16 @@ +export const enum WalletErrorType { + ConnectionCancelled = "ConnectionCancelled", +} + +export class WalletError extends Error { + private type: WalletErrorType; + constructor(type: WalletErrorType, message: string) { + super(message); + this.name = "WalletError"; + this.type = type; + } + + public getType(): WalletErrorType { + return this.type; + } +} diff --git a/src/utils/wallet/providers/keystone/index.ts b/src/utils/wallet/providers/keystone/index.ts index 3ee0e5e..d80ca29 100644 --- a/src/utils/wallet/providers/keystone/index.ts +++ b/src/utils/wallet/providers/keystone/index.ts @@ -29,6 +29,7 @@ import { getTipHeight, pushTx, } from "../../../mempool_api"; +import { WalletError, WalletErrorType } from "../../errors"; import { Fees, Network, UTXO, WalletProvider } from "../../wallet_provider"; import BIP322 from "./bip322"; @@ -87,9 +88,14 @@ export class KeystoneWallet extends WalletProvider { "The scanned QR code is not the sync code from the Keystone hardware wallet. Please verify the code and try again.", }, ); - - if (decodedResult.status !== ReadStatus.success) + if (decodedResult.status === ReadStatus.canceled) { + throw new WalletError( + WalletErrorType.ConnectionCancelled, + "Connection cancelled", + ); + } else if (decodedResult.status !== ReadStatus.success) { throw new Error("Error reading QR code, Please try again."); + } // parse the QR Code and get extended public key and other required information const accountData = this.dataSdk.parseAccount(decodedResult.result); From 563cdd5576c8b294efcd342aa1488373a09fc077 Mon Sep 17 00:00:00 2001 From: Jeremy <168515712+jeremy-babylonchain@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:08:02 +0800 Subject: [PATCH 19/56] add icon and tooltip on FP (#263) * add icon and tooltip --------- Co-authored-by: wjrjerome --- .../FinalityProviders/FinalityProvider.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx b/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx index 78163bf..ed89325 100644 --- a/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx +++ b/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx @@ -38,12 +38,23 @@ export const FinalityProvider: React.FC = ({
{moniker ? ( -
+

{moniker}

verified
) : ( - "-" +
+ + + + + - +
)}
@@ -66,7 +77,7 @@ export const FinalityProvider: React.FC = ({

Comission:

- {maxDecimals(Number(comission) * 100, 2)}% + {moniker ? `${maxDecimals(Number(comission) * 100, 2)}%` : "-"} Date: Tue, 18 Jun 2024 12:27:09 +0200 Subject: [PATCH 20/56] feature: Filter staking delegations local storage (#260) * filter delegations local storage * mempool return * delegations.delegations * simplify fetched tx logic * duration comment * hashes comparison * rm unused clog --- src/app/page.tsx | 48 ++++++++++---- .../filterDelegationsLocalStorage.ts | 62 +++++++++++++++++++ src/utils/mempool_api.ts | 19 ++++++ 3 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 src/utils/local_storage/filterDelegationsLocalStorage.ts diff --git a/src/app/page.tsx b/src/app/page.tsx index 8247cf4..4d2d7d7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -8,6 +8,7 @@ import { useLocalStorage } from "usehooks-ts"; import { network } from "@/config/network.config"; import { getCurrentGlobalParamsVersion } from "@/utils/globalParams"; +import { filterDelegationsLocalStorage } from "@/utils/local_storage/filterDelegationsLocalStorage"; import { getDelegationsLocalStorageKey } from "@/utils/local_storage/getDelegationsLocalStorageKey"; import { WalletError, WalletErrorType } from "@/utils/wallet/errors"; import { @@ -277,22 +278,43 @@ const Home: React.FC = () => { } }, [btcWallet]); - // Remove the delegations that are already present in the API + // Clean up the local storage delegations useEffect(() => { - if (!delegations) { + if (!delegations?.delegations) { return; } - setDelegationsLocalStorage((localDelegations) => - localDelegations?.filter( - (localDelegation) => - !delegations?.delegations.find( - (delegation) => - delegation?.stakingTxHashHex === - localDelegation?.stakingTxHashHex, - ), - ), - ); - }, [delegations, setDelegationsLocalStorage]); + + const updateDelegations = async () => { + // Filter the delegations that are still valid + const validDelegations = await filterDelegationsLocalStorage( + delegationsLocalStorage, + delegations.delegations, + ); + + // Extract the stakingTxHashHex from the validDelegations + const validDelegationsHashes = validDelegations + .map((delegation) => delegation.stakingTxHashHex) + .sort(); + const delegationsLocalStorageHashes = delegationsLocalStorage + .map((delegation) => delegation.stakingTxHashHex) + .sort(); + + // Check if the validDelegations are different from the current delegationsLocalStorage + const areDelegationsDifferent = + validDelegationsHashes.length !== + delegationsLocalStorageHashes.length || + validDelegationsHashes.some( + (hash, index) => hash !== delegationsLocalStorageHashes[index], + ); + + // Update the local storage delegations if they are different to avoid unnecessary updates + if (areDelegationsDifferent) { + setDelegationsLocalStorage(validDelegations); + } + }; + + updateDelegations(); + }, [delegations, setDelegationsLocalStorage, delegationsLocalStorage]); // Finality providers key-value map { pk: moniker } const finalityProvidersKV = finalityProviders?.finalityProviders.reduce( diff --git a/src/utils/local_storage/filterDelegationsLocalStorage.ts b/src/utils/local_storage/filterDelegationsLocalStorage.ts new file mode 100644 index 0000000..2440471 --- /dev/null +++ b/src/utils/local_storage/filterDelegationsLocalStorage.ts @@ -0,0 +1,62 @@ +import { Delegation } from "@/app/types/delegations"; + +import { getTxInfo } from "../mempool_api"; + +// Duration after which a delegation should be removed from the local storage +// if not identified by the API or mempool. +const maxDelegationPendingDuration = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + +// Filter delegations from the local storage +// Returns the delegations that are valid and should be kept in the local storage +export const filterDelegationsLocalStorage = async ( + delegationsLocalStorage: Delegation[], + delegationsFromAPI: Delegation[], +): Promise => { + const validDelegations: Delegation[] = []; + + // `continue` will not add the delegation to the validDelegations array + for (const localDelegation of delegationsLocalStorage) { + // Check if the delegation is already present in the API + const delegationInAPI = delegationsFromAPI.find( + (delegation) => + delegation?.stakingTxHashHex === localDelegation?.stakingTxHashHex, + ); + + if (delegationInAPI) { + continue; + } + + // Check if the delegation has exceeded the max duration + const startTimestamp = new Date( + localDelegation.stakingTx.startTimestamp, + ).getTime(); + const currentTime = Date.now(); + const hasExceededDuration = + currentTime - startTimestamp > maxDelegationPendingDuration; + + if (hasExceededDuration) { + // We are removing the delegation from the local storage + // only if it has exceeded the max duration and is not in the mempool + // otherwise (low fees as example), we keep it in the local storage + + // Check if the transaction is in the mempool + let isInMempool = true; + try { + const fetchedTx = await getTxInfo(localDelegation.stakingTxHashHex); + if (!fetchedTx) { + throw new Error("Transaction not found in the mempool"); + } + } catch (_error) { + isInMempool = false; + } + + if (!isInMempool) { + continue; + } + } + + validDelegations.push(localDelegation); + } + + return validDelegations; +}; diff --git a/src/utils/mempool_api.ts b/src/utils/mempool_api.ts index 5f77060..a28bed4 100644 --- a/src/utils/mempool_api.ts +++ b/src/utils/mempool_api.ts @@ -43,6 +43,11 @@ function validateAddressUrl(address: string): URL { return new URL(mempoolAPI + "v1/validate-address/" + address); } +// URL for the transaction info endpoint +function txInfoUrl(txId: string): URL { + return new URL(mempoolAPI + "tx/" + txId); +} + /** * Pushes a transaction to the Bitcoin network. * @param txHex - The hex string corresponding to the full transaction. @@ -184,3 +189,17 @@ export async function getFundingUTXOs( }; }); } + +/** + * Retrieve information about a transaction. + * @param txId - The transaction ID in string format. + * @returns A promise that resolves into the transaction information. + */ +export async function getTxInfo(txId: string): Promise { + const response = await fetch(txInfoUrl(txId)); + if (!response.ok) { + const err = await response.text(); + throw new Error(err); + } + return await response.json(); +} From c759c17b6bc340b99b6e6e107a476d3cec7e04f0 Mon Sep 17 00:00:00 2001 From: Crypto Minion <154598612+jrwbabylonchain@users.noreply.github.com> Date: Tue, 18 Jun 2024 20:39:21 +1000 Subject: [PATCH 21/56] fix: show nearest days and weeks (#257) * fix: show nearest days and weeks --- README.md | 1 + src/app/components/Delegations/Delegation.tsx | 5 +- .../components/Delegations/Delegations.tsx | 1 + src/app/components/Modals/PreviewModal.tsx | 14 +++-- .../components/Modals/UnbondWithdrawModal.tsx | 10 +++- .../components/Staking/Form/StakingTime.tsx | 13 +++-- src/app/components/Staking/Staking.tsx | 3 ++ src/utils/blocksToDisplayTime.ts | 52 +++++++++++++++++++ src/utils/blocksToWeeks.ts | 38 -------------- src/utils/getState.ts | 11 ++-- tests/utils/blocksToDisplayTime.test.ts | 27 ++++++++++ 11 files changed, 124 insertions(+), 51 deletions(-) create mode 100644 src/utils/blocksToDisplayTime.ts delete mode 100644 src/utils/blocksToWeeks.ts create mode 100644 tests/utils/blocksToDisplayTime.test.ts diff --git a/README.md b/README.md index 7367715..fece37f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ where, node queries - `NEXT_PUBLIC_API_URL` specifies the back-end API to use for the staking system queries +- `NEXT_PUBLIC_NETWORK` specifies the BTC network environment Then, to start a development server: diff --git a/src/app/components/Delegations/Delegation.tsx b/src/app/components/Delegations/Delegation.tsx index e55eb82..65109f9 100644 --- a/src/app/components/Delegations/Delegation.tsx +++ b/src/app/components/Delegations/Delegation.tsx @@ -102,12 +102,11 @@ export const Delegation: React.FC = ({ }; const renderStateTooltip = () => { - const confirmationDepth = globalParamsVersion?.confirmationDepth; // overflow should be shown only on active state if (isOverflow && isActive) { - return getStateTooltip(DelegationState.OVERFLOW, confirmationDepth); + return getStateTooltip(DelegationState.OVERFLOW, globalParamsVersion); } else { - return getStateTooltip(intermediateState || state, confirmationDepth); + return getStateTooltip(intermediateState || state, globalParamsVersion); } }; diff --git a/src/app/components/Delegations/Delegations.tsx b/src/app/components/Delegations/Delegations.tsx index 1db4ccc..b64d104 100644 --- a/src/app/components/Delegations/Delegations.tsx +++ b/src/app/components/Delegations/Delegations.tsx @@ -284,6 +284,7 @@ export const Delegations: React.FC = ({ {modalMode && txID && ( setModalOpen(false)} onProceed={() => { diff --git a/src/app/components/Modals/PreviewModal.tsx b/src/app/components/Modals/PreviewModal.tsx index dde571d..ee744b4 100644 --- a/src/app/components/Modals/PreviewModal.tsx +++ b/src/app/components/Modals/PreviewModal.tsx @@ -1,7 +1,7 @@ import { IoMdClose } from "react-icons/io"; import { getNetworkConfig } from "@/config/network.config"; -import { blocksToWeeks } from "@/utils/blocksToWeeks"; +import { blocksToDisplayTime } from "@/utils/blocksToDisplayTime"; import { satoshiToBtc } from "@/utils/btcConversions"; import { maxDecimals } from "@/utils/maxDecimals"; @@ -16,6 +16,7 @@ interface PreviewModalProps { stakingTimeBlocks: number; stakingFeeSat: number; feeRate: number; + unbondingTimeBlocks: number; } export const PreviewModal: React.FC = ({ @@ -24,6 +25,7 @@ export const PreviewModal: React.FC = ({ finalityProvider, stakingAmountSat, stakingTimeBlocks, + unbondingTimeBlocks, onSign, stakingFeeSat, feeRate, @@ -73,14 +75,20 @@ export const PreviewModal: React.FC = ({

Term

- {stakingTimeBlocks ? blocksToWeeks(stakingTimeBlocks, 5) : "-"} + {stakingTimeBlocks ? blocksToDisplayTime(stakingTimeBlocks) : "-"}

On-demand unbonding

-

Enabled (7 days unbonding time)

+

+ Enabled ( + {unbondingTimeBlocks + ? blocksToDisplayTime(unbondingTimeBlocks) + : "-"}{" "} + unbonding time) +

Attention!

diff --git a/src/app/components/Modals/UnbondWithdrawModal.tsx b/src/app/components/Modals/UnbondWithdrawModal.tsx index dead631..a7967a6 100644 --- a/src/app/components/Modals/UnbondWithdrawModal.tsx +++ b/src/app/components/Modals/UnbondWithdrawModal.tsx @@ -1,5 +1,7 @@ import { IoMdClose } from "react-icons/io"; +import { blocksToDisplayTime } from "@/utils/blocksToDisplayTime"; + import { GeneralModal } from "./GeneralModal"; export const MODE_UNBOND = "unbond"; @@ -7,6 +9,7 @@ export const MODE_WITHDRAW = "withdraw"; export type MODE = typeof MODE_UNBOND | typeof MODE_WITHDRAW; interface PreviewModalProps { + unbondingTimeBlocks: number; open: boolean; onClose: (value: boolean) => void; onProceed: () => void; @@ -14,6 +17,7 @@ interface PreviewModalProps { } export const UnbondWithdrawModal: React.FC = ({ + unbondingTimeBlocks, open, onClose, onProceed, @@ -23,7 +27,11 @@ export const UnbondWithdrawModal: React.FC = ({ const unbondContent = ( <> You are about to unbond your stake before its expiration. The expected - unbonding time will be about 7 days.
+ unbonding time will be about + + {unbondingTimeBlocks ? blocksToDisplayTime(unbondingTimeBlocks) : "-"} + + .
After unbonded, you will need to use this dashboard to withdraw your stake for it to appear in your wallet. OK to proceed? diff --git a/src/app/components/Staking/Form/StakingTime.tsx b/src/app/components/Staking/Form/StakingTime.tsx index 2b52653..a3969d5 100644 --- a/src/app/components/Staking/Form/StakingTime.tsx +++ b/src/app/components/Staking/Form/StakingTime.tsx @@ -1,13 +1,14 @@ import { ChangeEvent, FocusEvent, useEffect, useState } from "react"; import { getNetworkConfig } from "@/config/network.config"; -import { blocksToWeeks } from "@/utils/blocksToWeeks"; +import { blocksToDisplayTime } from "@/utils/blocksToDisplayTime"; import { validateNoDecimalPoints } from "./validation/validation"; interface StakingTimeProps { minStakingTimeBlocks: number; maxStakingTimeBlocks: number; + unbondingTimeBlocks: number; onStakingTimeBlocksChange: (inputTimeBlocks: number) => void; reset: boolean; } @@ -15,6 +16,7 @@ interface StakingTimeProps { export const StakingTime: React.FC = ({ minStakingTimeBlocks, maxStakingTimeBlocks, + unbondingTimeBlocks, onStakingTimeBlocksChange, reset, }) => { @@ -101,11 +103,16 @@ export const StakingTime: React.FC = ({

You can unbond and withdraw your stake anytime with an unbonding time - of 7 days. + of{" "} + {unbondingTimeBlocks ? blocksToDisplayTime(unbondingTimeBlocks) : "-"} + .

There is also a build-in maximum staking period of{" "} - {minStakingTimeBlocks ? blocksToWeeks(minStakingTimeBlocks, 5) : "-"}. + {minStakingTimeBlocks + ? blocksToDisplayTime(minStakingTimeBlocks) + : "-"} + .

If the stake is not unbonded before the end of this period, it will diff --git a/src/app/components/Staking/Staking.tsx b/src/app/components/Staking/Staking.tsx index 074d7e8..1061d1d 100644 --- a/src/app/components/Staking/Staking.tsx +++ b/src/app/components/Staking/Staking.tsx @@ -527,6 +527,7 @@ export const Staking: React.FC = ({ maxStakingAmountSat, minStakingTimeBlocks, maxStakingTimeBlocks, + unbondingTime, } = stakingParams; // Staking time is fixed @@ -560,6 +561,7 @@ export const Staking: React.FC = ({ @@ -606,6 +608,7 @@ export const Staking: React.FC = ({ stakingTimeBlocks={stakingTimeBlocksWithFixed} stakingFeeSat={stakingFeeSat} feeRate={feeRate} + unbondingTimeBlocks={unbondingTime} /> )}

diff --git a/src/utils/blocksToDisplayTime.ts b/src/utils/blocksToDisplayTime.ts new file mode 100644 index 0000000..6df678b --- /dev/null +++ b/src/utils/blocksToDisplayTime.ts @@ -0,0 +1,52 @@ +import { + add, + differenceInCalendarDays, + differenceInWeeks, + formatDistanceStrict, +} from "date-fns"; + +const BLOCKS_PER_HOUR = 6; +const WEEKS_PRECISION = 5; +const DAY_TO_WEEK_DISPLAY_THRESHOLD = 30; + +/** + * Converts a number of blocks into days or weeks + * Returns the time in days if the difference is less than 7 days + * Otherwise, returns the time in weeks + * + * @param {number} blocks - The number of blocks to convert. + * @returns {string} - The converted time in days or weeks. + * Rounded to 5 weeks if the difference is greater than 7 days. + * + * Examples of usage: + * blocksToDisplayTime(30000); // "29 weeks" + * blocksToDisplayTime(1); // "1 day" + * blocksToDisplayTime(200); // "2 days" + */ +export const blocksToDisplayTime = (blocks: number): string => { + // If no blocks are provided, throw an error + if (!blocks) throw new Error("No blocks provided"); + + // Calculate the equivalent time in hours + const hours = blocks / BLOCKS_PER_HOUR; + + // Calculate the start and end dates + // get the timestamp now + const startDate = new Date(); + const endDate = add(startDate, { hours }); + + const dayDifference = differenceInCalendarDays(endDate, startDate); + // If the difference is greater than or equal to 30 days, return the difference in weeks + if (dayDifference >= DAY_TO_WEEK_DISPLAY_THRESHOLD) { + // Calculate the difference in weeks + const weeks = differenceInWeeks(endDate, startDate); + const roundedWeeks = Math.round(weeks / WEEKS_PRECISION) * WEEKS_PRECISION; + return `${roundedWeeks} weeks`; + } + + // Otherwise, return the difference in days and round up to the nearest day + return formatDistanceStrict(startDate, endDate, { + unit: "day", + roundingMethod: "ceil", + }); +}; diff --git a/src/utils/blocksToWeeks.ts b/src/utils/blocksToWeeks.ts deleted file mode 100644 index a85df15..0000000 --- a/src/utils/blocksToWeeks.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { add, differenceInWeeks } from "date-fns"; - -const BLOCKS_PER_HOUR = 6; - -/** - * Converts a number of blocks into weeks with optional rounding precision. - * - * @param {number} blocks - The number of blocks to convert. - * @param {number} [precision] - The precision to round the result to. If not provided, the result is not rounded. - * @returns {string} - The converted time in weeks, rounded to the specified precision. - * - * Examples of usage: - * blocksToWeeks(30000); // "29 weeks" - * blocksToWeeks(63000, 5); // "65 weeks" - */ -export const blocksToWeeks = (blocks: number, precision?: number): string => { - // If no blocks are provided, throw an error - if (!blocks) throw new Error("No blocks provided"); - - // Calculate the equivalent time in hours - const hours = blocks / BLOCKS_PER_HOUR; - - // Calculate the start and end dates - const startDate = new Date(0); - const endDate = add(startDate, { hours }); - - // Calculate the difference in weeks - const weeks = differenceInWeeks(endDate, startDate); - - // Round the value to the nearest multiple of precision, if precision is provided - let roundedWeeks = weeks; - if (precision) { - roundedWeeks = Math.round(weeks / precision) * precision; - } - - // Return the calculated value, followed by the unit - return `${roundedWeeks} weeks`; -}; diff --git a/src/utils/getState.ts b/src/utils/getState.ts index f151fa7..e1b81a5 100644 --- a/src/utils/getState.ts +++ b/src/utils/getState.ts @@ -1,5 +1,7 @@ import { DelegationState } from "@/app/types/delegations"; +import { blocksToDisplayTime } from "./blocksToDisplayTime"; + // Convert state to human readable format export const getState = (state: string) => { switch (state) { @@ -30,20 +32,23 @@ export const getState = (state: string) => { }; // Create state tooltips for the additional information -export const getStateTooltip = (state: string, confirmationDepth?: number) => { +export const getStateTooltip = ( + state: string, + params?: { confirmationDepth: number; unbondingTime: number }, +) => { switch (state) { case DelegationState.ACTIVE: return "Stake is active"; case DelegationState.UNBONDING_REQUESTED: return "Unbonding requested"; case DelegationState.UNBONDING: - return "Unbonding process of 7 days has started"; + return `Unbonding process of ${params ? blocksToDisplayTime(params.unbondingTime) : "-"} has started`; case DelegationState.UNBONDED: return "Stake has been unbonded"; case DelegationState.WITHDRAWN: return "Stake has been withdrawn"; case DelegationState.PENDING: - return `Stake that is pending ${confirmationDepth || 10} Bitcoin confirmations will only be visible from this device`; + return `Stake that is pending ${params?.confirmationDepth || 10} Bitcoin confirmations will only be visible from this device`; case DelegationState.OVERFLOW: return "Stake is over the staking cap"; case DelegationState.EXPIRED: diff --git a/tests/utils/blocksToDisplayTime.test.ts b/tests/utils/blocksToDisplayTime.test.ts new file mode 100644 index 0000000..045d086 --- /dev/null +++ b/tests/utils/blocksToDisplayTime.test.ts @@ -0,0 +1,27 @@ +import { blocksToDisplayTime } from "@/utils/blocksToDisplayTime"; + +describe("blocksToDisplayTime", () => { + it("should throw error if block is 0", () => { + expect(() => blocksToDisplayTime(0)).toThrow("No blocks provided"); + }); + + it("should convert 1 block to 1 day", () => { + expect(blocksToDisplayTime(1)).toBe("1 day"); + }); + + it("should convert 200 blocks to 2 days", () => { + expect(blocksToDisplayTime(200)).toBe("2 days"); + }); + + it("should convert 900 blocks to 7 days", () => { + expect(blocksToDisplayTime(900)).toBe("7 days"); + }); + + it("should convert 30000 blocks to 30 weeks", () => { + expect(blocksToDisplayTime(30000)).toBe("30 weeks"); + }); + + it("should convert 4320 blocks to 5 weeks", () => { + expect(blocksToDisplayTime(4320)).toBe("5 weeks"); + }); +}); From 2d99062e02cf061961e627998b77b15dbec6fd72 Mon Sep 17 00:00:00 2001 From: Crypto Minion <154598612+jrwbabylonchain@users.noreply.github.com> Date: Tue, 18 Jun 2024 20:51:57 +1000 Subject: [PATCH 22/56] fix: update FAQ for overflow and use coinName instead of hardcoded sBTC (#264) --- src/app/components/FAQ/FAQ.tsx | 5 +++- src/app/components/FAQ/data/questions.ts | 33 ++++++++++++++---------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/app/components/FAQ/FAQ.tsx b/src/app/components/FAQ/FAQ.tsx index b4d1391..8beb426 100644 --- a/src/app/components/FAQ/FAQ.tsx +++ b/src/app/components/FAQ/FAQ.tsx @@ -1,14 +1,17 @@ +import { getNetworkConfig } from "@/config/network.config"; + import { Section } from "./Section"; import { questions } from "./data/questions"; interface FAQProps {} export const FAQ: React.FC = () => { + const { coinName } = getNetworkConfig(); return (

FAQ

- {questions.map((question) => ( + {questions(coinName).map((question) => (
[ { title: "What is Babylon?", content: `

Babylon is a suite of security-sharing protocols that bring Bitcoin\’s unparalleled security to the decentralized world. The latest protocol, Bitcoin Staking, enables Bitcoin holders to stake their Bitcoin to provide crypto-economic security to PoS (proof-of-stake) systems in a trustless and self-custodial way.

`, }, { title: "How does Bitcoin Staking Work?", - content: `

Signet Bitcoin holders lock their Signet Bitcoin using the trustless and self-custodial Bitcoin Staking script for a predetermined time (timelock) in exchange for voting power in an underlying PoS protocol. In return, Bitcoin holders will earn PoS staking rewards.


-

Finality providers perform the voting. A Signet Bitcoin staker can create a finality provider by itself and self-delegate or delegate its voting power to a third-party finality provider.


-

If a finality provider attacks the PoS system, the Signet Bitcoins behind the voting powers delegated to it will be subject to protocol slashing. This deters Signet Bitcoin stakers and finality providers from attacking the PoS system.

+ content: `

${coinName} holders lock their ${coinName} using the trustless and self-custodial Bitcoin Staking script for a predetermined time (timelock) in exchange for voting power in an underlying PoS protocol. In return, Bitcoin holders will earn PoS staking rewards.


+

Finality providers perform the voting. A ${coinName} staker can create a finality provider by itself and self-delegate or delegate its voting power to a third-party finality provider.


+

If a finality provider attacks the PoS system, the ${coinName}s behind the voting powers delegated to it will be subject to protocol slashing. This deters ${coinName} stakers and finality providers from attacking the PoS system.

`, }, { title: "What is the goal of this testnet?", - content: `

The goal of this testnet is to ensure the security of the staked Bitcoins by testing the user's interaction with the Signet BTC network. This will be a lock-only network without any PoS chain operating, meaning that the only participants of this testnet will be finality providers and Signet Bitcoin stakers.

`, + content: `

The goal of this testnet is to ensure the security of the staked Bitcoins by testing the user's interaction with the ${coinName} network. This will be a lock-only network without any PoS chain operating, meaning that the only participants of this testnet will be finality providers and ${coinName} stakers.

`, }, { title: "What does this staking dApp allow me to do?", - content: `

The staking dApp allows Signet Bitcoin holders to stake their Signet Bitcoin and delegate their voting power to a finality provider they select. Stakers can view their past staking history and on-demand unlock their stake for early withdrawal.

`, + content: `

The staking dApp allows ${coinName} holders to stake their ${coinName} and delegate their voting power to a finality provider they select. Stakers can view their past staking history and on-demand unlock their stake for early withdrawal.

`, }, { - title: "Does my Signet Bitcoin leave my wallet once staked?", - content: `

Yes, it leaves your wallet. Your wallet will not show it as your available balance because it is locked. However, it is not sent to any third party. It is locked in a self-custodial contract you control. This means that any subsequent movement of the Signet Bitcoin will need your approval, and you are the only one who can unbond the stake and withdraw.

`, + title: `Does my ${coinName} leave my wallet once staked?`, + content: `

Yes, it leaves your wallet. Your wallet will not show it as your available balance because it is locked. However, it is not sent to any third party. It is locked in a self-custodial contract you control. This means that any subsequent movement of the ${coinName} will need your approval, and you are the only one who can unbond the stake and withdraw.

`, }, { - title: "Is my Signet Bitcoin Safe? Could I get slashed?", - content: `

In this testnet, you are not required to sign any PoS slashing-related authorizations. Thus, in theory, the Signet Bitcoin in your self-custodial contract cannot be slashed due to the absence of your authorization.


+ title: `Is my ${coinName} Safe? Could I get slashed?`, + content: `

In this testnet, you are not required to sign any PoS slashing-related authorizations. Thus, in theory, the ${coinName} in your self-custodial contract cannot be slashed due to the absence of your authorization.


-

However, there are still risks associated with your Signet Bitcoin:


+

However, there are still risks associated with your ${coinName}:


  1. @@ -36,14 +36,19 @@ export const questions = [
  2. 2. System reliability
    - The Bitcoin staking system may be slow, unavailable, or compromised, which may cause the staking service to be unavailable or compromised, potentially leading to Signet Bitcoin not being unbindable or not withdrawable. + The Bitcoin staking system may be slow, unavailable, or compromised, which may cause the staking service to be unavailable or compromised, potentially leading to ${coinName} not being unbindable or not withdrawable.
`, }, { title: "How long will it take for my stake to become active?", - content: `

A stake’s status demonstrates the current stage of the staking process. All stake starts in a Pending state which denotes that the Signet Bitcoin Staking transaction does not yet have sufficient Signet BTC block confirmations. As soon as it receives 10 Signet Bitcoin block confirmations, the status transitions to Overflow or Active. A stake is Overflow if the system has already accumulated the maximum amount of Signet Bitcoin it can accept. Otherwise, the stake is Active. Overflow stake should be on-demand unbonded and withdrawn.

`, + content: `

A stake’s status demonstrates the current stage of the staking process. All stake starts in a Pending state which denotes that the ${coinName} Staking transaction does not yet have sufficient ${coinName} block confirmations. As soon as it receives 10 ${coinName} block confirmations, the status transitions to Overflow or Active.


+ +

In an amount-based cap, A stake is Overflow if the system has already accumulated the maximum amount of ${coinName} it can accept.


+

In a time-based cap, where there is a starting block height and ending block height, a stake is overflow if it is included in a ${coinName} block that is newer than the ending block.


+

Otherwise, the stake is Active.


+

Overflow stake should be on-demand unbonded and withdrawn.

`, }, { title: "Do I get rewards for staking?", @@ -51,7 +56,7 @@ export const questions = [ }, { title: "Are there any other ways to stake?", - content: `

Hands-on stakers can operate the btc-staker CLI program that allows for the creation of Signet Bitcoin staking transactions from the CLI.

+ content: `

Hands-on stakers can operate the btc-staker CLI program that allows for the creation of ${coinName} staking transactions from the CLI.

`, }, ]; From 5236e032b2df9528f8fe3ada8f885d846bfead37 Mon Sep 17 00:00:00 2001 From: Crypto Minion <154598612+jrwbabylonchain@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:21:03 +1000 Subject: [PATCH 23/56] chore: when there is 1 block left, use block instead of blocks (#265) --- src/app/components/Stats/Stats.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/components/Stats/Stats.tsx b/src/app/components/Stats/Stats.tsx index 85b8fab..ea13ed3 100644 --- a/src/app/components/Stats/Stats.tsx +++ b/src/app/components/Stats/Stats.tsx @@ -31,9 +31,10 @@ const buildNextCapText = ( ) => { const { stakingCapHeight, stakingCapSat, activationHeight } = nextVersion; if (stakingCapHeight) { + const remainingBlocks = activationHeight - btcHeight - 1; return { title: "Staking Window", - value: `opens in ${activationHeight - btcHeight - 1} blocks`, + value: `opens in ${remainingBlocks} ${remainingBlocks == 1 ? "block" : "blocks"}`, }; } else if (stakingCapSat) { return { @@ -61,7 +62,9 @@ const buildStakingCapSection = ( return { title: "Staking Window", value: - numOfBlockLeft > 0 ? `closes in ${numOfBlockLeft} blocks` : "closed", + numOfBlockLeft > 0 + ? `closes in ${numOfBlockLeft} ${numOfBlockLeft == 1 ? "block" : "blocks"}` + : "closed", }; } else if (stakingCapSat) { return { From baa00a259e7cc7ba235c0f994f77720ec41117e1 Mon Sep 17 00:00:00 2001 From: Govard Barkhatov Date: Tue, 18 Jun 2024 14:48:05 +0200 Subject: [PATCH 24/56] bump btc-staking-ts to 022 (#267) --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f339ae2..100c8d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@tanstack/react-query-next-experimental": "^5.28.14", "axios": "^1.6.8", "bitcoinjs-lib": "^6.1.5", - "btc-staking-ts": "^0.2.1", + "btc-staking-ts": "^0.2.2", "date-fns": "^3.6.0", "framer-motion": "^11.1.9", "next": "14.1.3", diff --git a/package.json b/package.json index aefd388..8bd60dd 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@tanstack/react-query-next-experimental": "^5.28.14", "axios": "^1.6.8", "bitcoinjs-lib": "^6.1.5", - "btc-staking-ts": "^0.2.1", + "btc-staking-ts": "^0.2.2", "date-fns": "^3.6.0", "framer-motion": "^11.1.9", "next": "14.1.3", From 1f3ade5b6f002c5af88b5fa6ce10b90d3e2d9974 Mon Sep 17 00:00:00 2001 From: Govard Barkhatov Date: Tue, 18 Jun 2024 15:06:53 +0200 Subject: [PATCH 25/56] feature: Max log scale calculation (#266) * next power of 2 calcs * max fee rate at least * calc +1 * LEAST_MAX_FEE_RATE --- src/utils/getFeeRateFromMempool.ts | 7 ++++++- src/utils/nextPowerOfTwo.ts | 5 ++++- tests/utils/nextPowerOfTwo.test.ts | 32 ++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 tests/utils/nextPowerOfTwo.test.ts diff --git a/src/utils/getFeeRateFromMempool.ts b/src/utils/getFeeRateFromMempool.ts index 6bc337e..ea4599f 100644 --- a/src/utils/getFeeRateFromMempool.ts +++ b/src/utils/getFeeRateFromMempool.ts @@ -4,10 +4,15 @@ import { Fees } from "./wallet/wallet_provider"; // Returns min, default and max fee rate from mempool export const getFeeRateFromMempool = (mempoolFeeRates?: Fees) => { if (mempoolFeeRates) { + // The maximum fee rate is at least 128 sat/vB + const LEAST_MAX_FEE_RATE = 128; return { minFeeRate: mempoolFeeRates.hourFee, defaultFeeRate: mempoolFeeRates.fastestFee, - maxFeeRate: nextPowerOfTwo(mempoolFeeRates?.fastestFee! * 2), + maxFeeRate: Math.max( + LEAST_MAX_FEE_RATE, + nextPowerOfTwo(mempoolFeeRates.fastestFee), + ), }; } else { return { diff --git a/src/utils/nextPowerOfTwo.ts b/src/utils/nextPowerOfTwo.ts index fa21595..2f24dbd 100644 --- a/src/utils/nextPowerOfTwo.ts +++ b/src/utils/nextPowerOfTwo.ts @@ -1,3 +1,6 @@ export const nextPowerOfTwo = (x: number) => { - return Math.pow(2, Math.ceil(Math.log2(x))); + if (x <= 0) return 2; + if (x === 1) return 4; + + return Math.pow(2, Math.ceil(Math.log2(x)) + 1); }; diff --git a/tests/utils/nextPowerOfTwo.test.ts b/tests/utils/nextPowerOfTwo.test.ts new file mode 100644 index 0000000..a448a75 --- /dev/null +++ b/tests/utils/nextPowerOfTwo.test.ts @@ -0,0 +1,32 @@ +import { nextPowerOfTwo } from "@/utils/nextPowerOfTwo"; + +describe("nextPowerOfTwo", () => { + it("should convert negative numbers to 2", () => { + expect(nextPowerOfTwo(-1)).toBe(2); + expect(nextPowerOfTwo(-100)).toBe(2); + }); + + it("should convert 0 to 2", () => { + expect(nextPowerOfTwo(0)).toBe(2); + }); + + it("should convert 1 to 4", () => { + expect(nextPowerOfTwo(1)).toBe(4); + }); + + it("should convert 2 to 4", () => { + expect(nextPowerOfTwo(2)).toBe(4); + }); + + it("should convert 5 to 16", () => { + expect(nextPowerOfTwo(5)).toBe(16); + }); + + it("should convert 250 to 512", () => { + expect(nextPowerOfTwo(250)).toBe(512); + }); + + it("should convert 1023 to 2048", () => { + expect(nextPowerOfTwo(1023)).toBe(2048); + }); +}); From 76d22bcc55dcaa4555bf843d6bea42254bc3e63b Mon Sep 17 00:00:00 2001 From: Jeremy <168515712+jeremy-babylonchain@users.noreply.github.com> Date: Wed, 19 Jun 2024 01:11:08 +0800 Subject: [PATCH 26/56] add terms modal (#262) * add terms modal * remove download button * update terms of use file * use react component instead * remove doc file * fix css * fix css * fix css --- public/babylonchain_terms_of_use.doc | Bin 28611 -> 0 bytes src/app/components/Footer/Footer.tsx | 30 +- src/app/components/Modals/ConnectModal.tsx | 13 +- .../components/Modals/Terms/TermsModal.tsx | 27 + .../components/Modals/Terms/data/terms.tsx | 798 ++++++++++++++++++ src/app/context/Terms/TermsContext.tsx | 32 + src/app/globals.css | 2 + src/app/page.tsx | 4 + src/app/providers.tsx | 25 +- src/app/styles/terms.css | 28 + 10 files changed, 922 insertions(+), 37 deletions(-) delete mode 100644 public/babylonchain_terms_of_use.doc create mode 100644 src/app/components/Modals/Terms/TermsModal.tsx create mode 100644 src/app/components/Modals/Terms/data/terms.tsx create mode 100644 src/app/context/Terms/TermsContext.tsx create mode 100644 src/app/styles/terms.css diff --git a/public/babylonchain_terms_of_use.doc b/public/babylonchain_terms_of_use.doc deleted file mode 100644 index 29246606a1d1169bcb2a3b878dd94d016994c14d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28611 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 LYG|qTZ)g7lU=u33 diff --git a/src/app/components/Footer/Footer.tsx b/src/app/components/Footer/Footer.tsx index 6823207..c727644 100644 --- a/src/app/components/Footer/Footer.tsx +++ b/src/app/components/Footer/Footer.tsx @@ -10,6 +10,8 @@ import { GoHome } from "react-icons/go"; import { IoMdBook } from "react-icons/io"; import { MdAlternateEmail } from "react-icons/md"; +import { useTerms } from "@/app/context/Terms/TermsContext"; + const iconLinks = [ { name: "Website", @@ -58,35 +60,23 @@ const iconLinks = [ }, ]; -const textLinks = [ - { - name: "Terms of Use", - url: "/babylonchain_terms_of_use.doc", - isExternal: false, - }, -]; - interface FooterProps {} export const Footer: React.FC = () => { + const { openTerms } = useTerms(); + return (

- {textLinks.map(({ name, url, isExternal }) => ( - - ))} +
{iconLinks.map(({ name, url, Icon }) => ( diff --git a/src/app/components/Modals/ConnectModal.tsx b/src/app/components/Modals/ConnectModal.tsx index 5a37a53..6ed9923 100644 --- a/src/app/components/Modals/ConnectModal.tsx +++ b/src/app/components/Modals/ConnectModal.tsx @@ -6,6 +6,7 @@ import { IoMdClose } from "react-icons/io"; import { PiWalletBold } from "react-icons/pi"; import { Tooltip } from "react-tooltip"; +import { useTerms } from "@/app/context/Terms/TermsContext"; import { getNetworkConfig } from "@/config/network.config"; import { BROWSER_INJECTED_WALLET_NAME, walletList } from "@/utils/wallet/list"; import { WalletProvider } from "@/utils/wallet/wallet_provider"; @@ -38,6 +39,8 @@ export const ConnectModal: React.FC = ({ // And whether or not it should be injected const BROWSER = "btcwallet"; + const { openTerms } = useTerms(); + useEffect(() => { const fetchWalletProviderDetails = async () => { // Check if the browser wallet is injectable @@ -141,14 +144,12 @@ export const ConnectModal: React.FC = ({ /> I certify that I have read and accept the updated{" "} - Terms of Use - + . diff --git a/src/app/components/Modals/Terms/TermsModal.tsx b/src/app/components/Modals/Terms/TermsModal.tsx new file mode 100644 index 0000000..e41c5f1 --- /dev/null +++ b/src/app/components/Modals/Terms/TermsModal.tsx @@ -0,0 +1,27 @@ +import { IoMdClose } from "react-icons/io"; + +import { GeneralModal } from "../GeneralModal"; + +import { Terms } from "./data/terms"; + +interface TermsModalProps { + open: boolean; + onClose: (value: boolean) => void; +} + +export const TermsModal: React.FC = ({ open, onClose }) => { + return ( + +
+

Terms of Use

+ +
+ +
+ ); +}; diff --git a/src/app/components/Modals/Terms/data/terms.tsx b/src/app/components/Modals/Terms/data/terms.tsx new file mode 100644 index 0000000..b7b92af --- /dev/null +++ b/src/app/components/Modals/Terms/data/terms.tsx @@ -0,0 +1,798 @@ +export const Terms = () => { + return ( +
+

Last updated [27 May 2024]

+
+

+ + https://btcstaking.testnet.babylonchain.io/ + {" "} + is a website-hosted user interface (the{" "} + “Interface”).{" "} + + BabylonChain.io + {" "} + is our website (“Website”). +

+
+

+ Please read these terms and conditions carefully before using the + Interface or Website for any reason. Your use of the Interface and + Website is conditional upon your agreement to the Terms set out below. + If you do not agree and you do not give your consent to be bound to the + Terms, do not use the Interface and, if presented with the option to{" "} + “accept” to the Terms of use, only + select “accept” if you certify + that you consent to be bound by the Terms. +

+
+

+ If you do not meet the eligibility requirements set forth in Section 7 + of the Terms or are otherwise not in strict compliance with these Terms, + you are expressly prohibited from using, accessing, or deriving any + benefit from the Interface and Website and you must not attempt to + access or use the Interface or Website. Use of a virtual private network + (e.g., a VPN) or other means by ineligible persons to access or use the + Interface is prohibited, and prohibited uses may attract legal liability + for fraudulent use of the Interface. +

+
+

Terms of use

+
+

+ These Terms of Use and any terms and conditions incorporated by + reference (collectively, the “Terms”) + govern access to and the use of the Interface by each individual, + entity, group, or association (collectively{" "} + User, Users, You) who views, + interacts, links to or otherwise uses or derives any benefit from the + Interface. +

+
+

+ By accessing, browsing, or using the Interface or Website, or by + acknowledging your agreement to the Terms on the Interface, you agree + that you have read, understood, and consented to be bound by all of the + Terms, Privacy Policy, and Disclosure which are incorporated by + reference into these Terms. +

+
+

+ Importantly, when you agree to these Terms by using or accessing the + Interface, you agree to a binding arbitration provision and a class + action waiver, both of which impact your rights as to how disputes are + resolved. +

+
+

+ From time to time and at any time, the Terms may be changed, amended, or + revised without notice or consultation. If you do not agree to the + revised Terms, then you should not continue to access or use the + Interface or Website. +

+
+

Binding Provisions

+
+

+ 1. Dispute Resolution; Arbitration Agreement +

+
+

+ If you have any dispute or claim arising out of or relating in any way + to the Interface or these Terms, you must send an email to{" "} + + contracts@babylonchain.io + {" "} + to resolve the matter via an informal, good faith negotiation process. + If that dispute or claim is not resolved within 60 days of sending such + an email, then you agree that all unresolved disputes or claims shall be + finally and exclusively settled by arbitration under the LCIA + Arbitration Rules in force at the time of the filing for arbitration of + such dispute. The arbitration shall be held before a single arbitrator + and shall be conducted in the English language on a confidential basis. + The arbitration will be held in the Cayman Islands. Any award made by + the arbitrator may be entered in any court of competent jurisdiction as + necessary. This section shall survive termination of these Terms, the + Interface, the Website, or any connection you may have to the + information you obtained from the Interface. +

+
+

+ 2. Class Action and Jury Trial Waiver +

+
+

+ You agree to bring all disputes or claims connected to the Interface or + the Website in your individual capacity and not as a plaintiff in or + member of any class action, collective action, private attorney general + action, or other representative proceeding. Further, you irrevocably + waive the right to demand a trial by jury. +

+
+

3. Governing Law

+
+

+ You agree that the laws of the Cayman Islands, without regard to the + principles of conflict of laws, govern these Terms. +

+
+

4. About the Interface

+
+

+ The Interface aggregates and publishes publicly available third-party + information. +

+
+

+ The Interface also offers interaction methods whereby the User can + indicate a transaction that the User would like to perform in connection + with the publicly available Babylon staking protocols (the{" "} + “Protocols”). The interaction methods + include accessing the functionalities of publicly deployed Protocol for + Users to self-authorize token transfers on relevant blockchains. +

+
+

5. About the Protocol

+
+

+ The Protocols are software source codes freely licensed to the public + under the license(s) identified in the applicable GitHub repository. +

+
+

+ 6. Interface relationship to Protocol +

+
+

+ Using the relevant blockchain systems, third-party supplied wallets, + devices, validator nodes or the Protocol does not require use of this + Interface. Anyone with an internet connection can connect directly to + the Protocol or blockchain without accessing or using the Interface. +

+
+

+ The Interface maintainers do not own, operate or control the blockchain + systems, wallets or devices, validator nodes, or the Protocol. +

+
+

+ The Interface aggregates and publishes publicly available information + about the Protocol in a user-friendly and convenient format. Such + information is also independently available from other sources—for + example, a User may directly review the blockchain transaction history, + account balances, and compatible block explorers on each relevant + blockchain. Users may also access code repositories for the various + Protocols on platforms like Github. +

+
+

+ By combining publicly available information with the User’s interactions + with the Interface, the Interface can draft standard transaction + messages compatible with the Protocol. Standard transaction messages are + designed to accomplish the User’s operational goals as expressed through + the interactions. If the User wishes, they may broadcast such messages + to the Bitcoin blockchain in order to initiate native Bitcoin staking. +

+
+

+ All draft transaction messages are delivered by the web Interface via an + API to a compatible third-party wallet application or device selected by + the User after pressing the{" "} + “Connect Wallet” (or similar) + button on the Interface. +

+
+

+ The User must personally review and authorize all transaction messages + that the User wishes to send to blockchain systems; this requires the + User to sign the relevant transaction message with a private + cryptographic key inaccessible to the Interface or the Interface + maintainers, or Interface contributors. The use of such associated + private cryptographic keys is beyond the control of the Interface, the + Interface maintainers, or contributors. +

+
+

+ The User-authorised message will then be broadcasted to blockchain + systems through the wallet application or device and the User may pay a + network fee to have the transaction message delivered through the + Protocol and record the results on the appropriate blockchain—resulting + in a token transaction being completed on that blockchain. +

+
+

+ The Interface maintainers and the Interface are not agents or + intermediaries of the User. The Interface or the Interface maintainers + do not store, have access to or control over any tokens, private keys, + passwords, accounts or other property of the User. The Interface or the + Interface maintainers are not capable of performing transactions or + sending transaction messages on behalf of the User. The Interface or the + Interface maintainers do not hold and cannot purchase, sell or trade any + tokens. All transactions relating to the Protocol are executed and + recorded solely through the User’s interactions with the respective + blockchains. The interactions are not under the control of or affiliated + with the Interface maintainers or the Interface. The Interface + maintainers do not collect any compensation from the User for use of the + Interface. +

+
+

7. Eligibility

+
+

If you use the interface you represent and declare that you:

+
    +
  1. + are of legal age in the jurisdiction in which you reside to use the + Interface and the Protocols, and you have legal capacity to consent + and agree to be bound by these Terms; +
  2. +
  3. + have all technical knowledge necessary or advisable to understand and + evaluate the risks of using the Interface and the Protocols; +
  4. +
  5. + comply with all applicable laws, rules and regulations in your + relevant jurisdiction and your use of the Interface is not prohibited + by and does not otherwise violate or facilitate the violation of any + applicable laws or regulations, or contribute to or facilitate any + illegal activity; +
  6. +
  7. + are not a resident, citizen, national or agent of, or an entity + organized, incorporated or doing business in, Belarus, Burundi, Crimea + and Sevastopol, Cuba, Democratic Republic of Congo, Iran, Iraq, Libya, + North Korea, Somalia, Sudan, Syria, Venezuela, Zimbabwe or any other + country to which the United States, the United Kingdom, the European + Union or any of its member states or the United Nations or any of its + member states (collectively, the{" "} + Major Jurisdictions) embargoes + goods or imposes similar sanctions (such embargoed or sanctioned + territories, collectively, the{" "} + Restricted Territories); +
  8. +
  9. + are not, and do not directly or indirectly own or control, and have + not received any assets from any blockchain address that is listed on + any sanctions list or equivalent maintained by any of the Major + Jurisdictions (such sanctions-listed persons, collectively,{" "} + Sanctions Lists Persons); and +
  10. +
  11. + do not intend to transact in or with any Restricted Territories or + Sanctions List Persons; +
  12. +
+
+

8. Permitted Use

+
+

+ The Permitted Use of the Interface is exclusively to aid technologically + sophisticated persons who wish to use the Interface for informational + purposes only as an aid to their own research, due diligence, and + decision-making. Before using any information from the Interface + (including any draft transaction messages) to engage in transactions, + each User must independently verify the accuracy of such information + (and the consistency of such draft transaction messages with the + User's intentions). +

+
+

9. Prohibited Uses

+
+

+ Each User must not, directly or indirectly, in connection with their use + of the Interface: +

+
    +
  1. use the Interface other than for the Permitted Use;
  2. +
  3. + use the Interface at any time when any representation of the User set + forth in the Terms is untrue or inaccurate; +
  4. +
  5. + rely on the Interface as a basis for or a source of advice concerning + any financial or legal decision making or transactions; +
  6. +
  7. + employ any device, scheme or artifice to defraud, or otherwise + materially mislead, any person; +
  8. +
  9. + engage in any act, practice or course of business that operates or + would operate as a fraud or deceit upon any person; +
  10. +
  11. + fail to comply with any applicable provision of these Terms or any + other terms or conditions, privacy policy, or other policy governing + the use of the Interface; +
  12. +
  13. + engage, attempt, or assist in any hack of or attack on the Interface + or any wallet application or device, including any{" "} + “sybil attack”,{" "} + “DoS attack”,{" "} + “griefing attack”, virus + deployment, or theft; +
  14. +
  15. + commit any violation of applicable laws, rules or regulations in your + relevant jurisdiction; +
  16. +
  17. + transact in securities, commodities futures, trading of commodities on + a leveraged, margined or financed basis, binary options (including + prediction-market transactions), real estate or real estate leases, + equipment leases, debt financings, equity financings or other similar + transactions, in each case, if such transactions do not comply with + all laws, rules and regulations applicable to the parties and assets + engaged therein; +
  18. +
  19. + engage in token-based or other financings of a business, enterprise, + venture, DAO, software development project or other initiative, + including ICOs, DAICOs, IEOs, or other token-based fundraising events; +
  20. +
  21. + engage in activity that violates any applicable law, rule, or + regulation concerning the integrity of trading markets, including, but + not limited to, the manipulative tactics commonly known as spoofing + and wash trading. +
  22. +
  23. + engage in any act, practice, or course of business that operates to + circumvent any sanctions or export controls targeting the User or the + country or territory where the User is located. +
  24. +
  25. + engage in any activity that infringes on or violates any copyright, + trademark, service mark, patent, right of publicity, right of privacy, + or other proprietary or intellectual property rights under any law. +
  26. +
  27. + engage in any activity that disguises or interferes in any way with + the IP address of a computer used to access or use the Interface or + that otherwise prevents correctly identifying the IP address of the + computer used to access the Interface. +
  28. +
  29. + engage in any activity that transmits, exchanges, or is otherwise + supported by the direct or indirect proceeds of criminal or fraudulent + activity; and +
  30. +
  31. + engage in any activity that contributes to or facilitates any of the + foregoing activities. +
  32. +
+
+

+ 10. Additional User Declarations +

+
+

+ Additionally, if you use the interface you consent to, represent, and + declare that you agree: +

+
    +
  1. + that the only duties and obligations connected with the Interface owed + to the User are set forth in these Terms; +
  2. +
  3. + that these Terms constitute legal, valid, and binding obligations + enforceable against the Users; +
  4. +
  5. + that the Interface shall be deemed to be based solely in the Cayman + Islands and that although the Interface may be available in other + jurisdictions, its availability does not give rise to general or + specific personal jurisdiction in any forum outside the Cayman + Islands; +
  6. +
  7. + that the Interface is provided for informational purposes only and it + is not directly or indirectly in control of the Protocol and related + blockchain systems or capable of performing or effecting any + transactions on your behalf; +
  8. +
  9. + that the Interface is only being provided as an aid to your own + independent research and evaluation of the Protocol and you should not + take, or refrain from taking, any action based on any information on + the Interface and without limitation from third party blog posts, + articles, links news feeds, tutorials, tweets, and videos; +
  10. +
  11. + that the ability of the Interface to connect with third-party wallet + applications or devices is not an endorsement or recommendation by or + on behalf of the Interface maintainers, and you assume all + responsibility for selecting and evaluating, and incurring the risks + of any bugs, defects, malfunctions or interruptions of any third-party + wallet applications or devices you directly or indirectly use in + connection with the Interface; +
  12. +
  13. + to not hold the Interface maintainers or any affiliates liable for any + damages that you may suffer in connection with your use of the + Interface or the Protocol; +
  14. +
  15. + that the information available on the Interface is not professional, + legal, business, investment, or any other advice related to any + financial product; +
  16. +
  17. + that the information is not an offer or recommendation or solicitation + to buy or sell any particular digital asset or to use any particular + investment strategy; +
  18. +
  19. + that before you make any financial, legal, or other decision in + connection with the interface, you should seek independent + professional advice from an individual who is licensed and qualified + in the area for which such advice would be appropriate; +
  20. +
  21. + that the Terms are not intended to, and do not, create or impose any + fiduciary duties on any party; +
  22. +
  23. + to the fullest extent permitted by law, you acknowledge and agree that + the Interface maintainers owe no fiduciary duties or liabilities to + you or any other party; +
  24. +
  25. + that to the extent, any such duties or liabilities may exist at law or + in equity, those duties and liabilities are hereby irrevocably + disclaimed, waived, and eliminated; +
  26. +
  27. + that you may suffer damages in connection with your use of the + Interface or the Protocol and the Interface and Interface maintainers + are not liable for such damages; +
  28. +
+
+

11. Certain risks

+
+

+ Each User acknowledges, agrees, consents to, and assumes the risks of, + the matters described in this Section 11. +

+
+
+ 11.1 Interface Maintainers Have No Business Plan and May Discontinue, + Limit, Terminate, or Refuse Support for the Interface +
+
+

+ There is no business plan or revenue model for the Interface. The + Interface maintainers do not have revenues or a viable long-term + business plan, and may become unable or unwilling to fund the + operational costs of the Interface on a long-term basis or to fund the + upgrade costs required to keep the Interface up to date with current and + upcoming technologies. +

+
+

+ The Interface is a free web application maintained at the sole and + absolute discretion of a community of contributors who may also be known + as Interface maintainers. + Individually and collectively they assume no duty, liability, + obligation, or undertaking to continue to maintain, or to make available + the Interface. The Interface maintainers may terminate or change the + Interface with respect to any aspect of the Interface at any time. +

+
+

+ The Interface maintainers have no obligation, duty, or liability to + ensure that the Interface is a complete and accurate source of all + information relating to the Protocol or any other subject matter. Even + if the Interface currently displays information about a particular token + or blockchain, the Interface may discontinue tracking and publishing + information about that token or blockchain at any time in the Interface + maintainers' sole and absolute discretion. In the event of such + discontinuation, Users may need to rely on third-party resources such as + block explorers or validator nodes in order to get equivalent + information, and, depending on the User’s level of expertise and the + quality of such third-party resources, this may result in the User + incurring damages due to delays or mistakes in processing information or + transactions. +

+
+
11.2 No Regulatory Supervision
+
+

+ The Interface maintainers and the Interface are not registered or + qualified with or licensed by, do not report to, and are not under the + active supervision of any government agency or financial regulatory + authority or organization. No government or regulator has approved or + has been consulted by the Interface maintainers regarding the accuracy + or completeness of any information available on the Interface. + Similarly, the technology, systems, blockchains, tokens, and persons + relevant to information published on the Interface may not be registered + with or under the supervision of or be registered or qualified with or + licensed by any government agency or financial regulatory authority or + organization. The Interface maintainers are not registered as brokers, + dealers, advisors, transfer agents or other intermediaries. +

+
+
11.3 Regulatory Uncertainty
+
+

+ Blockchain technologies and digital assets are subject to many legal and + regulatory uncertainties, and the Protocol or any tokens or blockchains + could be adversely impacted by one or more regulatory or legal + inquiries, actions, suits, investigations, claims, fines, or judgments, + which could impede or limit the ability of User to continue the use and + enjoyment of such assets and technologies. +

+
+
11.4 No Warranty
+
+

+ The Interface is provided on an “AS IS”{" "} + and “AS AVAILABLE” basis. You + acknowledge and agree that your access and use of the Interface are at + your own risk. There is no representation or warranty that access to the + Interface will be continuous, uninterrupted, timely, or secure; that the + information contained in the Interface will be accurate, reliable, + complete, or current, or that the Interface will be free from errors, + defects, viruses, or other harmful elements. No advice, information, or + statement made in connection with the Interface should be treated as + creating any warranty concerning the Interface. There is no endorsement, + guarantee, or assumption of responsibility for any advertisements, + offers, or statements made by third parties concerning the Interface. +

+
+

+ Further, there is no representation or warranty, from anyone, as to the + quality, origin, or ownership of any content found on or available + through the Interface and there shall be no liability for any errors, + misrepresentations, or omissions in, of, and about, the content, nor for + the availability of the content attributable to any contributor to the + Interface, including maintainers, and they shall not be liable for any + losses, injuries, or damages from the use, inability to use, or the + display of the content of the Interface. +

+
+
+ 11.5 Token Lists and Token Identification +
+
+

+ In providing information about tokens, the Interface associates or + presumes the association of a token name, symbol, or logo with a + specific smart contract deployed to one or more blockchain systems. In + making such associations, the Interface relies upon third-party + resources which may not be accurate or may not conform to a given User’s + expectations. Multiple smart contracts can utilize the same token name + or token symbol as one another, meaning that the name or symbol of a + token does not guarantee that it is the token desired by the User or + generally associated with such name or symbol. Users must not rely on + the name, symbol, or branding of a token on the Interface, but instead + must examine the specific smart contract associated with the name, + symbol, or branding and confirm that the token accords with User’s + expectations. +

+
+
+ 11.6 User Responsibility for Accounts & Security +
+
+

+ Users are solely responsible for all matters relating to their accounts, + addresses, and tokens and for ensuring that all uses thereof comply + fully with these Terms. Users are solely responsible for protecting the + data integrity and confidentiality of their information, and data or + private keys for any wallet applications or devices used in connection + with the Interface. The compatibility of the Interface with wallet + applications and devices or other third-party applications or devices is + not intended as, and you hereby agree not to construe such compatibility + as, an endorsement or recommendation thereof or a warranty, guarantee, + promise, or assurance regarding the fitness or security thereof. +

+
+
+ 11.7 No Interface Fees; Third-Party Fees Irreversible +
+
+

+ There are no fees or charges for use of the Interface. Use of the + Protocol and relevant blockchain may be subject to third-party + transaction fees. The Interface maintainers do not receive such fees and + have no ability to reverse or refund any amounts paid in error. +

+
+

12. License to Use Interface

+
+

+ Each User, subject to their eligibility, acceptance, and adherence to + these Terms, is hereby granted a personal, revocable, non-exclusive, + non-transferable, non-sublicensable license to view, access and use the + Interface for the Permitted Uses in accordance with these Terms. +

+
+

13. Privacy Policy

+
+

+ The Interface may directly or indirectly collect and temporarily store + personally identifiable information for operational purposes, including + for the purpose of identifying blockchain addresses or IP addresses that + may indicate the use of the Interface from prohibited jurisdictions or + by sanctioned persons or other Prohibited Uses. Except as required by + applicable law, the Interface maintainers will have no obligation of + confidentiality with respect to any information collected by the + Interface. +

+
+

14. Non-Reliance

+
+

+ The Users declare that they are knowledgeable, experienced, and + sophisticated in using and evaluating blockchain and related + technologies and assets, including blockchains, tokens, and proof of + stake smart contract systems. The Users declare that they have conducted + their own thorough independent investigation and analysis of the + Protocol and the other matters contemplated by these Terms, and have not + relied upon any information, statement, omission, representation, or + warranty, express or implied, written or oral, made by or on behalf of + Interface maintainers in connection therewith, except as expressly set + forth in these Terms. +

+
+

+ 15. Risks, Disclaimers, and Limitations of Liability +

+
+

+ Each User hereby acknowledges and agrees, and consents to, and assumes + the risks of, the matters described in Section 15 of the Terms. +

+
+
15.1 Third-Party Offerings and Content
+
+

+ References, links, or referrals to or connections with or reliance on + third-party resources, products, services, or content, including smart + contracts developed or operated by third parties, may be provided to + Users in connection with the Interface. In addition, third parties may + offer promotions related to the Interface. Interface maintainers do not + endorse or assume any responsibility for any activities, resources, + products, services, content, or promotions owned, controlled, operated, + or sponsored by third parties. If Users access any such resources, + products, services, or content or participate in any such promotions, + Users do so solely at their own risk. Each User hereby expressly waives + and releases Interface maintainers from all liability arising from the + User’s use of any such resources, products, services, or content or + participation in any such promotions. +

+
+

+ The User further acknowledges and agrees that Interface maintainers + shall not be responsible or liable, directly or indirectly, for any + damage or loss caused or alleged to be caused by or in connection with + the use of or reliance on any such resources, products, services, + content, or promotions from third parties. +

+
+
15.2 Cryptography Risks
+
+

+ Cryptography is a progressing field. Advances in code cracking or + technical advances such as the development of quantum computers may + present risks to blockchain systems, the Protocol, or tokens, including + the theft, loss, or inaccessibility thereof. +

+
+
15.3 Fork Handling
+
+

+ The Protocol, and all tokens may be subject to{" "} + Forks. Forks occur when some or + all persons running the software clients for a particular blockchain + system adopt a new client or a new version of an existing client that: + (i) changes the protocol rules in backward-compatible or + backward-incompatible manner that affects which transactions can be + added into later blocks, how later blocks are added to the blockchain, + or other matters relating to the future operation of the protocol; or + (ii) reorganizes or changes past blocks to alter the history of the + blockchain. Some forks are{" "} + “contentious” and thus may result + in two or more persistent alternative versions of the protocol or + blockchain, either of which may be viewed as or claimed to be the + legitimate or genuine continuation of the original. +

+
+

+ Interface maintainers cannot anticipate, control or influence the + occurrence or outcome of forks, and do not assume any risk, liability or + obligation in connection therewith. Without limiting the generality of + the foregoing, Interface maintainers do not assume any responsibility to + notify a User of pending, threatened or completed forks. Interface + maintainers will respond (or refrain from responding) to any forks in + such manner as Interface maintainers determine in their sole and + absolute discretion. Interface maintainers shall not have any duty or + obligation, or liability to a User if such response (or lack of such + response) acts to a User’s detriment. Each User assumes full + responsibility to independently remain apprised of and informed about + possible forks, and to manage the User’s own interests and risks in + connection therewith. +

+
+
+ 15.4 Essential Third-Party Software Dependencies +
+
+

+ The Protocol and other relevant blockchain systems and smart contracts + are public software utilities that are accessible directly through any + compatible third-party node or indirectly through any compatible + third-party “wallet” application + that interacts with such a node. Interacting with the Protocol does not + require the use of the Interface, but the Interface is only a convenient + and user-friendly option of reading and displaying data from the + Protocol and generating standard draft transaction messages compatible + with the Protocol. The User may choose to interact with the Protocol + using softwares other than the Interface. As the Interface does not + provide wallet software applications or nodes for blockchain systems, + such software constitutes an essential third-party software and user + dependency without which the Protocol cannot be used and tokens cannot + be traded or used. Furthermore, the Interface may use APIs and servers + of Interface maintainers or third parties and there are no guarantees as + to the continued operation, maintenance, availability, or security of + any of the foregoing dependencies. +

+
+
15.5 Tax Issues
+
+

+ The tax consequences of purchasing, selling, holding, transferring, or + locking tokens or otherwise utilizing the Protocol are uncertain and may + vary by jurisdiction. Interface Maintainers have undertaken no due + diligence or investigation into such tax consequences, and assume no + obligation or liability to optimize, facilitate or bear the tax + consequences to any person. +

+
+
15.6 Legal Limitations on Disclaimers
+
+

+ Some jurisdictions do not allow the exclusion of certain warranties, or + the limitation or exclusion of certain liabilities, and damages. + Accordingly, some of the disclaimers and limitations set forth in these + Terms may not apply in full to specific Users. The disclaimers and + limitations of liability provided in these terms shall apply to the + fullest extent as permitted by applicable law. +

+
+
15.7 Indemnification
+
+

+ Each User shall defend, indemnify, compensate, reimburse and hold + harmless the Interface maintainers from any claim, demand, action, + damage, loss, cost or expense, including without limitation reasonable + attorneys’ fees, arising out or relating to (a) User's use of, or + conduct in connection with, the Interface; (b) User's violation of + these Terms or any other applicable policy or contract of Interface + maintainers; or (c) User's violation of any rights of any other + person or entity. +

+
+

+ 16. Entire Representation, Consent and Agreement +

+
+

+ These Terms, including the Privacy Policy, constitute your entire + representation, consent, and agreement with respect to the subject + matter, including the Interface. These Terms, including the Privacy + Policy, and any disclosure and disclaimers incorporated by reference + supersede all prior Terms, written or oral understandings, + communications, and other agreements relating to the subject matter of + the Terms. +

+
+ ); +}; diff --git a/src/app/context/Terms/TermsContext.tsx b/src/app/context/Terms/TermsContext.tsx new file mode 100644 index 0000000..ded0986 --- /dev/null +++ b/src/app/context/Terms/TermsContext.tsx @@ -0,0 +1,32 @@ +import React, { ReactNode, createContext, useContext, useState } from "react"; + +interface TermsContextProps { + isTermsOpen: boolean; + openTerms: () => void; + closeTerms: () => void; +} + +const TermsContext = createContext(undefined); + +export const TermsProvider: React.FC<{ children: ReactNode }> = ({ + children, +}) => { + const [isTermsOpen, setIsTermsOpen] = useState(false); + + const openTerms = () => setIsTermsOpen(true); + const closeTerms = () => setIsTermsOpen(false); + + return ( + + {children} + + ); +}; + +export const useTerms = (): TermsContextProps => { + const context = useContext(TermsContext); + if (!context) { + throw new Error("useTerms must be used within a TermsModal"); + } + return context; +}; diff --git a/src/app/globals.css b/src/app/globals.css index 2428544..8e50c77 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,6 +2,8 @@ @tailwind components; @tailwind utilities; +@import url("styles/terms.css"); + :root { --primary: #ff7c2a; --secondary: "#0DB7BF"; diff --git a/src/app/page.tsx b/src/app/page.tsx index 4d2d7d7..02392d0 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -31,11 +31,13 @@ import { Footer } from "./components/Footer/Footer"; import { Header } from "./components/Header/Header"; import { ConnectModal } from "./components/Modals/ConnectModal"; import { ErrorModal } from "./components/Modals/ErrorModal"; +import { TermsModal } from "./components/Modals/Terms/TermsModal"; import { NetworkBadge } from "./components/NetworkBadge/NetworkBadge"; import { Staking } from "./components/Staking/Staking"; import { Stats } from "./components/Stats/Stats"; import { Summary } from "./components/Summary/Summary"; import { useError } from "./context/Error/ErrorContext"; +import { useTerms } from "./context/Terms/TermsContext"; import { Delegation, DelegationState } from "./types/delegations"; import { ErrorHandlerParam, ErrorState } from "./types/errors"; @@ -50,6 +52,7 @@ const Home: React.FC = () => { const [address, setAddress] = useState(""); const { error, isErrorOpen, showError, hideError, retryErrorAction } = useError(); + const { isTermsOpen, closeTerms } = useTerms(); const { data: paramWithContext, @@ -422,6 +425,7 @@ const Home: React.FC = () => { onClose={hideError} onRetry={retryErrorAction} /> + ); }; diff --git a/src/app/providers.tsx b/src/app/providers.tsx index bbfde96..a9bae2e 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -7,6 +7,7 @@ import { ThemeProvider } from "next-themes"; import React from "react"; import { ErrorProvider } from "./context/Error/ErrorContext"; +import { TermsProvider } from "./context/Terms/TermsContext"; import { GlobalParamsProvider } from "./context/api/GlobalParamsProvider"; import { StakingStatsProvider } from "./context/api/StakingStatsProvider"; import { BtcHeightProvider } from "./context/mempool/BtcHeightProvider"; @@ -17,17 +18,19 @@ function Providers({ children }: React.PropsWithChildren) { return ( - - - - - - {children} - - - - - + + + + + + + {children} + + + + + + Date: Wed, 19 Jun 2024 14:16:46 +1000 Subject: [PATCH 27/56] chore: fix missing space in unbonding text, as well as remove the ok to proceed text --- src/app/components/Modals/UnbondWithdrawModal.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/components/Modals/UnbondWithdrawModal.tsx b/src/app/components/Modals/UnbondWithdrawModal.tsx index a7967a6..bf4953f 100644 --- a/src/app/components/Modals/UnbondWithdrawModal.tsx +++ b/src/app/components/Modals/UnbondWithdrawModal.tsx @@ -27,19 +27,18 @@ export const UnbondWithdrawModal: React.FC = ({ const unbondContent = ( <> You are about to unbond your stake before its expiration. The expected - unbonding time will be about + unbonding time will be about{" "} {unbondingTimeBlocks ? blocksToDisplayTime(unbondingTimeBlocks) : "-"} .
After unbonded, you will need to use this dashboard to withdraw your stake - for it to appear in your wallet. OK to proceed? + for it to appear in your wallet. ); const withdrawTitle = "Withdraw"; - const withdrawContent = - "You are about to withdraw your stake. OK to proceed?"; + const withdrawContent = "You are about to withdraw your stake."; const title = mode === MODE_UNBOND ? unbondTitle : withdrawTitle; const content = mode === MODE_UNBOND ? unbondContent : withdrawContent; From b68b4ab9c3534b00e8c2418d2271a16681728100 Mon Sep 17 00:00:00 2001 From: Vitalis Salis Date: Wed, 19 Jun 2024 12:07:43 +0300 Subject: [PATCH 28/56] hotfix: Use the correct output when constructing withdrawal tx --- package.json | 2 +- src/utils/delegations/signWithdrawalTx.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 8bd60dd..c63fb11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-staking", - "version": "0.1.3", + "version": "0.2.1", "private": true, "scripts": { "dev": "next dev", diff --git a/src/utils/delegations/signWithdrawalTx.ts b/src/utils/delegations/signWithdrawalTx.ts index 923fb32..20852a6 100644 --- a/src/utils/delegations/signWithdrawalTx.ts +++ b/src/utils/delegations/signWithdrawalTx.ts @@ -86,7 +86,7 @@ export const signWithdrawalTx = async ( address, btcWalletNetwork, fees.fastestFee, - delegation.stakingTx.outputIndex, + 0, ); } else { // Withdraw funds from a staking transaction in which the timelock naturally expired @@ -100,7 +100,7 @@ export const signWithdrawalTx = async ( address, btcWalletNetwork, fees.fastestFee, - 0, // unbonding always has a single output + delegation.stakingTx.outputIndex, ); } From 58d779e9d2fc2d93a4fef0707c798e277fb65157 Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Wed, 19 Jun 2024 18:22:08 +1000 Subject: [PATCH 29/56] chore: add more unit tests for simple-staking --- package-lock.json | 4 +- src/app/components/Staking/Staking.tsx | 4 +- src/utils/delegations/signStakingTx.ts | 12 +- tests/helper/index.ts | 120 ++++++++-- tests/helper/internalPubkey.ts | 4 + tests/utils/delegations/signStakingTx.test.ts | 211 ++++++++++++++++++ tests/utils/isStakingSignReady.test.ts | 101 +++++++++ 7 files changed, 426 insertions(+), 30 deletions(-) create mode 100644 tests/helper/internalPubkey.ts create mode 100644 tests/utils/delegations/signStakingTx.test.ts create mode 100644 tests/utils/isStakingSignReady.test.ts diff --git a/package-lock.json b/package-lock.json index 100c8d7..6f63ff9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "simple-staking", - "version": "0.1.3", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "simple-staking", - "version": "0.1.3", + "version": "0.2.1", "dependencies": { "@bitcoinerlab/secp256k1": "^1.1.1", "@keystonehq/animated-qr": "^0.8.6", diff --git a/src/app/components/Staking/Staking.tsx b/src/app/components/Staking/Staking.tsx index 1061d1d..e4dbb6f 100644 --- a/src/app/components/Staking/Staking.tsx +++ b/src/app/components/Staking/Staking.tsx @@ -283,7 +283,7 @@ export const Staking: React.FC = ({ globalParamsVersion, stakingAmountSat, stakingTimeBlocks, - finalityProvider, + finalityProvider.btcPk, btcWalletNetwork, address, publicKeyNoCoord, @@ -347,7 +347,7 @@ export const Staking: React.FC = ({ paramWithCtx.currentVersion, stakingAmountSat, stakingTimeBlocks, - finalityProvider, + finalityProvider.btcPk, btcWalletNetwork, address, publicKeyNoCoord, diff --git a/src/utils/delegations/signStakingTx.ts b/src/utils/delegations/signStakingTx.ts index 72d874b..43e7b00 100644 --- a/src/utils/delegations/signStakingTx.ts +++ b/src/utils/delegations/signStakingTx.ts @@ -2,7 +2,6 @@ import { Transaction, networks } from "bitcoinjs-lib"; import { stakingTransaction } from "btc-staking-ts"; import { signPsbtTransaction } from "@/app/common/utils/psbt"; -import { FinalityProvider } from "@/app/types/finalityProviders"; import { GlobalParamsVersion } from "@/app/types/globalParams"; import { apiDataToStakingScripts } from "@/utils/apiDataToStakingScripts"; import { isTaproot } from "@/utils/wallet"; @@ -18,14 +17,15 @@ export const createStakingTx = ( globalParamsVersion: GlobalParamsVersion, stakingAmountSat: number, stakingTimeBlocks: number, - finalityProvider: FinalityProvider, + finalityProviderPublicKey: string, btcWalletNetwork: networks.Network, address: string, publicKeyNoCoord: string, feeRate: number, inputUTXOs: UTXO[], ) => { - // Data extraction + // Get the staking term, it will ignore the `stakingTimeBlocks` and use the value from params + // if the min and max staking time blocks are the same const stakingTerm = getStakingTerm(globalParamsVersion, stakingTimeBlocks); // Check the staking data @@ -50,7 +50,7 @@ export const createStakingTx = ( let scripts; try { scripts = apiDataToStakingScripts( - finalityProvider.btcPk, + finalityProviderPublicKey, stakingTerm, globalParamsVersion, publicKeyNoCoord, @@ -97,7 +97,7 @@ export const signStakingTx = async ( globalParamsVersion: GlobalParamsVersion, stakingAmountSat: number, stakingTimeBlocks: number, - finalityProvider: FinalityProvider, + finalityProviderPublicKey: string, btcWalletNetwork: networks.Network, address: string, publicKeyNoCoord: string, @@ -109,7 +109,7 @@ export const signStakingTx = async ( globalParamsVersion, stakingAmountSat, stakingTimeBlocks, - finalityProvider, + finalityProviderPublicKey, btcWalletNetwork, address, publicKeyNoCoord, diff --git a/tests/helper/index.ts b/tests/helper/index.ts index 12be2df..cf14d45 100644 --- a/tests/helper/index.ts +++ b/tests/helper/index.ts @@ -1,8 +1,13 @@ import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs"; import * as bitcoin from "bitcoinjs-lib"; +import { StakingScriptData, StakingScripts } from "btc-staking-ts"; import ECPairFactory from "ecpair"; +import { GlobalParamsVersion } from "@/app/types/globalParams"; import { UTXO } from "@/utils/wallet/wallet_provider"; + +// Initialize the ECC library +bitcoin.initEccLib(ecc); const ECPair = ECPairFactory(ecc); export class DataGenerator { @@ -20,7 +25,7 @@ export class DataGenerator { return randomBuffer.toString("hex"); }; - generateRandomKeyPairs = (isNoCoordPk = false) => { + generateRandomKeyPair = (isNoCoordPk = true) => { const keyPair = ECPair.makeRandom({ network: this.network }); const { privateKey, publicKey } = keyPair; if (!privateKey || !publicKey) { @@ -42,18 +47,23 @@ export class DataGenerator { return Math.floor(Math.random() * 65535) + 1; }; + generateRandomUnbondingTime = (stakingTerm: number) => { + return Math.floor(Math.random() * stakingTerm) + 1; + }; + generateRandomFeeRates = () => { return Math.floor(Math.random() * 1000) + 1; }; - // Convenant quorums are a list of public keys that are used to sign a covenant + // Convenant committee are a list of public keys that are used to sign a covenant generateRandomCovenantCommittee = (size: number): Buffer[] => { - const quorum: Buffer[] = []; + const committe: Buffer[] = []; for (let i = 0; i < size; i++) { - const keyPair = this.generateRandomKeyPairs(true); - quorum.push(Buffer.from(keyPair.publicKey, "hex")); + // covenant committee PKs are with coordiantes + const keyPair = this.generateRandomKeyPair(false); + committe.push(Buffer.from(keyPair.publicKey, "hex")); } - return quorum; + return committe; }; generateRandomTag = () => { @@ -64,16 +74,20 @@ export class DataGenerator { return buffer; }; + // Generate a single global param generateRandomGlobalParams = (isFixedTimelock = false) => { - // the commitee size is assunmed to be between 5 and 20 + const stakingTerm = this.generateRandomStakingTerm(); const committeeSize = Math.floor(Math.random() * 20) + 5; const covenantPks = this.generateRandomCovenantCommittee(committeeSize).map( (buffer) => buffer.toString("hex"), ); - // the covenantQuorum should always be around 2/3 of the committee size - const covenantQuorum = Math.floor((committeeSize * 2) / 3); - let maxStakingTimeBlocks = Math.floor(Math.random() * 100); + const covenantQuorum = Math.floor(Math.random() * (committeeSize - 1)) + 1; + const unbondingTime = this.generateRandomUnbondingTime(stakingTerm); + const tag = this.generateRandomTag().toString("hex"); + let minStakingTimeBlocks = Math.floor(Math.random() * 100); + let maxStakingTimeBlocks = + minStakingTimeBlocks + Math.floor(Math.random() * 1000); if (isFixedTimelock) { maxStakingTimeBlocks = minStakingTimeBlocks; } @@ -87,20 +101,24 @@ export class DataGenerator { activationHeight: Math.floor(Math.random() * 100), stakingCapSat: Math.floor(Math.random() * 100), stakingCapHeight: Math.floor(Math.random() * 100), - tag: this.generateRandomTag().toString("hex"), covenantPks, covenantQuorum, - unbondingTime: this.generateRandomStakingTerm(), unbondingFeeSat: Math.floor(Math.random() * 1000), maxStakingAmountSat, minStakingAmountSat, maxStakingTimeBlocks, minStakingTimeBlocks, confirmationDepth: Math.floor(Math.random() * 20), + unbondingTime, + tag, }; }; getTaprootAddress = (publicKey: string) => { + // Remove the prefix if it exists + if (publicKey.length == 66) { + publicKey = publicKey.slice(2); + } const internalPubkey = Buffer.from(publicKey, "hex"); const { address } = bitcoin.payments.p2tr({ internalPubkey, @@ -130,15 +148,77 @@ export class DataGenerator { return this.network; }; + generateMockStakingScripts = ( + globalParams: GlobalParamsVersion, + publicKeyNoCoord: string, + finalityProviderPk: string, + stakingTxTimelock: number, + ): StakingScripts => { + // Convert covenant PKs to buffers + const covenantPKsBuffer = globalParams.covenantPks.map((pk: string) => + Buffer.from(pk, "hex"), + ); + + // Create staking script data + let stakingScriptData; + try { + stakingScriptData = new StakingScriptData( + Buffer.from(publicKeyNoCoord, "hex"), + [Buffer.from(finalityProviderPk, "hex")], + covenantPKsBuffer, + globalParams.covenantQuorum, + stakingTxTimelock, + globalParams.unbondingTime, + Buffer.from(globalParams.tag, "hex"), + ); + } catch (error: Error | any) { + throw new Error(error?.message || "Cannot build staking script data"); + } + + // Build scripts + let scripts; + try { + scripts = stakingScriptData.buildScripts(); + } catch (error: Error | any) { + throw new Error(error?.message || "Error while recreating scripts"); + } + + return scripts; + }; + generateRandomUTXOs = ( - dataGenerator: DataGenerator, - numUTXOs: number, + minAvailableBalance: number, + numberOfUTXOs: number, ): UTXO[] => { - return Array.from({ length: numUTXOs }, () => ({ - txid: dataGenerator.generateRandomTxId(), - vout: Math.floor(Math.random() * 10), - scriptPubKey: this.generateRandomKeyPairs().publicKey, - value: Math.floor(Math.random() * 9000) + 1000, - })); + const utxos = []; + let sum = 0; + for (let i = 0; i < numberOfUTXOs; i++) { + utxos.push({ + txid: this.generateRandomTxId(), + vout: Math.floor(Math.random() * 10), + scriptPubKey: this.generateRandomKeyPair().publicKey, + value: Math.floor(Math.random() * 9000) + minAvailableBalance, + }); + sum += utxos[i].value; + if (sum >= minAvailableBalance) { + break; + } + } + return utxos; + }; + /** + * Generates a random integer between min and max. + * + * @param {number} min - The minimum number. + * @param {number} max - The maximum number. + * @returns {number} - A random integer between min and max. + */ + getRandomIntegerBetween = (min: number, max: number): number => { + if (min > max) { + throw new Error( + "The minimum number should be less than or equal to the maximum number.", + ); + } + return Math.floor(Math.random() * (max - min + 1)) + min; }; } diff --git a/tests/helper/internalPubkey.ts b/tests/helper/internalPubkey.ts new file mode 100644 index 0000000..2fb0ace --- /dev/null +++ b/tests/helper/internalPubkey.ts @@ -0,0 +1,4 @@ +// internalPubkey denotes an unspendable internal public key to be used for the taproot output +const key = + "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"; +export const internalPubkey = Buffer.from(key, "hex").subarray(1, 33); // Do a subarray(1, 33) to get the public coordinate diff --git a/tests/utils/delegations/signStakingTx.test.ts b/tests/utils/delegations/signStakingTx.test.ts new file mode 100644 index 0000000..9b1f024 --- /dev/null +++ b/tests/utils/delegations/signStakingTx.test.ts @@ -0,0 +1,211 @@ +import { createStakingTx } from "@/utils/delegations/signStakingTx"; +import { toNetwork } from "@/utils/wallet"; +import { Network } from "@/utils/wallet/wallet_provider"; + +import { DataGenerator } from "../../helper"; + +describe("utils/isStakingSignReady", () => { + const networks = [Network.MAINNET, Network.SIGNET]; + networks.forEach((n) => { + const network = toNetwork(n); + const dataGen = new DataGenerator(network); + const randomFpKeys = dataGen.generateRandomKeyPair(true); + const randomUserKeys = dataGen.generateRandomKeyPair(true); + const randomFeeRate = 1; // keep test simple by setting this to unit 1 + + [true, false].forEach((isFixed) => { + const randomParam = dataGen.generateRandomGlobalParams(isFixed); + const randomStakingAmount = dataGen.getRandomIntegerBetween( + randomParam.minStakingAmountSat, + randomParam.maxStakingAmountSat, + ); + const randomStakingTimeBlocks = dataGen.getRandomIntegerBetween( + randomParam.minStakingTimeBlocks, + randomParam.maxStakingTimeBlocks, + ); + const randomInputUTXOs = dataGen.generateRandomUTXOs( + randomStakingAmount + Math.floor(Math.random() * 1000000), + Math.floor(Math.random() * 10) + 1, + ); + const testTermDescription = isFixed ? "fixed term" : "variable term"; + describe(`${testTermDescription} - createStakingTx`, () => { + it("should successfully create a staking transaction", () => { + const { stakingFeeSat, stakingTerm, unsignedStakingPsbt } = + createStakingTx( + randomParam, + randomStakingAmount, + randomStakingTimeBlocks, + randomFpKeys.publicKey, + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + randomFeeRate, + randomInputUTXOs, + ); + + expect(stakingTerm).toBe(randomStakingTimeBlocks); + const matchedStakingOutput = unsignedStakingPsbt.txOutputs.find( + (output) => { + return output.value === randomStakingAmount; + }, + ); + expect(matchedStakingOutput).toBeDefined(); + expect(stakingFeeSat).toBeGreaterThan(0); + }); + + it(`${testTermDescription} - should successfully create a staking transaction with change amount in output`, () => { + const utxo = dataGen.generateRandomUTXOs( + randomStakingAmount + Math.floor(Math.random() * 1000000), + 1, // make it a single utxo so we always have change + ); + const { stakingFeeSat, stakingTerm, unsignedStakingPsbt } = + createStakingTx( + randomParam, + randomStakingAmount, + randomStakingTimeBlocks, + randomFpKeys.publicKey, + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + randomFeeRate, + utxo, + ); + + expect(stakingTerm).toBe(randomStakingTimeBlocks); + const matchedStakingOutput = unsignedStakingPsbt.txOutputs.find( + (output) => { + return output.value === randomStakingAmount; + }, + ); + expect(matchedStakingOutput).toBeDefined(); + expect(stakingFeeSat).toBeGreaterThan(0); + + const changeOutput = unsignedStakingPsbt.txOutputs.find((output) => { + return ( + output.address == + dataGen.getTaprootAddress(randomUserKeys.publicKey) + ); + }); + expect(changeOutput).toBeDefined(); + }); + }); + + it(`${testTermDescription} - should throw an error if the staking amount is less than the minimum staking amount`, () => { + expect(() => + createStakingTx( + randomParam, + randomParam.minStakingAmountSat - 1, + randomStakingTimeBlocks, + randomFpKeys.publicKey, + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + randomFeeRate, + randomInputUTXOs, + ), + ).toThrow("Invalid staking data"); + }); + + it(`${testTermDescription} - should throw an error if the staking amount is greater than the maximum staking amount`, () => { + expect(() => + createStakingTx( + randomParam, + randomParam.maxStakingAmountSat + 1, + randomStakingTimeBlocks, + randomFpKeys.publicKey, + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + randomFeeRate, + randomInputUTXOs, + ), + ).toThrow("Invalid staking data"); + }); + + it(`${testTermDescription} - should throw an error if the fee rate is less than or equal to 0`, () => { + expect(() => + createStakingTx( + randomParam, + randomStakingAmount, + randomStakingTimeBlocks, + randomFpKeys.publicKey, + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + 0, + randomInputUTXOs, + ), + ).toThrow("Invalid fee rate"); + }); + + it(`${testTermDescription} - should throw an error if there are no usable balance`, () => { + expect(() => + createStakingTx( + randomParam, + randomStakingAmount, + randomStakingTimeBlocks, + randomFpKeys.publicKey, + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + randomFeeRate, + [], + ), + ).toThrow("Not enough usable balance"); + }); + + // This test only test createStakingTx when apiDataToStakingScripts throw error. + // The different error cases are tested in the apiDataToStakingScripts.test.ts + it(`${testTermDescription} - should throw an error if the staking scripts cannot be built`, () => { + expect(() => + createStakingTx( + randomParam, + randomStakingAmount, + randomStakingTimeBlocks, + dataGen.generateRandomKeyPair(false).publicKey, // invalid finality provider key + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + randomFeeRate, + randomInputUTXOs, + ), + ).toThrow("Invalid script data provided"); + }); + + // More test cases when we have variable staking terms + if (!isFixed) { + it(`${testTermDescription} - should throw an error if the staking term is less than the minimum staking time blocks`, () => { + expect(() => + createStakingTx( + randomParam, + randomStakingAmount, + randomParam.minStakingTimeBlocks - 1, + randomFpKeys.publicKey, + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + randomFeeRate, + randomInputUTXOs, + ), + ).toThrow("Invalid staking data"); + }); + + it(`${testTermDescription} - should throw an error if the staking term is greater than the maximum staking time blocks`, () => { + expect(() => + createStakingTx( + randomParam, + randomStakingAmount, + randomParam.maxStakingTimeBlocks + 1, + randomFpKeys.publicKey, + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + randomFeeRate, + randomInputUTXOs, + ), + ).toThrow("Invalid staking data"); + }); + } + }); + }); +}); diff --git a/tests/utils/isStakingSignReady.test.ts b/tests/utils/isStakingSignReady.test.ts new file mode 100644 index 0000000..1dc9cc8 --- /dev/null +++ b/tests/utils/isStakingSignReady.test.ts @@ -0,0 +1,101 @@ +import { isStakingSignReady } from "@/utils/isStakingSignReady"; + +describe("utils/isStakingSignReady", () => { + it("should return false with reason if fpSelected is false", () => { + const result = isStakingSignReady(1, 2, 3, 4, 5, 6, false); + expect(result.isReady).toBe(false); + expect(result.reason).toBe("Please select a finality provider"); + }); + + it("should return false with reason if amount is not ready", () => { + const notReadyAmountInputs = [ + { + minAmount: 0, + maxAmount: 0, + amount: 0, + }, + { + minAmount: 0, + maxAmount: 0, + amount: 1, + }, + { + minAmount: 1, + maxAmount: 0, + amount: 0, + }, + { + minAmount: 1, + maxAmount: 9, + amount: 10, + }, + { + minAmount: 4, + maxAmount: 10, + amount: 3, + }, + ]; + notReadyAmountInputs.forEach((input) => { + const result = isStakingSignReady( + input.minAmount, + input.maxAmount, + 3, + 4, + input.amount, + 6, + true, + ); + expect(result.isReady).toBe(false); + expect(result.reason).toBe("Please enter a valid stake amount"); + }); + }); + + it("should return false with reason if time is not ready", () => { + const notReadyTimeInputs = [ + { + minTime: 0, + maxTime: 0, + time: 0, + }, + { + minTime: 0, + maxTime: 0, + time: 1, + }, + { + minTime: 1, + maxTime: 0, + time: 0, + }, + { + minTime: 1, + maxTime: 9, + time: 10, + }, + { + minTime: 4, + maxTime: 10, + time: 3, + }, + ]; + notReadyTimeInputs.forEach((input) => { + const result = isStakingSignReady( + 1, + 10, + input.minTime, + input.maxTime, + 5, + input.time, + true, + ); + expect(result.isReady).toBe(false); + expect(result.reason).toBe("Please enter a valid staking period"); + }); + }); + + it("should return true with empty reason if amount and time are ready", () => { + const result = isStakingSignReady(1, 10, 20, 30, 5, 25, true); + expect(result.isReady).toBe(true); + expect(result.reason).toBe(""); + }); +}); From 94a422c735eb011f91e4f220709dbbf47cf306fe Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Thu, 20 Jun 2024 19:27:48 +1000 Subject: [PATCH 30/56] chore: add test for signStakingTx --- tests/helper/internalPubkey.ts | 4 - tests/utils/apiDataToStakingScripts.test.ts | 2 +- .../utils/delegations/createStakingTx.test.ts | 210 ++++++++++++++ tests/utils/delegations/signStakingTx.test.ts | 267 ++++++------------ 4 files changed, 290 insertions(+), 193 deletions(-) delete mode 100644 tests/helper/internalPubkey.ts create mode 100644 tests/utils/delegations/createStakingTx.test.ts diff --git a/tests/helper/internalPubkey.ts b/tests/helper/internalPubkey.ts deleted file mode 100644 index 2fb0ace..0000000 --- a/tests/helper/internalPubkey.ts +++ /dev/null @@ -1,4 +0,0 @@ -// internalPubkey denotes an unspendable internal public key to be used for the taproot output -const key = - "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"; -export const internalPubkey = Buffer.from(key, "hex").subarray(1, 33); // Do a subarray(1, 33) to get the public coordinate diff --git a/tests/utils/apiDataToStakingScripts.test.ts b/tests/utils/apiDataToStakingScripts.test.ts index 6edc35f..725871d 100644 --- a/tests/utils/apiDataToStakingScripts.test.ts +++ b/tests/utils/apiDataToStakingScripts.test.ts @@ -7,7 +7,7 @@ import { DataGenerator } from "../helper"; describe("apiDataToStakingScripts", () => { const dataGen = new DataGenerator(bitcoin.networks.testnet); it("should throw an error if the publicKeyNoCoord is not set", () => { - const { publicKey: finalityProviderPk } = dataGen.generateRandomKeyPairs(); + const { publicKey: finalityProviderPk } = dataGen.generateRandomKeyPair(); const stakingTxTimelock = dataGen.generateRandomStakingTerm(); const globalParams = dataGen.generateRandomGlobalParams(); expect(() => { diff --git a/tests/utils/delegations/createStakingTx.test.ts b/tests/utils/delegations/createStakingTx.test.ts new file mode 100644 index 0000000..405f511 --- /dev/null +++ b/tests/utils/delegations/createStakingTx.test.ts @@ -0,0 +1,210 @@ +import { createStakingTx } from "@/utils/delegations/signStakingTx"; +import { toNetwork } from "@/utils/wallet"; +import { Network } from "@/utils/wallet/wallet_provider"; + +import { DataGenerator } from "../../helper"; + +describe("utils/delegations/createStakingTx", () => { + const networks = [Network.MAINNET, Network.SIGNET]; + networks.forEach((n) => { + const network = toNetwork(n); + const dataGen = new DataGenerator(network); + const randomFpKeys = dataGen.generateRandomKeyPair(true); + const randomUserKeys = dataGen.generateRandomKeyPair(true); + const feeRate = 1; // keep test simple by setting this to unit 1 + + [true, false].forEach((isFixed) => { + const randomParam = dataGen.generateRandomGlobalParams(isFixed); + const randomStakingAmount = dataGen.getRandomIntegerBetween( + randomParam.minStakingAmountSat, + randomParam.maxStakingAmountSat, + ); + const randomStakingTimeBlocks = dataGen.getRandomIntegerBetween( + randomParam.minStakingTimeBlocks, + randomParam.maxStakingTimeBlocks, + ); + const randomInputUTXOs = dataGen.generateRandomUTXOs( + randomStakingAmount + Math.floor(Math.random() * 1000000), + Math.floor(Math.random() * 10) + 1, + ); + const testTermDescription = isFixed ? "fixed term" : "variable term"; + + it("should successfully create a staking transaction", () => { + const { stakingFeeSat, stakingTerm, unsignedStakingPsbt } = + createStakingTx( + randomParam, + randomStakingAmount, + randomStakingTimeBlocks, + randomFpKeys.publicKey, + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + feeRate, + randomInputUTXOs, + ); + + expect(stakingTerm).toBe(randomStakingTimeBlocks); + const matchedStakingOutput = unsignedStakingPsbt.txOutputs.find( + (output) => { + return output.value === randomStakingAmount; + }, + ); + expect(matchedStakingOutput).toBeDefined(); + expect(stakingFeeSat).toBeGreaterThan(0); + }); + + it(`${testTermDescription} - should successfully create a staking transaction with change amount in output`, () => { + const utxo = dataGen.generateRandomUTXOs( + randomStakingAmount + Math.floor(Math.random() * 1000000), + 1, // make it a single utxo so we always have change + ); + const { stakingFeeSat, stakingTerm, unsignedStakingPsbt } = + createStakingTx( + randomParam, + randomStakingAmount, + randomStakingTimeBlocks, + randomFpKeys.publicKey, + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + feeRate, + utxo, + ); + + expect(stakingTerm).toBe(randomStakingTimeBlocks); + const matchedStakingOutput = unsignedStakingPsbt.txOutputs.find( + (output) => { + return output.value === randomStakingAmount; + }, + ); + expect(matchedStakingOutput).toBeDefined(); + expect(stakingFeeSat).toBeGreaterThan(0); + + const changeOutput = unsignedStakingPsbt.txOutputs.find((output) => { + return ( + output.address == + dataGen.getTaprootAddress(randomUserKeys.publicKey) + ); + }); + expect(changeOutput).toBeDefined(); + }); + + it(`${testTermDescription} - should throw an error if the staking amount is less than the minimum staking amount`, () => { + expect(() => + createStakingTx( + randomParam, + randomParam.minStakingAmountSat - 1, + randomStakingTimeBlocks, + randomFpKeys.publicKey, + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + feeRate, + randomInputUTXOs, + ), + ).toThrow("Invalid staking data"); + }); + + it(`${testTermDescription} - should throw an error if the staking amount is greater than the maximum staking amount`, () => { + expect(() => + createStakingTx( + randomParam, + randomParam.maxStakingAmountSat + 1, + randomStakingTimeBlocks, + randomFpKeys.publicKey, + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + feeRate, + randomInputUTXOs, + ), + ).toThrow("Invalid staking data"); + }); + + it(`${testTermDescription} - should throw an error if the fee rate is less than or equal to 0`, () => { + expect(() => + createStakingTx( + randomParam, + randomStakingAmount, + randomStakingTimeBlocks, + randomFpKeys.publicKey, + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + 0, + randomInputUTXOs, + ), + ).toThrow("Invalid fee rate"); + }); + + it(`${testTermDescription} - should throw an error if there are no usable balance`, () => { + expect(() => + createStakingTx( + randomParam, + randomStakingAmount, + randomStakingTimeBlocks, + randomFpKeys.publicKey, + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + feeRate, + [], + ), + ).toThrow("Not enough usable balance"); + }); + + // This test only test createStakingTx when apiDataToStakingScripts throw error. + // The different error cases are tested in the apiDataToStakingScripts.test.ts + it(`${testTermDescription} - should throw an error if the staking scripts cannot be built`, () => { + expect(() => + createStakingTx( + randomParam, + randomStakingAmount, + randomStakingTimeBlocks, + dataGen.generateRandomKeyPair(false).publicKey, // invalid finality provider key + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + feeRate, + randomInputUTXOs, + ), + ).toThrow("Invalid script data provided"); + }); + + // More test cases when we have variable staking terms + if (!isFixed) { + it(`${testTermDescription} - should throw an error if the staking term is less than the minimum staking time blocks`, () => { + expect(() => + createStakingTx( + randomParam, + randomStakingAmount, + randomParam.minStakingTimeBlocks - 1, + randomFpKeys.publicKey, + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + feeRate, + randomInputUTXOs, + ), + ).toThrow("Invalid staking data"); + }); + + it(`${testTermDescription} - should throw an error if the staking term is greater than the maximum staking time blocks`, () => { + expect(() => + createStakingTx( + randomParam, + randomStakingAmount, + randomParam.maxStakingTimeBlocks + 1, + randomFpKeys.publicKey, + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + feeRate, + randomInputUTXOs, + ), + ).toThrow("Invalid staking data"); + }); + } + }); + }); +}); diff --git a/tests/utils/delegations/signStakingTx.test.ts b/tests/utils/delegations/signStakingTx.test.ts index 9b1f024..a913ea4 100644 --- a/tests/utils/delegations/signStakingTx.test.ts +++ b/tests/utils/delegations/signStakingTx.test.ts @@ -1,210 +1,101 @@ -import { createStakingTx } from "@/utils/delegations/signStakingTx"; +import { signStakingTx } from "@/utils/delegations/signStakingTx"; import { toNetwork } from "@/utils/wallet"; import { Network } from "@/utils/wallet/wallet_provider"; import { DataGenerator } from "../../helper"; -describe("utils/isStakingSignReady", () => { +// Mock the signPsbtTransaction function +jest.mock("@/app/common/utils/psbt", () => ({ + signPsbtTransaction: jest.fn(), +})); + +describe("utils/delegations/signStakingTx", () => { const networks = [Network.MAINNET, Network.SIGNET]; networks.forEach((n) => { const network = toNetwork(n); const dataGen = new DataGenerator(network); const randomFpKeys = dataGen.generateRandomKeyPair(true); const randomUserKeys = dataGen.generateRandomKeyPair(true); - const randomFeeRate = 1; // keep test simple by setting this to unit 1 - - [true, false].forEach((isFixed) => { - const randomParam = dataGen.generateRandomGlobalParams(isFixed); - const randomStakingAmount = dataGen.getRandomIntegerBetween( - randomParam.minStakingAmountSat, - randomParam.maxStakingAmountSat, - ); - const randomStakingTimeBlocks = dataGen.getRandomIntegerBetween( - randomParam.minStakingTimeBlocks, - randomParam.maxStakingTimeBlocks, - ); - const randomInputUTXOs = dataGen.generateRandomUTXOs( - randomStakingAmount + Math.floor(Math.random() * 1000000), - Math.floor(Math.random() * 10) + 1, - ); - const testTermDescription = isFixed ? "fixed term" : "variable term"; - describe(`${testTermDescription} - createStakingTx`, () => { - it("should successfully create a staking transaction", () => { - const { stakingFeeSat, stakingTerm, unsignedStakingPsbt } = - createStakingTx( - randomParam, - randomStakingAmount, - randomStakingTimeBlocks, - randomFpKeys.publicKey, - network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, - randomFeeRate, - randomInputUTXOs, - ); - - expect(stakingTerm).toBe(randomStakingTimeBlocks); - const matchedStakingOutput = unsignedStakingPsbt.txOutputs.find( - (output) => { - return output.value === randomStakingAmount; - }, - ); - expect(matchedStakingOutput).toBeDefined(); - expect(stakingFeeSat).toBeGreaterThan(0); - }); - - it(`${testTermDescription} - should successfully create a staking transaction with change amount in output`, () => { - const utxo = dataGen.generateRandomUTXOs( - randomStakingAmount + Math.floor(Math.random() * 1000000), - 1, // make it a single utxo so we always have change - ); - const { stakingFeeSat, stakingTerm, unsignedStakingPsbt } = - createStakingTx( - randomParam, - randomStakingAmount, - randomStakingTimeBlocks, - randomFpKeys.publicKey, - network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, - randomFeeRate, - utxo, - ); - - expect(stakingTerm).toBe(randomStakingTimeBlocks); - const matchedStakingOutput = unsignedStakingPsbt.txOutputs.find( - (output) => { - return output.value === randomStakingAmount; - }, - ); - expect(matchedStakingOutput).toBeDefined(); - expect(stakingFeeSat).toBeGreaterThan(0); + const feeRate = 1; // keep test simple by setting this to unit 1 + const randomParam = dataGen.generateRandomGlobalParams(true); + const randomStakingAmount = dataGen.getRandomIntegerBetween( + randomParam.minStakingAmountSat, + randomParam.maxStakingAmountSat, + ); + const randomStakingTimeBlocks = dataGen.getRandomIntegerBetween( + randomParam.minStakingTimeBlocks, + randomParam.maxStakingTimeBlocks, + ); + const randomInputUTXOs = dataGen.generateRandomUTXOs( + randomStakingAmount + Math.floor(Math.random() * 1000000), + Math.floor(Math.random() * 10) + 1, + ); + const txHex = dataGen.generateRandomTxId(); - const changeOutput = unsignedStakingPsbt.txOutputs.find((output) => { - return ( - output.address == - dataGen.getTaprootAddress(randomUserKeys.publicKey) - ); - }); - expect(changeOutput).toBeDefined(); - }); - }); - - it(`${testTermDescription} - should throw an error if the staking amount is less than the minimum staking amount`, () => { - expect(() => - createStakingTx( - randomParam, - randomParam.minStakingAmountSat - 1, - randomStakingTimeBlocks, - randomFpKeys.publicKey, - network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, - randomFeeRate, - randomInputUTXOs, - ), - ).toThrow("Invalid staking data"); - }); + // mock the btcWallet + const btcWallet = { + signPsbt: jest.fn(), + getWalletProviderName: jest.fn(), + pushTx: jest.fn().mockResolvedValue(true), + }; - it(`${testTermDescription} - should throw an error if the staking amount is greater than the maximum staking amount`, () => { - expect(() => - createStakingTx( - randomParam, - randomParam.maxStakingAmountSat + 1, - randomStakingTimeBlocks, - randomFpKeys.publicKey, - network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, - randomFeeRate, - randomInputUTXOs, - ), - ).toThrow("Invalid staking data"); + it("should successfully sign a staking transaction", async () => { + // Import the mock function + const { signPsbtTransaction } = require("@/app/common/utils/psbt"); + signPsbtTransaction.mockImplementationOnce(() => { + return async () => { + return { + toHex: () => txHex, + }; + }; }); - it(`${testTermDescription} - should throw an error if the fee rate is less than or equal to 0`, () => { - expect(() => - createStakingTx( - randomParam, - randomStakingAmount, - randomStakingTimeBlocks, - randomFpKeys.publicKey, - network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, - 0, - randomInputUTXOs, - ), - ).toThrow("Invalid fee rate"); - }); + const result = await signStakingTx( + btcWallet as any, + randomParam, + randomStakingAmount, + randomStakingTimeBlocks, + randomFpKeys.publicKey, + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + feeRate, + randomInputUTXOs, + ); - it(`${testTermDescription} - should throw an error if there are no usable balance`, () => { - expect(() => - createStakingTx( - randomParam, - randomStakingAmount, - randomStakingTimeBlocks, - randomFpKeys.publicKey, - network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, - randomFeeRate, - [], - ), - ).toThrow("Not enough usable balance"); - }); + expect(result).toBeDefined(); + expect(btcWallet.pushTx).toHaveBeenCalled(); + // check the signed transaction hex + expect(result.stakingTxHex).toBe(txHex); + // check the staking term + expect(result.stakingTerm).toBe(randomStakingTimeBlocks); + }); - // This test only test createStakingTx when apiDataToStakingScripts throw error. - // The different error cases are tested in the apiDataToStakingScripts.test.ts - it(`${testTermDescription} - should throw an error if the staking scripts cannot be built`, () => { - expect(() => - createStakingTx( - randomParam, - randomStakingAmount, - randomStakingTimeBlocks, - dataGen.generateRandomKeyPair(false).publicKey, // invalid finality provider key - network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, - randomFeeRate, - randomInputUTXOs, - ), - ).toThrow("Invalid script data provided"); + it("should throw an error when signing a staking transaction", async () => { + // Import the mock function + const { signPsbtTransaction } = require("@/app/common/utils/psbt"); + signPsbtTransaction.mockImplementationOnce(() => { + return async () => { + throw new Error("signing error"); + }; }); - // More test cases when we have variable staking terms - if (!isFixed) { - it(`${testTermDescription} - should throw an error if the staking term is less than the minimum staking time blocks`, () => { - expect(() => - createStakingTx( - randomParam, - randomStakingAmount, - randomParam.minStakingTimeBlocks - 1, - randomFpKeys.publicKey, - network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, - randomFeeRate, - randomInputUTXOs, - ), - ).toThrow("Invalid staking data"); - }); - - it(`${testTermDescription} - should throw an error if the staking term is greater than the maximum staking time blocks`, () => { - expect(() => - createStakingTx( - randomParam, - randomStakingAmount, - randomParam.maxStakingTimeBlocks + 1, - randomFpKeys.publicKey, - network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, - randomFeeRate, - randomInputUTXOs, - ), - ).toThrow("Invalid staking data"); - }); + try { + await signStakingTx( + btcWallet as any, + randomParam, + randomStakingAmount, + randomStakingTimeBlocks, + randomFpKeys.publicKey, + network, + dataGen.getTaprootAddress(randomUserKeys.publicKey), + randomUserKeys.publicKey, + feeRate, + randomInputUTXOs, + ); + } catch (error: any) { + expect(error).toBeDefined(); + expect(error.message).toBe("signing error"); } }); }); From 17b089d4c3bf4149fbedf63a2adc7f8913cee236 Mon Sep 17 00:00:00 2001 From: Filippos Malandrakis Date: Fri, 21 Jun 2024 13:42:43 +0300 Subject: [PATCH 31/56] chore: Remove CD --- .circleci/config.yml | 128 ---------------------------------- .circleci/values-staging.yaml | 61 ---------------- .circleci/values-testnet.yaml | 78 --------------------- package.json | 2 +- 4 files changed, 1 insertion(+), 268 deletions(-) delete mode 100644 .circleci/values-staging.yaml delete mode 100644 .circleci/values-testnet.yaml diff --git a/.circleci/config.yml b/.circleci/config.yml index c389963..ac0f978 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -71,101 +71,6 @@ jobs: repo: "simple-staking" tag: "$CIRCLE_SHA1,$CIRCLE_TAG" - deploy_staging: - machine: - image: ubuntu-2204:2024.01.1 - resource_class: large - steps: - - checkout - - aws-ecr/ecr-login: - aws-access-key-id: AWS_ACCESS_KEY_ID - aws-secret-access-key: AWS_SECRET_ACCESS_KEY - region: "$AWS_REGION" - - kubernetes/install-kubeconfig: - kubeconfig: TESTNET_KUBECONFIG - - helm/install-helm-client - - run: - name: Fetch and replace config placeholders from CircleCi env vars - command: | - HELM_VALUES=/home/circleci/project/.circleci/values-staging.yaml - sed -i "s/API_STAGING_FQDN/$API_STAGING_FQDN/g" $HELM_VALUES - sed -i "s/DASHBOARD_STAGING_FQDN/$DASHBOARD_STAGING_FQDN/g" $HELM_VALUES - - run: - name: Perform a dry run of the new release - command: | - helm upgrade --install --debug --dry-run \ - -n $DEPLOY_STAGING_NAMESPACE \ - --values /home/circleci/project/.circleci/values-staging.yaml \ - --version $HELM_CHART_VERSION \ - --set deployment.version=$CIRCLE_SHA1 \ - simple-staking $HELM_CHART_REPO - - run: - name: Release new service version in an atomic way - command: | - helm upgrade --install --debug --atomic --wait \ - -n $DEPLOY_STAGING_NAMESPACE --create-namespace \ - --values /home/circleci/project/.circleci/values-staging.yaml \ - --version $HELM_CHART_VERSION \ - --set deployment.version=$CIRCLE_SHA1 \ - simple-staking $HELM_CHART_REPO - - deploy_testnet: - machine: - image: ubuntu-2204:2024.01.1 - resource_class: large - steps: - - checkout - - aws-ecr/ecr-login: - aws-access-key-id: AWS_ACCESS_KEY_ID - aws-secret-access-key: AWS_SECRET_ACCESS_KEY - region: "$AWS_REGION" - - kubernetes/install-kubeconfig: - kubeconfig: TESTNET_KUBECONFIG - - helm/install-helm-client - - run: - name: Fetch and replace config placeholders from CircleCi env vars - command: | - HELM_VALUES=/home/circleci/project/.circleci/values-testnet.yaml - sed -i "s/API_FQDN/$API_FQDN/g" $HELM_VALUES - sed -i "s/DASHBOARD_FQDN/$DASHBOARD_FQDN/g" $HELM_VALUES - - run: - name: Perform a dry run of the new release - command: | - helm upgrade --install --debug --dry-run \ - -n $DEPLOY_TESTNET_NAMESPACE \ - --values /home/circleci/project/.circleci/values-testnet.yaml \ - --version $HELM_CHART_VERSION \ - --set deployment.version=$CIRCLE_SHA1 \ - simple-staking $HELM_CHART_REPO - - run: - name: Release new service version in an atomic way - command: | - helm upgrade --install --debug --atomic --wait \ - -n $DEPLOY_TESTNET_NAMESPACE --create-namespace \ - --values /home/circleci/project/.circleci/values-testnet.yaml \ - --version $HELM_CHART_VERSION \ - --set deployment.version=$CIRCLE_SHA1 \ - simple-staking $HELM_CHART_REPO - - rollback_testnet: - machine: - image: ubuntu-2204:2024.01.1 - resource_class: large - steps: - - checkout - - aws-ecr/ecr-login: - aws-access-key-id: AWS_ACCESS_KEY_ID - aws-secret-access-key: AWS_SECRET_ACCESS_KEY - region: "$AWS_REGION" - - kubernetes/install-kubeconfig: - kubeconfig: TESTNET_KUBECONFIG - - helm/install-helm-client - - run: - name: Rollback Helm Chart to previous release - command: | - helm rollback --cleanup-on-fail --force --recreate-pods --wait \ - --debug -n $DEPLOY_TESTNET_NAMESPACE simple-staking - workflows: CICD: jobs: @@ -188,36 +93,3 @@ workflows: only: - dev - main - - deploy_staging: - requires: - - push_docker - filters: - branches: - only: - - dev - - require_approval_deploy: - type: approval - requires: - - push_docker - filters: - branches: - only: - - main - - deploy_testnet: - requires: - - require_approval_deploy - filters: - branches: - only: - - main - - require_approval_rollback: - type: approval - requires: - - deploy_testnet - filters: - branches: - only: - - main - - rollback_testnet: - requires: - - require_approval_rollback diff --git a/.circleci/values-staging.yaml b/.circleci/values-staging.yaml deleted file mode 100644 index b39d803..0000000 --- a/.circleci/values-staging.yaml +++ /dev/null @@ -1,61 +0,0 @@ -namespace: phase-1-staging -name: simple-staking -deployment: - image: 490721144737.dkr.ecr.us-east-1.amazonaws.com/simple-staking - version: REPLACEME - replicas: 1 - ports: - - protocol: TCP - containerPort: 3000 - name: simple-staking - env: - - name: MEMPOOL_API - value: https://babylon.mempool.space - - name: API_URL - value: API_STAGING_FQDN - - name: NETWORK - value: signet - resources: - requests: - memory: 100Mi - cpu: 100m - limits: - memory: 1Gi - cpu: 1000m - nodeSelector: - workload: "staging" -service: - type: NodePort - ports: - - protocol: TCP - port: 80 - targetPort: simple-staking - name: simple-staking -ingress: - enabled: true - tlsCertArn: arn:aws:acm:us-east-2:490721144737:certificate/a18510a6-948f-4595-87d1-e43c8b2e0c8f - groupName: "testnet3-public" - scheme: "internet-facing" - hosts: - - host: DASHBOARD_STAGING_FQDN - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: ssl-redirect - port: - name: use-annotation - - path: / - pathType: Prefix - backend: - service: - name: simple-staking - port: - name: simple-staking -externalDns: - fqdn: DASHBOARD_STAGING_FQDN - ttl: 60 -purgeCloudflareCache: - enabled: false diff --git a/.circleci/values-testnet.yaml b/.circleci/values-testnet.yaml deleted file mode 100644 index 344401b..0000000 --- a/.circleci/values-testnet.yaml +++ /dev/null @@ -1,78 +0,0 @@ -namespace: simple-staking -name: simple-staking -deployment: - image: 490721144737.dkr.ecr.us-east-1.amazonaws.com/simple-staking - version: REPLACEME - replicas: 2 - ports: - - protocol: TCP - containerPort: 3000 - name: simple-staking - env: - - name: MEMPOOL_API - value: https://babylon.mempool.space - - name: API_URL - value: API_FQDN - - name: NETWORK - value: signet - resources: - requests: - memory: 100Mi - cpu: 100m - limits: - memory: 1Gi - cpu: 1000m - nodeSelector: - workload: "webservices" - affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - podAffinityTerm: - labelSelector: - matchLabels: - app: staking-api-service - topologyKey: topology.kubernetes.io/zone - weight: 1 -service: - type: NodePort - ports: - - protocol: TCP - port: 80 - targetPort: simple-staking - name: simple-staking -ingress: - enabled: true - tlsCertArn: arn:aws:acm:us-east-2:490721144737:certificate/2c6cb5ad-8899-4871-9206-ea8e978aebae - groupName: "testnet3-public" - scheme: "internet-facing" - hosts: - - host: DASHBOARD_FQDN - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: ssl-redirect - port: - name: use-annotation - - path: / - pathType: Prefix - backend: - service: - name: simple-staking - port: - name: simple-staking -externalDns: - fqdn: DASHBOARD_FQDN - ttl: 60 -purgeCloudflareCache: - enabled: true - image: 490721144737.dkr.ecr.us-east-1.amazonaws.com/infra-toolkit - version: v0.0.7 - hosts: - - "https://$DASHBOARD_FQDN" -encryptedConfig: - cloudflareApiKey: AgBnGZ+hi3nFWQRxqu3utitXDmaRdyY/sGwGfJ7Rpa/Sb2Tgl6WhVKjWbugagCnJECsrifPhCpxGJ1rCxScOEOZ30mletKXOPtzjxNLzC3wxX0R/lA9ZKwCzsHlL2Cn/dSg/rZN5N1EL2qEL0cZx3jN12ulYIDx9QJ78lFJw2FZpSJ/yRS8JzKFOXuLa2lsLtK+UWxHkW1B+0Ot7VimqqBDKh3gBWBFLYEP0EBJ/RPMtngnR7hE9thsr0/Ax1dWqe8ru++RME+xNLOJ9/2sTwwtA6qNObjSMERkjMcwpkxurdOqVSBKgDs/jPph4NYwTJMh82ZM2I5h/LaElDR7V4WibDIWV+A/V+CBG+c7YGpa7DU/R4iwzN2DntknAheDRq1P8RJdEKxaU1eFtfcQXcwgUe24mnm3ZS+ubWWEDC+cba0DVPxW0OlfhuK+xCo6r6tMHqt3D3oq6Ulwd/eT5JQFfA5DJfwPBG+l2jszhkZQ502ZNo5goHQZG0A/NwExfALOSl6q9v8EylMNoJKu6taEElwUd4c7t3CNrlbnOfVPY/C3klehzsidfm2KDKa33FeAABG10oWUVNihqSB2Dy3Nv7gct1pnvqAfr17RYnVR5YmMN2Hz0Fqm1zK+IC5mXSkc3+ChyLMQZEHRO2eQn0mp9z7XVG54KyOgicieowmoFUiIq3M6Osd2ZbFufST/9cvqk+A04EIB+frHYPsVdRdiS0SGVXr5jt48dXA6H8t586inV1WEI - cloudflareEmail: AgAOzSfPDy5xaGSGH2+fEYsavgEUzIzzu4pRMEaX+3/b1bt04q9DYhF9QVadq+qtZBufYIUUvq3S2T9qVzGaCyVBrdd1d/RpD6GRDyBXFjoA+gPx9xXrxi5gN116f/LkNEbpqw7KN+F5iCu8QXd4QNgxZRoKgY7C00+w52uoiRz04P+VoMR1DXQJ8VtsZqHr6sOAn7gfpoTN0ckeo+rn2BaU9E2aTQ78BI2ZA5r/V6u9CRA50VsYX0yGOuLBBSCHwFIul0DIKXVtefkP4CcpHjlYG4Q9eoPF80QmK7/d/I9VTlbLnXjyfnFNEZwWxeqA9mscKJvp8FAYEEW2kksHylPBHnw1VKqinx17gRyVO4ahbQPVNWvE7c+4hvp0tVapRoAs8I5tfp74bXFxluLYXV+OT7QUTv3P61SIZWPJ7yG57EQHugjwp9XNVes//Tam8eJlomXlCmv6FQozFPuL3FuesN7D2tN2THouAn5du1Uq7zVcuKd0kNw5VC4kie0IAL/xjdiR4E5BPlNFshYF+dpYzAmqGA5FF9yJp4Y6tHapnDWxn6uC6H5L/WvVQO00vYp81as/ukB6mqtdTUCYvIhAepC6Wj8avsvKg9AI/jdpj3WUHpB0GlZgTW4/FwmAKE+F8Qig3lSTGYLVkxT4AdZJTrN54z6YUjW6kQP1Q3YFdwcy1YNUMeV26mrhjfdJJsrnvuoJTZVShF5Kakh67QyET+a/ssGlh6Q= - cloudflareZoneId: AgCQtx3/mVYqGfJCxpXPoS8ypr+NczwnilvgDhuLz4GmUw11IyBaN/GuP+iCweFj2RSVFeLbvtikL2Tpbvio/+/IOBPeO086PiHmzKAbhC/HukhD/gTt/WlpPwSaaElnkNzDfniYeTg45KzryYneSwc/QZYUlf+fAiRByfvZeHySjL8xiBk/6qiK7jTt0WUxESuFd5F4bH9cqxDso5bALMpJ6BUPI0CX6EZAnq2pvXopM8vDBboGHd4DNq7JIgveBNkMw5x3AXeW7PJHHV7CS9k0VHk0PvRR/FdYYDyk0dAv1BcrRNw2hNJmLljo2plalks0AWSmubu8OMdfVEhYM3VMbjZovNNjUxZZtDcB+VZGUJETaj7dT0JFeMtt3aUuFEOwg5oMor7mxfpJIbj6fqq0PJ6LNBtjQ3LMsVcDpJ+7sbJ9lwDQefhSDJh0YM59i7orA6wjllvclAdAcUzlMA5d59ibqFczgFN6NE5HnpFejnjIUBOejwpdTdYkRKyt7LftpmUWcpvzuUDw+LPHxNb976YRdZ+VL2nAjBGU8NJ7xzVboAIRAGKIP3X5ZQR7tjowFS8jcN0DGM4j+RjE+pRL7PeeIVPBUMQIC/pCwU6hLKNN2nmpMEp5zruz6XFTYWzEWvVuey/sIchPo9oS7zIY5gKSsuBx1CrFucoc4WilxyLurSleGe49NnUAqTzJgn64NuocbymfsKpV40R85YKjUCrSIFajPUiYZM9rjjiTww== diff --git a/package.json b/package.json index c63fb11..b104bd4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-staking", - "version": "0.2.1", + "version": "0.2.2", "private": true, "scripts": { "dev": "next dev", From 53d2650bf8db060af1d317c648a344664a4ae4c5 Mon Sep 17 00:00:00 2001 From: Jeremy <168515712+jeremy-babylonchain@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:03:27 -0700 Subject: [PATCH 32/56] update FP alignment UI (#275) --- .../Staking/FinalityProviders/FinalityProvider.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx b/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx index ed89325..9ac6c85 100644 --- a/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx +++ b/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx @@ -38,22 +38,22 @@ export const FinalityProvider: React.FC = ({
{moniker ? ( -
-

{moniker}

+
verified +

{moniker}

) : ( -
+
- + - - + No data provided
)}
From 2412ebd606ccaa287ad71e488832d2e160adb028 Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Wed, 26 Jun 2024 21:30:46 +1000 Subject: [PATCH 33/56] fix: should not allow user to choose the FP which has the same PK as wallet --- src/app/components/Staking/Staking.tsx | 31 +++++++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/app/components/Staking/Staking.tsx b/src/app/components/Staking/Staking.tsx index 1061d1d..14bfb4a 100644 --- a/src/app/components/Staking/Staking.tsx +++ b/src/app/components/Staking/Staking.tsx @@ -389,13 +389,32 @@ export const Staking: React.FC = ({ // Select the finality provider from the list const handleChooseFinalityProvider = (btcPkHex: string) => { - if (!finalityProviders) { - throw new Error("Finality providers not loaded"); - } + let found: FinalityProviderInterface | undefined; + try { + if (!finalityProviders) { + throw new Error("Finality providers not loaded"); + } + + found = finalityProviders.find((fp) => fp?.btcPk === btcPkHex); + if (!found) { + throw new Error("Finality provider not found"); + } - const found = finalityProviders.find((fp) => fp?.btcPk === btcPkHex); - if (!found) { - throw new Error("Finality provider not found"); + if (found.btcPk === publicKeyNoCoord) { + throw new Error( + "Can not select the same finality provider which has the same public key as the wallet", + ); + } + } catch (error: any) { + showError({ + error: { + message: error.message, + errorState: ErrorState.STAKING, + errorTime: new Date(), + }, + retryAction: () => handleChooseFinalityProvider(btcPkHex), + }); + return; } setFinalityProvider(found); From e0009308e3bc17409415c7e2b2df7f4f93325ec9 Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Wed, 26 Jun 2024 21:32:19 +1000 Subject: [PATCH 34/56] chore: bump version to 0.2.3 contain hotfix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b104bd4..a8c7181 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-staking", - "version": "0.2.2", + "version": "0.2.3", "private": true, "scripts": { "dev": "next dev", From 215898114bbb3219e12e6510be8e907550346ef6 Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Wed, 26 Jun 2024 23:26:55 +1000 Subject: [PATCH 35/56] chore: update the msg --- src/app/components/Staking/Staking.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/Staking/Staking.tsx b/src/app/components/Staking/Staking.tsx index 14bfb4a..4603a7d 100644 --- a/src/app/components/Staking/Staking.tsx +++ b/src/app/components/Staking/Staking.tsx @@ -402,7 +402,7 @@ export const Staking: React.FC = ({ if (found.btcPk === publicKeyNoCoord) { throw new Error( - "Can not select the same finality provider which has the same public key as the wallet", + "Cannot select a finality provider with the same public key as the wallet", ); } } catch (error: any) { From 32524b9c87d3f17fe499e268e7e6525854d01a5f Mon Sep 17 00:00:00 2001 From: Govard Barkhatov Date: Thu, 27 Jun 2024 19:28:42 +0200 Subject: [PATCH 36/56] feature: Test updateDelegations (#286) * move to a separate file * pure function * base test * calculateDelegationsDiff * validDelegationsLocalStorage * rm identical * add multiple clauses * validDelegationsLocalStorage * updateDelegationsLocalStorage --- src/app/page.tsx | 34 ++--- .../local_storage/calculateDelegationsDiff.ts | 34 +++++ .../calculateDelegationsDiff.test.ts | 136 ++++++++++++++++++ 3 files changed, 178 insertions(+), 26 deletions(-) create mode 100644 src/utils/local_storage/calculateDelegationsDiff.ts create mode 100644 tests/utils/local_storage/calculateDelegationsDiff.test.ts diff --git a/src/app/page.tsx b/src/app/page.tsx index 02392d0..9b245c7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -8,7 +8,7 @@ import { useLocalStorage } from "usehooks-ts"; import { network } from "@/config/network.config"; import { getCurrentGlobalParamsVersion } from "@/utils/globalParams"; -import { filterDelegationsLocalStorage } from "@/utils/local_storage/filterDelegationsLocalStorage"; +import { calculateDelegationsDiff } from "@/utils/local_storage/calculateDelegationsDiff"; import { getDelegationsLocalStorageKey } from "@/utils/local_storage/getDelegationsLocalStorageKey"; import { WalletError, WalletErrorType } from "@/utils/wallet/errors"; import { @@ -287,36 +287,18 @@ const Home: React.FC = () => { return; } - const updateDelegations = async () => { - // Filter the delegations that are still valid - const validDelegations = await filterDelegationsLocalStorage( - delegationsLocalStorage, - delegations.delegations, - ); - - // Extract the stakingTxHashHex from the validDelegations - const validDelegationsHashes = validDelegations - .map((delegation) => delegation.stakingTxHashHex) - .sort(); - const delegationsLocalStorageHashes = delegationsLocalStorage - .map((delegation) => delegation.stakingTxHashHex) - .sort(); - - // Check if the validDelegations are different from the current delegationsLocalStorage - const areDelegationsDifferent = - validDelegationsHashes.length !== - delegationsLocalStorageHashes.length || - validDelegationsHashes.some( - (hash, index) => hash !== delegationsLocalStorageHashes[index], + const updateDelegationsLocalStorage = async () => { + const { areDelegationsDifferent, delegations: newDelegations } = + await calculateDelegationsDiff( + delegations.delegations, + delegationsLocalStorage, ); - - // Update the local storage delegations if they are different to avoid unnecessary updates if (areDelegationsDifferent) { - setDelegationsLocalStorage(validDelegations); + setDelegationsLocalStorage(newDelegations); } }; - updateDelegations(); + updateDelegationsLocalStorage(); }, [delegations, setDelegationsLocalStorage, delegationsLocalStorage]); // Finality providers key-value map { pk: moniker } diff --git a/src/utils/local_storage/calculateDelegationsDiff.ts b/src/utils/local_storage/calculateDelegationsDiff.ts new file mode 100644 index 0000000..4c5ebe7 --- /dev/null +++ b/src/utils/local_storage/calculateDelegationsDiff.ts @@ -0,0 +1,34 @@ +import { Delegation } from "@/app/types/delegations"; + +import { filterDelegationsLocalStorage } from "./filterDelegationsLocalStorage"; + +export const calculateDelegationsDiff = async ( + delegations: Delegation[], + delegationsLocalStorage: Delegation[], +): Promise<{ areDelegationsDifferent: boolean; delegations: Delegation[] }> => { + // Filter the delegations that are still valid + const validDelegationsLocalStorage = await filterDelegationsLocalStorage( + delegationsLocalStorage, + delegations, + ); + + // Extract the stakingTxHashHex + const validDelegationsHashes = validDelegationsLocalStorage + .map((delegation) => delegation.stakingTxHashHex) + .sort(); + const delegationsLocalStorageHashes = delegationsLocalStorage + .map((delegation) => delegation.stakingTxHashHex) + .sort(); + + // Check if the validDelegations are different from the current delegationsLocalStorage + const areDelegationsDifferent = + validDelegationsHashes.length !== delegationsLocalStorageHashes.length || + validDelegationsHashes.some( + (hash, index) => hash !== delegationsLocalStorageHashes[index], + ); + + return { + areDelegationsDifferent, + delegations: validDelegationsLocalStorage, + }; +}; diff --git a/tests/utils/local_storage/calculateDelegationsDiff.test.ts b/tests/utils/local_storage/calculateDelegationsDiff.test.ts new file mode 100644 index 0000000..a409492 --- /dev/null +++ b/tests/utils/local_storage/calculateDelegationsDiff.test.ts @@ -0,0 +1,136 @@ +import { Delegation } from "@/app/types/delegations"; +import { calculateDelegationsDiff } from "@/utils/local_storage/calculateDelegationsDiff"; +import { filterDelegationsLocalStorage } from "@/utils/local_storage/filterDelegationsLocalStorage"; + +// Mock the filterDelegationsLocalStorage function +jest.mock("@/utils/local_storage/filterDelegationsLocalStorage"); + +describe("utils/local_storage/calculateDelegationsDiff", () => { + const mockDelegationsHash1: Delegation[] = [ + { stakingTxHashHex: "hash1" } as Delegation, + ]; + const mockDelegationsHash2: Delegation[] = [ + { stakingTxHashHex: "hash2" } as Delegation, + ]; + + const mockDelegationsMultiple1: Delegation[] = [ + { stakingTxHashHex: "hash1" } as Delegation, + { stakingTxHashHex: "hash3" } as Delegation, + { stakingTxHashHex: "hash5" } as Delegation, + ]; + + const mockDelegationsMultiple2: Delegation[] = [ + { stakingTxHashHex: "hash2" } as Delegation, + { stakingTxHashHex: "hash4" } as Delegation, + { stakingTxHashHex: "hash6" } as Delegation, + ]; + + beforeEach(() => { + // Clear all instances and calls to constructor and all methods: + jest.clearAllMocks(); + }); + + it("should return new delegations if they are different from local storage", async () => { + (filterDelegationsLocalStorage as jest.Mock).mockResolvedValue( + mockDelegationsHash1, + ); + + const result = await calculateDelegationsDiff( + mockDelegationsHash1, + mockDelegationsHash2, + ); + + expect(result.areDelegationsDifferent).toBe(true); + expect(result.delegations).toEqual(mockDelegationsHash1); + }); + + it("should return original delegations if they are the same as local storage", async () => { + (filterDelegationsLocalStorage as jest.Mock).mockResolvedValue( + mockDelegationsHash2, + ); + + const result = await calculateDelegationsDiff( + mockDelegationsHash2, + mockDelegationsHash2, + ); + + expect(result.areDelegationsDifferent).toBe(false); + expect(result.delegations).toEqual(mockDelegationsHash2); + }); + + it("should handle empty API delegations correctly", async () => { + (filterDelegationsLocalStorage as jest.Mock).mockResolvedValue([]); + + const result = await calculateDelegationsDiff([], mockDelegationsHash2); + + expect(result.areDelegationsDifferent).toBe(true); + expect(result.delegations).toEqual([]); + }); + + it("should handle empty local storage correctly", async () => { + (filterDelegationsLocalStorage as jest.Mock).mockResolvedValue( + mockDelegationsHash1, + ); + + const result = await calculateDelegationsDiff(mockDelegationsHash1, []); + + expect(result.areDelegationsDifferent).toBe(true); + expect(result.delegations).toEqual(mockDelegationsHash1); + }); + + it("should handle both empty delegations and local storage correctly", async () => { + (filterDelegationsLocalStorage as jest.Mock).mockResolvedValue([]); + + const result = await calculateDelegationsDiff([], []); + + expect(result.areDelegationsDifferent).toBe(false); + expect(result.delegations).toEqual([]); + }); + + it("should handle multiple items in delegations and local storage correctly", async () => { + (filterDelegationsLocalStorage as jest.Mock).mockResolvedValue( + mockDelegationsMultiple1, + ); + + const result = await calculateDelegationsDiff( + mockDelegationsMultiple1, + mockDelegationsMultiple2, + ); + + expect(result.areDelegationsDifferent).toBe(true); + expect(result.delegations).toEqual(mockDelegationsMultiple1); + }); + + it("should handle multiple items when delegations are the same", async () => { + (filterDelegationsLocalStorage as jest.Mock).mockResolvedValue( + mockDelegationsMultiple1, + ); + + const result = await calculateDelegationsDiff( + mockDelegationsMultiple1, + mockDelegationsMultiple1, + ); + + expect(result.areDelegationsDifferent).toBe(false); + expect(result.delegations).toEqual(mockDelegationsMultiple1); + }); + + it("should handle case where filtered delegations are different", async () => { + const filteredDelegations = [ + { stakingTxHashHex: "hash1" } as Delegation, + { stakingTxHashHex: "hash2" } as Delegation, + ]; + + (filterDelegationsLocalStorage as jest.Mock).mockResolvedValue( + filteredDelegations, + ); + + const result = await calculateDelegationsDiff( + mockDelegationsMultiple1, + mockDelegationsMultiple2, + ); + + expect(result.areDelegationsDifferent).toBe(true); + expect(result.delegations).toEqual(filteredDelegations); + }); +}); From bc731cb49d90a160684d05bd1cb08428c89cb28d Mon Sep 17 00:00:00 2001 From: Govard Barkhatov Date: Thu, 27 Jun 2024 19:18:44 +0200 Subject: [PATCH 37/56] local storage tests --- .../mockGenerateDelegationsData.tsx | 32 +++- .../filterDelegationsLocalStorage.test.ts | 158 ++++++++++++++++++ .../getDelegationsLocalStorageKey.test.ts | 24 +++ ...rmediateDelegationsLocalStorageKey.test.ts | 24 +++ .../toLocalStorageDelegation.test.ts | 44 +++++ ...LocalStorageIntermediateDelegation.test.ts | 88 ++++++++++ 6 files changed, 368 insertions(+), 2 deletions(-) create mode 100644 tests/utils/local_storage/filterDelegationsLocalStorage.test.ts create mode 100644 tests/utils/local_storage/getDelegationsLocalStorageKey.test.ts create mode 100644 tests/utils/local_storage/getIntermediateDelegationsLocalStorageKey.test.ts create mode 100644 tests/utils/local_storage/toLocalStorageDelegation.test.ts create mode 100644 tests/utils/local_storage/toLocalStorageIntermediateDelegation.test.ts diff --git a/src/app/mock/Delegations/mockGenerateDelegationsData.tsx b/src/app/mock/Delegations/mockGenerateDelegationsData.tsx index 0f6805b..d0cb83d 100644 --- a/src/app/mock/Delegations/mockGenerateDelegationsData.tsx +++ b/src/app/mock/Delegations/mockGenerateDelegationsData.tsx @@ -1,4 +1,4 @@ -import { Delegation } from "@/app/types/delegations"; +import { Delegation, DelegationState } from "@/app/types/delegations"; function generateRandomHex(size: number) { return [...Array(size)] @@ -11,7 +11,7 @@ function getRandomState() { return states[Math.floor(Math.random() * states.length)]; } -export function generateRandomDelegationData(count: number) { +export function generateRandomDelegationData(count: number): Delegation[] { const data: Delegation[] = []; for (let i = 0; i < count; i++) { @@ -43,3 +43,31 @@ export function generateRandomDelegationData(count: number) { return data; } + +export function generateMockDelegation( + hash: string, + staker: string, + provider: string, + value: number, + hoursAgo: number, +): Delegation { + const currentTimestamp = new Date().getTime(); + return { + stakingTxHashHex: hash, + stakerPkHex: staker, + finalityProviderPkHex: provider, + state: DelegationState.PENDING, + stakingValueSat: value, + stakingTx: { + txHex: `txHex-${hash}`, + outputIndex: 0, + startTimestamp: new Date( + currentTimestamp - hoursAgo * 60 * 60 * 1000, + ).toISOString(), + startHeight: 0, + timelock: 3600, + }, + isOverflow: false, + unbondingTx: undefined, + }; +} diff --git a/tests/utils/local_storage/filterDelegationsLocalStorage.test.ts b/tests/utils/local_storage/filterDelegationsLocalStorage.test.ts new file mode 100644 index 0000000..9e897c4 --- /dev/null +++ b/tests/utils/local_storage/filterDelegationsLocalStorage.test.ts @@ -0,0 +1,158 @@ +import { generateMockDelegation } from "@/app/mock/Delegations/mockGenerateDelegationsData"; +import { Delegation } from "@/app/types/delegations"; +import { filterDelegationsLocalStorage } from "@/utils/local_storage/filterDelegationsLocalStorage"; +import { getTxInfo } from "@/utils/mempool_api"; + +// Mock the getTxInfo function +jest.mock("@/utils/mempool_api"); + +describe("utils/local_storage/filterDelegationsLocalStorage", () => { + const notExceedingLimitDelegations: Delegation[] = [ + generateMockDelegation("hash2", "staker2", "provider2", 2000, 23), // 23 hours ago + generateMockDelegation("hash4", "staker4", "provider4", 4000, 0), // Current time + generateMockDelegation("hash5", "staker5", "provider5", 5000, 22), // 22 hours ago + ]; + + const exceedingLimitDelegations: Delegation[] = [ + generateMockDelegation("hash1", "staker1", "provider1", 1000, 25), // 25 hours ago + generateMockDelegation("hash3", "staker3", "provider3", 3000, 26), // 26 hours ago + generateMockDelegation("hash6", "staker6", "provider6", 6000, 27), // 27 hours ago + ]; + + const mockDelegationsLocalStorage: Delegation[] = [ + ...notExceedingLimitDelegations, + ...exceedingLimitDelegations, + ]; + + const mockDelegationsAPI: Delegation[] = [ + generateMockDelegation("hash7", "staker7", "provider7", 7000, 0), + generateMockDelegation("hash8", "staker8", "provider8", 8000, 0), + ]; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should return valid delegations not present in the API and not exceeding the max duration", async () => { + (getTxInfo as jest.Mock).mockRejectedValue( + new Error("Transaction not found in the mempool"), + ); + + const result = await filterDelegationsLocalStorage( + mockDelegationsLocalStorage, + mockDelegationsAPI, + ); + + // Expect delegations not exceeding the max duration and not in the API to be returned + expect(result).toEqual(notExceedingLimitDelegations); + }); + + it("should remove delegations that exceed max duration and are not in the mempool", async () => { + (getTxInfo as jest.Mock).mockRejectedValue( + new Error("Transaction not found in the mempool"), + ); + + const result = await filterDelegationsLocalStorage( + mockDelegationsLocalStorage, + mockDelegationsAPI, + ); + + // Expect delegations exceeding the max duration and not found in the mempool to be removed + exceedingLimitDelegations.forEach((delegation) => { + expect(result).not.toContain(delegation); + }); + + // Ensure remaining delegations are the ones not exceeding the limit + expect(result).toEqual(notExceedingLimitDelegations); + }); + + it("should keep delegations that exceed max duration but are found in the mempool", async () => { + (getTxInfo as jest.Mock).mockImplementation((txHash) => { + if (txHash === "hash1" || txHash === "hash3" || txHash === "hash6") { + return Promise.resolve({}); + } else { + return Promise.reject( + new Error("Transaction not found in the mempool"), + ); + } + }); + + const result = await filterDelegationsLocalStorage( + mockDelegationsLocalStorage, + mockDelegationsAPI, + ); + + // Sort both arrays to avoid issues with order + const sortedResult = result.sort((a, b) => + a.stakingTxHashHex.localeCompare(b.stakingTxHashHex), + ); + const expectedSortedResult = [ + ...exceedingLimitDelegations, + ...notExceedingLimitDelegations, + ].sort((a, b) => a.stakingTxHashHex.localeCompare(b.stakingTxHashHex)); + + // Expect delegations exceeding the max duration but found in the mempool to be kept + expect(sortedResult).toEqual(expectedSortedResult); + }); + + it("should keep delegations that are within the max duration", async () => { + const recentDelegation: Delegation = generateMockDelegation( + "hash9", + "staker9", + "provider9", + 9000, + 0, + ); + + const delegationsLocalStorage = [recentDelegation]; + const result = await filterDelegationsLocalStorage( + delegationsLocalStorage, + mockDelegationsAPI, + ); + expect(result).toEqual([recentDelegation]); + }); + + it("should handle local storage delegations exactly on the max duration boundary", async () => { + const boundaryDelegation: Delegation = generateMockDelegation( + "hash10", + "staker10", + "provider10", + 10000, + 24, + ); + + const delegationsLocalStorage = [boundaryDelegation]; + const result = await filterDelegationsLocalStorage( + delegationsLocalStorage, + mockDelegationsAPI, + ); + + expect(result).toEqual([boundaryDelegation]); + }); + + it("should handle no API data but local storage items are present", async () => { + (getTxInfo as jest.Mock).mockRejectedValue( + new Error("Transaction not found in the mempool"), + ); + + const result = await filterDelegationsLocalStorage( + mockDelegationsLocalStorage, + [], + ); + + // Expect only delegations not exceeding the max duration to be returned + expect(result).toEqual(notExceedingLimitDelegations); + }); + + it("should handle API data present but no local storage items", async () => { + const result = await filterDelegationsLocalStorage([], mockDelegationsAPI); + + // Expect result to be empty as there are no local storage items to filter + expect(result).toEqual([]); + }); + + it("should handle no API data and no local storage items", async () => { + const result = await filterDelegationsLocalStorage([], []); + expect(result).toEqual([]); + }); +}); diff --git a/tests/utils/local_storage/getDelegationsLocalStorageKey.test.ts b/tests/utils/local_storage/getDelegationsLocalStorageKey.test.ts new file mode 100644 index 0000000..755f497 --- /dev/null +++ b/tests/utils/local_storage/getDelegationsLocalStorageKey.test.ts @@ -0,0 +1,24 @@ +import { getDelegationsLocalStorageKey } from "@/utils/local_storage/getDelegationsLocalStorageKey"; + +describe("utils/local_storage/getDelegationsLocalStorageKey", () => { + it("should return the correct key when pk is provided", () => { + const pk = "someRandomKey"; + const expectedKey = "bbn-staking-delegations-someRandomKey"; + expect(getDelegationsLocalStorageKey(pk)).toBe(expectedKey); + }); + + it("should return an empty string when pk is an empty string", () => { + const pk = ""; + expect(getDelegationsLocalStorageKey(pk)).toBe(""); + }); + + it("should return an empty string when pk is null", () => { + const pk: any = null; + expect(getDelegationsLocalStorageKey(pk)).toBe(""); + }); + + it("should return an empty string when pk is undefined", () => { + const pk: any = undefined; + expect(getDelegationsLocalStorageKey(pk)).toBe(""); + }); +}); diff --git a/tests/utils/local_storage/getIntermediateDelegationsLocalStorageKey.test.ts b/tests/utils/local_storage/getIntermediateDelegationsLocalStorageKey.test.ts new file mode 100644 index 0000000..82f7f58 --- /dev/null +++ b/tests/utils/local_storage/getIntermediateDelegationsLocalStorageKey.test.ts @@ -0,0 +1,24 @@ +import { getIntermediateDelegationsLocalStorageKey } from "@/utils/local_storage/getIntermediateDelegationsLocalStorageKey"; + +describe("utils/local_storage/getIntermediateDelegationsLocalStorageKey", () => { + it("should return the correct key when pk is provided", () => { + const pk = "someRandomKey"; + const expectedKey = "bbn-staking-intermediate-delegations-someRandomKey"; + expect(getIntermediateDelegationsLocalStorageKey(pk)).toBe(expectedKey); + }); + + it("should return an empty string when pk is an empty string", () => { + const pk = ""; + expect(getIntermediateDelegationsLocalStorageKey(pk)).toBe(""); + }); + + it("should return an empty string when pk is null", () => { + const pk: any = null; + expect(getIntermediateDelegationsLocalStorageKey(pk)).toBe(""); + }); + + it("should return an empty string when pk is undefined", () => { + const pk: any = undefined; + expect(getIntermediateDelegationsLocalStorageKey(pk)).toBe(""); + }); +}); diff --git a/tests/utils/local_storage/toLocalStorageDelegation.test.ts b/tests/utils/local_storage/toLocalStorageDelegation.test.ts new file mode 100644 index 0000000..0a26c2f --- /dev/null +++ b/tests/utils/local_storage/toLocalStorageDelegation.test.ts @@ -0,0 +1,44 @@ +import { DelegationState } from "@/app/types/delegations"; +import { toLocalStorageDelegation } from "@/utils/local_storage/toLocalStorageDelegation"; + +describe("utils/local_storage/toLocalStorageDelegation", () => { + it("should create a delegation object with the correct values", () => { + const stakingTxHashHex = "hash1"; + const stakerPkHex = "staker1"; + const finalityProviderPkHex = "provider1"; + const stakingValueSat = 1000; + const stakingTxHex = "txHex1"; + const timelock = 3600; + + const result = toLocalStorageDelegation( + stakingTxHashHex, + stakerPkHex, + finalityProviderPkHex, + stakingValueSat, + stakingTxHex, + timelock, + ); + + expect(result).toEqual({ + stakingTxHashHex, + stakerPkHex, + finalityProviderPkHex, + state: DelegationState.PENDING, + stakingValueSat, + stakingTx: { + txHex: stakingTxHex, + outputIndex: 0, + startTimestamp: expect.any(String), + startHeight: 0, + timelock, + }, + isOverflow: false, + unbondingTx: undefined, + }); + + // Validate startTimestamp is a valid ISO date string + expect(new Date(result.stakingTx.startTimestamp).toISOString()).toBe( + result.stakingTx.startTimestamp, + ); + }); +}); diff --git a/tests/utils/local_storage/toLocalStorageIntermediateDelegation.test.ts b/tests/utils/local_storage/toLocalStorageIntermediateDelegation.test.ts new file mode 100644 index 0000000..c72b065 --- /dev/null +++ b/tests/utils/local_storage/toLocalStorageIntermediateDelegation.test.ts @@ -0,0 +1,88 @@ +import { DelegationState } from "@/app/types/delegations"; +import { toLocalStorageIntermediateDelegation } from "@/utils/local_storage/toLocalStorageIntermediateDelegation"; + +describe("utils/local_storage/toLocalStorageIntermediateDelegation", () => { + it("should create an intermediate unbonding delegation object with the correct values", () => { + const stakingTxHashHex = "hash2"; + const stakerPkHex = "staker2"; + const finalityProviderPkHex = "provider2"; + const stakingValueSat = 2000; + const stakingTxHex = "txHex2"; + const timelock = 7200; + const state = DelegationState.INTERMEDIATE_UNBONDING; + + const result = toLocalStorageIntermediateDelegation( + stakingTxHashHex, + stakerPkHex, + finalityProviderPkHex, + stakingValueSat, + stakingTxHex, + timelock, + state, + ); + + expect(result).toEqual({ + stakingTxHashHex, + stakerPkHex, + finalityProviderPkHex, + state, + stakingValueSat, + stakingTx: { + txHex: stakingTxHex, + outputIndex: 0, + startTimestamp: expect.any(String), + startHeight: 0, + timelock, + }, + isOverflow: false, + unbondingTx: undefined, + }); + + // Validate startTimestamp is a valid ISO date string + expect(new Date(result.stakingTx.startTimestamp).toISOString()).toBe( + result.stakingTx.startTimestamp, + ); + }); + + it("should create an intermediate withdrawal delegation object with the correct values", () => { + const stakingTxHashHex = "hash3"; + const stakerPkHex = "staker3"; + const finalityProviderPkHex = "provider3"; + const stakingValueSat = 3000; + const stakingTxHex = "txHex3"; + const timelock = 14400; + const state = DelegationState.INTERMEDIATE_WITHDRAWAL; + + const result = toLocalStorageIntermediateDelegation( + stakingTxHashHex, + stakerPkHex, + finalityProviderPkHex, + stakingValueSat, + stakingTxHex, + timelock, + state, + ); + + expect(result).toEqual({ + stakingTxHashHex, + stakerPkHex, + finalityProviderPkHex, + state, + stakingValueSat, + stakingTx: { + txHex: stakingTxHex, + outputIndex: 0, + startTimestamp: expect.any(String), + startHeight: 0, + timelock, + }, + isOverflow: false, + unbondingTx: undefined, + }); + + // Validate startTimestamp is a valid ISO date string + expect(new Date(result.stakingTx.startTimestamp).toISOString()).toBe( + result.stakingTx.startTimestamp, + ); + }); +}); From 8b05d4017f4727b946b54bf4f4d2011da776190b Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Fri, 28 Jun 2024 14:12:15 +1000 Subject: [PATCH 38/56] fix: block to fix shall rounded to ceil --- src/utils/blocksToDisplayTime.ts | 4 +++- tests/utils/blocksToDisplayTime.test.ts | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/utils/blocksToDisplayTime.ts b/src/utils/blocksToDisplayTime.ts index 6df678b..f6d7926 100644 --- a/src/utils/blocksToDisplayTime.ts +++ b/src/utils/blocksToDisplayTime.ts @@ -39,7 +39,9 @@ export const blocksToDisplayTime = (blocks: number): string => { // If the difference is greater than or equal to 30 days, return the difference in weeks if (dayDifference >= DAY_TO_WEEK_DISPLAY_THRESHOLD) { // Calculate the difference in weeks - const weeks = differenceInWeeks(endDate, startDate); + const weeks = differenceInWeeks(endDate, startDate, { + roundingMethod: "ceil", + }); const roundedWeeks = Math.round(weeks / WEEKS_PRECISION) * WEEKS_PRECISION; return `${roundedWeeks} weeks`; } diff --git a/tests/utils/blocksToDisplayTime.test.ts b/tests/utils/blocksToDisplayTime.test.ts index 045d086..cac2472 100644 --- a/tests/utils/blocksToDisplayTime.test.ts +++ b/tests/utils/blocksToDisplayTime.test.ts @@ -24,4 +24,8 @@ describe("blocksToDisplayTime", () => { it("should convert 4320 blocks to 5 weeks", () => { expect(blocksToDisplayTime(4320)).toBe("5 weeks"); }); + + it("should convert 63000 blocks to 65 weeks", () => { + expect(blocksToDisplayTime(63000)).toBe("65 weeks"); + }); }); From 085381ac1ead2393c0c2989d3f9b19520f1ea703 Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Fri, 28 Jun 2024 20:14:39 +1000 Subject: [PATCH 39/56] fix: accurate fee estimation and bump simple staking version to 0.2.4 --- package-lock.json | 12 ++++++------ package.json | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 100c8d7..ba29884 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "simple-staking", - "version": "0.1.3", + "version": "0.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "simple-staking", - "version": "0.1.3", + "version": "0.2.3", "dependencies": { "@bitcoinerlab/secp256k1": "^1.1.1", "@keystonehq/animated-qr": "^0.8.6", @@ -17,7 +17,7 @@ "@tanstack/react-query-next-experimental": "^5.28.14", "axios": "^1.6.8", "bitcoinjs-lib": "^6.1.5", - "btc-staking-ts": "^0.2.2", + "btc-staking-ts": "^0.2.7", "date-fns": "^3.6.0", "framer-motion": "^11.1.9", "next": "14.1.3", @@ -6160,9 +6160,9 @@ } }, "node_modules/btc-staking-ts": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/btc-staking-ts/-/btc-staking-ts-0.2.2.tgz", - "integrity": "sha512-h+/AeD5ei/q7LioipROGJExDvnIJ1Oa/1wH4ARt+JglZ019IPcLiKrDhDUk0CNaCL/nWR/U7o2XCW4TckPi2EQ==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/btc-staking-ts/-/btc-staking-ts-0.2.7.tgz", + "integrity": "sha512-AaIrnLZ1oceNW6n33YZXBHmbiq3D7h7Lk29gbl9+fPzrcKdMODXIiD5sPgxw6hazn+PmzJADufnJrOjW+2dXlw==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3", diff --git a/package.json b/package.json index a8c7181..3479b75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-staking", - "version": "0.2.3", + "version": "0.2.4", "private": true, "scripts": { "dev": "next dev", @@ -30,7 +30,7 @@ "@tanstack/react-query-next-experimental": "^5.28.14", "axios": "^1.6.8", "bitcoinjs-lib": "^6.1.5", - "btc-staking-ts": "^0.2.2", + "btc-staking-ts": "^0.2.7", "date-fns": "^3.6.0", "framer-motion": "^11.1.9", "next": "14.1.3", From f95a6a260ae9f83509c89104a6adf0f3304101cc Mon Sep 17 00:00:00 2001 From: Govard Barkhatov Date: Mon, 1 Jul 2024 11:26:09 +0200 Subject: [PATCH 40/56] remove mock dir, move generateMockDelegations to test helper --- .../mockGenerateDelegationsData.tsx | 73 ------------------- .../mock/Delegations/mockGetDelegations.tsx | 40 ---------- tests/helper/generateMockDelegation.ts | 29 ++++++++ .../filterDelegationsLocalStorage.test.ts | 3 +- 4 files changed, 31 insertions(+), 114 deletions(-) delete mode 100644 src/app/mock/Delegations/mockGenerateDelegationsData.tsx delete mode 100644 src/app/mock/Delegations/mockGetDelegations.tsx create mode 100644 tests/helper/generateMockDelegation.ts diff --git a/src/app/mock/Delegations/mockGenerateDelegationsData.tsx b/src/app/mock/Delegations/mockGenerateDelegationsData.tsx deleted file mode 100644 index d0cb83d..0000000 --- a/src/app/mock/Delegations/mockGenerateDelegationsData.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Delegation, DelegationState } from "@/app/types/delegations"; - -function generateRandomHex(size: number) { - return [...Array(size)] - .map(() => Math.floor(Math.random() * 16).toString(16)) - .join(""); -} - -function getRandomState() { - const states = ["completed", "active", "unbonding", "pending"]; - return states[Math.floor(Math.random() * states.length)]; -} - -export function generateRandomDelegationData(count: number): Delegation[] { - const data: Delegation[] = []; - - for (let i = 0; i < count; i++) { - const staking_value = Math.floor(Math.random() * 2000) + 1000; // Random value between 1000 and 3000 - const start_height = Math.floor(Math.random() * 20) + 10; // Random start height between 10 and 30 - const start_timestamp = Math.floor(Math.random() * 100000).toString(); // Random timestamp as string - const is_overflow = Math.random() > 0.5; // Randomly true or false - - data.push({ - stakingTxHashHex: generateRandomHex(32), - stakerPkHex: generateRandomHex(32), - finalityProviderPkHex: generateRandomHex(32), - state: getRandomState(), - stakingValueSat: staking_value, - stakingTx: { - startHeight: start_height, - startTimestamp: start_timestamp, - txHex: generateRandomHex(32), - outputIndex: 2, - timelock: 1, - }, - unbondingTx: { - txHex: generateRandomHex(32), - outputIndex: 2, - }, - isOverflow: is_overflow, - }); - } - - return data; -} - -export function generateMockDelegation( - hash: string, - staker: string, - provider: string, - value: number, - hoursAgo: number, -): Delegation { - const currentTimestamp = new Date().getTime(); - return { - stakingTxHashHex: hash, - stakerPkHex: staker, - finalityProviderPkHex: provider, - state: DelegationState.PENDING, - stakingValueSat: value, - stakingTx: { - txHex: `txHex-${hash}`, - outputIndex: 0, - startTimestamp: new Date( - currentTimestamp - hoursAgo * 60 * 60 * 1000, - ).toISOString(), - startHeight: 0, - timelock: 3600, - }, - isOverflow: false, - unbondingTx: undefined, - }; -} diff --git a/src/app/mock/Delegations/mockGetDelegations.tsx b/src/app/mock/Delegations/mockGetDelegations.tsx deleted file mode 100644 index 480b715..0000000 --- a/src/app/mock/Delegations/mockGetDelegations.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Pagination } from "@/app/types/api"; -import { Delegation } from "@/app/types/delegations"; - -import { generateRandomDelegationData } from "./mockGenerateDelegationsData"; - -const allMockData = generateRandomDelegationData(1000); - -export interface PaginatedDelegations { - data: Delegation[]; - pagination: Pagination; -} - -export const mockGetDelegations = async ( - key: string, - publicKeyNoCoord?: string, -): Promise => { - if (!publicKeyNoCoord) { - throw new Error("No public key provided"); - } - - const pageSize = 100; - const pageIndex = parseInt(key) || 0; - const startIndex = pageIndex * pageSize; - const endIndex = startIndex + pageSize; - - const data = allMockData.slice(startIndex, endIndex); - const nextKey = endIndex >= allMockData.length ? null : pageIndex + 1; - - const sleep = (ms: number) => - new Promise((resolve) => setTimeout(resolve, ms)); - - await sleep(3000); // mock 3 seconds delay - - return { - data, - pagination: { - next_key: nextKey?.toString() || "", - }, - }; -}; diff --git a/tests/helper/generateMockDelegation.ts b/tests/helper/generateMockDelegation.ts new file mode 100644 index 0000000..96e2ced --- /dev/null +++ b/tests/helper/generateMockDelegation.ts @@ -0,0 +1,29 @@ +import { Delegation, DelegationState } from "@/app/types/delegations"; + +export function generateMockDelegation( + hash: string, + staker: string, + provider: string, + value: number, + hoursAgo: number, +): Delegation { + const currentTimestamp = new Date().getTime(); + return { + stakingTxHashHex: hash, + stakerPkHex: staker, + finalityProviderPkHex: provider, + state: DelegationState.PENDING, + stakingValueSat: value, + stakingTx: { + txHex: `txHex-${hash}`, + outputIndex: 0, + startTimestamp: new Date( + currentTimestamp - hoursAgo * 60 * 60 * 1000, + ).toISOString(), + startHeight: 0, + timelock: 3600, + }, + isOverflow: false, + unbondingTx: undefined, + }; +} diff --git a/tests/utils/local_storage/filterDelegationsLocalStorage.test.ts b/tests/utils/local_storage/filterDelegationsLocalStorage.test.ts index 9e897c4..bedbe6f 100644 --- a/tests/utils/local_storage/filterDelegationsLocalStorage.test.ts +++ b/tests/utils/local_storage/filterDelegationsLocalStorage.test.ts @@ -1,8 +1,9 @@ -import { generateMockDelegation } from "@/app/mock/Delegations/mockGenerateDelegationsData"; import { Delegation } from "@/app/types/delegations"; import { filterDelegationsLocalStorage } from "@/utils/local_storage/filterDelegationsLocalStorage"; import { getTxInfo } from "@/utils/mempool_api"; +import { generateMockDelegation } from "../../helper/generateMockDelegation"; + // Mock the getTxInfo function jest.mock("@/utils/mempool_api"); From a9e5da3bb470c480cfbd9eed985d1beaeddee931 Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Wed, 26 Jun 2024 22:36:43 +1000 Subject: [PATCH 41/56] chore: add withdraw unit tests --- package-lock.json | 10 +- .../components/Delegations/Delegations.tsx | 1 - src/utils/delegations/signUnbondingTx.ts | 4 +- src/utils/globalParams.ts | 3 + tests/helper/index.ts | 203 ++++++++++++---- tests/utils/apiDataToStakingScripts.test.ts | 3 +- .../utils/delegations/createStakingTx.test.ts | 85 ++++--- tests/utils/delegations/signStakingTx.test.ts | 38 +-- .../utils/delegations/signUnbondingTx.test.ts | 230 ++++++++++++++++++ 9 files changed, 466 insertions(+), 111 deletions(-) create mode 100644 tests/utils/delegations/signUnbondingTx.test.ts diff --git a/package-lock.json b/package-lock.json index 6f63ff9..4362983 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "simple-staking", - "version": "0.2.1", + "version": "0.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "simple-staking", - "version": "0.2.1", + "version": "0.2.3", "dependencies": { "@bitcoinerlab/secp256k1": "^1.1.1", "@keystonehq/animated-qr": "^0.8.6", @@ -6160,9 +6160,9 @@ } }, "node_modules/btc-staking-ts": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/btc-staking-ts/-/btc-staking-ts-0.2.2.tgz", - "integrity": "sha512-h+/AeD5ei/q7LioipROGJExDvnIJ1Oa/1wH4ARt+JglZ019IPcLiKrDhDUk0CNaCL/nWR/U7o2XCW4TckPi2EQ==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/btc-staking-ts/-/btc-staking-ts-0.2.3.tgz", + "integrity": "sha512-AyX4dRo78VycGpt0LRyZxSvdpwbt00hv9gYVd6N1UVxCoai6gAUYNNURE0ZfCOmfx8cLeYn1iUmBTD6JsUuk4w==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3", diff --git a/src/app/components/Delegations/Delegations.tsx b/src/app/components/Delegations/Delegations.tsx index b64d104..e99c509 100644 --- a/src/app/components/Delegations/Delegations.tsx +++ b/src/app/components/Delegations/Delegations.tsx @@ -99,7 +99,6 @@ export const Delegations: React.FC = ({ const { delegation } = await signUnbondingTx( id, delegationsAPI, - globalParamsVersion, publicKeyNoCoord, btcWalletNetwork, signPsbtTx, diff --git a/src/utils/delegations/signUnbondingTx.ts b/src/utils/delegations/signUnbondingTx.ts index a801e2c..d54eac1 100644 --- a/src/utils/delegations/signUnbondingTx.ts +++ b/src/utils/delegations/signUnbondingTx.ts @@ -6,7 +6,6 @@ import { getUnbondingEligibility } from "@/app/api/getUnbondingEligibility"; import { postUnbonding } from "@/app/api/postUnbonding"; import { SignPsbtTransaction } from "@/app/common/utils/psbt"; import { Delegation as DelegationInterface } from "@/app/types/delegations"; -import { GlobalParamsVersion } from "@/app/types/globalParams"; import { apiDataToStakingScripts } from "@/utils/apiDataToStakingScripts"; import { getCurrentGlobalParamsVersion } from "@/utils/globalParams"; @@ -26,13 +25,12 @@ const getStakerSignature = (unbondingTx: Transaction): string => { export const signUnbondingTx = async ( id: string, delegationsAPI: DelegationInterface[], - globalParamsVersion: GlobalParamsVersion, publicKeyNoCoord: string, btcWalletNetwork: networks.Network, signPsbtTx: SignPsbtTransaction, ): Promise<{ unbondingTxHex: string; delegation: DelegationInterface }> => { // Check if the data is available - if (!delegationsAPI || !globalParamsVersion) { + if (!delegationsAPI) { throw new Error("No back-end API data available"); } diff --git a/src/utils/globalParams.ts b/src/utils/globalParams.ts index 81b5252..118189a 100644 --- a/src/utils/globalParams.ts +++ b/src/utils/globalParams.ts @@ -15,6 +15,9 @@ export const getCurrentGlobalParamsVersion = ( height: number, versionedParams: GlobalParamsVersion[], ): ParamsWithContext => { + if (!versionedParams.length) { + throw new Error("No global params versions found"); + } // Step 1: Sort the versions in descending order based on activationHeight const sorted = versionedParams.sort( (a, b) => b.activationHeight - a.activationHeight, diff --git a/tests/helper/index.ts b/tests/helper/index.ts index cf14d45..f205aba 100644 --- a/tests/helper/index.ts +++ b/tests/helper/index.ts @@ -4,12 +4,23 @@ import { StakingScriptData, StakingScripts } from "btc-staking-ts"; import ECPairFactory from "ecpair"; import { GlobalParamsVersion } from "@/app/types/globalParams"; +import { createStakingTx } from "@/utils/delegations/signStakingTx"; +import { getCurrentGlobalParamsVersion } from "@/utils/globalParams"; import { UTXO } from "@/utils/wallet/wallet_provider"; // Initialize the ECC library bitcoin.initEccLib(ecc); const ECPair = ECPairFactory(ecc); +export const DEFAULT_TEST_FEE_RATE = 10; +export const COMPRESSED_PUBLIC_KEY_HEX_LENGTH = 66; + +export interface KeyPairs { + keyPair: bitcoin.Signer; + publicKey: string; + noCoordPublicKey: string; +} + export class DataGenerator { private network: bitcoin.networks.Network; @@ -25,19 +36,17 @@ export class DataGenerator { return randomBuffer.toString("hex"); }; - generateRandomKeyPair = (isNoCoordPk = true) => { + generateRandomKeyPair = (): KeyPairs => { const keyPair = ECPair.makeRandom({ network: this.network }); const { privateKey, publicKey } = keyPair; if (!privateKey || !publicKey) { throw new Error("Failed to generate random key pair"); } let pk = publicKey.toString("hex"); - - pk = isNoCoordPk ? pk.slice(2) : pk; - return { - privateKey: privateKey.toString("hex"), + keyPair, publicKey: pk, + noCoordPublicKey: pk.slice(2), }; }; @@ -60,7 +69,7 @@ export class DataGenerator { const committe: Buffer[] = []; for (let i = 0; i < size; i++) { // covenant committee PKs are with coordiantes - const keyPair = this.generateRandomKeyPair(false); + const keyPair = this.generateRandomKeyPair(); committe.push(Buffer.from(keyPair.publicKey, "hex")); } return committe; @@ -75,7 +84,10 @@ export class DataGenerator { }; // Generate a single global param - generateRandomGlobalParams = (isFixedTimelock = false) => { + generateRandomGlobalParams = ( + isFixedTimelock = false, + previousPram: GlobalParamsVersion | undefined = undefined, + ) => { const stakingTerm = this.generateRandomStakingTerm(); const committeeSize = Math.floor(Math.random() * 20) + 5; const covenantPks = this.generateRandomCovenantCommittee(committeeSize).map( @@ -96,14 +108,27 @@ export class DataGenerator { const maxStakingAmountSat = minStakingAmountSat + Math.floor(Math.random() * 1000); + const defaultParams = { + version: 0, + activationHeight: 0, + stakingCapSat: 0, + stakingCapHeight: 0, + covenantPks: covenantPks, + covenantQuorum: covenantQuorum, + }; + const prev = previousPram ? previousPram : defaultParams; + return { - version: Math.floor(Math.random() * 100), - activationHeight: Math.floor(Math.random() * 100), - stakingCapSat: Math.floor(Math.random() * 100), - stakingCapHeight: Math.floor(Math.random() * 100), - covenantPks, - covenantQuorum, - unbondingFeeSat: Math.floor(Math.random() * 1000), + version: prev.version ? prev.version + 1 : 0, + activationHeight: + prev.activationHeight + this.getRandomIntegerBetween(0, 10000), + stakingCapSat: + prev.stakingCapSat + this.getRandomIntegerBetween(0, 10000), + stakingCapHeight: + prev.stakingCapHeight + Math.floor(Math.random() * 10000), + covenantPks: prev.covenantPks, + covenantQuorum: prev.covenantQuorum, + unbondingFeeSat: this.getRandomIntegerBetween(0, 100), maxStakingAmountSat, minStakingAmountSat, maxStakingTimeBlocks, @@ -114,34 +139,19 @@ export class DataGenerator { }; }; - getTaprootAddress = (publicKey: string) => { - // Remove the prefix if it exists - if (publicKey.length == 66) { - publicKey = publicKey.slice(2); - } - const internalPubkey = Buffer.from(publicKey, "hex"); - const { address } = bitcoin.payments.p2tr({ - internalPubkey, - network: this.network, - }); - if (!address) { - throw new Error("Failed to generate taproot address from public key"); - } - return address; - }; - - getNativeSegwitAddress = (publicKey: string) => { - const internalPubkey = Buffer.from(publicKey, "hex"); - const { address } = bitcoin.payments.p2wpkh({ - pubkey: internalPubkey, - network: this.network, - }); - if (!address) { - throw new Error( - "Failed to generate native segwit address from public key", + // Generate a list of global params + generateGlobalPramsVersions = ( + numVersions: number, + ): GlobalParamsVersion[] => { + let versions = []; + let lastVersion; + for (let i = 0; i < numVersions; i++) { + const isFixedTimelock = Math.random() > 0.5; + versions.push( + this.generateRandomGlobalParams(isFixedTimelock, lastVersion), ); } - return address; + return versions; }; getNetwork = () => { @@ -189,6 +199,7 @@ export class DataGenerator { generateRandomUTXOs = ( minAvailableBalance: number, numberOfUTXOs: number, + scriptPubKey: string, ): UTXO[] => { const utxos = []; let sum = 0; @@ -196,7 +207,7 @@ export class DataGenerator { utxos.push({ txid: this.generateRandomTxId(), vout: Math.floor(Math.random() * 10), - scriptPubKey: this.generateRandomKeyPair().publicKey, + scriptPubKey: scriptPubKey, value: Math.floor(Math.random() * 9000) + minAvailableBalance, }); sum += utxos[i].value; @@ -221,4 +232,114 @@ export class DataGenerator { } return Math.floor(Math.random() * (max - min + 1)) + min; }; + + getAddressAndScriptPubKey = (publicKey: string) => { + return { + taproot: this.getTaprootAddress(publicKey), + nativeSegwit: this.getNativeSegwitAddress(publicKey), + }; + }; + + createRandomStakingTx = ( + globalParams: GlobalParamsVersion[], + txHeight: number, + stakerKeysPairs?: KeyPairs, + ) => { + const { currentVersion: selectedParam } = getCurrentGlobalParamsVersion( + txHeight, + globalParams, + ); + if (!selectedParam) { + throw new Error("Current version not found"); + } + const stakerKeys = stakerKeysPairs + ? stakerKeysPairs + : this.generateRandomKeyPair(); + const { scriptPubKey, address } = this.getAddressAndScriptPubKey( + stakerKeys.publicKey, + ).nativeSegwit; + const stakingAmount = this.getRandomIntegerBetween( + selectedParam.minStakingAmountSat, + selectedParam.maxStakingAmountSat, + ); + const stakingTerm = this.getRandomIntegerBetween( + selectedParam.minStakingTimeBlocks, + selectedParam.maxStakingTimeBlocks, + ); + const { unsignedStakingPsbt } = createStakingTx( + selectedParam, + stakingAmount, + stakingTerm, + this.generateRandomKeyPair().noCoordPublicKey, // FP key + this.network, + address, + stakerKeys.noCoordPublicKey, // staker public key + DEFAULT_TEST_FEE_RATE, + this.generateRandomUTXOs( + stakingAmount + 1000000, // add some extra satoshis to cover the fee + this.getRandomIntegerBetween(1, 10), + scriptPubKey, + ), + ); + return unsignedStakingPsbt + .signAllInputs(stakerKeys.keyPair) + .finalizeAllInputs() + .extractTransaction(); + }; + + private getTaprootAddress = (publicKey: string) => { + // Remove the prefix if it exists + if (publicKey.length == COMPRESSED_PUBLIC_KEY_HEX_LENGTH) { + publicKey = publicKey.slice(2); + } + const internalPubkey = Buffer.from(publicKey, "hex"); + const { address, output: scriptPubKey } = bitcoin.payments.p2tr({ + internalPubkey, + network: this.network, + }); + if (!address || !scriptPubKey) { + throw new Error( + "Failed to generate taproot address or script from public key", + ); + } + return { + address, + scriptPubKey: scriptPubKey.toString("hex"), + }; + }; + + private getNativeSegwitAddress = (publicKey: string) => { + if (publicKey.length != COMPRESSED_PUBLIC_KEY_HEX_LENGTH) { + throw new Error( + "Invalid public key length for generating native segwit address", + ); + } + const internalPubkey = Buffer.from(publicKey, "hex"); + const { address, output: scriptPubKey } = bitcoin.payments.p2wpkh({ + pubkey: internalPubkey, + network: this.network, + }); + if (!address || !scriptPubKey) { + throw new Error( + "Failed to generate native segwit address or script from public key", + ); + } + return { + address, + scriptPubKey: scriptPubKey.toString("hex"), + }; + }; } + +export const testingNetworks = [ + { + networkName: "mainnet", + network: bitcoin.networks.bitcoin, + dataGenerator: new DataGenerator(bitcoin.networks.bitcoin), + }, + { + networkName: "testnet", + network: bitcoin.networks.testnet, + dataGenerator: new DataGenerator(bitcoin.networks.testnet), + }, +]; diff --git a/tests/utils/apiDataToStakingScripts.test.ts b/tests/utils/apiDataToStakingScripts.test.ts index 725871d..19ccc27 100644 --- a/tests/utils/apiDataToStakingScripts.test.ts +++ b/tests/utils/apiDataToStakingScripts.test.ts @@ -7,7 +7,8 @@ import { DataGenerator } from "../helper"; describe("apiDataToStakingScripts", () => { const dataGen = new DataGenerator(bitcoin.networks.testnet); it("should throw an error if the publicKeyNoCoord is not set", () => { - const { publicKey: finalityProviderPk } = dataGen.generateRandomKeyPair(); + const { noCoordPublicKey: finalityProviderPk } = + dataGen.generateRandomKeyPair(); const stakingTxTimelock = dataGen.generateRandomStakingTerm(); const globalParams = dataGen.generateRandomGlobalParams(); expect(() => { diff --git a/tests/utils/delegations/createStakingTx.test.ts b/tests/utils/delegations/createStakingTx.test.ts index 405f511..773b616 100644 --- a/tests/utils/delegations/createStakingTx.test.ts +++ b/tests/utils/delegations/createStakingTx.test.ts @@ -1,19 +1,19 @@ +import { initBTCCurve } from "btc-staking-ts"; + import { createStakingTx } from "@/utils/delegations/signStakingTx"; -import { toNetwork } from "@/utils/wallet"; -import { Network } from "@/utils/wallet/wallet_provider"; -import { DataGenerator } from "../../helper"; +import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; describe("utils/delegations/createStakingTx", () => { - const networks = [Network.MAINNET, Network.SIGNET]; - networks.forEach((n) => { - const network = toNetwork(n); - const dataGen = new DataGenerator(network); - const randomFpKeys = dataGen.generateRandomKeyPair(true); - const randomUserKeys = dataGen.generateRandomKeyPair(true); - const feeRate = 1; // keep test simple by setting this to unit 1 - + initBTCCurve(); + testingNetworks.forEach(({ network, dataGenerator: dataGen }) => { [true, false].forEach((isFixed) => { + const randomFpKeys = dataGen.generateRandomKeyPair(); + const randomStakerKeys = dataGen.generateRandomKeyPair(); + const feeRate = DEFAULT_TEST_FEE_RATE; + const { address: stakerTaprootAddress, scriptPubKey } = + dataGen.getAddressAndScriptPubKey(randomStakerKeys.publicKey).taproot; + const randomParam = dataGen.generateRandomGlobalParams(isFixed); const randomStakingAmount = dataGen.getRandomIntegerBetween( randomParam.minStakingAmountSat, @@ -24,8 +24,9 @@ describe("utils/delegations/createStakingTx", () => { randomParam.maxStakingTimeBlocks, ); const randomInputUTXOs = dataGen.generateRandomUTXOs( - randomStakingAmount + Math.floor(Math.random() * 1000000), + randomStakingAmount + Math.floor(Math.random() * 100000000), Math.floor(Math.random() * 10) + 1, + scriptPubKey, ); const testTermDescription = isFixed ? "fixed term" : "variable term"; @@ -35,10 +36,10 @@ describe("utils/delegations/createStakingTx", () => { randomParam, randomStakingAmount, randomStakingTimeBlocks, - randomFpKeys.publicKey, + randomFpKeys.noCoordPublicKey, network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, + stakerTaprootAddress, + randomStakerKeys.noCoordPublicKey, feeRate, randomInputUTXOs, ); @@ -57,16 +58,17 @@ describe("utils/delegations/createStakingTx", () => { const utxo = dataGen.generateRandomUTXOs( randomStakingAmount + Math.floor(Math.random() * 1000000), 1, // make it a single utxo so we always have change + scriptPubKey, ); const { stakingFeeSat, stakingTerm, unsignedStakingPsbt } = createStakingTx( randomParam, randomStakingAmount, randomStakingTimeBlocks, - randomFpKeys.publicKey, + randomFpKeys.noCoordPublicKey, network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, + stakerTaprootAddress, + randomStakerKeys.noCoordPublicKey, feeRate, utxo, ); @@ -81,10 +83,7 @@ describe("utils/delegations/createStakingTx", () => { expect(stakingFeeSat).toBeGreaterThan(0); const changeOutput = unsignedStakingPsbt.txOutputs.find((output) => { - return ( - output.address == - dataGen.getTaprootAddress(randomUserKeys.publicKey) - ); + return output.address == stakerTaprootAddress; }); expect(changeOutput).toBeDefined(); }); @@ -95,10 +94,10 @@ describe("utils/delegations/createStakingTx", () => { randomParam, randomParam.minStakingAmountSat - 1, randomStakingTimeBlocks, - randomFpKeys.publicKey, + randomFpKeys.noCoordPublicKey, network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, + stakerTaprootAddress, + randomStakerKeys.noCoordPublicKey, feeRate, randomInputUTXOs, ), @@ -111,10 +110,10 @@ describe("utils/delegations/createStakingTx", () => { randomParam, randomParam.maxStakingAmountSat + 1, randomStakingTimeBlocks, - randomFpKeys.publicKey, + randomFpKeys.noCoordPublicKey, network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, + stakerTaprootAddress, + randomStakerKeys.noCoordPublicKey, feeRate, randomInputUTXOs, ), @@ -127,10 +126,10 @@ describe("utils/delegations/createStakingTx", () => { randomParam, randomStakingAmount, randomStakingTimeBlocks, - randomFpKeys.publicKey, + randomFpKeys.noCoordPublicKey, network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, + stakerTaprootAddress, + randomStakerKeys.noCoordPublicKey, 0, randomInputUTXOs, ), @@ -143,10 +142,10 @@ describe("utils/delegations/createStakingTx", () => { randomParam, randomStakingAmount, randomStakingTimeBlocks, - randomFpKeys.publicKey, + randomFpKeys.noCoordPublicKey, network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, + stakerTaprootAddress, + randomStakerKeys.noCoordPublicKey, feeRate, [], ), @@ -161,10 +160,10 @@ describe("utils/delegations/createStakingTx", () => { randomParam, randomStakingAmount, randomStakingTimeBlocks, - dataGen.generateRandomKeyPair(false).publicKey, // invalid finality provider key + dataGen.generateRandomKeyPair().publicKey, // invalid finality provider key network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, + stakerTaprootAddress, + randomStakerKeys.noCoordPublicKey, feeRate, randomInputUTXOs, ), @@ -179,10 +178,10 @@ describe("utils/delegations/createStakingTx", () => { randomParam, randomStakingAmount, randomParam.minStakingTimeBlocks - 1, - randomFpKeys.publicKey, + randomFpKeys.noCoordPublicKey, network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, + stakerTaprootAddress, + randomStakerKeys.noCoordPublicKey, feeRate, randomInputUTXOs, ), @@ -195,10 +194,10 @@ describe("utils/delegations/createStakingTx", () => { randomParam, randomStakingAmount, randomParam.maxStakingTimeBlocks + 1, - randomFpKeys.publicKey, + randomFpKeys.noCoordPublicKey, network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, + stakerTaprootAddress, + randomStakerKeys.noCoordPublicKey, feeRate, randomInputUTXOs, ), diff --git a/tests/utils/delegations/signStakingTx.test.ts b/tests/utils/delegations/signStakingTx.test.ts index a913ea4..ea1f386 100644 --- a/tests/utils/delegations/signStakingTx.test.ts +++ b/tests/utils/delegations/signStakingTx.test.ts @@ -1,8 +1,8 @@ +import { initBTCCurve } from "btc-staking-ts"; + import { signStakingTx } from "@/utils/delegations/signStakingTx"; -import { toNetwork } from "@/utils/wallet"; -import { Network } from "@/utils/wallet/wallet_provider"; -import { DataGenerator } from "../../helper"; +import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; // Mock the signPsbtTransaction function jest.mock("@/app/common/utils/psbt", () => ({ @@ -10,13 +10,11 @@ jest.mock("@/app/common/utils/psbt", () => ({ })); describe("utils/delegations/signStakingTx", () => { - const networks = [Network.MAINNET, Network.SIGNET]; - networks.forEach((n) => { - const network = toNetwork(n); - const dataGen = new DataGenerator(network); - const randomFpKeys = dataGen.generateRandomKeyPair(true); - const randomUserKeys = dataGen.generateRandomKeyPair(true); - const feeRate = 1; // keep test simple by setting this to unit 1 + initBTCCurve(); + testingNetworks.forEach(({ network, dataGenerator: dataGen }) => { + const randomFpKeys = dataGen.generateRandomKeyPair(); + const randomUserKeys = dataGen.generateRandomKeyPair(); + const feeRate = DEFAULT_TEST_FEE_RATE; const randomParam = dataGen.generateRandomGlobalParams(true); const randomStakingAmount = dataGen.getRandomIntegerBetween( randomParam.minStakingAmountSat, @@ -26,9 +24,15 @@ describe("utils/delegations/signStakingTx", () => { randomParam.minStakingTimeBlocks, randomParam.maxStakingTimeBlocks, ); + const { address: randomTaprootAddress, scriptPubKey } = + dataGen.getAddressAndScriptPubKey( + dataGen.generateRandomKeyPair().publicKey, + ).taproot; + const randomInputUTXOs = dataGen.generateRandomUTXOs( - randomStakingAmount + Math.floor(Math.random() * 1000000), + randomStakingAmount + Math.floor(Math.random() * 100000000), Math.floor(Math.random() * 10) + 1, + scriptPubKey, ); const txHex = dataGen.generateRandomTxId(); @@ -55,10 +59,10 @@ describe("utils/delegations/signStakingTx", () => { randomParam, randomStakingAmount, randomStakingTimeBlocks, - randomFpKeys.publicKey, + randomFpKeys.noCoordPublicKey, network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, + randomTaprootAddress, + randomUserKeys.noCoordPublicKey, feeRate, randomInputUTXOs, ); @@ -86,10 +90,10 @@ describe("utils/delegations/signStakingTx", () => { randomParam, randomStakingAmount, randomStakingTimeBlocks, - randomFpKeys.publicKey, + randomFpKeys.noCoordPublicKey, network, - dataGen.getTaprootAddress(randomUserKeys.publicKey), - randomUserKeys.publicKey, + randomTaprootAddress, + randomUserKeys.noCoordPublicKey, feeRate, randomInputUTXOs, ); diff --git a/tests/utils/delegations/signUnbondingTx.test.ts b/tests/utils/delegations/signUnbondingTx.test.ts new file mode 100644 index 0000000..fa2bbd9 --- /dev/null +++ b/tests/utils/delegations/signUnbondingTx.test.ts @@ -0,0 +1,230 @@ +import { Psbt, Transaction } from "bitcoinjs-lib"; +import { initBTCCurve } from "btc-staking-ts"; + +import { signUnbondingTx } from "@/utils/delegations/signUnbondingTx"; + +import { testingNetworks } from "../../helper"; + +jest.mock("@/app/api/getUnbondingEligibility", () => ({ + getUnbondingEligibility: jest.fn(), +})); +jest.mock("@/app/api/getGlobalParams", () => ({ + getGlobalParams: jest.fn(), +})); +jest.mock("@/app/api/postUnbonding", () => ({ + postUnbonding: jest.fn(), +})); + +describe("signUnbondingTx", () => { + initBTCCurve(); + const { network, dataGenerator } = testingNetworks[0]; + const randomTxId = dataGenerator.generateRandomTxId(); + const randomGlobalParamsVersions = dataGenerator.generateGlobalPramsVersions( + dataGenerator.getRandomIntegerBetween(1, 20), + ); + const stakerKeys = dataGenerator.generateRandomKeyPair(); + const randomStakingTxHeight = + randomGlobalParamsVersions[ + dataGenerator.getRandomIntegerBetween( + 0, + randomGlobalParamsVersions.length - 1, + ) + ].activationHeight + 1; + const randomStakingTx = dataGenerator.createRandomStakingTx( + randomGlobalParamsVersions, + randomStakingTxHeight, + stakerKeys, + ); + const mockedDelegationApi = [ + { + stakingTxHashHex: randomTxId, + finalityProviderPkHex: + dataGenerator.generateRandomKeyPair().noCoordPublicKey, + stakingTx: { + startHeight: randomStakingTxHeight, + timelock: randomStakingTx.locktime, + txHex: randomStakingTx.toHex(), + outputIndex: 0, + }, + }, + ] as any; + const mockedSignPsbtTx = jest + .fn() + .mockImplementation(async (psbtHex: string) => { + const psbt = Psbt.fromHex(psbtHex); + return psbt + .signAllInputs(stakerKeys.keyPair) + .finalizeAllInputs() + .extractTransaction(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should throw an error if no back-end API data is available", async () => { + expect( + signUnbondingTx( + randomTxId, + [], + stakerKeys.noCoordPublicKey, + network, + mockedSignPsbtTx, + ), + ).rejects.toThrow("Delegation not found"); + }); + + it("should throw an error if txId not found in delegationApi", async () => { + const delegationApi = [ + { stakingTxHashHex: dataGenerator.generateRandomTxId() }, + ] as any; + expect( + signUnbondingTx( + randomTxId, + delegationApi, + stakerKeys.noCoordPublicKey, + network, + mockedSignPsbtTx, + ), + ).rejects.toThrow("Delegation not found"); + }); + + it("should throw an error if the stakingTx is not eligible for unbonding", async () => { + const { + getUnbondingEligibility, + } = require("@/app/api/getUnbondingEligibility"); + getUnbondingEligibility.mockImplementationOnce(async () => { + return false; + }); + + expect( + signUnbondingTx( + randomTxId, + mockedDelegationApi, + stakerKeys.noCoordPublicKey, + network, + mockedSignPsbtTx, + ), + ).rejects.toThrow("Not eligible for unbonding"); + }); + + it("should throw an error if global param is not loaded", async () => { + const { + getUnbondingEligibility, + } = require("@/app/api/getUnbondingEligibility"); + getUnbondingEligibility.mockImplementationOnce(async () => { + return true; + }); + + const { getGlobalParams } = require("@/app/api/getGlobalParams"); + getGlobalParams.mockImplementationOnce(async () => { + return []; + }); + + expect( + signUnbondingTx( + randomTxId, + mockedDelegationApi, + stakerKeys.noCoordPublicKey, + network, + mockedSignPsbtTx, + ), + ).rejects.toThrow("No global params versions found"); + }); + + it("should throw an error if the current version is not found", async () => { + const { + getUnbondingEligibility, + } = require("@/app/api/getUnbondingEligibility"); + getUnbondingEligibility.mockImplementationOnce(async () => { + return true; + }); + + const { getGlobalParams } = require("@/app/api/getGlobalParams"); + getGlobalParams.mockImplementationOnce(async () => { + return randomGlobalParamsVersions; + }); + + const firstVersion = randomGlobalParamsVersions[0]; + const delegationApi = [ + { + stakingTxHashHex: randomTxId, + stakingTx: { + // Make height lower than the first version + activationHeight: firstVersion.activationHeight - 1, + }, + }, + ] as any; + expect( + signUnbondingTx( + randomTxId, + delegationApi, + stakerKeys.noCoordPublicKey, + network, + mockedSignPsbtTx, + ), + ).rejects.toThrow("Current version not found"); + }); + + it("should throw error if fail to signPsbtTx", async () => { + const { + getUnbondingEligibility, + } = require("@/app/api/getUnbondingEligibility"); + getUnbondingEligibility.mockImplementationOnce(async () => { + return true; + }); + + const { getGlobalParams } = require("@/app/api/getGlobalParams"); + getGlobalParams.mockImplementationOnce(async () => { + return randomGlobalParamsVersions; + }); + + mockedSignPsbtTx.mockRejectedValueOnce(new Error("oops!")); + + expect( + signUnbondingTx( + randomTxId, + mockedDelegationApi, + stakerKeys.noCoordPublicKey, + network, + mockedSignPsbtTx, + ), + ).rejects.toThrow("Failed to sign PSBT for the unbonding transaction"); + }); + + it("should return the signed unbonding transaction", async () => { + const { + getUnbondingEligibility, + } = require("@/app/api/getUnbondingEligibility"); + getUnbondingEligibility.mockImplementationOnce(async () => { + return true; + }); + + const { getGlobalParams } = require("@/app/api/getGlobalParams"); + getGlobalParams.mockImplementationOnce(async () => { + return randomGlobalParamsVersions; + }); + + const { postUnbonding } = require("@/app/api/postUnbonding"); + postUnbonding.mockImplementationOnce(async () => { + return; + }); + + const { unbondingTxHex, delegation } = await signUnbondingTx( + randomTxId, + mockedDelegationApi, + stakerKeys.noCoordPublicKey, + network, + mockedSignPsbtTx, + ); + const unbondingTx = Transaction.fromHex(unbondingTxHex); + const sig = unbondingTx.ins[0].witness[0].toString("hex"); + const unbondingTxId = unbondingTx.getId(); + expect(postUnbonding).toHaveBeenCalledWith( + sig, + delegation.stakingTxHashHex, + unbondingTxId, + unbondingTxHex, + ); + }); +}); From 6ed5cf93f0301cba973b1d03514837ea34e7eb57 Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Fri, 28 Jun 2024 20:14:39 +1000 Subject: [PATCH 42/56] fix: accurate fee estimation and bump simple staking version to 0.2.4 --- package-lock.json | 12 ++++++------ package.json | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 100c8d7..64feca2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "simple-staking", - "version": "0.1.3", + "version": "0.2.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "simple-staking", - "version": "0.1.3", + "version": "0.2.4", "dependencies": { "@bitcoinerlab/secp256k1": "^1.1.1", "@keystonehq/animated-qr": "^0.8.6", @@ -17,7 +17,7 @@ "@tanstack/react-query-next-experimental": "^5.28.14", "axios": "^1.6.8", "bitcoinjs-lib": "^6.1.5", - "btc-staking-ts": "^0.2.2", + "btc-staking-ts": "^0.2.7", "date-fns": "^3.6.0", "framer-motion": "^11.1.9", "next": "14.1.3", @@ -6160,9 +6160,9 @@ } }, "node_modules/btc-staking-ts": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/btc-staking-ts/-/btc-staking-ts-0.2.2.tgz", - "integrity": "sha512-h+/AeD5ei/q7LioipROGJExDvnIJ1Oa/1wH4ARt+JglZ019IPcLiKrDhDUk0CNaCL/nWR/U7o2XCW4TckPi2EQ==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/btc-staking-ts/-/btc-staking-ts-0.2.7.tgz", + "integrity": "sha512-AaIrnLZ1oceNW6n33YZXBHmbiq3D7h7Lk29gbl9+fPzrcKdMODXIiD5sPgxw6hazn+PmzJADufnJrOjW+2dXlw==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3", diff --git a/package.json b/package.json index a8c7181..3479b75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-staking", - "version": "0.2.3", + "version": "0.2.4", "private": true, "scripts": { "dev": "next dev", @@ -30,7 +30,7 @@ "@tanstack/react-query-next-experimental": "^5.28.14", "axios": "^1.6.8", "bitcoinjs-lib": "^6.1.5", - "btc-staking-ts": "^0.2.2", + "btc-staking-ts": "^0.2.7", "date-fns": "^3.6.0", "framer-motion": "^11.1.9", "next": "14.1.3", From 3b71c6bd71411b166b95498965508a115c959b74 Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Mon, 1 Jul 2024 23:59:16 +1000 Subject: [PATCH 43/56] fix: have enough amount when creating staking tx --- tests/helper/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/helper/index.ts b/tests/helper/index.ts index f205aba..f435368 100644 --- a/tests/helper/index.ts +++ b/tests/helper/index.ts @@ -104,9 +104,11 @@ export class DataGenerator { maxStakingTimeBlocks = minStakingTimeBlocks; } - const minStakingAmountSat = Math.floor(Math.random() * 100); - const maxStakingAmountSat = - minStakingAmountSat + Math.floor(Math.random() * 1000); + const minStakingAmountSat = this.getRandomIntegerBetween(10000, 100000); + const maxStakingAmountSat = this.getRandomIntegerBetween( + minStakingAmountSat, + 100000000, + ); const defaultParams = { version: 0, From e417560038fc3d6ae51e18cc1b1afe0f4f68fb6b Mon Sep 17 00:00:00 2001 From: Govard Barkhatov Date: Tue, 2 Jul 2024 04:31:56 +0200 Subject: [PATCH 44/56] feature: Unable to select FP without data (#306) * unable to select fp without data --- .../FinalityProviders/FinalityProvider.tsx | 28 ++++++++++++++----- .../FinalityProviders/FinalityProviders.tsx | 8 ++++-- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx b/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx index 9ac6c85..a76e297 100644 --- a/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx +++ b/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx @@ -12,7 +12,7 @@ interface FinalityProviderProps { moniker: string; pkHex: string; stakeSat: number; - comission: string; + commission: string; onClick: () => void; selected: boolean; } @@ -21,7 +21,7 @@ export const FinalityProvider: React.FC = ({ moniker, pkHex, stakeSat, - comission, + commission, onClick, selected, }) => { @@ -30,14 +30,26 @@ export const FinalityProvider: React.FC = ({ const { coinName } = getNetworkConfig(); + const finalityProviderHasData = moniker && pkHex && commission; + + const handleClick = () => { + if (finalityProviderHasData) { + onClick(); + } + }; + return (
- {moniker ? ( + {finalityProviderHasData ? (
verified

{moniker}

@@ -76,8 +88,10 @@ export const FinalityProvider: React.FC = ({
-

Comission:

- {moniker ? `${maxDecimals(Number(comission) * 100, 2)}%` : "-"} +

Commission:

+ {finalityProviderHasData + ? `${maxDecimals(Number(commission) * 100, 2)}%` + : "-"} = ({ {finalityProviders?.map((fp) => ( onFinalityProviderChange(fp.btcPk)} + onClick={() => { + onFinalityProviderChange(fp.btcPk); + }} /> ))} From 95f7d2d4c92551eea2798537c6606d5249b537a5 Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Tue, 2 Jul 2024 13:25:08 +1000 Subject: [PATCH 45/56] fix: add op_return value size when calculating fee --- package-lock.json | 12 ++++++------ package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index ba29884..a5d480e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "simple-staking", - "version": "0.2.3", + "version": "0.2.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "simple-staking", - "version": "0.2.3", + "version": "0.2.4", "dependencies": { "@bitcoinerlab/secp256k1": "^1.1.1", "@keystonehq/animated-qr": "^0.8.6", @@ -17,7 +17,7 @@ "@tanstack/react-query-next-experimental": "^5.28.14", "axios": "^1.6.8", "bitcoinjs-lib": "^6.1.5", - "btc-staking-ts": "^0.2.7", + "btc-staking-ts": "^0.2.8", "date-fns": "^3.6.0", "framer-motion": "^11.1.9", "next": "14.1.3", @@ -6160,9 +6160,9 @@ } }, "node_modules/btc-staking-ts": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/btc-staking-ts/-/btc-staking-ts-0.2.7.tgz", - "integrity": "sha512-AaIrnLZ1oceNW6n33YZXBHmbiq3D7h7Lk29gbl9+fPzrcKdMODXIiD5sPgxw6hazn+PmzJADufnJrOjW+2dXlw==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/btc-staking-ts/-/btc-staking-ts-0.2.8.tgz", + "integrity": "sha512-6x1Z/+vVedZ+XqK2KJrQT5Yyy1v01pyePfYh6hk5GsjkHeEv9uhKlb+ZKksKSP6g7qNOl5s5nheZ9ohWlDV9xg==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3", diff --git a/package.json b/package.json index 3479b75..65919bc 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@tanstack/react-query-next-experimental": "^5.28.14", "axios": "^1.6.8", "bitcoinjs-lib": "^6.1.5", - "btc-staking-ts": "^0.2.7", + "btc-staking-ts": "^0.2.8", "date-fns": "^3.6.0", "framer-motion": "^11.1.9", "next": "14.1.3", From 9084caf7f63459155c00bbb5dcf26e25f2735e29 Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Tue, 2 Jul 2024 14:22:17 +1000 Subject: [PATCH 46/56] fix: minor preview modal text change --- src/app/components/Modals/PreviewModal.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/app/components/Modals/PreviewModal.tsx b/src/app/components/Modals/PreviewModal.tsx index ee744b4..41dd666 100644 --- a/src/app/components/Modals/PreviewModal.tsx +++ b/src/app/components/Modals/PreviewModal.tsx @@ -55,7 +55,7 @@ export const PreviewModal: React.FC = ({

{finalityProvider || "-"}

-

Amount

+

Stake Amount

{`${maxDecimals(satoshiToBtc(stakingAmountSat), 8)} ${coinName}`}

@@ -65,9 +65,7 @@ export const PreviewModal: React.FC = ({

{feeRate} sat/vB

-

- Transaction fee amount -

+

Transaction fee

{`${maxDecimals(satoshiToBtc(stakingFeeSat), 8)} ${coinName}`}

From 115b7d2bad4ff589675f83542de9b8cf25313379 Mon Sep 17 00:00:00 2001 From: Jeremy <168515712+jeremy-babylonchain@users.noreply.github.com> Date: Tue, 2 Jul 2024 21:00:15 +0800 Subject: [PATCH 47/56] Hotfix - Error handling of the unbonding & withdraw request failure (#300) * add handler * update error return * update blocksToDisplayTime --- src/app/components/Modals/PreviewModal.tsx | 11 +++-------- src/app/components/Modals/UnbondWithdrawModal.tsx | 7 +++---- src/app/components/Staking/Form/StakingTime.tsx | 9 ++------- src/utils/blocksToDisplayTime.ts | 8 ++++---- src/utils/getState.ts | 2 +- tests/utils/blocksToDisplayTime.test.ts | 4 ++-- 6 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/app/components/Modals/PreviewModal.tsx b/src/app/components/Modals/PreviewModal.tsx index 41dd666..ec66501 100644 --- a/src/app/components/Modals/PreviewModal.tsx +++ b/src/app/components/Modals/PreviewModal.tsx @@ -72,20 +72,15 @@ export const PreviewModal: React.FC = ({

Term

-

- {stakingTimeBlocks ? blocksToDisplayTime(stakingTimeBlocks) : "-"} -

+

{blocksToDisplayTime(stakingTimeBlocks)}

On-demand unbonding

- Enabled ( - {unbondingTimeBlocks - ? blocksToDisplayTime(unbondingTimeBlocks) - : "-"}{" "} - unbonding time) + Enabled ({blocksToDisplayTime(unbondingTimeBlocks)} unbonding + time)

diff --git a/src/app/components/Modals/UnbondWithdrawModal.tsx b/src/app/components/Modals/UnbondWithdrawModal.tsx index bf4953f..12007d9 100644 --- a/src/app/components/Modals/UnbondWithdrawModal.tsx +++ b/src/app/components/Modals/UnbondWithdrawModal.tsx @@ -24,14 +24,13 @@ export const UnbondWithdrawModal: React.FC = ({ mode, }) => { const unbondTitle = "Unbond"; + const unbondContent = ( <> You are about to unbond your stake before its expiration. The expected unbonding time will be about{" "} - - {unbondingTimeBlocks ? blocksToDisplayTime(unbondingTimeBlocks) : "-"} - - .
+ {blocksToDisplayTime(unbondingTimeBlocks)}. +
After unbonded, you will need to use this dashboard to withdraw your stake for it to appear in your wallet. diff --git a/src/app/components/Staking/Form/StakingTime.tsx b/src/app/components/Staking/Form/StakingTime.tsx index a3969d5..43f819d 100644 --- a/src/app/components/Staking/Form/StakingTime.tsx +++ b/src/app/components/Staking/Form/StakingTime.tsx @@ -103,16 +103,11 @@ export const StakingTime: React.FC = ({

You can unbond and withdraw your stake anytime with an unbonding time - of{" "} - {unbondingTimeBlocks ? blocksToDisplayTime(unbondingTimeBlocks) : "-"} - . + of {blocksToDisplayTime(unbondingTimeBlocks)}.

There is also a build-in maximum staking period of{" "} - {minStakingTimeBlocks - ? blocksToDisplayTime(minStakingTimeBlocks) - : "-"} - . + {blocksToDisplayTime(minStakingTimeBlocks)}.

If the stake is not unbonded before the end of this period, it will diff --git a/src/utils/blocksToDisplayTime.ts b/src/utils/blocksToDisplayTime.ts index f6d7926..d43b265 100644 --- a/src/utils/blocksToDisplayTime.ts +++ b/src/utils/blocksToDisplayTime.ts @@ -14,7 +14,7 @@ const DAY_TO_WEEK_DISPLAY_THRESHOLD = 30; * Returns the time in days if the difference is less than 7 days * Otherwise, returns the time in weeks * - * @param {number} blocks - The number of blocks to convert. + * @param {number | undefined} blocks - The number of blocks to convert. * @returns {string} - The converted time in days or weeks. * Rounded to 5 weeks if the difference is greater than 7 days. * @@ -23,9 +23,9 @@ const DAY_TO_WEEK_DISPLAY_THRESHOLD = 30; * blocksToDisplayTime(1); // "1 day" * blocksToDisplayTime(200); // "2 days" */ -export const blocksToDisplayTime = (blocks: number): string => { - // If no blocks are provided, throw an error - if (!blocks) throw new Error("No blocks provided"); +export const blocksToDisplayTime = (blocks: number | undefined): string => { + // If no blocks are provided, return default value + if (!blocks) return "-"; // Calculate the equivalent time in hours const hours = blocks / BLOCKS_PER_HOUR; diff --git a/src/utils/getState.ts b/src/utils/getState.ts index e1b81a5..4e72426 100644 --- a/src/utils/getState.ts +++ b/src/utils/getState.ts @@ -42,7 +42,7 @@ export const getStateTooltip = ( case DelegationState.UNBONDING_REQUESTED: return "Unbonding requested"; case DelegationState.UNBONDING: - return `Unbonding process of ${params ? blocksToDisplayTime(params.unbondingTime) : "-"} has started`; + return `Unbonding process of ${blocksToDisplayTime(params?.unbondingTime)} has started`; case DelegationState.UNBONDED: return "Stake has been unbonded"; case DelegationState.WITHDRAWN: diff --git a/tests/utils/blocksToDisplayTime.test.ts b/tests/utils/blocksToDisplayTime.test.ts index cac2472..149e662 100644 --- a/tests/utils/blocksToDisplayTime.test.ts +++ b/tests/utils/blocksToDisplayTime.test.ts @@ -1,8 +1,8 @@ import { blocksToDisplayTime } from "@/utils/blocksToDisplayTime"; describe("blocksToDisplayTime", () => { - it("should throw error if block is 0", () => { - expect(() => blocksToDisplayTime(0)).toThrow("No blocks provided"); + it("should return '-' if block is 0", () => { + expect(blocksToDisplayTime(0)).toBe("-"); }); it("should convert 1 block to 1 day", () => { From 2671bd5d46dcd92dde9e796f17d7d2b398bc4266 Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Tue, 2 Jul 2024 23:03:18 +1000 Subject: [PATCH 48/56] fix: use virtualSize instead of bytelength --- src/utils/delegations/fee.ts | 32 ++++++++++ src/utils/delegations/signStakingTx.ts | 6 +- src/utils/delegations/signWithdrawalTx.ts | 19 +++++- tests/utils/delegations/fee.test.ts | 61 +++++++++++++++++++ tests/utils/delegations/signStakingTx.test.ts | 3 + 5 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 src/utils/delegations/fee.ts create mode 100644 tests/utils/delegations/fee.test.ts diff --git a/src/utils/delegations/fee.ts b/src/utils/delegations/fee.ts new file mode 100644 index 0000000..8fa0a0a --- /dev/null +++ b/src/utils/delegations/fee.ts @@ -0,0 +1,32 @@ +import { Transaction } from "bitcoinjs-lib"; + +const FEE_TOLERANCE_COEFFICIENT = 2; + +/** + * Performs a safety check on the estimated transaction fee. + * The function calculates the expected transaction fee based on the transaction + * virtual size and the provided fee rate. It then defines an acceptable range + * for the estimated fee using the FEE_TOLERANCE_COEFFICIENT. + * If the estimated fee is outside this acceptable range, an error is thrown + * indicating whether the fee is too high or too low. + * + * @param {Transaction} tx - The Bitcoin transaction object. + * @param {number} feeRate - The fee rate in satoshis per byte. + * @param {number} estimatedFee - The estimated fee for the transaction in satoshis. + * @throws Will throw an error if the estimated fee is too high or too low compared to the calculated fee. + */ +export const txFeeSafetyCheck = ( + tx: Transaction, + feeRate: number, + estimatedFee: number, +) => { + const txFee = tx.virtualSize() * feeRate; + const lowerBound = txFee / FEE_TOLERANCE_COEFFICIENT; + const upperBound = txFee * FEE_TOLERANCE_COEFFICIENT; + + if (estimatedFee > upperBound) { + throw new Error("Estimated fee is too high"); + } else if (estimatedFee < lowerBound) { + throw new Error("Estimated fee is too low"); + } +}; diff --git a/src/utils/delegations/signStakingTx.ts b/src/utils/delegations/signStakingTx.ts index 43e7b00..d0fbb4e 100644 --- a/src/utils/delegations/signStakingTx.ts +++ b/src/utils/delegations/signStakingTx.ts @@ -9,6 +9,8 @@ import { UTXO, WalletProvider } from "@/utils/wallet/wallet_provider"; import { getStakingTerm } from "../getStakingTerm"; +import { txFeeSafetyCheck } from "./fee"; + // Returns: // - unsignedStakingPsbt: the unsigned staking transaction // - stakingTerm: the staking term @@ -105,7 +107,7 @@ export const signStakingTx = async ( inputUTXOs: UTXO[], ): Promise<{ stakingTxHex: string; stakingTerm: number }> => { // Create the staking transaction - let { unsignedStakingPsbt, stakingTerm } = createStakingTx( + let { unsignedStakingPsbt, stakingTerm, stakingFeeSat } = createStakingTx( globalParamsVersion, stakingAmountSat, stakingTimeBlocks, @@ -130,6 +132,8 @@ export const signStakingTx = async ( // Get the staking transaction hex const stakingTxHex = stakingTx.toHex(); + txFeeSafetyCheck(stakingTx, feeRate, stakingFeeSat); + // Broadcast the staking transaction await btcWallet.pushTx(stakingTxHex); diff --git a/src/utils/delegations/signWithdrawalTx.ts b/src/utils/delegations/signWithdrawalTx.ts index 20852a6..962f73c 100644 --- a/src/utils/delegations/signWithdrawalTx.ts +++ b/src/utils/delegations/signWithdrawalTx.ts @@ -12,6 +12,11 @@ import { GlobalParamsVersion } from "@/app/types/globalParams"; import { apiDataToStakingScripts } from "@/utils/apiDataToStakingScripts"; import { getCurrentGlobalParamsVersion } from "@/utils/globalParams"; +import { getFeeRateFromMempool } from "../getFeeRateFromMempool"; +import { Fees } from "../wallet/wallet_provider"; + +import { txFeeSafetyCheck } from "./fee"; + // Sign a withdrawal transaction // Returns: // - withdrawalTx: the signed withdrawal transaction @@ -24,7 +29,7 @@ export const signWithdrawalTx = async ( btcWalletNetwork: networks.Network, signPsbtTx: SignPsbtTransaction, address: string, - getNetworkFees: () => Promise<{ fastestFee: number }>, + getNetworkFees: () => Promise, pushTx: (txHex: string) => Promise, ): Promise<{ withdrawalTxHex: string; @@ -73,6 +78,8 @@ export const signWithdrawalTx = async ( publicKeyNoCoord, ); + const feeRate = getFeeRateFromMempool(fees); + // Create the withdrawal transaction let withdrawPsbtTxResult: PsbtTransactionResult; if (delegation?.unbondingTx) { @@ -85,7 +92,7 @@ export const signWithdrawalTx = async ( Transaction.fromHex(delegation.unbondingTx.txHex), address, btcWalletNetwork, - fees.fastestFee, + feeRate.defaultFeeRate, 0, ); } else { @@ -99,7 +106,7 @@ export const signWithdrawalTx = async ( Transaction.fromHex(delegation.stakingTx.txHex), address, btcWalletNetwork, - fees.fastestFee, + feeRate.defaultFeeRate, delegation.stakingTx.outputIndex, ); } @@ -115,6 +122,12 @@ export const signWithdrawalTx = async ( // Get the withdrawal transaction hex const withdrawalTxHex = withdrawalTx.toHex(); + // Perform a safety check on the estimated transaction fee + txFeeSafetyCheck( + withdrawalTx, + feeRate.defaultFeeRate, + withdrawPsbtTxResult.fee, + ); // Broadcast withdrawal transaction await pushTx(withdrawalTxHex); diff --git a/tests/utils/delegations/fee.test.ts b/tests/utils/delegations/fee.test.ts new file mode 100644 index 0000000..8197bf3 --- /dev/null +++ b/tests/utils/delegations/fee.test.ts @@ -0,0 +1,61 @@ +import { txFeeSafetyCheck } from "@/utils/delegations/fee"; + +import { testingNetworks } from "../../helper"; + +describe("txFeeSafetyCheck", () => { + testingNetworks.map(({ networkName, dataGenerator }) => { + const feeRate = dataGenerator.generateRandomFeeRates(); + const globalParams = dataGenerator.generateGlobalPramsVersions( + dataGenerator.getRandomIntegerBetween(1, 10), + ); + const randomParam = + globalParams[ + dataGenerator.getRandomIntegerBetween(0, globalParams.length - 1) + ]; + const tx = dataGenerator.createRandomStakingTx( + globalParams, + randomParam.activationHeight + 1, + ); + describe(`on ${networkName} - `, () => { + test("should not throw an error if the estimated fee is within the acceptable range", () => { + let estimatedFee = (tx.virtualSize() * feeRate) / 2 + 1; + expect(() => { + txFeeSafetyCheck(tx, feeRate, estimatedFee); + }).not.toThrow(); + + estimatedFee = tx.virtualSize() * feeRate * 2 - 1; + expect(() => { + txFeeSafetyCheck(tx, feeRate, estimatedFee); + }).not.toThrow(); + }); + + test("should throw an error if the estimated fee is too high", () => { + const estimatedFee = tx.virtualSize() * feeRate * 2 + 1; + + expect(() => { + txFeeSafetyCheck(tx, feeRate, estimatedFee); + }).toThrow("Estimated fee is too high"); + }); + + test("should throw an error if the estimated fee is too low", () => { + const estimatedFee = (tx.virtualSize() * feeRate) / 2 - 1; + + expect(() => { + txFeeSafetyCheck(tx, feeRate, estimatedFee); + }).toThrow("Estimated fee is too low"); + }); + + test("should not throw an error if the estimated fee is exactly within the boundary", () => { + let estimatedFee = (tx.virtualSize() * feeRate) / 2; + expect(() => { + txFeeSafetyCheck(tx, feeRate, estimatedFee); + }).not.toThrow(); + + estimatedFee = tx.virtualSize() * feeRate * 2; + expect(() => { + txFeeSafetyCheck(tx, feeRate, estimatedFee); + }).not.toThrow(); + }); + }); + }); +}); diff --git a/tests/utils/delegations/signStakingTx.test.ts b/tests/utils/delegations/signStakingTx.test.ts index ea1f386..8b39960 100644 --- a/tests/utils/delegations/signStakingTx.test.ts +++ b/tests/utils/delegations/signStakingTx.test.ts @@ -8,6 +8,9 @@ import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; jest.mock("@/app/common/utils/psbt", () => ({ signPsbtTransaction: jest.fn(), })); +jest.mock("@/utils/delegations/fee", () => ({ + txFeeSafetyCheck: jest.fn().mockReturnValue(undefined), +})); describe("utils/delegations/signStakingTx", () => { initBTCCurve(); From dd6c975e335efe01bfcc81cc269b731aa4775cdb Mon Sep 17 00:00:00 2001 From: Jeremy <168515712+jeremy-babylonchain@users.noreply.github.com> Date: Tue, 2 Jul 2024 22:02:52 +0800 Subject: [PATCH 49/56] add tooltips on negative pending stake (#311) update remove redundant func --- src/app/components/Stats/Stats.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/app/components/Stats/Stats.tsx b/src/app/components/Stats/Stats.tsx index ea13ed3..f5d3784 100644 --- a/src/app/components/Stats/Stats.tsx +++ b/src/app/components/Stats/Stats.tsx @@ -145,6 +145,11 @@ export const Stats: React.FC = () => { ? `${maxDecimals(satoshiToBtc(stakingStats.unconfirmedTVLSat - stakingStats.activeTVLSat), 8)} ${coinName}` : 0, icon: pendingStake, + tooltip: + stakingStats && + stakingStats.unconfirmedTVLSat - stakingStats.activeTVLSat < 0 + ? "Pending TVL can be negative when there are unbonding requests" + : undefined, }, ], [ From 72d56cec15c19e844f0c979bfa11b9c0c83ab15d Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Wed, 3 Jul 2024 15:27:40 +1000 Subject: [PATCH 50/56] chore: cut release version v0.2.5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a5d480e..052e3e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "simple-staking", - "version": "0.2.4", + "version": "0.2.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "simple-staking", - "version": "0.2.4", + "version": "0.2.5", "dependencies": { "@bitcoinerlab/secp256k1": "^1.1.1", "@keystonehq/animated-qr": "^0.8.6", diff --git a/package.json b/package.json index 65919bc..f645af4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-staking", - "version": "0.2.4", + "version": "0.2.5", "private": true, "scripts": { "dev": "next dev", From 61484d3bc801b3927ca8aebb6e8aa33a0ba54954 Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Mon, 8 Jul 2024 17:19:55 +1000 Subject: [PATCH 51/56] chore: add tx fee FAQ --- package-lock.json | 4 ++-- package.json | 2 +- src/app/components/FAQ/data/questions.ts | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 052e3e7..94ee128 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "simple-staking", - "version": "0.2.5", + "version": "0.2.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "simple-staking", - "version": "0.2.5", + "version": "0.2.6", "dependencies": { "@bitcoinerlab/secp256k1": "^1.1.1", "@keystonehq/animated-qr": "^0.8.6", diff --git a/package.json b/package.json index f645af4..5006124 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-staking", - "version": "0.2.5", + "version": "0.2.6", "private": true, "scripts": { "dev": "next dev", diff --git a/src/app/components/FAQ/data/questions.ts b/src/app/components/FAQ/data/questions.ts index ad049f9..362a792 100644 --- a/src/app/components/FAQ/data/questions.ts +++ b/src/app/components/FAQ/data/questions.ts @@ -59,4 +59,20 @@ export const questions = (coinName: string) => [ content: `

Hands-on stakers can operate the btc-staker CLI program that allows for the creation of ${coinName} staking transactions from the CLI.

`, }, + { + title: "Will I pay any fees for staking?", + content: `

Yes, there are three transaction fees associated with staking, all charged by the Bitcoin network:


+
    +
  1. + 1. Staking Transaction Fee (Fs): This fee is for the staking transaction. To stake amount S, you need at least S + Fs in your wallet. It is calculated in real-time based on current network conditions. +

  2. +
  3. + 2. Unbonding Transaction Fee (Fu): If you unbond your stake before it expires, this fee is deducted from your stake S, resulting in a withdrawable amount of S - Fu. Fu is a calculated static value to ensure inclusion in busy network conditions. +

  4. +
  5. + 3. Withdraw Transaction Fee (Fw): This fee is for the withdrawal transaction that transfers the stake back to your wallet. It is deducted from your withdrawable stake, which is either S (if you wait until expiration) or S - Fu (if unbonded early). This fee ensures fast inclusion based on current network conditions. +
  6. +

+

In summary, to stake S, you need S + Fs, and upon completion, you get S - Fw or S - Fu - Fw back, depending on whether you wait for expiration or unbond early.

`, + }, ]; From 6c17e81518d9b0c57b2e6e3e1dfcaf7e8ff0eec7 Mon Sep 17 00:00:00 2001 From: Jeremy <168515712+jeremy-babylonchain@users.noreply.github.com> Date: Tue, 9 Jul 2024 11:16:48 +0800 Subject: [PATCH 52/56] resolve useEffects warning (#326) --- src/app/page.tsx | 92 +++++++++++++++++++++++++++--------------------- 1 file changed, 51 insertions(+), 41 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 9b245c7..53d8a82 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,7 +3,7 @@ import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { networks } from "bitcoinjs-lib"; import { initBTCCurve } from "btc-staking-ts"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useLocalStorage } from "usehooks-ts"; import { network } from "@/config/network.config"; @@ -194,6 +194,13 @@ const Home: React.FC = () => { hasGlobalParamsVersionError, hasDelegationsError, isRefetchFinalityProvidersError, + finalityProvidersError, + refetchFinalityProvidersData, + delegationsError, + refetchDelegationData, + globalParamsVersionError, + refetchGlobalParamsVersion, + showError, ]); // Initializing btc curve is a required one-time operation @@ -223,48 +230,51 @@ const Home: React.FC = () => { setAddress(""); }; - const handleConnectBTC = async (walletProvider: WalletProvider) => { - // close the modal - setConnectModalOpen(false); + const handleConnectBTC = useCallback( + async (walletProvider: WalletProvider) => { + // close the modal + setConnectModalOpen(false); - try { - await walletProvider.connectWallet(); - const address = await walletProvider.getAddress(); - // check if the wallet address type is supported in babylon - const supported = isSupportedAddressType(address); - if (!supported) { - throw new Error( - "Invalid address type. Please use a Native SegWit or Taproot", - ); - } + try { + await walletProvider.connectWallet(); + const address = await walletProvider.getAddress(); + // check if the wallet address type is supported in babylon + const supported = isSupportedAddressType(address); + if (!supported) { + throw new Error( + "Invalid address type. Please use a Native SegWit or Taproot", + ); + } - const balanceSat = await walletProvider.getBalance(); - const publicKeyNoCoord = getPublicKeyNoCoord( - await walletProvider.getPublicKeyHex(), - ); - setBTCWallet(walletProvider); - setBTCWalletBalanceSat(balanceSat); - setBTCWalletNetwork(toNetwork(await walletProvider.getNetwork())); - setAddress(address); - setPublicKeyNoCoord(publicKeyNoCoord.toString("hex")); - } catch (error: Error | any) { - if ( - error instanceof WalletError && - error.getType() === WalletErrorType.ConnectionCancelled - ) { - // User cancelled the connection, hence do nothing - return; + const balanceSat = await walletProvider.getBalance(); + const publicKeyNoCoord = getPublicKeyNoCoord( + await walletProvider.getPublicKeyHex(), + ); + setBTCWallet(walletProvider); + setBTCWalletBalanceSat(balanceSat); + setBTCWalletNetwork(toNetwork(await walletProvider.getNetwork())); + setAddress(address); + setPublicKeyNoCoord(publicKeyNoCoord.toString("hex")); + } catch (error: Error | any) { + if ( + error instanceof WalletError && + error.getType() === WalletErrorType.ConnectionCancelled + ) { + // User cancelled the connection, hence do nothing + return; + } + showError({ + error: { + message: error.message, + errorState: ErrorState.WALLET, + errorTime: new Date(), + }, + retryAction: () => handleConnectBTC(walletProvider), + }); } - showError({ - error: { - message: error.message, - errorState: ErrorState.WALLET, - errorTime: new Date(), - }, - retryAction: () => handleConnectBTC(walletProvider), - }); - } - }; + }, + [showError], + ); // Subscribe to account changes useEffect(() => { @@ -279,7 +289,7 @@ const Home: React.FC = () => { once = true; }; } - }, [btcWallet]); + }, [btcWallet, handleConnectBTC]); // Clean up the local storage delegations useEffect(() => { From 019a070d13d0871ea0ff610766e20391d81d44e0 Mon Sep 17 00:00:00 2001 From: wjrjerome Date: Mon, 8 Jul 2024 23:20:11 +1000 Subject: [PATCH 53/56] chore: add fee wanring to unbonding and withdraw tx --- .../components/Delegations/Delegations.tsx | 2 +- .../components/Modals/UnbondWithdrawModal.tsx | 28 +++++++++++++++---- src/utils/delegations/signWithdrawalTx.ts | 4 +-- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/app/components/Delegations/Delegations.tsx b/src/app/components/Delegations/Delegations.tsx index e99c509..aae948a 100644 --- a/src/app/components/Delegations/Delegations.tsx +++ b/src/app/components/Delegations/Delegations.tsx @@ -129,7 +129,6 @@ export const Delegations: React.FC = ({ const { delegation } = await signWithdrawalTx( id, delegationsAPI, - globalParamsVersion, publicKeyNoCoord, btcWalletNetwork, signPsbtTx, @@ -284,6 +283,7 @@ export const Delegations: React.FC = ({ {modalMode && txID && ( setModalOpen(false)} onProceed={() => { diff --git a/src/app/components/Modals/UnbondWithdrawModal.tsx b/src/app/components/Modals/UnbondWithdrawModal.tsx index 12007d9..2dd7090 100644 --- a/src/app/components/Modals/UnbondWithdrawModal.tsx +++ b/src/app/components/Modals/UnbondWithdrawModal.tsx @@ -1,6 +1,9 @@ import { IoMdClose } from "react-icons/io"; +import { getNetworkConfig } from "@/config/network.config"; import { blocksToDisplayTime } from "@/utils/blocksToDisplayTime"; +import { satoshiToBtc } from "@/utils/btcConversions"; +import { maxDecimals } from "@/utils/maxDecimals"; import { GeneralModal } from "./GeneralModal"; @@ -10,6 +13,7 @@ export type MODE = typeof MODE_UNBOND | typeof MODE_WITHDRAW; interface PreviewModalProps { unbondingTimeBlocks: number; + unbondingFeeSat: number; open: boolean; onClose: (value: boolean) => void; onProceed: () => void; @@ -18,26 +22,38 @@ interface PreviewModalProps { export const UnbondWithdrawModal: React.FC = ({ unbondingTimeBlocks, + unbondingFeeSat, open, onClose, onProceed, mode, }) => { + const { coinName, networkName } = getNetworkConfig(); + const unbondTitle = "Unbond"; const unbondContent = ( <> - You are about to unbond your stake before its expiration. The expected - unbonding time will be about{" "} - {blocksToDisplayTime(unbondingTimeBlocks)}. -
+ You are about to unbond your stake before its expiration.
A + transaction fee of{" "} + + {maxDecimals(satoshiToBtc(unbondingFeeSat), 8) || 0} {coinName} + {" "} + will be deduced from your stake by the {networkName} network.
+ The expected unbonding time will be about{" "} + {blocksToDisplayTime(unbondingTimeBlocks)}.
After unbonded, you will need to use this dashboard to withdraw your stake for it to appear in your wallet. ); const withdrawTitle = "Withdraw"; - const withdrawContent = "You are about to withdraw your stake."; + const withdrawContent = ( + <> + You are about to withdraw your stake.
A transaction fee will be + deduced from your stake by the {networkName} network + + ); const title = mode === MODE_UNBOND ? unbondTitle : withdrawTitle; const content = mode === MODE_UNBOND ? unbondContent : withdrawContent; @@ -54,7 +70,7 @@ export const UnbondWithdrawModal: React.FC = ({
-

{content}

+

{content}