Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
063672b
feat: add total locked token metrics
pyropy Feb 3, 2026
d3e54d3
feat: add totalStreamingLockup propety to Token
pyropy Feb 4, 2026
cc1e294
fix: only remove streaming lockup when rail is finalized
pyropy Feb 5, 2026
65291c1
fix: do not return early from handleRailLockupModified if no payer to…
pyropy Feb 5, 2026
54f78ee
Merge branch 'main' of github.com:FilOzone/filecoin-pay-explorer into…
pyropy Feb 5, 2026
3d90978
fix: rebuild subgraph
pyropy Feb 5, 2026
fc4a45b
fix: update streaming lockup for terminated rails
pyropy Feb 5, 2026
8cc8493
fix: default usdfc lockup to 0
pyropy Feb 9, 2026
2ae2acd
fix: handle settlement and rate streaming lockup modifications
pyropy Feb 9, 2026
802f016
feat(subgraph): define unified token lockup accounting
silent-cipher Feb 10, 2026
01449bc
chore: generate types
silent-cipher Feb 10, 2026
9da1ba8
feat(explorer): sync token lockup calc with subgraph
silent-cipher Feb 10, 2026
c883b87
feat(explorer): enable loading state for token lockup
silent-cipher Feb 10, 2026
6939010
fix(explorer): accept string inputs for lockup calculation
silent-cipher Feb 10, 2026
32a5612
test(subgraph): add assertions for zero lockup after rail lifecycle
silent-cipher Feb 11, 2026
2551433
refactor(subgraph): replace settleTokenLockup with pure function
silent-cipher Feb 11, 2026
f8cae7d
Merge branch 'main' into feat/add-total-locked-token-metrics
silent-cipher Feb 19, 2026
8b779bb
refactor(explorer): centralize viem client creation with caching
silent-cipher Feb 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions apps/explorer/src/components/Home/Stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ import {
EmptyTitle,
} from "@filecoin-pay/ui/components/empty";
import type { IconProps } from "@phosphor-icons/react";
import { CoinsIcon } from "@phosphor-icons/react";
import { CoinsIcon, LockIcon } from "@phosphor-icons/react";
import { AlertCircle } from "lucide-react";
import { useMemo } from "react";
import { zeroAddress } from "viem";
import { getChain } from "@/constants/chains";
import { useBlockNumber } from "@/hooks/useBlockNumber";
import useNetwork from "@/hooks/useNetwork";
import { useStatsDashboard } from "@/hooks/useStatsDashboard";
import { formatToken } from "@/utils/formatter";
import { calculateTotalLockup } from "@/utils/lockup";
import { MetricItem } from "../shared";

interface StatsLayoutProps {
Expand Down Expand Up @@ -73,6 +75,7 @@ const Stats: React.FC = () => {

const chain = getChain(network);
const { data, isLoading, isError, error, refetch } = useStatsDashboard(chain.contracts.usdfc.address, zeroAddress);
const { data: blockNumber, isLoading: loadingBlockNumber } = useBlockNumber();

const cards = useMemo<MetricCard[]>(
() => [
Expand Down Expand Up @@ -108,8 +111,42 @@ const Stats: React.FC = () => {
: `${DEFAULT_TOKEN_VALUE} FIL`,
icon: CoinsIcon,
},
{
title: "Total USDFC Locked",
value: data?.usdfcToken
? formatToken(
calculateTotalLockup(
data.usdfcToken.lockupCurrent,
data.usdfcToken.lockupRate,
data.usdfcToken.lockupLastSettledUntilEpoch,
blockNumber,
),
data.usdfcToken.decimals,
"USDFC",
)
: `${DEFAULT_TOKEN_VALUE} USDFC`,
icon: LockIcon,
isLoading: loadingBlockNumber,
},
{
title: "Total FIL Locked",
value: data?.filToken
? formatToken(
calculateTotalLockup(
data.filToken.lockupCurrent,
data.filToken.lockupRate,
data.filToken.lockupLastSettledUntilEpoch,
blockNumber,
),
data.filToken.decimals,
"FIL",
)
: `${DEFAULT_TOKEN_VALUE} FIL`,
icon: LockIcon,
isLoading: loadingBlockNumber,
},
],
[data],
[data, blockNumber, loadingBlockNumber],
);

return (
Expand Down
32 changes: 32 additions & 0 deletions apps/explorer/src/hooks/useBlockNumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useQuery } from "@tanstack/react-query";
import { getPublicClient } from "@/services/viem/client";
import useNetwork from "./useNetwork";

interface UseBlockNumberOptions {
refetchInterval?: number | false;
}

/**
* Custom hook to fetch the current block number using viem's public client.
*
* @returns Query result containing the current block number
*
* @example
* ```tsx
* const { data: blockNumber, isLoading } = useBlockNumber();
* ```
*/
export function useBlockNumber(options?: UseBlockNumberOptions) {
const { network } = useNetwork();

return useQuery({
queryKey: ["blockNumber", network],
queryFn: async () => {
const publicClient = getPublicClient(network);
const blockNumber = await publicClient.getBlockNumber();
return blockNumber;
},
refetchInterval: options?.refetchInterval || false,
placeholderData: (previousData) => previousData,
});
}
17 changes: 0 additions & 17 deletions apps/explorer/src/hooks/useTokenDetails.ts

This file was deleted.

22 changes: 6 additions & 16 deletions apps/explorer/src/services/grapql/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,35 +435,25 @@ export const GET_ACCOUNT_APPROVALS = gql`
}
`;

// Token Details Queries

export const GET_TOKEN_DETAILS = gql`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto for removal?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was using this query before the merging the current upstream branch, after which I've seen that it's no longer used so I decided to delete it. YAGNI.

query GetTokenDetails($id: Bytes!) {
token(id: $id) {
id
name
symbol
decimals
totalSettledAmount
userFunds
totalUsers
}
}
`;

export const GET_STATS_DASHBOARD = gql`
query GetStatsDashboard($usdfcAddress: Bytes!, $filAddress: Bytes!) {
usdfcToken: token(id: $usdfcAddress) {
decimals
totalSettledAmount
totalOneTimePayment
userFunds
lockupCurrent
lockupRate
lockupLastSettledUntilEpoch
}
filToken: token(id: $filAddress) {
decimals
totalSettledAmount
totalOneTimePayment
userFunds
lockupCurrent
lockupRate
lockupLastSettledUntilEpoch
}
}
`;
21 changes: 21 additions & 0 deletions apps/explorer/src/services/viem/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createPublicClient, http, type PublicClient } from "viem";
import { getChain } from "@/constants/chains";
import type { Network } from "@/types";

const clients = new Map<Network, PublicClient>();

export function getPublicClient(network: Network): PublicClient {
const existing = clients.get(network);
if (existing) {
return existing;
}

const chain = getChain(network);
const client = createPublicClient({
chain,
transport: http(),
});

clients.set(network, client);
return client;
}
68 changes: 68 additions & 0 deletions apps/explorer/src/utils/lockup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Calculate total lockup for a token using the current block number.
*
* Formula: lockupCurrent + lockupRate * (blockNumber - lockupLastSettledUntilEpoch)
*
* @param lockupCurrent - The current fixed lockup amount
* @param lockupRate - The rate at which lockup increases per block
* @param lockupLastSettledUntilEpoch - The block number when lockup was last settled
* @param blockNumber - The current block number
* @returns The total lockup amount as a string, or "0" if calculation fails
*
* @example
* ```ts
* const totalLockup = calculateTotalLockup(
* "1000",
* "10",
* "100",
* 150n
* );
* // Returns: "1500" (1000 + 10 * (150 - 100))
* ```
*/
export function calculateTotalLockup(
lockupCurrent: bigint | string | undefined,
lockupRate: bigint | string | undefined,
lockupLastSettledUntilEpoch: bigint | string | undefined,
blockNumber: bigint | string | undefined,
): string {
// Handle missing data
if (
lockupCurrent === undefined ||
lockupRate === undefined ||
lockupLastSettledUntilEpoch === undefined ||
blockNumber === undefined
) {
return "0";
}

try {
lockupCurrent = BigInt(lockupCurrent);
lockupRate = BigInt(lockupRate);
lockupLastSettledUntilEpoch = BigInt(lockupLastSettledUntilEpoch);
blockNumber = BigInt(blockNumber);

// If no streaming lockup (rate is 0), return only fixed lockup
if (lockupRate === 0n) {
return lockupCurrent.toString();
}

// If current block is before or equal to last settled, return only fixed lockup
if (blockNumber <= lockupLastSettledUntilEpoch) {
return lockupCurrent.toString();
}

// Calculate streaming lockup: rate * (blockNumber - lastSettled)
const blockDelta = blockNumber - lockupLastSettledUntilEpoch;
const streamingLockup = lockupRate * blockDelta;

// Total lockup = fixed + streaming
const totalLockup = lockupCurrent + streamingLockup;

// Ensure non-negative result
return totalLockup >= 0n ? totalLockup.toString() : "0";
} catch (error) {
console.error("Error calculating total lockup:", error);
return "0";
}
}
7 changes: 5 additions & 2 deletions packages/subgraph/schemas/schema.v1.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ type Token @entity(immutable: false) {
totalUsers: BigInt!
userFunds: BigInt! # same as sum of all UserToken.funds
operatorCommission: BigInt!
lockupCurrent: BigInt!
lockupRate: BigInt!
lockupLastSettledUntilEpoch: BigInt!
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a global property for a token, but it's actually a per-rail property. Will this be updated on every payment rail settlement?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, on rail settlement, lockupCurrent and lockupLastSettledUntilEpoch will be updated accordingly.

userTokens: [UserToken!]! @derivedFrom(field: "token")
}

Expand Down Expand Up @@ -128,8 +131,8 @@ type Rail @entity(immutable: false) {

type RateChangeQueue @entity(immutable: false) {
id: Bytes! # railId,startEpoch
startEpoch: BigInt!
untilEpoch: BigInt!
startEpoch: BigInt! # exclusive: rate applies starting at `startEpoch + 1`
untilEpoch: BigInt! # inclusive: rate applies through `untilEpoch`
rate: BigInt!
rail: Rail!
}
Expand Down
Loading