From e33a3dd00a5ca0aa63bc55ac75f95775fb8d367e Mon Sep 17 00:00:00 2001 From: ultraviolet10 Date: Mon, 19 May 2025 19:46:58 +0530 Subject: [PATCH 1/7] feat: add visual indicator for user's gas remaining gas budget fetched from the paymaster --- .../src/components/interface/GlobalHeader.tsx | 2 + .../interface/home/UserGasBudgetIndicator.tsx | 50 ++++++++++++++++ apps/iframe/src/constants/contracts.ts | 1 + apps/iframe/src/hooks/useReadUserGasBudget.ts | 58 +++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx create mode 100644 apps/iframe/src/hooks/useReadUserGasBudget.ts diff --git a/apps/iframe/src/components/interface/GlobalHeader.tsx b/apps/iframe/src/components/interface/GlobalHeader.tsx index 5489f5dfdf..55ea83c605 100644 --- a/apps/iframe/src/components/interface/GlobalHeader.tsx +++ b/apps/iframe/src/components/interface/GlobalHeader.tsx @@ -5,6 +5,7 @@ import { Link, useLocation } from "@tanstack/react-router" import { useAtom } from "jotai" import { appMessageBus } from "#src/services/eventBus" import { secondaryMenuVisibilityAtom } from "#src/state/interfaceState" +import { UserGasBudgetIndicator } from "./home/UserGasBudgetIndicator" function signalClosed() { void appMessageBus.emit(Msgs.WalletVisibility, { isOpen: false }) @@ -27,6 +28,7 @@ const GlobalHeader = () => {
+ + ) +} diff --git a/apps/iframe/src/constants/contracts.ts b/apps/iframe/src/constants/contracts.ts index 0ac89291f8..f6d375c327 100644 --- a/apps/iframe/src/constants/contracts.ts +++ b/apps/iframe/src/constants/contracts.ts @@ -46,6 +46,7 @@ export const abis = getBoopAbis(chainId) export const entryPointAbi = abis.EntryPoint export const extensibleAccountAbi = abis.HappyAccountImpl export const sessionKeyValidatorAbi = abis.SessionKeyValidator +export const happyPaymasterAbi = abis.HappyPaymaster //== Paymaster Selectors =========================================================================== diff --git a/apps/iframe/src/hooks/useReadUserGasBudget.ts b/apps/iframe/src/hooks/useReadUserGasBudget.ts new file mode 100644 index 0000000000..89929a4ed8 --- /dev/null +++ b/apps/iframe/src/hooks/useReadUserGasBudget.ts @@ -0,0 +1,58 @@ +import type { Address, UInt32 } from "@happy.tech/common" +import { type UseReadContractReturnType, useReadContract } from "wagmi" +import { happyPaymaster, happyPaymasterAbi } from "#src/constants/contracts" + +// constant defined in the HappyPaymaster contract +const MAX_GAS_BUDGET: UInt32 = 1_000_000_000 + +export type BatteryHealthIndicator = 0 | 1 | 2 | 3 | 4 + +const BUDGET_THRESHOLDS = [ + 0, + MAX_GAS_BUDGET / 4, // 250_000_000 + (MAX_GAS_BUDGET * 2) / 4, // 500_000_000 + (MAX_GAS_BUDGET * 3) / 4, // 750_000_000 + MAX_GAS_BUDGET, +] + +export type UserGasBudgetInfo = { + batteryHealth: BatteryHealthIndicator + batteryPct: number +} + +export type UseReadUserGasBudgetReturnType = UseReadContractReturnType< + typeof happyPaymasterAbi, + "getBudget", + [Address], + UserGasBudgetInfo +> + +export const useReadUserGasBudget = (userAddress?: Address): UseReadUserGasBudgetReturnType => { + if (!userAddress) throw new Error("No user found!") + + const result = useReadContract({ + address: happyPaymaster, + abi: happyPaymasterAbi, + functionName: "getBudget", + args: [userAddress], + query: { + enabled: Boolean(userAddress), + select(userGasBudget) { + let batteryHealth: BatteryHealthIndicator = 0 + for (let i = BUDGET_THRESHOLDS.length - 1; i >= 0; i--) { + if (userGasBudget >= BUDGET_THRESHOLDS[i]) { + batteryHealth = i as BatteryHealthIndicator + break + } + } + + return { + batteryHealth, + batteryPct: Number(((userGasBudget / MAX_GAS_BUDGET) * 100).toFixed(2)), + } + }, + }, + }) + + return result +} From 32f658945b708841d1b83fed88c49cbb7d645845 Mon Sep 17 00:00:00 2001 From: ultraviolet10 Date: Mon, 19 May 2025 20:01:06 +0530 Subject: [PATCH 2/7] refactor: comments + fmt --- .../interface/home/UserGasBudgetIndicator.tsx | 5 ++--- apps/iframe/src/hooks/useReadUserGasBudget.ts | 10 +++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx b/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx index 7ff583a21e..64fde92776 100644 --- a/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx +++ b/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx @@ -35,9 +35,8 @@ export const UserGasBudgetIndicator = () => { isLoading, } = useReadUserGasBudget(user?.address) - console.log({ batteryHealth, batteryPct }) - - if (isLoading || batteryHealth === undefined) return + if (isLoading || batteryHealth === undefined) + return const BatteryIcon = batteryIconStates[batteryHealth] const colorClass = colorMap[batteryHealth] diff --git a/apps/iframe/src/hooks/useReadUserGasBudget.ts b/apps/iframe/src/hooks/useReadUserGasBudget.ts index 89929a4ed8..126ac050a0 100644 --- a/apps/iframe/src/hooks/useReadUserGasBudget.ts +++ b/apps/iframe/src/hooks/useReadUserGasBudget.ts @@ -8,11 +8,11 @@ const MAX_GAS_BUDGET: UInt32 = 1_000_000_000 export type BatteryHealthIndicator = 0 | 1 | 2 | 3 | 4 const BUDGET_THRESHOLDS = [ - 0, - MAX_GAS_BUDGET / 4, // 250_000_000 - (MAX_GAS_BUDGET * 2) / 4, // 500_000_000 - (MAX_GAS_BUDGET * 3) / 4, // 750_000_000 - MAX_GAS_BUDGET, + 0, // Empty (Critical) + MAX_GAS_BUDGET / 4, // 1: Low (250_000_000) + (MAX_GAS_BUDGET * 2) / 4, // 2: Medium (500_000_000) + (MAX_GAS_BUDGET * 3) / 4, // 3: High (750_000_000) + MAX_GAS_BUDGET, // 4: Full (1_000_000_000) ] export type UserGasBudgetInfo = { From c9690940414f29c79f3ed8934fa8f2c5a87c256c Mon Sep 17 00:00:00 2001 From: ultraviolet10 Date: Mon, 19 May 2025 22:07:41 +0530 Subject: [PATCH 3/7] hook refetch time limit, changeset note --- .changeset/nine-apes-attend.md | 5 +++++ apps/iframe/src/hooks/useReadUserGasBudget.ts | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/nine-apes-attend.md diff --git a/.changeset/nine-apes-attend.md b/.changeset/nine-apes-attend.md new file mode 100644 index 0000000000..4b4c92e8ad --- /dev/null +++ b/.changeset/nine-apes-attend.md @@ -0,0 +1,5 @@ +--- +"@happy.tech/iframe": minor +--- + +Adds a visual indicator in the wallet for user's paymaster gas budget. diff --git a/apps/iframe/src/hooks/useReadUserGasBudget.ts b/apps/iframe/src/hooks/useReadUserGasBudget.ts index 126ac050a0..0a0deed68a 100644 --- a/apps/iframe/src/hooks/useReadUserGasBudget.ts +++ b/apps/iframe/src/hooks/useReadUserGasBudget.ts @@ -36,7 +36,8 @@ export const useReadUserGasBudget = (userAddress?: Address): UseReadUserGasBudge functionName: "getBudget", args: [userAddress], query: { - enabled: Boolean(userAddress), + enabled: Boolean(!!userAddress), + refetchInterval: 2000, select(userGasBudget) { let batteryHealth: BatteryHealthIndicator = 0 for (let i = BUDGET_THRESHOLDS.length - 1; i >= 0; i--) { From 056ab28cdb1d89b45c17c2ba1462d72ce2f41dca Mon Sep 17 00:00:00 2001 From: ultraviolet10 Date: Tue, 20 May 2025 18:04:31 +0530 Subject: [PATCH 4/7] comments + icon update --- .../interface/home/UserGasBudgetIndicator.tsx | 7 ++-- apps/iframe/src/hooks/useReadUserGasBudget.ts | 33 +++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx b/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx index 64fde92776..aff8b1ad18 100644 --- a/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx +++ b/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx @@ -1,9 +1,9 @@ import { - BatteryEmpty, BatteryFull, BatteryHigh, BatteryLow, BatteryMedium, + BatteryWarning, type Icon, Spinner, } from "@phosphor-icons/react" @@ -20,8 +20,11 @@ const colorMap: Record = { 4: "text-success", } +/** + * Maps each battery health level to the corresponding Phosphor Icon component. + */ const batteryIconStates: Record = { - 0: BatteryEmpty, + 0: BatteryWarning, 1: BatteryLow, 2: BatteryMedium, 3: BatteryHigh, diff --git a/apps/iframe/src/hooks/useReadUserGasBudget.ts b/apps/iframe/src/hooks/useReadUserGasBudget.ts index 0a0deed68a..2277016179 100644 --- a/apps/iframe/src/hooks/useReadUserGasBudget.ts +++ b/apps/iframe/src/hooks/useReadUserGasBudget.ts @@ -2,11 +2,19 @@ import type { Address, UInt32 } from "@happy.tech/common" import { type UseReadContractReturnType, useReadContract } from "wagmi" import { happyPaymaster, happyPaymasterAbi } from "#src/constants/contracts" -// constant defined in the HappyPaymaster contract +/** + * Maximum cumulative gas budget per user as defined in the HappyPaymaster contract. + */ const MAX_GAS_BUDGET: UInt32 = 1_000_000_000 +/** + * Battery health level index (0–4) representing gas budget tiers. + */ export type BatteryHealthIndicator = 0 | 1 | 2 | 3 | 4 +/** + * Thresholds dividing the `MAX_GAS_BUDGET` into five discrete battery levels. + */ const BUDGET_THRESHOLDS = [ 0, // Empty (Critical) MAX_GAS_BUDGET / 4, // 1: Low (250_000_000) @@ -15,11 +23,19 @@ const BUDGET_THRESHOLDS = [ MAX_GAS_BUDGET, // 4: Full (1_000_000_000) ] +/** + * Structured gas budget info for UI display: + * - `batteryHealth`: discrete level 0–4 + * - `batteryPct`: percentage of MAX_GAS_BUDGET (0–100). + */ export type UserGasBudgetInfo = { batteryHealth: BatteryHealthIndicator batteryPct: number } +/** + * Wagmi return type for the `getBudget` read call, mapped to `UserGasBudgetInfo`. + */ export type UseReadUserGasBudgetReturnType = UseReadContractReturnType< typeof happyPaymasterAbi, "getBudget", @@ -27,6 +43,15 @@ export type UseReadUserGasBudgetReturnType = UseReadContractReturnType< UserGasBudgetInfo > +/** + * Hook to fetch and map a user's gas budget from the HappyPaymaster contract. + * + * @param userAddress - address of the user whose budget to read + * @returns Wagmi query object with: + * - `data`: `{ batteryHealth, batteryPct }` + * - `isLoading`, `isError`, `refetch`, etc. + * @throws Error if no userAddress is provided. + */ export const useReadUserGasBudget = (userAddress?: Address): UseReadUserGasBudgetReturnType => { if (!userAddress) throw new Error("No user found!") @@ -38,10 +63,14 @@ export const useReadUserGasBudget = (userAddress?: Address): UseReadUserGasBudge query: { enabled: Boolean(!!userAddress), refetchInterval: 2000, + /** + * Maps the raw onchain gas budget to UI-friendly battery info. + * @param userGasBudget - raw uint32 gas budget from contract + */ select(userGasBudget) { let batteryHealth: BatteryHealthIndicator = 0 for (let i = BUDGET_THRESHOLDS.length - 1; i >= 0; i--) { - if (userGasBudget >= BUDGET_THRESHOLDS[i]) { + if (0 >= BUDGET_THRESHOLDS[i]) { batteryHealth = i as BatteryHealthIndicator break } From 9f97b0a22a97389f576e544fed1cc2a3a0f8dde7 Mon Sep 17 00:00:00 2001 From: ultraviolet10 Date: Thu, 22 May 2025 17:00:36 +0530 Subject: [PATCH 5/7] refactor: update battery health indicator to use new icon imports and simplify type definitions --- .../interface/home/UserGasBudgetIndicator.tsx | 30 +++++++++--------- apps/iframe/src/hooks/useReadUserGasBudget.ts | 31 +++---------------- 2 files changed, 19 insertions(+), 42 deletions(-) diff --git a/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx b/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx index aff8b1ad18..63f9252c7c 100644 --- a/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx +++ b/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx @@ -1,18 +1,18 @@ import { - BatteryFull, - BatteryHigh, - BatteryLow, - BatteryMedium, - BatteryWarning, + BatteryFullIcon, + BatteryHighIcon, + BatteryLowIcon, + BatteryMediumIcon, + BatteryWarningIcon, type Icon, - Spinner, + SpinnerIcon, } from "@phosphor-icons/react" import { cx } from "class-variance-authority" import type { ClassValue } from "class-variance-authority/types" -import { type BatteryHealthIndicator, useReadUserGasBudget } from "#src/hooks/useReadUserGasBudget.ts" +import { useReadUserGasBudget } from "#src/hooks/useReadUserGasBudget.ts" import { getUser } from "#src/state/user.ts" -const colorMap: Record = { +const colorMap: Record = { 0: "text-error animate-pulse", 1: "text-warning/60", 2: "text-warning", @@ -23,12 +23,12 @@ const colorMap: Record = { /** * Maps each battery health level to the corresponding Phosphor Icon component. */ -const batteryIconStates: Record = { - 0: BatteryWarning, - 1: BatteryLow, - 2: BatteryMedium, - 3: BatteryHigh, - 4: BatteryFull, +const batteryIconStates: Record = { + 0: BatteryWarningIcon, + 1: BatteryLowIcon, + 2: BatteryMediumIcon, + 3: BatteryHighIcon, + 4: BatteryFullIcon, } export const UserGasBudgetIndicator = () => { @@ -39,7 +39,7 @@ export const UserGasBudgetIndicator = () => { } = useReadUserGasBudget(user?.address) if (isLoading || batteryHealth === undefined) - return + return const BatteryIcon = batteryIconStates[batteryHealth] const colorClass = colorMap[batteryHealth] diff --git a/apps/iframe/src/hooks/useReadUserGasBudget.ts b/apps/iframe/src/hooks/useReadUserGasBudget.ts index 2277016179..e0db390402 100644 --- a/apps/iframe/src/hooks/useReadUserGasBudget.ts +++ b/apps/iframe/src/hooks/useReadUserGasBudget.ts @@ -7,29 +7,13 @@ import { happyPaymaster, happyPaymasterAbi } from "#src/constants/contracts" */ const MAX_GAS_BUDGET: UInt32 = 1_000_000_000 -/** - * Battery health level index (0–4) representing gas budget tiers. - */ -export type BatteryHealthIndicator = 0 | 1 | 2 | 3 | 4 - -/** - * Thresholds dividing the `MAX_GAS_BUDGET` into five discrete battery levels. - */ -const BUDGET_THRESHOLDS = [ - 0, // Empty (Critical) - MAX_GAS_BUDGET / 4, // 1: Low (250_000_000) - (MAX_GAS_BUDGET * 2) / 4, // 2: Medium (500_000_000) - (MAX_GAS_BUDGET * 3) / 4, // 3: High (750_000_000) - MAX_GAS_BUDGET, // 4: Full (1_000_000_000) -] - /** * Structured gas budget info for UI display: * - `batteryHealth`: discrete level 0–4 * - `batteryPct`: percentage of MAX_GAS_BUDGET (0–100). */ export type UserGasBudgetInfo = { - batteryHealth: BatteryHealthIndicator + batteryHealth: number batteryPct: number } @@ -68,17 +52,10 @@ export const useReadUserGasBudget = (userAddress?: Address): UseReadUserGasBudge * @param userGasBudget - raw uint32 gas budget from contract */ select(userGasBudget) { - let batteryHealth: BatteryHealthIndicator = 0 - for (let i = BUDGET_THRESHOLDS.length - 1; i >= 0; i--) { - if (0 >= BUDGET_THRESHOLDS[i]) { - batteryHealth = i as BatteryHealthIndicator - break - } - } - + const pct = userGasBudget / MAX_GAS_BUDGET return { - batteryHealth, - batteryPct: Number(((userGasBudget / MAX_GAS_BUDGET) * 100).toFixed(2)), + batteryHealth: Math.round(pct * 4), + batteryPct: Number((pct * 100).toFixed(2)), } }, }, From 5878a81c32685aa5f8b9d3f82913904b19a543f8 Mon Sep 17 00:00:00 2001 From: ultraviolet10 Date: Thu, 22 May 2025 17:05:31 +0530 Subject: [PATCH 6/7] spinny spin --- .../src/components/interface/home/UserGasBudgetIndicator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx b/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx index 63f9252c7c..44559bbe73 100644 --- a/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx +++ b/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx @@ -39,7 +39,7 @@ export const UserGasBudgetIndicator = () => { } = useReadUserGasBudget(user?.address) if (isLoading || batteryHealth === undefined) - return + return const BatteryIcon = batteryIconStates[batteryHealth] const colorClass = colorMap[batteryHealth] From 39a8757f2b5e00a26b9a9970cc0fc01ce73ce334 Mon Sep 17 00:00:00 2001 From: ultraviolet10 Date: Thu, 22 May 2025 17:16:56 +0530 Subject: [PATCH 7/7] tighter bucket (wip) [skip ci] --- .../interface/home/UserGasBudgetIndicator.tsx | 7 ++++++- apps/iframe/src/hooks/useReadUserGasBudget.ts | 17 +++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx b/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx index 44559bbe73..f71ff2ddff 100644 --- a/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx +++ b/apps/iframe/src/components/interface/home/UserGasBudgetIndicator.tsx @@ -39,7 +39,12 @@ export const UserGasBudgetIndicator = () => { } = useReadUserGasBudget(user?.address) if (isLoading || batteryHealth === undefined) - return + return ( + + ) const BatteryIcon = batteryIconStates[batteryHealth] const colorClass = colorMap[batteryHealth] diff --git a/apps/iframe/src/hooks/useReadUserGasBudget.ts b/apps/iframe/src/hooks/useReadUserGasBudget.ts index e0db390402..46d9f00665 100644 --- a/apps/iframe/src/hooks/useReadUserGasBudget.ts +++ b/apps/iframe/src/hooks/useReadUserGasBudget.ts @@ -33,8 +33,8 @@ export type UseReadUserGasBudgetReturnType = UseReadContractReturnType< * @param userAddress - address of the user whose budget to read * @returns Wagmi query object with: * - `data`: `{ batteryHealth, batteryPct }` - * - `isLoading`, `isError`, `refetch`, etc. - * @throws Error if no userAddress is provided. + * - `isLoading`, `isError`, `refetch`, etc. (cf. {@link UseReadUserGasBudgetReturnType}) + * @throws `Error` if no userAddress is provided. */ export const useReadUserGasBudget = (userAddress?: Address): UseReadUserGasBudgetReturnType => { if (!userAddress) throw new Error("No user found!") @@ -48,13 +48,18 @@ export const useReadUserGasBudget = (userAddress?: Address): UseReadUserGasBudge enabled: Boolean(!!userAddress), refetchInterval: 2000, /** - * Maps the raw onchain gas budget to UI-friendly battery info. - * @param userGasBudget - raw uint32 gas budget from contract + * Maps the raw onchain `userGasBudget` to UI-friendly battery info: + * 1. Calculate `pct` as the ratio of `userGasBudget` to `MAX_GAS_BUDGET` (0.0 – 1.0). + * 2. Derive `batteryHealth` (0–4) by flooring `pct * 4` into four equal buckets and + * clamping between 0 and 4 to match discrete battery levels. + * 3. Compute `batteryPct` as a percentage string with two decimals (0 – 100). + * + * @param userGasBudget - raw uint32 gas budget from HappyPaymaster */ select(userGasBudget) { - const pct = userGasBudget / MAX_GAS_BUDGET + const pct = Number(userGasBudget) / MAX_GAS_BUDGET return { - batteryHealth: Math.round(pct * 4), + batteryHealth: Math.min(4, Math.max(0, Math.floor(pct * 4))), batteryPct: Number((pct * 100).toFixed(2)), } },