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:
- Unused capacity does not roll over.
subOrZeroclamps at zero, so an idle hour does not give twice the cap the next hour — every fresh window starts at exactlymaxPerWindow. - Over-cap mints are delayed, not rejected.
executionAllowedAtis set towindowStartTimestamp + windowSize * mintedInCurrentWindow / maxPerWindow, so overflow drains hour-by-hour through thesubOrZerostep.
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
publicClientpointing at the Flare network you want to query (Coston2 by default in the starter).
Direct Minting Limits Script
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
- Window sizes.
HOURLY_WINDOW_SECONDSandDAILY_WINDOW_SECONDSare thewindowSizeSecondsvalues theMintingRateLimiter.sollibrary uses —3600aligns to UTC hour boundaries,86400aligns to 00:00 UTC. - Format helpers.
formatUbaprints raw UBA (units of basis assets) alongside the XRP equivalent viadropsToXrp, andformatTimestampshows ISO time with a relative offset so it is clear whether a timestamp is in the future or the past. - Replay the limiter slide.
computeWindowStatemirrors the window-advancement logic inMintingRateLimiter.sol: it advanceswindowStartpast every tumble that has elapsed and drainsmintedInCurrentWindowbywindowsElapsed * limit. Without this off-chain replay, you would see stale numbers between writes. - Print one window's live state.
printWindowshows the live cap, used amount, and percentage, remaining headroom, the effective window start, and the next UTC tumble. - Resolve
AssetManagerFXRP.getContractAddressByName("AssetManagerFXRP")looks up the asset manager through the Flare Contract Registry. - Read everything in parallel.
Promise.allfetches the AMG granularity, the hourly and daily caps, the raw limiter state for hourly and daily windows, theunblockUntilTimestamp, and the large-minting threshold and delay in one round trip. - Convert AMG to UBA.
The limiter stores
mintedInCurrentWindowin AMG (uint64) for cheap on-chain storage. Multiplying byassetMintingGranularityUBArebases it into UBA (units of basis assets) so it can be compared against the UBA-denominated cap. - Honor the unblock flag.
When
getDirectMintingsUnblockUntilTimestampreturns a future timestamp, governance has temporarily turned off the limiter; treat full caps as available. - 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 withDirectMintingDelayedand anexecutionAllowedAtin the future.
Important Notes
- Large mintings have an independent fixed delay.
Mintings above
directMintingLargeMintingThresholdUBAare delayed bydirectMintingLargeMintingDelaySecondseven if the hourly and daily windows have headroom. - Reads are stale between writes. Always advance the window off-chain when displaying live limiter state.
- Plan UI flows around
DirectMintingDelayedso users are not surprised by a deferred execution.
To continue your FAssets development journey, you can:
- Mint with the 32-byte memo flow in Direct Mint FXRP.
- Mint with a reserved tag in Direct Mint FXRP with Tag.
- Read the protocol details in Direct Minting.