web/src/derive.ts49 lines · 2363 bytes
| 1 | // Pure helpers for numbers on screen. No DOM, no URL, so the tests run them in Node. |
| 2 | import { formatEther, formatGwei } from 'viem' |
| 3 | import type { TxCost } from './store' |
| 4 | import { toBig } from './types' |
| 5 | |
| 6 | const HEX = /^0x[0-9a-fA-F]+$/ |
| 7 | |
| 8 | /** Explorer link; null without a prefix (local fork, offline) or when the value is not hex */ |
| 9 | export function explorerUrl(base: string | null | undefined, value: string | null | undefined): string | null { |
| 10 | if (!base || !value || !HEX.test(value)) return null |
| 11 | return base + value |
| 12 | } |
| 13 | |
| 14 | /** gasUsed x effectiveGasPrice from a receipt; null when a field is missing (never a made-up default) */ |
| 15 | export function txCost(r: TxCost | undefined): { gasUsed: bigint; priceWei: bigint; costWei: bigint } | null { |
| 16 | const gasUsed = toBig(r?.gasUsed) |
| 17 | const priceWei = toBig(r?.effectiveGasPrice) |
| 18 | if (gasUsed === null || priceWei === null) return null |
| 19 | return { gasUsed, priceWei, costWei: gasUsed * priceWei } |
| 20 | } |
| 21 | |
| 22 | /** 4 significant digits with thousands separators (12345.6 -> 12,350; 0.0001234 -> 0.0001234) */ |
| 23 | const sig4 = (x: number): string => x.toLocaleString('en-US', { maximumSignificantDigits: 4 }) |
| 24 | |
| 25 | /** Fees on Ethereum: Sepolia gas prices can be a few dozen wei, so small amounts stay in wei, then gwei / ETH */ |
| 26 | export function fmtEth(wei: bigint): string { |
| 27 | if (wei < 1_000_000_000n) return `${wei.toLocaleString('en-US')} wei` |
| 28 | if (wei < 1_000_000_000_000_000n) return `${sig4(Number(formatGwei(wei)))} gwei` |
| 29 | return `${sig4(Number(formatEther(wei)))} ETH` |
| 30 | } |
| 31 | |
| 32 | /** Gas price: below 0.001 gwei it is written in wei */ |
| 33 | export function fmtGasPrice(wei: bigint): string { |
| 34 | if (wei < 1_000_000n) return `${wei.toLocaleString('en-US')} wei` |
| 35 | return `${sig4(Number(formatGwei(wei)))} gwei` |
| 36 | } |
| 37 | |
| 38 | /** A measured block time is believable (a fast-mined fork measures close to 0) */ |
| 39 | export function plausibleBlockMs(bt: number | null, minMs: number): number | null { |
| 40 | return bt !== null && bt >= minMs ? bt : null |
| 41 | } |
| 42 | |
| 43 | /** The workflow id carried by a Verdict, or null when it is not a real one: the CRE CLI simulator writes a |
| 44 | * placeholder (0x1111…1111), and a zero or repeated-digit id says nothing about which workflow ran */ |
| 45 | export function realWorkflowId(w: string | null | undefined): string | null { |
| 46 | if (!w || !/^0x[0-9a-fA-F]{64}$/.test(w)) return null |
| 47 | if (/^0x([0-9a-fA-F])\1{63}$/.test(w)) return null |
| 48 | return w |
| 49 | } |