Skip to main content

Check Direct Minting Limits

Overview

This guide reads the on-chain direct minting rate limits, replays the tumbling-window state off-chain, and prints the maximum amount a single mint can request right now without being delayed.

Direct minting is throttled by the MintingRateLimiter library: an hourly and a daily window cap how much FXRP can be minted before the asset manager starts pushing new mintings into a future window via DirectMintingDelayed. A pre-flight check lets a frontend or executor avoid surprising the user with a delay.

The complete runnable example is available in the flare-viem-starter repository.

How the windows work

MintingRateLimiter library uses clock-aligned tumbling windows, not rolling windows. On initialization, the window start is snapped to a multiple of windowSizeSeconds:

windowStartTimestamp = block.timestamp - block.timestamp % windowSizeSeconds

With windowSizeSeconds = 3600, the hourly window aligns to UTC hour boundaries (00:00-01:00, 01:00-02:00, …). The daily window (86400 seconds) aligns to 00:00 UTC.

On every write, the limiter advances the window:

windowsElapsed        = (now - windowStartTimestamp) / windowSizeSeconds
mintedInCurrentWindow = subOrZero(mintedInCurrentWindow, windowsElapsed * maxPerWindow)
windowStartTimestamp += windowsElapsed * windowSizeSeconds

Two consequences worth noting:

  1. Unused capacity does not roll over. subOrZero clamps at zero, so an idle hour does not give twice the cap the next hour — every fresh window starts at exactly maxPerWindow.
  2. Over-cap mints are delayed, not rejected. executionAllowedAt is set to windowStartTimestamp + windowSize * mintedInCurrentWindow / maxPerWindow, so overflow drains hour-by-hour through the subOrZero step.

Reading the limiter state on its own returns stale (windowStart, minted) values until the next write touches the contract, so this script replays the slide off-chain to show the values as they would be right now.

A mint can be delayed by either the hourly/daily window or the large-minting threshold — whichever pushes executionAllowedAt further into the future wins.

Prerequisites

  • The flare-viem-starter cloned locally with dependencies installed.
  • A configured publicClient pointing at the Flare network you want to query (Coston2 by default in the starter).

Direct Minting Limits Script

src/fassets/direct-minting-limits.ts
import { dropsToXrp } from "xrpl";
import { getContractAddressByName } from "./utils/flare-contract-registry";
import {
getAssetMintingGranularityUBA,
getDirectMintingDailyLimitUBA,
getDirectMintingDailyLimiterState,
getDirectMintingHourlyLimitUBA,
getDirectMintingHourlyLimiterState,
getDirectMintingLargeMintingDelaySeconds,
getDirectMintingLargeMintingThresholdUBA,
getDirectMintingsUnblockUntilTimestamp,
} from "./utils/fassets";

// 1. Window sizes are clock-aligned tumbling, not rolling. Hourly snaps to
// UTC hour boundaries; daily snaps to 00:00 UTC.
const HOURLY_WINDOW_SECONDS = 3600n;
const DAILY_WINDOW_SECONDS = 86400n;

// 2. Format helpers.
function formatUba(uba: bigint): string {
return `${uba.toString()} UBA (${dropsToXrp(uba.toString())} XRP)`;
}

function formatTimestamp(secondsSinceEpoch: bigint, now: bigint): string {
const iso = new Date(Number(secondsSinceEpoch) * 1000).toISOString();
const delta = Number(secondsSinceEpoch - now);
const relative = delta >= 0 ? `in ${delta}s` : `${-delta}s ago`;
return `${iso} (${relative})`;
}

function bigintMin(a: bigint, b: bigint): bigint {
return a < b ? a : b;
}

// 3. Replay the limiter slide off-chain. Reading state alone returns stale
// `(windowStart, minted)` values until the next write touches the limiter,
// so we re-anchor the window and drain `mintedInCurrentWindow` using the
// same window-advancement logic as MintingRateLimiter.sol.
function computeWindowState({
now,
windowStartTimestamp,
mintedInCurrentWindowUBA,
limitUBA,
windowSizeSeconds,
}: {
now: bigint;
windowStartTimestamp: bigint;
mintedInCurrentWindowUBA: bigint;
limitUBA: bigint;
windowSizeSeconds: bigint;
}) {
let effectiveStart = windowStartTimestamp;
let usedUBA = mintedInCurrentWindowUBA;

if (
windowStartTimestamp > 0n &&
now >= windowStartTimestamp + windowSizeSeconds
) {
const windowsElapsed = (now - windowStartTimestamp) / windowSizeSeconds;
effectiveStart = windowStartTimestamp + windowsElapsed * windowSizeSeconds;
const drained = windowsElapsed * limitUBA;
usedUBA = drained >= usedUBA ? 0n : usedUBA - drained;
}

const remainingUBA = limitUBA > usedUBA ? limitUBA - usedUBA : 0n;
const nextResetAt = effectiveStart + windowSizeSeconds;

return { effectiveStart, usedUBA, remainingUBA, nextResetAt };
}

// 4. Print one window.
function printWindow(
label: string,
opts: {
limitUBA: bigint;
usedUBA: bigint;
remainingUBA: bigint;
effectiveStart: bigint;
nextResetAt: bigint;
now: bigint;
},
) {
const { limitUBA, usedUBA, remainingUBA, effectiveStart, nextResetAt, now } =
opts;
const usedPct =
limitUBA === 0n ? 0 : Number((usedUBA * 10000n) / limitUBA) / 100;
const row = (key: string, value: string) =>
console.log(`${key.padEnd(17)} ${value}`);
console.log(`=== ${label} ===`);
row("Limit:", formatUba(limitUBA));
row("Used:", `${formatUba(usedUBA)} (${usedPct.toFixed(2)}%)`);
row("Remaining:", formatUba(remainingUBA));
row("Window started:", formatTimestamp(effectiveStart, now));
row("Window resets at:", formatTimestamp(nextResetAt, now));
console.log();
}

async function main() {
// 5. Resolve `AssetManagerFXRP` through the Flare Contract Registry.
const assetManagerAddress =
await getContractAddressByName("AssetManagerFXRP");
console.log("AssetManagerFXRP address:", assetManagerAddress, "\n");

// 6. Read AMG granularity, hourly and daily caps, raw limiter state, the
// unblock flag, and the large-minting threshold and delay in parallel.
const [
amgGranularityUBA,
hourlyLimitUBA,
dailyLimitUBA,
hourlyState,
dailyState,
unblockUntilTimestamp,
largeThresholdUBA,
largeDelaySeconds,
] = await Promise.all([
getAssetMintingGranularityUBA(assetManagerAddress),
getDirectMintingHourlyLimitUBA(assetManagerAddress),
getDirectMintingDailyLimitUBA(assetManagerAddress),
getDirectMintingHourlyLimiterState(assetManagerAddress),
getDirectMintingDailyLimiterState(assetManagerAddress),
getDirectMintingsUnblockUntilTimestamp(assetManagerAddress),
getDirectMintingLargeMintingThresholdUBA(assetManagerAddress),
getDirectMintingLargeMintingDelaySeconds(assetManagerAddress),
]);

const now = BigInt(Math.floor(Date.now() / 1000));
const limiterDisabled = unblockUntilTimestamp > now;

// 7. Limiter state is returned as raw AMG (uint64); convert to UBA via
// `assetMintingGranularityUBA` before passing into the window math.
const computeAndPrint = (
label: string,
limitUBA: bigint,
state: readonly [bigint, bigint],
sizeSeconds: bigint,
) => {
const [windowStart, mintedAmg] = state;
const result = computeWindowState({
now,
windowStartTimestamp: windowStart,
mintedInCurrentWindowUBA: mintedAmg * amgGranularityUBA,
limitUBA,
windowSizeSeconds: sizeSeconds,
});
printWindow(label, { ...result, limitUBA, now });
return result;
};

const hourly = computeAndPrint(
"Hourly window",
hourlyLimitUBA,
hourlyState,
HOURLY_WINDOW_SECONDS,
);
const daily = computeAndPrint(
"Daily window",
dailyLimitUBA,
dailyState,
DAILY_WINDOW_SECONDS,
);

// 8. Honor the unblock flag. While `directMintingsUnblockUntilTimestamp`
// is in the future, governance has temporarily disabled the limiter
// and the full caps are available.
console.log("=== Other flags ===");
if (limiterDisabled) {
console.log(
"Limiter DISABLED until:",
formatTimestamp(unblockUntilTimestamp, now),
"— hourly/daily caps are not enforced right now.",
);
} else {
console.log(
"Limiter active (unblockUntilTimestamp =",
unblockUntilTimestamp.toString() + ")",
);
}
console.log("Large minting threshold:", formatUba(largeThresholdUBA));
console.log("Large minting delay: ", `${largeDelaySeconds.toString()}s`);
console.log();

// 9. Pre-flight gate: how much can a single mint safely request right now
// without being delayed? It must fit both the hourly and daily windows.
const hourlyHeadroomUBA = limiterDisabled
? hourlyLimitUBA
: hourly.remainingUBA;
const dailyHeadroomUBA = limiterDisabled ? dailyLimitUBA : daily.remainingUBA;
const safeRemainingUBA = bigintMin(hourlyHeadroomUBA, dailyHeadroomUBA);
console.log(
"Maximum single mint that fits both windows:",
formatUba(safeRemainingUBA),
);
}

void main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

Code Breakdown

  1. Window sizes. HOURLY_WINDOW_SECONDS and DAILY_WINDOW_SECONDS are the windowSizeSeconds values the MintingRateLimiter.sol library uses — 3600 aligns to UTC hour boundaries, 86400 aligns to 00:00 UTC.
  2. Format helpers. formatUba prints raw UBA (units of basis assets) alongside the XRP equivalent via dropsToXrp, and formatTimestamp shows ISO time with a relative offset so it is clear whether a timestamp is in the future or the past.
  3. Replay the limiter slide. computeWindowState mirrors the window-advancement logic in MintingRateLimiter.sol: it advances windowStart past every tumble that has elapsed and drains mintedInCurrentWindow by windowsElapsed * limit. Without this off-chain replay, you would see stale numbers between writes.
  4. Print one window's live state. printWindow shows the live cap, used amount, and percentage, remaining headroom, the effective window start, and the next UTC tumble.
  5. Resolve AssetManagerFXRP. getContractAddressByName("AssetManagerFXRP") looks up the asset manager through the Flare Contract Registry.
  6. Read everything in parallel. Promise.all fetches the AMG granularity, the hourly and daily caps, the raw limiter state for hourly and daily windows, the unblockUntilTimestamp, and the large-minting threshold and delay in one round trip.
  7. Convert AMG to UBA. The limiter stores mintedInCurrentWindow in AMG (uint64) for cheap on-chain storage. Multiplying by assetMintingGranularityUBA rebases it into UBA (units of basis assets) so it can be compared against the UBA-denominated cap.
  8. Honor the unblock flag. When getDirectMintingsUnblockUntilTimestamp returns a future timestamp, governance has temporarily turned off the limiter; treat full caps as available.
  9. Pre-flight gate. bigintMin(hourlyHeadroom, dailyHeadroom) is the largest amount that fits both windows. A request larger than this will not revert — it will mint with DirectMintingDelayed and an executionAllowedAt in the future.

Important Notes

What's next

To continue your FAssets development journey, you can: