diff --git a/apps/explorer/src/components/Home/Stats.tsx b/apps/explorer/src/components/Home/Stats.tsx index b133a59..a7fef59 100644 --- a/apps/explorer/src/components/Home/Stats.tsx +++ b/apps/explorer/src/components/Home/Stats.tsx @@ -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 { @@ -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( () => [ @@ -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 ( diff --git a/apps/explorer/src/hooks/useBlockNumber.ts b/apps/explorer/src/hooks/useBlockNumber.ts new file mode 100644 index 0000000..5dc9e80 --- /dev/null +++ b/apps/explorer/src/hooks/useBlockNumber.ts @@ -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, + }); +} diff --git a/apps/explorer/src/hooks/useTokenDetails.ts b/apps/explorer/src/hooks/useTokenDetails.ts deleted file mode 100644 index f046a16..0000000 --- a/apps/explorer/src/hooks/useTokenDetails.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Token } from "@filecoin-pay/types"; -import type { Hex } from "viem"; -import { GET_TOKEN_DETAILS } from "@/services/grapql/queries"; -import { useGraphQLQuery } from "./useGraphQLQuery"; - -interface TokenDetailsResponse { - token: Token; -} - -export const useTokenDetails = (tokenAddress: Hex) => - useGraphQLQuery({ - queryKey: ["token", tokenAddress], - query: GET_TOKEN_DETAILS, - variables: { id: tokenAddress }, - select: (data) => data.token || null, - enabled: !!tokenAddress, - }); diff --git a/apps/explorer/src/services/grapql/queries.ts b/apps/explorer/src/services/grapql/queries.ts index 5912bbf..bfbbb5f 100644 --- a/apps/explorer/src/services/grapql/queries.ts +++ b/apps/explorer/src/services/grapql/queries.ts @@ -435,22 +435,6 @@ export const GET_ACCOUNT_APPROVALS = gql` } `; -// Token Details Queries - -export const GET_TOKEN_DETAILS = gql` - 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) { @@ -458,12 +442,18 @@ export const GET_STATS_DASHBOARD = gql` totalSettledAmount totalOneTimePayment userFunds + lockupCurrent + lockupRate + lockupLastSettledUntilEpoch } filToken: token(id: $filAddress) { decimals totalSettledAmount totalOneTimePayment userFunds + lockupCurrent + lockupRate + lockupLastSettledUntilEpoch } } `; diff --git a/apps/explorer/src/services/viem/client.ts b/apps/explorer/src/services/viem/client.ts new file mode 100644 index 0000000..ee5cc62 --- /dev/null +++ b/apps/explorer/src/services/viem/client.ts @@ -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(); + +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; +} diff --git a/apps/explorer/src/utils/lockup.ts b/apps/explorer/src/utils/lockup.ts new file mode 100644 index 0000000..4120105 --- /dev/null +++ b/apps/explorer/src/utils/lockup.ts @@ -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"; + } +} diff --git a/packages/subgraph/schemas/schema.v1.graphql b/packages/subgraph/schemas/schema.v1.graphql index 77d5857..75676bd 100644 --- a/packages/subgraph/schemas/schema.v1.graphql +++ b/packages/subgraph/schemas/schema.v1.graphql @@ -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! userTokens: [UserToken!]! @derivedFrom(field: "token") } @@ -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! } diff --git a/packages/subgraph/src/payments.ts b/packages/subgraph/src/payments.ts index e740e49..88c0f39 100644 --- a/packages/subgraph/src/payments.ts +++ b/packages/subgraph/src/payments.ts @@ -14,6 +14,7 @@ import { } from "../generated/Payments/Payments"; import { Account, OperatorApproval, Rail, Settlement, Token, UserToken } from "../generated/schema"; import { + computeSettledLockup, createOneTimePayment, createOrLoadAccountByAddress, createOrLoadOperator, @@ -21,8 +22,10 @@ import { createOrLoadUserToken, createRail, createRateChangeQueue, + epochsRateChangeApplicable, getLockupLastSettledUntilTimestamp, getTokenDetails, + remainingEpochsForTerminatedRail, updateOperatorLockup, updateOperatorRate, updateOperatorTokenLockup, @@ -202,6 +205,16 @@ export function handleRailTerminated(event: RailTerminatedEvent): void { rail.save(); + const token = Token.load(rail.token); + if (token) { + // settle token lockup before updating lockup rate + token.lockupCurrent = computeSettledLockup(token, event.block.number); + token.lockupLastSettledUntilEpoch = event.block.number; + + token.lockupRate = token.lockupRate.minus(rail.paymentRate); + token.save(); + } + // collect rail state change metrics MetricsCollectionOrchestrator.collectRailStateChangeMetrics( previousRailState, @@ -225,20 +238,34 @@ export function handleRailLockupModified(event: RailLockupModifiedEvent): void { return; } - const isTerminated = rail.state === "TERMINATED"; - const payerToken = UserToken.load(rail.payer.concat(rail.token)); + const isTerminated = rail.state == "TERMINATED"; const operatorApprovalId = rail.payer.concat(rail.operator).concat(rail.token); const operatorApproval = OperatorApproval.load(operatorApprovalId); const operatorToken = createOrLoadOperatorToken(rail.operator, rail.token).operatorToken; - rail.lockupFixed = event.params.newLockupFixed; + rail.lockupFixed = newLockupFixed; if (!isTerminated) { - rail.lockupPeriod = event.params.newLockupPeriod; + rail.lockupPeriod = newLockupPeriod; } rail.save(); - if (!payerToken) { - return; + // Update token lockup metrics + const token = Token.load(rail.token); + if (token) { + // No need to settle token lockup here because lockupRate is unchanged; deltas are independent of elapsed time. + // Fixed lockup delta + const fixedDelta = newLockupFixed.minus(oldLockupFixed); + token.lockupCurrent = token.lockupCurrent.plus(fixedDelta); + + // Streaming lockup delta (only if not terminated) + if (!isTerminated) { + const oldStreaming = rail.paymentRate.times(oldLockupPeriod); + const newStreaming = rail.paymentRate.times(newLockupPeriod); + const streamingDelta = newStreaming.minus(oldStreaming); + token.lockupCurrent = token.lockupCurrent.plus(streamingDelta); + } + + token.save(); } let oldLockup = oldLockupFixed; @@ -265,7 +292,8 @@ export function handleRailRateModified(event: RailRateModifiedEvent): void { return; } - if (oldRate.equals(ZERO_BIG_INT) && newRate.gt(ZERO_BIG_INT) && rail.state !== "Active") { + // Only transition from ZERORATE to ACTIVE, not from TERMINATED or FINALIZED + if (oldRate.equals(ZERO_BIG_INT) && newRate.gt(ZERO_BIG_INT) && rail.state == "ZERORATE") { rail.state = "ACTIVE"; // Collect rail State change metrics @@ -309,7 +337,8 @@ export function handleRailRateModified(event: RailRateModifiedEvent): void { return; } - const isTerminated = rail.state === "TERMINATED"; + // Not using strict equality because it evaluates to false for "TERMINATED" state in tests + const isTerminated = rail.state == "TERMINATED"; if (!isTerminated) { updateOperatorRate(operatorApproval, oldRate, newRate); updateOperatorTokenRate(operatorToken, oldRate, newRate); @@ -321,21 +350,40 @@ export function handleRailRateModified(event: RailRateModifiedEvent): void { } if (oldRate.notEqual(newRate)) { - let effectiveLockupPeriod = ZERO_BIG_INT; + let effectiveLockupPeriod = rail.lockupPeriod; if (isTerminated) { - effectiveLockupPeriod = rail.endEpoch.minus(event.block.number); - if (effectiveLockupPeriod.lt(ZERO_BIG_INT)) { - effectiveLockupPeriod = ZERO_BIG_INT; - } - } else if (payerToken) { - effectiveLockupPeriod = rail.lockupPeriod.minus(event.block.number.minus(payerToken.lockupLastSettledUntilEpoch)); + effectiveLockupPeriod = remainingEpochsForTerminatedRail(rail, event.block.number); } + if (effectiveLockupPeriod.gt(ZERO_BIG_INT)) { const oldLockup = oldRate.times(effectiveLockupPeriod); const newLockup = newRate.times(effectiveLockupPeriod); // update operator lockup usage and save updateOperatorLockup(operatorApproval, oldLockup, newLockup); updateOperatorTokenLockup(operatorToken, oldLockup, newLockup); + } + + // Update token streaming lockup (for all rails including terminated) + // Uses lockupPeriod for consistency with handleRailFinalized + const token = Token.load(rail.token); + if (token) { + // settle token lockup untile current epoch + token.lockupCurrent = computeSettledLockup(token, event.block.number); + token.lockupLastSettledUntilEpoch = event.block.number; + + const oldStreaming = oldRate.times(effectiveLockupPeriod); + const newStreaming = newRate.times(effectiveLockupPeriod); + const streamingDelta = newStreaming.minus(oldStreaming); + token.lockupCurrent = token.lockupCurrent.plus(streamingDelta); + + // update lockup rate only if the rail is not terminated + // for terminated rails, the lockup rate is already updated during rail termination + if (!isTerminated) token.lockupRate = token.lockupRate.minus(oldRate).plus(newRate); + + token.save(); + } + + if (effectiveLockupPeriod.gt(ZERO_BIG_INT)) { return; } } @@ -359,6 +407,9 @@ export function handleRailSettled(event: RailSettledEvent): void { return; } + // Capture previous settledUpto before updating (needed for lockup calculation) + const previousSettledUpto = rail.settledUpto; + // Update rail aggregate data rail.totalSettledAmount = rail.totalSettledAmount.plus(totalSettledAmount); rail.totalSettlements = rail.totalSettlements.plus(ONE_BIG_INT); @@ -393,6 +444,10 @@ export function handleRailSettled(event: RailSettledEvent): void { ).userToken; const token = Token.load(rail.token); if (token) { + // settle token lockup just to make sure we don't end up with negative lockup current (still not necessary to call) + token.lockupCurrent = computeSettledLockup(token, event.block.number); + token.lockupLastSettledUntilEpoch = event.block.number; + // Subtract the network fee from user funds since it is not retained by the user. // The fee is either burned or deposited into the Filecoin-pay contract account. // @@ -402,6 +457,33 @@ export function handleRailSettled(event: RailSettledEvent): void { // by summing all `userTokens.funds`. token.userFunds = token.userFunds.minus(networkFee); token.totalSettledAmount = token.totalSettledAmount.plus(totalSettledAmount); + + // Reduce streaming lockup by rate × actualSettledDuration. + // Settlement window is (previousSettledUpto, settledUpTo]. + // RateChangeQueue applies for (startEpoch, untilEpoch], i.e., startEpoch is exclusive. + // https://github.com/FilOzone/filecoin-pay/blob/c916dc5cd059c48ca5d7588416af9e6025fa1fc6/src/FilecoinPayV1.sol#L1471-L1472 + const rateChanges = rail.rateChangeQueue.load(); + const rateChangeCount = rateChanges.length; + let lockupReduction = ZERO_BIG_INT; + + // Calculate lockup reduction from historical rate changes + for (let i = 0; i < rateChangeCount; i++) { + const rateChange = rateChanges[i]; + const duration = epochsRateChangeApplicable(rateChange, previousSettledUpto, event.params.settledUpTo); + lockupReduction = lockupReduction.plus(rateChange.rate.times(duration)); + } + + // Calculate lockup reduction from current rate (for epochs not covered by rate change queue) + // Start from the later of: last queue entry's untilEpoch OR previousSettledUpto + // This handles cases where the rail was already settled beyond the last rate change + const lastQueueEpoch = rateChangeCount > 0 ? rateChanges[rateChangeCount - 1].untilEpoch : previousSettledUpto; + const currentRateStartEpoch = lastQueueEpoch.gt(previousSettledUpto) ? lastQueueEpoch : previousSettledUpto; + if (currentRateStartEpoch.lt(event.params.settledUpTo)) { + const currentRateDuration = event.params.settledUpTo.minus(currentRateStartEpoch); + lockupReduction = lockupReduction.plus(rail.paymentRate.times(currentRateDuration)); + } + + token.lockupCurrent = token.lockupCurrent.minus(lockupReduction); token.save(); } @@ -544,6 +626,7 @@ export function handleRailOneTimePaymentProcessed(event: RailOneTimePaymentProce const token = Token.load(rail.token); if (token) { token.userFunds = token.userFunds.minus(networkFee); + token.lockupCurrent = token.lockupCurrent.minus(totalAmount); token.totalOneTimePayment = token.totalOneTimePayment.plus(totalAmount); token.save(); } @@ -609,6 +692,13 @@ export function handleRailFinalized(event: RailFinalizedEvent): void { updateOperatorLockup(operatorApproval, oldLockup, ZERO_BIG_INT); updateOperatorTokenLockup(operatorToken, oldLockup, ZERO_BIG_INT); + // Reduce token lockup metrics by rail's fixed lockup + const token = Token.load(rail.token); + if (token) { + token.lockupCurrent = token.lockupCurrent.minus(rail.lockupFixed); + token.save(); + } + rail.state = "FINALIZED"; rail.save(); diff --git a/packages/subgraph/src/utils/helpers.ts b/packages/subgraph/src/utils/helpers.ts index e8de8a7..7257364 100644 --- a/packages/subgraph/src/utils/helpers.ts +++ b/packages/subgraph/src/utils/helpers.ts @@ -1,5 +1,5 @@ /* eslint-disable no-underscore-dangle */ -import { Address, Bytes, BigInt as GraphBN } from "@graphprotocol/graph-ts"; +import { Address, Bytes, BigInt as GraphBN, log } from "@graphprotocol/graph-ts"; import { erc20 } from "../../generated/Payments/erc20"; import { RailOneTimePaymentProcessed } from "../../generated/Payments/Payments"; import { @@ -120,6 +120,9 @@ export const getTokenDetails = (address: Address): TokenDetails => { token.totalSettledAmount = ZERO_BIG_INT; token.userFunds = ZERO_BIG_INT; token.operatorCommission = ZERO_BIG_INT; + token.lockupCurrent = ZERO_BIG_INT; + token.lockupRate = ZERO_BIG_INT; + token.lockupLastSettledUntilEpoch = ZERO_BIG_INT; token.totalUsers = ZERO_BIG_INT; return new TokenDetails(token, true); @@ -370,3 +373,44 @@ export function getLockupLastSettledUntilTimestamp( return blockTimestamp.minus(blockNumber.minus(lockupLastSettledUntilEpoch).times(EPOCH_DURATION)); } + +export function remainingEpochsForTerminatedRail(rail: Rail, blockNumber: GraphBN): GraphBN { + if (blockNumber.gt(rail.endEpoch)) { + return ZERO_BIG_INT; + } + + return rail.endEpoch.minus(blockNumber); +} + +/** + * Computes the settled amount of tokens that should be locked up until the given epoch + * @param token the token to compute the settled amount for + * @param untilEpoch the epoch until which the settled amount should be computed + * @returns the settled amount of tokens that should be locked up until the given epoch + */ +export function computeSettledLockup(token: Token, untilEpoch: GraphBN): GraphBN { + const duration = untilEpoch.minus(token.lockupLastSettledUntilEpoch); + return token.lockupCurrent.plus(token.lockupRate.times(duration)); +} + +/** + * Returns the number of epochs in the settlement window where this rate applies. + * + * RateChangeQueue semantics: + * - `startEpoch` is exclusive: rate applies starting at `startEpoch + 1` + * - `untilEpoch` is inclusive: rate applies through `untilEpoch` + * + * Settlement window semantics: + * - `start` is exclusive (previous settled epoch) + * - `end` is inclusive (newly settled up to) + */ +export function epochsRateChangeApplicable(rateChange: RateChangeQueue, start: GraphBN, end: GraphBN): GraphBN { + const windowStart = rateChange.startEpoch.gt(start) ? rateChange.startEpoch : start; + const windowEnd = rateChange.untilEpoch.lt(end) ? rateChange.untilEpoch : end; + + if (windowEnd.le(windowStart)) { + return ZERO_BIG_INT; + } + + return windowEnd.minus(windowStart); +} diff --git a/packages/subgraph/tests/payments/fixtures.ts b/packages/subgraph/tests/payments/fixtures.ts index a229ce3..eaf5ae5 100644 --- a/packages/subgraph/tests/payments/fixtures.ts +++ b/packages/subgraph/tests/payments/fixtures.ts @@ -249,3 +249,15 @@ export function assertRailLockupParams(railId: GraphBN, lockupPeriod: GraphBN, l assert.fieldEquals("Rail", railEntityId, "lockupPeriod", lockupPeriod.toString()); assert.fieldEquals("Rail", railEntityId, "lockupFixed", lockupFixed.toString()); } + +export function assertTokenTotalLockup( + tokenAddress: Address, + lockupCurrent: GraphBN, + lockupRate: GraphBN, + lastSettledUntilEpoch: GraphBN, +): void { + const tokenId = tokenAddress.toHexString(); + assert.fieldEquals("Token", tokenId, "lockupCurrent", lockupCurrent.toString()); + assert.fieldEquals("Token", tokenId, "lockupRate", lockupRate.toString()); + assert.fieldEquals("Token", tokenId, "lockupLastSettledUntilEpoch", lastSettledUntilEpoch.toString()); +} diff --git a/packages/subgraph/tests/payments/payments.test.ts b/packages/subgraph/tests/payments/payments.test.ts index 0814ea5..8a5a84a 100644 --- a/packages/subgraph/tests/payments/payments.test.ts +++ b/packages/subgraph/tests/payments/payments.test.ts @@ -47,6 +47,7 @@ import { assertRailParams, assertRailRateParams, assertTokenState, + assertTokenTotalLockup, assertUserTokenState, calculateNetworkFee, calculateOperatorCommission, @@ -357,6 +358,9 @@ describe("Payments", () => { assert.fieldEquals("OperatorToken", operatorTokenEntityIdStr, "lockupUsage", lockupFixed.toString()); assert.fieldEquals("OperatorApproval", operatorApprovalEntityIdStr, "lockupUsage", lockupFixed.toString()); + // Assert 1: Verify token lockup is updated (no streaming since paymentRate = 0) + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupFixed, ZERO_BIG_INT, ZERO_BIG_INT); + // Act 2: Modify lockup to new values (lockupPeriod -> newLockupPeriod, lockupFixed -> newLockupFixed) const newLockupPeriod = GraphBN.fromI32(5740); const newLockupFixed = GraphBN.fromI32(1_000_000); @@ -373,6 +377,9 @@ describe("Payments", () => { assertRailLockupParams(railId, newLockupPeriod, newLockupFixed); assert.fieldEquals("OperatorToken", operatorTokenEntityIdStr, "lockupUsage", newLockupFixed.toString()); assert.fieldEquals("OperatorApproval", operatorApprovalEntityIdStr, "lockupUsage", newLockupFixed.toString()); + + // Assert 2: Verify token lockup is updated to new value (no streaming since paymentRate = 0) + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, newLockupFixed, ZERO_BIG_INT, ZERO_BIG_INT); }); test("should handle one time payment properly", () => { @@ -422,6 +429,9 @@ describe("Payments", () => { assert.fieldEquals("OperatorToken", operatorTokenEntityIdStr, "lockupUsage", lockupFixed.toString()); assert.fieldEquals("OperatorApproval", operatorApprovalEntityIdStr, "lockupUsage", lockupFixed.toString()); + // Verify: Token lockup is set before payment (no streaming since paymentRate = 0) + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupFixed, ZERO_BIG_INT, ZERO_BIG_INT); + // Act: Process one-time payment // Payment breakdown: totalAmount = netPayeeAmount + operatorCommission + networkFee const totalAmount = GraphBN.fromI64(1_000_000_000_000); // 10^12 = 0.000001 FIL @@ -503,6 +513,9 @@ describe("Payments", () => { assert.fieldEquals("UserToken", payeeTokenEntityIdStr, "funds", netPayeeAmount.toString()); assert.fieldEquals("UserToken", payeeTokenEntityIdStr, "fundsCollected", netPayeeAmount.toString()); assert.fieldEquals("UserToken", serviceFeeRecipientTokenIdStr, "funds", operatorCommission.toString()); + + // Assert: Token fixed lockup reduced by one-time payment amount + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupFixed.minus(totalAmount), ZERO_BIG_INT, ZERO_BIG_INT); }); test("should handle rail settled properly", () => { @@ -590,17 +603,44 @@ describe("Payments", () => { const commissionRateBps = GraphBN.fromI32(250); // 2.5% setupCompleteRail(depositAmount, railId, commissionRateBps); + // update rail lockup + const lockupPeriod = GraphBN.fromI64(2880); + const lockupFixed = GraphBN.fromI64(1_000_000_000_000); // 10^12 = 0.000001 FIL + const railLockupModifiedEvent = createRailLockupModifiedEvent( + railId, + ZERO_BIG_INT, + lockupPeriod, + ZERO_BIG_INT, + lockupFixed, + ); + handleRailLockupModified(railLockupModifiedEvent); + + // token lockup state after RailLockupModified event + let lockupCurrent = lockupFixed; + let lockupRate = ZERO_BIG_INT; + let lockupLastSettledAt = railLockupModifiedEvent.block.number; + // change rail state to active by modifying rate to paymentRate from 0 const paymentRate = TEST_AMOUNTS.PAYMENT_RATE_LOW; const railRateModifiedEvent = createRailRateModifiedEvent(railId, ZERO_BIG_INT, paymentRate); handleRailRateModified(railRateModifiedEvent); + // token lockup state after RailRateModified event + lockupCurrent = lockupCurrent.plus(paymentRate.times(lockupPeriod)); + lockupRate = paymentRate; + lockupLastSettledAt = railRateModifiedEvent.block.number; + // Act: Terminate the rail with a future endEpoch - const endEpoch = railRateModifiedEvent.block.number.plus(GraphBN.fromI64(5000)); + const endEpoch = railRateModifiedEvent.block.number.plus(GraphBN.fromI64(2980)); const railTerminatedEvent = createRailTerminatedEvent(railId, TEST_ADDRESSES.ACCOUNT, endEpoch); railTerminatedEvent.block.number = railRateModifiedEvent.block.number.plus(GraphBN.fromI64(100)); handleRailTerminated(railTerminatedEvent); + // token lockup state after RailTerminated event + lockupCurrent = lockupCurrent.plus(paymentRate.times(GraphBN.fromI64(100))); + lockupRate = ZERO_BIG_INT; + lockupLastSettledAt = railTerminatedEvent.block.number; + // Assert: Rail state transitions to TERMINATED // endEpoch is set correctly const railEntityId = getRailEntityId(railId).toHex(); @@ -610,6 +650,9 @@ describe("Payments", () => { // Payer's lockupRate is cleared to zero const payerTokenIdStr = getUserTokenEntityId(TEST_ADDRESSES.ACCOUNT, TEST_ADDRESSES.TOKEN).toHexString(); assert.fieldEquals("UserToken", payerTokenIdStr, "lockupRate", ZERO_BIG_INT.toString()); + + // Assert: Token's lockup + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupCurrent, lockupRate, lockupLastSettledAt); }); test("should handle rail finalized properly", () => { @@ -619,11 +662,6 @@ describe("Payments", () => { const commissionRateBps = GraphBN.fromI32(300); // 3% setupCompleteRail(depositAmount, railId, commissionRateBps); - // Set payment rate (0 -> paymentRate) - const paymentRate = TEST_AMOUNTS.PAYMENT_RATE_HIGH; - const railRateModifiedEvent = createRailRateModifiedEvent(railId, ZERO_BIG_INT, paymentRate); - handleRailRateModified(railRateModifiedEvent); - // Set lockup params (0 -> lockupPeriod, 0 -> lockupFixed) const lockupPeriod = GraphBN.fromI64(1000); const lockupFixed = TEST_AMOUNTS.LOCKUP_FIXED_SMALL; @@ -636,10 +674,21 @@ describe("Payments", () => { ); handleRailLockupModified(railLockupModifiedEvent); + // Set payment rate (0 -> paymentRate) + const paymentRate = TEST_AMOUNTS.PAYMENT_RATE_HIGH; + const railRateModifiedEvent = createRailRateModifiedEvent(railId, ZERO_BIG_INT, paymentRate); + handleRailRateModified(railRateModifiedEvent); + // Verify: Rail is ACTIVE with correct rate and lockup params assertRailRateParams(railId, "ACTIVE", paymentRate, railRateModifiedEvent.block.number.toString()); assertRailLockupParams(railId, lockupPeriod, lockupFixed); + // Verify: Token lockup state after RailLockupModified and RailRateModified events + let lockupCurrent = lockupFixed.plus(paymentRate.times(lockupPeriod)); + let lockupRate = paymentRate; + let lockupLastSettledUntilEpoch = railRateModifiedEvent.block.number; + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupCurrent, lockupRate, lockupLastSettledUntilEpoch); + // Verify: Operator usage reflects lockupFixed + (paymentRate * lockupPeriod) const expectedLockupUsage = lockupFixed.plus(paymentRate.times(lockupPeriod)); assertOperatorApprovalState( @@ -660,16 +709,34 @@ describe("Payments", () => { paymentRate.toString(), // rateUsage ); + // Act: Terminate the rail with a future endEpoch + const endEpoch = railRateModifiedEvent.block.number.plus(GraphBN.fromI64(2980)); + const railTerminatedEvent = createRailTerminatedEvent(railId, TEST_ADDRESSES.ACCOUNT, endEpoch); + railTerminatedEvent.block.number = railRateModifiedEvent.block.number.plus(GraphBN.fromI64(100)); + handleRailTerminated(railTerminatedEvent); + + lockupCurrent = lockupCurrent.plus(paymentRate.times(GraphBN.fromI64(100))); + lockupRate = ZERO_BIG_INT; + lockupLastSettledUntilEpoch = railTerminatedEvent.block.number; + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupCurrent, lockupRate, lockupLastSettledUntilEpoch); + // Act: Finalize the rail const railFinalizedEvent = createRailFinalizedEvent(railId); - railFinalizedEvent.block.number = railRateModifiedEvent.block.number.plus(GraphBN.fromI64(200)); + railFinalizedEvent.block.number = railRateModifiedEvent.block.number.plus(GraphBN.fromI64(3000)); handleRailFinalized(railFinalizedEvent); + // token lockup state after RailFinalized event + lockupCurrent = lockupCurrent.minus(lockupFixed); + // no update in lockup rate and last settled until epoch + // Assert: Rail state transitions to FINALIZED const railEntityId = getRailEntityId(railId).toHex(); assert.fieldEquals("Rail", railEntityId, "state", "FINALIZED"); // Assert: Operator lockup usage is cleared assertOperatorLockupCleared(TEST_ADDRESSES.OPERATOR, TEST_ADDRESSES.TOKEN); + + // Assert: Token lockup state after RailFinalized event + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupCurrent, lockupRate, lockupLastSettledUntilEpoch); }); }); diff --git a/packages/subgraph/tests/payments/token-lockup.test.ts b/packages/subgraph/tests/payments/token-lockup.test.ts new file mode 100644 index 0000000..4e8961c --- /dev/null +++ b/packages/subgraph/tests/payments/token-lockup.test.ts @@ -0,0 +1,290 @@ +import { BigInt as GraphBN } from "@graphprotocol/graph-ts"; +import { afterEach, assert, beforeAll, clearStore, describe, test } from "matchstick-as"; +import { + handleRailFinalized, + handleRailLockupModified, + handleRailRateModified, + handleRailSettled, + handleRailTerminated, +} from "../../src/payments"; +import { ZERO_BIG_INT } from "../../src/utils/metrics"; +import { mockERC20Contract } from "../mocks"; +import { + createRailFinalizedEvent, + createRailLockupModifiedEvent, + createRailRateModifiedEvent, + createRailSettledEvent, + createRailTerminatedEvent, +} from "./events"; +import { + assertTokenTotalLockup, + calculateNetworkFee, + setupCompleteRail, + TEST_ADDRESSES, + TEST_AMOUNTS, +} from "./fixtures"; + +describe("Token Lockup Tracking", () => { + const tokenName = "Test Token"; + const tokenSymbol = "TEST"; + const tokenDecimals = 18; + + beforeAll(() => { + mockERC20Contract(TEST_ADDRESSES.TOKEN, tokenName, tokenSymbol, tokenDecimals); + }); + + afterEach(() => { + clearStore(); + }); + + test("should track token lockup correctly through complete rail lifecycle", () => { + // lockup state + let lockupCurrent = ZERO_BIG_INT; + let lockupRate = ZERO_BIG_INT; + let lockupLastSettledUntilEpoch = ZERO_BIG_INT; + // Arrange: Setup First Rail + let blockNumber = GraphBN.fromI64(1); + const depositAmount = TEST_AMOUNTS.MEDIUM_DEPOSIT; + const railId1 = GraphBN.fromI64(1); + setupCompleteRail(depositAmount, railId1, ZERO_BIG_INT); + + const lockupPeriod1 = GraphBN.fromI32(30); + const lockupFixed1 = TEST_AMOUNTS.LOCKUP_FIXED_SMALL; + const railLockupModifiedEvent = createRailLockupModifiedEvent( + railId1, + ZERO_BIG_INT, + lockupPeriod1, + ZERO_BIG_INT, + lockupFixed1, + ); + railLockupModifiedEvent.block.number = blockNumber; + handleRailLockupModified(railLockupModifiedEvent); + + // lockup state + lockupCurrent = lockupFixed1; + // no update in lockupRate and lockupLastSettledUntilEpoch + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupCurrent, lockupRate, lockupLastSettledUntilEpoch); + + // update rail rate at block = 10 + blockNumber = GraphBN.fromI64(10); + const newRate = TEST_AMOUNTS.PAYMENT_RATE_MEDIUM; + const railRateModifiedEvent = createRailRateModifiedEvent(railId1, ZERO_BIG_INT, newRate); + railRateModifiedEvent.block.number = blockNumber; + handleRailRateModified(railRateModifiedEvent); + + // lockup state after RailRateModified event + lockupCurrent = lockupCurrent.plus(newRate.times(lockupPeriod1)); + lockupRate = lockupRate.plus(newRate); + lockupLastSettledUntilEpoch = railRateModifiedEvent.block.number; + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupCurrent, lockupRate, lockupLastSettledUntilEpoch); + + // Create Another rail + const railId2 = GraphBN.fromI64(2); + setupCompleteRail(depositAmount, railId2, ZERO_BIG_INT); + + // rail2: modify rail lockup at block = 30 + blockNumber = GraphBN.fromI64(30); + const lockupPeriod2 = GraphBN.fromI32(30); + const lockupFixed2 = TEST_AMOUNTS.LOCKUP_FIXED_MEDIUM; + const railLockupModifiedEvent2 = createRailLockupModifiedEvent( + railId2, + ZERO_BIG_INT, + lockupPeriod2, + ZERO_BIG_INT, + lockupFixed2, + ); + railLockupModifiedEvent2.block.number = blockNumber; + handleRailLockupModified(railLockupModifiedEvent2); + + // lockup state after RailLockupModified event for rail2 + lockupCurrent = lockupCurrent.plus(lockupFixed2); + // no update in lockupRate and lockupLastSettledUntilEpoch + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupCurrent, lockupRate, lockupLastSettledUntilEpoch); + + // rail1: modify rail rate at block = 50 + blockNumber = GraphBN.fromI64(50); + const newRate2 = TEST_AMOUNTS.PAYMENT_RATE_LOW; + const railRateModifiedEvent2 = createRailRateModifiedEvent(railId1, newRate, newRate2); + railRateModifiedEvent2.block.number = blockNumber; + handleRailRateModified(railRateModifiedEvent2); + + // lockup state after RailRateModified event for rail1 + // settleTokenLockup till block 50 + lockupCurrent = lockupCurrent.plus(blockNumber.minus(lockupLastSettledUntilEpoch).times(lockupRate)); + lockupCurrent = lockupCurrent.minus(newRate.times(lockupPeriod1)).plus(newRate2.times(lockupPeriod1)); + lockupRate = lockupRate.minus(newRate).plus(newRate2); + lockupLastSettledUntilEpoch = railRateModifiedEvent2.block.number; + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupCurrent, lockupRate, lockupLastSettledUntilEpoch); + + // rail2: modify rail rate at block = 60 + blockNumber = GraphBN.fromI64(60); + const newRate3 = TEST_AMOUNTS.PAYMENT_RATE_HIGH; + const railRateModifiedEvent3 = createRailRateModifiedEvent(railId2, ZERO_BIG_INT, newRate3); + railRateModifiedEvent3.block.number = blockNumber; + handleRailRateModified(railRateModifiedEvent3); + + // lockup state after RailRateModified event for rail2 + // settleTokenLockup till block 60 + lockupCurrent = lockupCurrent.plus(blockNumber.minus(lockupLastSettledUntilEpoch).times(lockupRate)); + lockupCurrent = lockupCurrent.plus(newRate3.times(lockupPeriod2)); + lockupRate = lockupRate.plus(newRate3); + lockupLastSettledUntilEpoch = railRateModifiedEvent3.block.number; + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupCurrent, lockupRate, lockupLastSettledUntilEpoch); + + // rail1: modify rail rate at block = 80 + blockNumber = GraphBN.fromI64(80); + const newRate4 = TEST_AMOUNTS.PAYMENT_RATE_HIGH; + const railRateModifiedEvent4 = createRailRateModifiedEvent(railId1, newRate2, newRate4); + railRateModifiedEvent4.block.number = blockNumber; + handleRailRateModified(railRateModifiedEvent4); + + // lockup state after RailRateModified event for rail1 + // settleTokenLockup till block 80 + lockupCurrent = lockupCurrent.plus(blockNumber.minus(lockupLastSettledUntilEpoch).times(lockupRate)); + lockupCurrent = lockupCurrent.minus(newRate2.times(lockupPeriod1)).plus(newRate4.times(lockupPeriod1)); + lockupRate = lockupRate.minus(newRate2).plus(newRate4); + lockupLastSettledUntilEpoch = railRateModifiedEvent4.block.number; + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupCurrent, lockupRate, lockupLastSettledUntilEpoch); + + // rail1: settle rail until block 90 at block = 100 + blockNumber = GraphBN.fromI64(100); + const totalSettledAmount = GraphBN.fromI64(40) + .times(TEST_AMOUNTS.PAYMENT_RATE_MEDIUM) + .plus(GraphBN.fromI64(30).times(TEST_AMOUNTS.PAYMENT_RATE_LOW)) + .plus(GraphBN.fromI64(10).times(TEST_AMOUNTS.PAYMENT_RATE_HIGH)); + const networkFee = calculateNetworkFee(totalSettledAmount); + const operatorCommission = ZERO_BIG_INT; + const totalNetPayeeAmount = totalSettledAmount.minus(networkFee).minus(operatorCommission); + const settledUpto = GraphBN.fromI64(90); + const railSettledEvent = createRailSettledEvent( + railId1, + totalSettledAmount, + totalNetPayeeAmount, + operatorCommission, + networkFee, + settledUpto, + ); + railSettledEvent.block.number = blockNumber; + handleRailSettled(railSettledEvent); + + // lockup state after RailSettled event for rail1 + // settleTokenLockup till block 100 + lockupCurrent = lockupCurrent.plus(blockNumber.minus(lockupLastSettledUntilEpoch).times(lockupRate)); + lockupCurrent = lockupCurrent.minus(totalSettledAmount); // not necessary that lockup reduction is same as totalSettledAmount (in case of validator) + lockupLastSettledUntilEpoch = railSettledEvent.block.number; + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupCurrent, lockupRate, lockupLastSettledUntilEpoch); + + // rail1: terminate rail1 at block = 120 + blockNumber = GraphBN.fromI64(120); + const endEpoch = blockNumber.plus(lockupPeriod1); + const railTerminatedEvent = createRailTerminatedEvent(railId1, TEST_ADDRESSES.ACCOUNT, endEpoch); + railTerminatedEvent.block.number = blockNumber; + handleRailTerminated(railTerminatedEvent); + + // lockup state after RailTerminated event for rail1 + // settleTokenLockup till block 120 + lockupCurrent = lockupCurrent.plus(blockNumber.minus(lockupLastSettledUntilEpoch).times(lockupRate)); + lockupRate = lockupRate.minus(newRate4); + lockupLastSettledUntilEpoch = railTerminatedEvent.block.number; + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupCurrent, lockupRate, lockupLastSettledUntilEpoch); + + // rail1: Settle terminated rail until block 150 at block = 160 + blockNumber = GraphBN.fromI64(160); + const totalSettledAmount2 = GraphBN.fromI64(60).times(TEST_AMOUNTS.PAYMENT_RATE_HIGH); + const networkFee2 = calculateNetworkFee(totalSettledAmount2); + const operatorCommission2 = ZERO_BIG_INT; + const totalNetPayeeAmount2 = totalSettledAmount2.minus(networkFee2).minus(operatorCommission2); + const settledUpto2 = endEpoch; + const railSettledEvent2 = createRailSettledEvent( + railId1, + totalSettledAmount2, + totalNetPayeeAmount2, + operatorCommission2, + networkFee2, + settledUpto2, + ); + railSettledEvent2.block.number = blockNumber; + handleRailSettled(railSettledEvent2); + + const finalizeRailEvent1 = createRailFinalizedEvent(railId1); + finalizeRailEvent1.block.number = blockNumber; + handleRailFinalized(finalizeRailEvent1); + + // lockup state after RailSettled event for rail1 + // settleTokenLockup till block 160 + lockupCurrent = lockupCurrent.plus(blockNumber.minus(lockupLastSettledUntilEpoch).times(lockupRate)); + lockupCurrent = lockupCurrent.minus(totalSettledAmount2); + lockupCurrent = lockupCurrent.minus(lockupFixed1); + lockupLastSettledUntilEpoch = railSettledEvent2.block.number; + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupCurrent, lockupRate, lockupLastSettledUntilEpoch); + + // rail2: terminate rail2 at block = 180 + blockNumber = GraphBN.fromI64(180); + const endEpoch2 = blockNumber.plus(lockupPeriod2); + const railTerminatedEvent2 = createRailTerminatedEvent(railId2, TEST_ADDRESSES.ACCOUNT, endEpoch2); + railTerminatedEvent2.block.number = blockNumber; + handleRailTerminated(railTerminatedEvent2); + + // lockup state after RailTerminated event for rail2 + // settleTokenLockup till block 180 + lockupCurrent = lockupCurrent.plus(blockNumber.minus(lockupLastSettledUntilEpoch).times(lockupRate)); + lockupRate = lockupRate.minus(newRate3); + lockupLastSettledUntilEpoch = railTerminatedEvent2.block.number; + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupCurrent, lockupRate, lockupLastSettledUntilEpoch); + + // rail2: modify terminated rail's rate at block = 190 + blockNumber = GraphBN.fromI64(190); + const newRate5 = TEST_AMOUNTS.PAYMENT_RATE_LOW; + const railRateModifiedEvent5 = createRailRateModifiedEvent(railId2, newRate3, newRate5); + railRateModifiedEvent5.block.number = blockNumber; + handleRailRateModified(railRateModifiedEvent5); + + // lockup state after RailRateModified event for rail2 + // settleTokenLockup till block 190 + lockupCurrent = lockupCurrent.plus(blockNumber.minus(lockupLastSettledUntilEpoch).times(lockupRate)); + const effectiveLockupPeriod = endEpoch2.minus(blockNumber); + lockupCurrent = lockupCurrent.plus(effectiveLockupPeriod.times(newRate5.minus(newRate3))); + lockupLastSettledUntilEpoch = railRateModifiedEvent5.block.number; + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupCurrent, lockupRate, lockupLastSettledUntilEpoch); + + // rail2: settle rail2 after end epoch at block = 220 + blockNumber = GraphBN.fromI64(220); + const totalSettledAmount3 = GraphBN.fromI64(130) + .times(TEST_AMOUNTS.PAYMENT_RATE_HIGH) + .plus(GraphBN.fromI64(20).times(TEST_AMOUNTS.PAYMENT_RATE_LOW)); + const networkFee3 = calculateNetworkFee(totalSettledAmount3); + const operatorCommission3 = ZERO_BIG_INT; + const totalNetPayeeAmount3 = totalSettledAmount3.minus(networkFee3).minus(operatorCommission3); + const settledUpto3 = endEpoch2; + const railSettledEvent3 = createRailSettledEvent( + railId2, + totalSettledAmount3, + totalNetPayeeAmount3, + operatorCommission3, + networkFee3, + settledUpto3, + ); + railSettledEvent3.block.number = blockNumber; + handleRailSettled(railSettledEvent3); + + const finalizeRailEvent2 = createRailFinalizedEvent(railId2); + finalizeRailEvent2.block.number = blockNumber; + handleRailFinalized(finalizeRailEvent2); + + // lockup state after RailSettled event for rail2 + // settleTokenLockup till block 220 + lockupCurrent = lockupCurrent.plus(blockNumber.minus(lockupLastSettledUntilEpoch).times(lockupRate)); + lockupCurrent = lockupCurrent.minus(totalSettledAmount3); + lockupCurrent = lockupCurrent.minus(lockupFixed2); + lockupLastSettledUntilEpoch = railSettledEvent3.block.number; + assertTokenTotalLockup(TEST_ADDRESSES.TOKEN, lockupCurrent, lockupRate, lockupLastSettledUntilEpoch); + + // After complete rail lifecycle, lockupCurrent and lockupRate should be zero + // Settlements drain accumulated streaming lockup from lockupCurrent + // oneTimePayments and rail finalization are responsible for draining fixed lockup from lockupCurrent + // Rail terminations reduce lockupRate to zero by moving each rail's rate + assert.bigIntEquals(lockupCurrent, ZERO_BIG_INT); + assert.bigIntEquals(lockupRate, ZERO_BIG_INT); + assert.bigIntEquals(lockupLastSettledUntilEpoch, blockNumber); + }); +}); diff --git a/packages/types/src/generated/graphql.ts b/packages/types/src/generated/graphql.ts index 47e9c40..e39c1b7 100644 --- a/packages/types/src/generated/graphql.ts +++ b/packages/types/src/generated/graphql.ts @@ -206,6 +206,9 @@ export type Token = { __typename: "Token"; decimals: Scalars["BigInt"]["output"]; id: Scalars["Bytes"]["output"]; + lockupCurrent: Scalars["BigInt"]["output"]; + lockupLastSettledUntilEpoch: Scalars["BigInt"]["output"]; + lockupRate: Scalars["BigInt"]["output"]; name: Scalars["String"]["output"]; operatorCommission: Scalars["BigInt"]["output"]; symbol: Scalars["String"]["output"];