From a97bddb6cd12c948dad5f487ffcaf4bde168b5ab Mon Sep 17 00:00:00 2001 From: Crypto Minion <154598612+jrwbabylonlab@users.noreply.github.com> Date: Thu, 28 Nov 2024 18:46:33 +1100 Subject: [PATCH] fix: inclusion proof hash shall have reverse order (#405) --- e2e/constants/staking.ts | 2 +- src/app/components/Connect/ConnectSmall.tsx | 2 +- src/app/components/Connect/ConnectedSmall.tsx | 2 +- src/app/components/Delegations/Delegation.tsx | 4 +- src/app/components/Modals/PreviewModal.tsx | 4 +- src/app/components/Stakers/Staker.tsx | 2 +- .../FinalityProviders/FinalityProvider.tsx | 2 +- .../components/Staking/Form/StakingAmount.tsx | 2 +- .../components/Staking/Form/StakingFee.tsx | 2 +- .../components/Staking/Form/StakingTime.tsx | 2 +- src/app/components/Stats/Stats.tsx | 2 +- src/app/components/Summary/Summary.tsx | 2 +- .../hooks/services/useTransactionService.tsx | 26 +++---- .../DelegationList/components/Amount.tsx | 2 +- .../DelegationList/components/Status.tsx | 2 +- .../components/Delegations.tsx | 2 +- src/utils/{btcConversions.ts => btc.ts} | 13 ++++ src/utils/buffer.ts | 17 +++++ src/utils/formatTime.ts | 35 --------- src/utils/mempool_api.ts | 34 ++++++--- src/utils/{blocksToDisplayTime.ts => time.ts} | 36 +++++++++ tests/utils/blocksToDisplayTime.test.ts | 31 -------- tests/utils/btcConvensions.test.ts | 2 +- tests/utils/formatTime.test.ts | 2 +- tests/utils/time.test.ts | 74 +++++++++++++++++++ 25 files changed, 195 insertions(+), 109 deletions(-) rename src/utils/{btcConversions.ts => btc.ts} (51%) create mode 100644 src/utils/buffer.ts delete mode 100644 src/utils/formatTime.ts rename src/utils/{blocksToDisplayTime.ts => time.ts} (70%) delete mode 100644 tests/utils/blocksToDisplayTime.test.ts create mode 100644 tests/utils/time.test.ts diff --git a/e2e/constants/staking.ts b/e2e/constants/staking.ts index 0aaf1bab..bfad4590 100644 --- a/e2e/constants/staking.ts +++ b/e2e/constants/staking.ts @@ -1,4 +1,4 @@ -import { satoshiToBtc } from "@/utils/btcConversions"; +import { satoshiToBtc } from "@/utils/btc"; export const STAKING_AMOUNT_SAT = 50000; export const STAKING_AMOUNT_BTC = satoshiToBtc(STAKING_AMOUNT_SAT); diff --git a/src/app/components/Connect/ConnectSmall.tsx b/src/app/components/Connect/ConnectSmall.tsx index 128cb054..81c4ac1f 100644 --- a/src/app/components/Connect/ConnectSmall.tsx +++ b/src/app/components/Connect/ConnectSmall.tsx @@ -9,7 +9,7 @@ import { useOnClickOutside } from "usehooks-ts"; import { useHealthCheck } from "@/app/hooks/useHealthCheck"; import { useAppState } from "@/app/state"; import { getNetworkConfig } from "@/config/network.config"; -import { satoshiToBtc } from "@/utils/btcConversions"; +import { satoshiToBtc } from "@/utils/btc"; import { maxDecimals } from "@/utils/maxDecimals"; import { trim } from "@/utils/trim"; diff --git a/src/app/components/Connect/ConnectedSmall.tsx b/src/app/components/Connect/ConnectedSmall.tsx index 9f5661ac..429164fa 100644 --- a/src/app/components/Connect/ConnectedSmall.tsx +++ b/src/app/components/Connect/ConnectedSmall.tsx @@ -5,7 +5,7 @@ import { useOnClickOutside } from "usehooks-ts"; import { useAppState } from "@/app/state"; import { getNetworkConfig } from "@/config/network.config"; -import { satoshiToBtc } from "@/utils/btcConversions"; +import { satoshiToBtc } from "@/utils/btc"; import { maxDecimals } from "@/utils/maxDecimals"; import { trim } from "@/utils/trim"; diff --git a/src/app/components/Delegations/Delegation.tsx b/src/app/components/Delegations/Delegation.tsx index 9ca81c28..971e7e89 100644 --- a/src/app/components/Delegations/Delegation.tsx +++ b/src/app/components/Delegations/Delegation.tsx @@ -15,10 +15,10 @@ import { } from "@/app/types/delegations"; import { shouldDisplayPoints } from "@/config"; import { getNetworkConfig } from "@/config/network.config"; -import { satoshiToBtc } from "@/utils/btcConversions"; -import { durationTillNow } from "@/utils/formatTime"; +import { satoshiToBtc } from "@/utils/btc"; import { getState, getStateTooltip } from "@/utils/getState"; import { maxDecimals } from "@/utils/maxDecimals"; +import { durationTillNow } from "@/utils/time"; import { trim } from "@/utils/trim"; import { DelegationPoints } from "../Points/DelegationPoints"; diff --git a/src/app/components/Modals/PreviewModal.tsx b/src/app/components/Modals/PreviewModal.tsx index 060d31d4..8146b778 100644 --- a/src/app/components/Modals/PreviewModal.tsx +++ b/src/app/components/Modals/PreviewModal.tsx @@ -14,9 +14,9 @@ import { import { useNetworkInfo } from "@/app/hooks/api/useNetworkInfo"; import { useIsMobileView } from "@/app/hooks/useBreakpoint"; import { getNetworkConfig } from "@/config/network.config"; -import { blocksToDisplayTime } from "@/utils/blocksToDisplayTime"; -import { satoshiToBtc } from "@/utils/btcConversions"; +import { satoshiToBtc } from "@/utils/btc"; import { maxDecimals } from "@/utils/maxDecimals"; +import { blocksToDisplayTime } from "@/utils/time"; interface PreviewModalProps { isOpen: boolean; diff --git a/src/app/components/Stakers/Staker.tsx b/src/app/components/Stakers/Staker.tsx index 41db1ef1..b9aa8459 100644 --- a/src/app/components/Stakers/Staker.tsx +++ b/src/app/components/Stakers/Staker.tsx @@ -1,5 +1,5 @@ import { getNetworkConfig } from "@/config/network.config"; -import { satoshiToBtc } from "@/utils/btcConversions"; +import { satoshiToBtc } from "@/utils/btc"; import { maxDecimals } from "@/utils/maxDecimals"; import { Hash } from "../Hash/Hash"; diff --git a/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx b/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx index 4a6069f4..9b0379e3 100644 --- a/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx +++ b/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx @@ -8,7 +8,7 @@ import blue from "@/app/assets/blue-check.svg"; import { Hash } from "@/app/components/Hash/Hash"; import { FinalityProviderState } from "@/app/types/finalityProviders"; import { getNetworkConfig } from "@/config/network.config"; -import { satoshiToBtc } from "@/utils/btcConversions"; +import { satoshiToBtc } from "@/utils/btc"; import { maxDecimals } from "@/utils/maxDecimals"; interface FinalityProviderProps { diff --git a/src/app/components/Staking/Form/StakingAmount.tsx b/src/app/components/Staking/Form/StakingAmount.tsx index 9da311e2..55a53a12 100644 --- a/src/app/components/Staking/Form/StakingAmount.tsx +++ b/src/app/components/Staking/Form/StakingAmount.tsx @@ -1,7 +1,7 @@ import { ChangeEvent, FocusEvent, useEffect, useState } from "react"; import { getNetworkConfig } from "@/config/network.config"; -import { btcToSatoshi, satoshiToBtc } from "@/utils/btcConversions"; +import { btcToSatoshi, satoshiToBtc } from "@/utils/btc"; import { maxDecimals } from "@/utils/maxDecimals"; import { validateDecimalPoints } from "./validation/validation"; diff --git a/src/app/components/Staking/Form/StakingFee.tsx b/src/app/components/Staking/Form/StakingFee.tsx index bb02e6e4..df994a27 100644 --- a/src/app/components/Staking/Form/StakingFee.tsx +++ b/src/app/components/Staking/Form/StakingFee.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { getNetworkConfig } from "@/config/network.config"; -import { satoshiToBtc } from "@/utils/btcConversions"; +import { satoshiToBtc } from "@/utils/btc"; import { getFeeRateFromMempool } from "@/utils/getFeeRateFromMempool"; import { Fees } from "@/utils/wallet/btc_wallet_provider"; diff --git a/src/app/components/Staking/Form/StakingTime.tsx b/src/app/components/Staking/Form/StakingTime.tsx index 43f819de..746fd418 100644 --- a/src/app/components/Staking/Form/StakingTime.tsx +++ b/src/app/components/Staking/Form/StakingTime.tsx @@ -1,7 +1,7 @@ import { ChangeEvent, FocusEvent, useEffect, useState } from "react"; import { getNetworkConfig } from "@/config/network.config"; -import { blocksToDisplayTime } from "@/utils/blocksToDisplayTime"; +import { blocksToDisplayTime } from "@/utils/time"; import { validateNoDecimalPoints } from "./validation/validation"; diff --git a/src/app/components/Stats/Stats.tsx b/src/app/components/Stats/Stats.tsx index 4e757452..fa77d464 100644 --- a/src/app/components/Stats/Stats.tsx +++ b/src/app/components/Stats/Stats.tsx @@ -3,7 +3,7 @@ import { memo } from "react"; import { useSystemStats } from "@/app/hooks/api/useSystemStats"; import { getNetworkConfig } from "@/config/network.config"; -import { satoshiToBtc } from "@/utils/btcConversions"; +import { satoshiToBtc } from "@/utils/btc"; import { maxDecimals } from "@/utils/maxDecimals"; import { StatItem } from "./StatItem"; diff --git a/src/app/components/Summary/Summary.tsx b/src/app/components/Summary/Summary.tsx index 4ad19407..abf96098 100644 --- a/src/app/components/Summary/Summary.tsx +++ b/src/app/components/Summary/Summary.tsx @@ -9,7 +9,7 @@ import { useAppState } from "@/app/state"; import { useDelegationState } from "@/app/state/DelegationState"; import { shouldDisplayPoints } from "@/config"; import { getNetworkConfig } from "@/config/network.config"; -import { satoshiToBtc } from "@/utils/btcConversions"; +import { satoshiToBtc } from "@/utils/btc"; import { maxDecimals } from "@/utils/maxDecimals"; import { Network } from "@/utils/wallet/btc_wallet_provider"; diff --git a/src/app/hooks/services/useTransactionService.tsx b/src/app/hooks/services/useTransactionService.tsx index 033f3377..6083ca5a 100644 --- a/src/app/hooks/services/useTransactionService.tsx +++ b/src/app/hooks/services/useTransactionService.tsx @@ -18,13 +18,15 @@ import { useBTCWallet } from "@/app/context/wallet/BTCWalletProvider"; import { useCosmosWallet } from "@/app/context/wallet/CosmosWalletProvider"; import { useAppState } from "@/app/state"; import { BbnStakingParamsVersion, Params } from "@/app/types/networkInfo"; +import { deriveMerkleProof } from "@/utils/btc"; +import { reverseBuffer } from "@/utils/buffer"; import { clearTxSignatures, extractSchnorrSignaturesFromTransaction, uint8ArrayToHex, } from "@/utils/delegations"; import { getFeeRateFromMempool } from "@/utils/getFeeRateFromMempool"; -import { getTxInfo, getTxMerkleProof, MerkleProof } from "@/utils/mempool_api"; +import { getTxInfo, getTxMerkleProof } from "@/utils/mempool_api"; import { useNetworkFees } from "../api/useNetworkFees"; @@ -742,27 +744,25 @@ const checkWalletConnection = ( const getInclusionProof = async ( stakingTx: Transaction, ): Promise => { + // TODO: Use the hook instead // Get the merkle proof - let txMerkleProof: MerkleProof; - try { - // TODO: Use the hook instead - txMerkleProof = await getTxMerkleProof(stakingTx.getId()); - } catch (err) { - throw new Error("Failed to get the merkle proof", { cause: err }); - } + const { pos, merkle } = await getTxMerkleProof(stakingTx.getId()); + const proofHex = deriveMerkleProof(merkle); + // TODO: Use the hook instead - const txInfo = await getTxInfo(stakingTx.getId()); - const blockHash = txInfo.status.block_hash; + const { + status: { blockHash }, + } = await getTxInfo(stakingTx.getId()); - const hash = Uint8Array.from(Buffer.from(blockHash, "hex")); + const hash = reverseBuffer(Uint8Array.from(Buffer.from(blockHash, "hex"))); const inclusionProofKey: btccheckpoint.TransactionKey = btccheckpoint.TransactionKey.fromPartial({ - index: txMerkleProof.pos, + index: pos, hash, }); return btcstaking.InclusionProof.fromPartial({ key: inclusionProofKey, - proof: Uint8Array.from(Buffer.from(txMerkleProof.proofHex, "hex")), + proof: Uint8Array.from(Buffer.from(proofHex, "hex")), }); }; diff --git a/src/components/delegations/DelegationList/components/Amount.tsx b/src/components/delegations/DelegationList/components/Amount.tsx index b092379d..a20d33d9 100644 --- a/src/components/delegations/DelegationList/components/Amount.tsx +++ b/src/components/delegations/DelegationList/components/Amount.tsx @@ -1,7 +1,7 @@ import { FaBitcoin } from "react-icons/fa"; import { getNetworkConfig } from "@/config/network.config"; -import { satoshiToBtc } from "@/utils/btcConversions"; +import { satoshiToBtc } from "@/utils/btc"; import { maxDecimals } from "@/utils/maxDecimals"; interface Amount { diff --git a/src/components/delegations/DelegationList/components/Status.tsx b/src/components/delegations/DelegationList/components/Status.tsx index 125b1d4d..264207c7 100644 --- a/src/components/delegations/DelegationList/components/Status.tsx +++ b/src/components/delegations/DelegationList/components/Status.tsx @@ -2,7 +2,7 @@ import { useAppState } from "@/app/state"; import { DelegationV2StakingState as state } from "@/app/types/delegationsV2"; import { BbnStakingParamsVersion } from "@/app/types/networkInfo"; import { Hint } from "@/components/common/Hint"; -import { blocksToDisplayTime } from "@/utils/blocksToDisplayTime"; +import { blocksToDisplayTime } from "@/utils/time"; interface StatusProps { value: string; diff --git a/src/components/staking/FinalityProviders/components/Delegations.tsx b/src/components/staking/FinalityProviders/components/Delegations.tsx index 52891e31..aabef6d5 100644 --- a/src/components/staking/FinalityProviders/components/Delegations.tsx +++ b/src/components/staking/FinalityProviders/components/Delegations.tsx @@ -1,6 +1,6 @@ import { Hint } from "@/components/common/Hint"; import { getNetworkConfig } from "@/config/network.config"; -import { satoshiToBtc } from "@/utils/btcConversions"; +import { satoshiToBtc } from "@/utils/btc"; import { maxDecimals } from "@/utils/maxDecimals"; interface DelegationProps { diff --git a/src/utils/btcConversions.ts b/src/utils/btc.ts similarity index 51% rename from src/utils/btcConversions.ts rename to src/utils/btc.ts index 4cb34798..0a946563 100644 --- a/src/utils/btcConversions.ts +++ b/src/utils/btc.ts @@ -17,3 +17,16 @@ export function satoshiToBtc(satoshi: number): number { export function btcToSatoshi(btc: number): number { return Math.round(btc * 1e8); } + +/** + * Derives the merkle proof from the list of hex strings. Note the + * sibling hashes are reversed from hex before concatenation. + * @param merkle - The merkle proof hex strings. + * @returns The merkle proof in hex string format. + */ +export const deriveMerkleProof = (merkle: string[]) => { + const proofHex = merkle.reduce((acc: string, m: string) => { + return acc + Buffer.from(m, "hex").reverse().toString("hex"); + }, ""); + return proofHex; +}; diff --git a/src/utils/buffer.ts b/src/utils/buffer.ts new file mode 100644 index 00000000..2c053d0c --- /dev/null +++ b/src/utils/buffer.ts @@ -0,0 +1,17 @@ +/** + * Reverses the order of bytes in a buffer. + * @param buffer - The buffer to reverse. + * @returns A new buffer with the bytes reversed. + */ +export const reverseBuffer = (buffer: Uint8Array): Uint8Array => { + if (buffer.length < 1) return buffer; + let j = buffer.length - 1; + let tmp = 0; + for (let i = 0; i < buffer.length / 2; i++) { + tmp = buffer[i]; + buffer[i] = buffer[j]; + buffer[j] = tmp; + j--; + } + return buffer; +}; diff --git a/src/utils/formatTime.ts b/src/utils/formatTime.ts deleted file mode 100644 index 357ad954..00000000 --- a/src/utils/formatTime.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { formatDuration, intervalToDuration } from "date-fns"; - -interface Duration { - years?: number; - months?: number; - weeks?: number; - days?: number; - hours?: number; - minutes?: number; - seconds?: number; -} - -export const durationTillNow = (time: string, currentTime: number) => { - if (!time || time.startsWith("000")) return "Ongoing"; - - const duration = intervalToDuration({ - end: currentTime, - start: new Date(time), - }); - let format: (keyof Duration)[] = ["days", "hours", "minutes"]; - - if (!duration.days && !duration.hours && !duration.minutes) { - format.push("seconds"); - } - - const formattedTime = formatDuration(duration, { - format, - }); - - if (formattedTime) { - return `${formattedTime} ago`; - } else { - return "Just now"; - } -}; diff --git a/src/utils/mempool_api.ts b/src/utils/mempool_api.ts index 58c50702..3f892170 100644 --- a/src/utils/mempool_api.ts +++ b/src/utils/mempool_api.ts @@ -6,7 +6,7 @@ const { mempoolApiUrl } = getNetworkConfig(); export interface MerkleProof { blockHeight: number; - proofHex: string; + merkle: string[]; pos: number; } @@ -21,9 +21,9 @@ interface TxInfo { fee: number; status: { confirmed: boolean; - block_height: number; - block_hash: string; - block_time: number; + blockHeight: number; + blockHash: string; + blockTime: number; }; } @@ -243,7 +243,24 @@ export async function getTxInfo(txId: string): Promise { const err = await response.text(); throw new ServerError(err, response.status); } - return await response.json(); + const { txid, version, locktime, vin, vout, size, weight, fee, status } = + await response.json(); + return { + txid, + version, + locktime, + vin, + vout, + size, + weight, + fee, + status: { + confirmed: status.confirmed, + blockHeight: status.block_height, + blockHash: status.block_hash, + blockTime: status.block_time, + }, + }; } /** @@ -267,15 +284,10 @@ export async function getTxMerkleProof(txId: string): Promise { if (!block_height || !merkle.length || !pos) { throw new Error("Invalid transaction merkle proof result returned"); } - // The merkle proof from mempool is a list of hex strings, - // so we need to concatenate them to form the final proof - const proofHex = merkle.reduce((acc: string, m: string) => { - return acc + m; - }, ""); return { blockHeight: block_height, - proofHex, + merkle, pos, }; } diff --git a/src/utils/blocksToDisplayTime.ts b/src/utils/time.ts similarity index 70% rename from src/utils/blocksToDisplayTime.ts rename to src/utils/time.ts index d43b265b..32a89f14 100644 --- a/src/utils/blocksToDisplayTime.ts +++ b/src/utils/time.ts @@ -3,6 +3,8 @@ import { differenceInCalendarDays, differenceInWeeks, formatDistanceStrict, + formatDuration, + intervalToDuration, } from "date-fns"; const BLOCKS_PER_HOUR = 6; @@ -52,3 +54,37 @@ export const blocksToDisplayTime = (blocks: number | undefined): string => { roundingMethod: "ceil", }); }; + +interface Duration { + years?: number; + months?: number; + weeks?: number; + days?: number; + hours?: number; + minutes?: number; + seconds?: number; +} + +export const durationTillNow = (time: string, currentTime: number) => { + if (!time || time.startsWith("000")) return "Ongoing"; + + const duration = intervalToDuration({ + end: currentTime, + start: new Date(time), + }); + let format: (keyof Duration)[] = ["days", "hours", "minutes"]; + + if (!duration.days && !duration.hours && !duration.minutes) { + format.push("seconds"); + } + + const formattedTime = formatDuration(duration, { + format, + }); + + if (formattedTime) { + return `${formattedTime} ago`; + } else { + return "Just now"; + } +}; diff --git a/tests/utils/blocksToDisplayTime.test.ts b/tests/utils/blocksToDisplayTime.test.ts deleted file mode 100644 index 149e662d..00000000 --- a/tests/utils/blocksToDisplayTime.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { blocksToDisplayTime } from "@/utils/blocksToDisplayTime"; - -describe("blocksToDisplayTime", () => { - it("should return '-' if block is 0", () => { - expect(blocksToDisplayTime(0)).toBe("-"); - }); - - 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"); - }); - - it("should convert 63000 blocks to 65 weeks", () => { - expect(blocksToDisplayTime(63000)).toBe("65 weeks"); - }); -}); diff --git a/tests/utils/btcConvensions.test.ts b/tests/utils/btcConvensions.test.ts index 223e34f8..d85fd3c0 100644 --- a/tests/utils/btcConvensions.test.ts +++ b/tests/utils/btcConvensions.test.ts @@ -1,4 +1,4 @@ -import { btcToSatoshi, satoshiToBtc } from "@/utils/btcConversions"; +import { btcToSatoshi, satoshiToBtc } from "@/utils/btc"; describe("satoshiToBtc", () => { it("should convert 0 satoshis to 0 BTC", () => { diff --git a/tests/utils/formatTime.test.ts b/tests/utils/formatTime.test.ts index d1d261e7..5d0137a2 100644 --- a/tests/utils/formatTime.test.ts +++ b/tests/utils/formatTime.test.ts @@ -1,4 +1,4 @@ -import { durationTillNow } from "@/utils/formatTime"; +import { durationTillNow } from "@/utils/time"; describe("durationTillNow", () => { const currentTime = new Date("2024-01-01T12:00:00Z").getTime(); diff --git a/tests/utils/time.test.ts b/tests/utils/time.test.ts new file mode 100644 index 00000000..e8dd5f27 --- /dev/null +++ b/tests/utils/time.test.ts @@ -0,0 +1,74 @@ +import { blocksToDisplayTime, durationTillNow } from "@/utils/time"; + +describe("blocksToDisplayTime", () => { + it("should return '-' if block is 0", () => { + expect(blocksToDisplayTime(0)).toBe("-"); + }); + + 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"); + }); + + it("should convert 63000 blocks to 65 weeks", () => { + expect(blocksToDisplayTime(63000)).toBe("65 weeks"); + }); +}); + +describe("durationTillNow", () => { + const currentTime = new Date("2024-01-01T12:00:00Z").getTime(); + + it('should return "Ongoing" if time is empty', () => { + expect(durationTillNow("", currentTime)).toBe("Ongoing"); + }); + + it('should return "Ongoing" if time starts with "000"', () => { + expect(durationTillNow("0000-00-00T00:00:00Z", currentTime)).toBe( + "Ongoing", + ); + }); + + it("should return the correct duration in days, hours, and minutes", () => { + const pastTime = new Date("2023-12-31T10:00:00Z").toISOString(); + expect(durationTillNow(pastTime, currentTime)).toBe("1 day 2 hours ago"); + }); + + it("should return the correct duration in hours", () => { + const pastTime = new Date("2024-01-01T10:00:00Z").toISOString(); + expect(durationTillNow(pastTime, currentTime)).toBe("2 hours ago"); + }); + + it("should return the correct duration in minutes", () => { + const pastTime = new Date("2024-01-01T11:50:00Z").toISOString(); + expect(durationTillNow(pastTime, currentTime)).toBe("10 minutes ago"); + }); + + it("should return the correct duration in seconds if less than a minute", () => { + const pastTime = new Date("2024-01-01T11:59:30Z").toISOString(); + expect(durationTillNow(pastTime, currentTime)).toBe("30 seconds ago"); + }); + + it('should return "Just now" if the duration is less than a second', () => { + let pastTime = new Date("2024-01-01T12:00:00Z").toISOString(); + expect(durationTillNow(pastTime, currentTime)).toBe("Just now"); + + // test the ms + pastTime = new Date("2024-01-01T11:59:59.999Z").toISOString(); + expect(durationTillNow(pastTime, currentTime)).toBe("Just now"); + }); +});