Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export function AdditionalOpportunitiesTab() {
setVestPending(true)
try {
// TODO: open vesting modal with amount input + confirmation
await vestEsSO4(address, so4Stats?.esSO4Balance ?? 0)
await vestEsSO4(address, so4Stats.esSO4Balance)
} finally {
setVestPending(false)
}
Expand All @@ -84,8 +84,8 @@ export function AdditionalOpportunitiesTab() {
}
}

const hasEsSO4 = (so4Stats?.esSO4Balance ?? 0) > 0
const hasMultiplierPoints = (so4Stats?.multiplierPoints ?? 0) > 0
const hasEsSO4 = so4Stats.esSO4Balance > 0
const hasMultiplierPoints = so4Stats.multiplierPoints > 0

return (
<div className="space-y-4">
Expand All @@ -108,7 +108,7 @@ export function AdditionalOpportunitiesTab() {
<div className="flex flex-wrap gap-x-8 gap-y-3">
<StatRow
label="esSO4 balance"
value={formatToken(so4Stats?.esSO4Balance ?? 0, "esSO4", { minDecimals: 2 })}
value={formatToken(so4Stats.esSO4Balance, "esSO4", { minDecimals: 2 })}
isLoading={isLoading}
/>
<StatRow label="Vesting duration" value="12 months" />
Expand All @@ -134,7 +134,7 @@ export function AdditionalOpportunitiesTab() {
<div className="flex flex-wrap gap-x-8 gap-y-3">
<StatRow
label="Multiplier Points"
value={formatToken(so4Stats?.multiplierPoints ?? 0, "MP", { minDecimals: 2 })}
value={formatToken(so4Stats.multiplierPoints, "MP", { minDecimals: 2 })}
isLoading={isLoading}
/>
<StatRow label="Boost cap" value="100% of base APR" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import { Button } from "@workspace/ui/components/button"
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@workspace/ui/components/dialog"
import { Input } from "@workspace/ui/components/input"
import { usePoolsApy } from "../../hooks/use-earn-data"
import { depositGLV, depositGM } from "../../lib/earn"
import { formatPct, formatUsd, formatToken } from "@/shared/lib/format"
import { useMarketPoolAmounts } from "../../hooks/useMarketPoolAmounts"
import { useGLVVaultData, useGMPoolData, useStakingInfo } from "../../queries"
import { formatPct, formatToken, formatUsd } from "@/shared/lib/format"
import { fromSorobanAmount } from "@/shared/lib/bignum"
import { TokenIcon } from "@/shared/components/TokenIcon"
import { useWalletStore } from "@/features/wallet/store/wallet-store"
Expand Down Expand Up @@ -122,7 +122,7 @@ export function DiscoverTab() {
.sort((a, b) => (sort === "apy" ? b.apy - a.apy : b.tvlUsd - a.tvlUsd))
}, [gmPools, glvVaults, filter, sort])

function handleEarn(id: string, kind: "gm" | "glv", name: string) {
async function handleEarn(id: string, kind: "gm" | "glv", name: string) {
setDepositTarget({ id, kind, name })
setDepositAmount("")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export function AssetsList() {
const [pending, setPending] = useState<string | null>(null)

const isLoading = gmLoading || glvLoading || so4Loading
const hasSO4 = (so4Stats?.stakedAmount ?? 0) > 0
const hasSO4 = so4Stats.stakedAmount > 0
const hasAny = hasSO4 || gmPositions.length > 0 || glvPositions.length > 0

async function runAction(key: string, fn: () => Promise<unknown>) {
Expand Down Expand Up @@ -134,7 +134,7 @@ export function AssetsList() {
</td>
<td className="px-5 py-3.5 text-right font-mono text-muted-foreground">—</td>
<td className="px-5 py-3.5 text-right font-mono">
{formatUsd(so4Stats?.stakedValueUsd ?? 0)}
{formatUsd(so4Stats.stakedValueUsd)}
</td>
<td className="px-5 py-3.5 text-right">
<Button
Expand All @@ -143,7 +143,7 @@ export function AssetsList() {
disabled={pending === "so4"}
onClick={() =>
void runAction("so4", () =>
unstakeSO4("DUMMY_ACCOUNT", so4Stats?.stakedAmount ?? 0),
unstakeSO4("DUMMY_ACCOUNT", so4Stats.stakedAmount),
)
}
>
Expand Down
10 changes: 5 additions & 5 deletions apps/web/src/features/earn/components/portfolio/rewards-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function RewardsBar() {
}
}

const hasPendingRewards = (stats?.totalPendingRewardsUsd ?? 0) > 0
const hasPendingRewards = stats.totalPendingRewardsUsd > 0

return (
<div className="space-y-3">
Expand Down Expand Up @@ -116,31 +116,31 @@ export function RewardsBar() {
<div className="flex flex-wrap items-center gap-x-8 gap-y-4 rounded-xl border border-border bg-card px-6 py-4">
<StatItem
label="Total investment value"
value={formatUsd(stats?.totalInvestmentUsd ?? 0)}
value={formatUsd(stats.totalInvestmentUsd)}
isLoading={isLoading}
/>

<div className="h-8 w-px shrink-0 bg-border" />

<StatItem
label="Total earned"
value={formatUsd(stats?.totalEarnedUsd ?? 0)}
value={formatUsd(stats.totalEarnedUsd)}
isLoading={isLoading}
/>

<div className="h-8 w-px shrink-0 bg-border" />

<StatItem
label="Total pending rewards"
value={formatUsd(stats?.totalPendingRewardsUsd ?? 0)}
value={formatUsd(stats.totalPendingRewardsUsd)}
isLoading={isLoading}
/>

<div className="h-8 w-px shrink-0 bg-border" />

<StatItem
label="Staking Power Share"
value={formatPct(stats?.stakingPowerSharePct ?? 0, { sign: false })}
value={formatPct(stats.stakingPowerSharePct, { sign: false })}
isLoading={isLoading}
/>

Expand Down
35 changes: 8 additions & 27 deletions apps/web/src/features/earn/lib/earn.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,35 @@
import { toast } from "sonner"
import { GLV_VAULTS, GM_POOLS } from "../data/pools"
import { submitTx } from "@/shared/hooks/useTxSubmit"
import { NETWORK } from "@/app/config/network"
import { queryClient } from "@/app/providers/QueryProvider"
import { prepareAndSign } from "@/lib/soroban/tx-builder"
import { parseSorobanError } from "@/lib/contracts"
import { walletKit } from "@/features/wallet/lib/wallet-kit"
import { queryKeys } from "@/shared/lib/query-keys"
import { toSorobanAmount } from "@/shared/lib/bignum"
import {
buildStakeSO4Transaction,
buildUnstakeSO4Transaction,
buildClaimRewardsTransaction,
buildCompoundTransaction,
} from "@/lib/contracts"
import {
buildCreateDepositTransaction,
buildCreateWithdrawalTransaction,
buildDepositForVestingTransaction,
buildStakeSO4Transaction,
buildUnstakeSO4Transaction,
parseSorobanError,
} from "@/lib/contracts"
import { buildDepositForVestingTransaction } from "@/lib/contracts"
import { walletKit } from "@/features/wallet/lib/wallet-kit"
import { queryKeys } from "@/shared/lib/query-keys"
import { toSorobanAmount } from "@/shared/lib/bignum"
import {
createDeposit as createGlvDeposit,
createWithdrawal as createGlvWithdrawal,
} from "@/lib/glv-router-client"
import { GM_POOLS, GLV_VAULTS } from "../data/pools"

const SO4_DECIMALS = 7
const GM_TOKEN_DECIMALS = 7
const GLV_TOKEN_DECIMALS = 7

function fakeTxDelay(ms = 1500): Promise<void> {
return new Promise((res) => setTimeout(res, ms))
}

function isValidAccount(account: string): boolean {
return /^G[A-Z2-7]{55}$/.test(account)
}

async function runMockWrite(loadingMessage: string, successMessage: string, delay = 1500): Promise<string> {
return submitTx(async () => "", {
loadingMessage,
successMessage,
successDescription: () => "Tx: DUMMY_TX_HASH",
execute: async () => {
await fakeTxDelay(delay)
return "DUMMY_TX_HASH"
},
onError: (error) => (error instanceof Error ? error.message : "Transaction failed"),
})
}

async function invalidateStakingQueries(account: string): Promise<void> {
await Promise.all([
queryClient.invalidateQueries({ queryKey: queryKeys.earn.stakingInfo(account) }),
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/features/earn/queries/useStakingInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ export function useStakingInfo() {
const info = await stakingRouter.getStakerInfo(address!)

return {
stakedSO4: info.stakedSO4 ?? info.stakedAmount,
stakedEsSO4: info.stakedEsSO4 ?? info.esSO4Balance,
stakedSO4: info.stakedSO4,
stakedEsSO4: info.stakedEsSO4,
stakedMultiplierPoints: info.stakedMultiplierPoints,
pendingEsSO4Rewards: info.pendingEsSO4Rewards ?? info.accruedRewards,
pendingEsSO4Rewards: info.pendingEsSO4Rewards,
pendingWethFees: info.pendingWethFees,
}
},
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/features/earn/queries/useVestingSchedule.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query"
import type { VestingSchedule } from "@/lib/contracts"
import { getVestingRouterClient } from "@/lib/contracts"
import { useWalletStore } from "@/features/wallet/store/wallet-store"
import { getVestingRouterClient, type VestingSchedule } from "@/lib/contracts"
import { queryKeys } from "@/shared/lib/query-keys"

/**
Expand Down
31 changes: 19 additions & 12 deletions apps/web/src/features/faucet/components/faucet-page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Skeleton } from "@workspace/ui/components/skeleton"
import { Button } from "@workspace/ui/components/button"
import { FAUCET_TOKENS, type FaucetTokenConfig } from "../data/tokens" // eslint-disable-line import/consistent-type-specifier-style
import { FAUCET_CONTRACT_ID } from "../lib/clients"
import { useFaucetData } from "../hooks/useFaucetData"
import { useClaim } from "../hooks/useClaim"
import { Navbar } from "@/ui/Navbar"
import { TokenIcon } from "@/shared/components/TokenIcon"
import { formatToken } from "@/shared/lib/format"
Expand All @@ -8,10 +12,6 @@ import { useWalletStore } from "@/features/wallet/store/wallet-store"
import { useNetwork } from "@/features/wallet/hooks/useNetwork"
import { NetworkMismatchBanner } from "@/features/wallet/components/NetworkMismatchBanner"
import { ConnectButton } from "@/features/wallet/components/ConnectButton"
import { FAUCET_TOKENS, type FaucetTokenConfig } from "../data/tokens"
import { FAUCET_CONTRACT_ID } from "../lib/clients"
import { useFaucetData } from "../hooks/useFaucetData"
import { useClaim } from "../hooks/useClaim"

// ── Token card ────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -90,7 +90,14 @@ function TokenCard({
disabled={isDisabled || isPending}
onClick={() => onClaim(token)}
>
Claim
{isPending ? (
<span className="flex items-center gap-1.5">
<span className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
Claiming
</span>
) : (
"Claim"
)}
</Button>
</div>
</div>
Expand All @@ -104,10 +111,10 @@ export function FaucetPage() {
const isConnected = useWalletStore((state) => state.status === "connected")
const { mismatch } = useNetwork()
const { data, isLoading } = useFaucetData(address)
const { claim, isPending } = useClaim()
const { claimOne, claimAll, pendingTokens, isBulkPending } = useClaim()

const isTestnet = NETWORK.name === "testnet"
const claimDisabled = !isConnected || isPending || mismatch
const claimDisabled = !isConnected || mismatch

return (
<div className="flex min-h-svh flex-col bg-background text-foreground">
Expand Down Expand Up @@ -148,9 +155,9 @@ export function FaucetPage() {
lastClaimLedger={data?.lastClaimLedgers[token.symbol]}
cooldownLedgers={data?.cooldownLedgers}
isLoading={isLoading}
isPending={isPending}
isPending={pendingTokens.has(token.contractId) || isBulkPending}
isDisabled={claimDisabled}
onClaim={(selectedToken) => claim([selectedToken.contractId])}
onClaim={(selectedToken) => claimOne(selectedToken.contractId)}
/>
))}
</div>
Expand Down Expand Up @@ -181,10 +188,10 @@ export function FaucetPage() {
<Button
variant="default"
className="w-full"
disabled={claimDisabled}
onClick={() => claim()}
disabled={claimDisabled || isBulkPending}
onClick={() => claimAll()}
>
{isPending ? (
{isBulkPending ? (
<span className="flex items-center gap-2">
<span className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-current border-t-transparent" />
Claiming…
Expand Down
Loading
Loading