SDK 2.1 adds a version-selected engine to the existing vesting math. Build it with createVestingEngine("v1"), then pass every time and plaintext amount explicitly. The returned object performs no I/O, reads no ambient clock and does not inspect a manager contract.
Preview a draft#
Schedule uses absolute start, cliff and end times as numbers. Amounts, chain time, fee basis points and edit ratios are bigints. Validation returns every violation it can determine; the calculation methods throw on values outside their contract widths.
import {
createVestingEngine,
type Schedule,
} from "@tokenops/sdk/fhe-vesting";
const engine = createVestingEngine("v1");
const schedule: Schedule = {
start: 1_800_000_000,
cliff: 1_802_592_000,
end: 1_831_536_000,
initialBps: 1_000,
cliffBps: 1_500,
intervalSeconds: 86_400,
timelockSeconds: 0,
revocable: true,
};
const allocation = 1_000_000n;
const recipient = "0x00000000000000000000000000000000000000a1" as const;
const violations = engine.validateSchedule(schedule, recipient, allocation);
if (violations.length > 0) throw new Error(JSON.stringify(violations));
// Use a block timestamp in production. The engine never reads a clock or RPC.
const now = 1_815_552_000n;
const vested = engine.vested(allocation, schedule, now, 0n);
const claimable = engine.claimable(vested, 100_000n);
const state = engine.claimState({
t: now,
schedule,
paused: false,
isRecipient: true,
revealedClaimable: claimable,
});- Pass the latest block timestamp as
t, notDate.now(). The unit is seconds. - A zero
revokeTimestampmeans not revoked. A non-zero value caps vesting at that instant. revealedClaimable: nullproduces stateunknown. The engine never decrypts an FHE handle.
Adapt client schedule shapes explicitly#
The manager client keeps the contract's field names. The versioned engine intentionally uses a smaller neutral Schedule shape. Convert a read or create draft at the boundary; keep VestingInfo.revokeTimestamp separate for vested and isTerminal.
import type {
Schedule,
VestingInfo,
VestingParams,
} from "@tokenops/sdk/fhe-vesting";
export function scheduleFromInfo(info: VestingInfo): Schedule {
return {
start: info.startTimestamp,
cliff: info.cliffReleaseTimestamp,
end: info.endTimestamp,
initialBps: info.initialUnlockBps,
cliffBps: info.cliffAmountBps,
intervalSeconds: info.releaseIntervalSecs,
timelockSeconds: info.timelock,
revocable: info.isRevocable,
};
}
export function scheduleFromParams(params: VestingParams): Schedule {
return {
start: params.startTimestamp,
cliff: params.startTimestamp + params.cliffSeconds,
end: params.endTimestamp,
initialBps: params.initialUnlockBps,
cliffBps: params.cliffAmountBps,
intervalSeconds: params.releaseIntervalSecs,
timelockSeconds: params.timelockSeconds,
revocable: params.isRevocable,
};
}Function surface#
| Function | Result | Contract rule reproduced |
|---|---|---|
vested | bigint | Initial and cliff unlocks, interval flooring, linear release and revoke-time cap. |
isTerminal / claimable | boolean / bigint | Whether vesting can still grow; max(vested - settled, 0). |
unlockInstant / claimState | bigint / state | Pause, recipient and timelock checks in contract order, then revealed claimable state. |
validateSchedule | Violation[] | All width, recipient, timestamp, basis-point, timelock and optional allocation checks. |
tokenFeeTransfer | { transfer, fee } | Distribution-token fee flooring; a zero transfer settles nothing and keeps no fee. |
splitAmounts | SplitResult | The original keeps its floored ratio and the child receives both remainders. |
revokeEffect | { unvested } | allocation - max(vestedAtRevoke, settled). |
Fees, splits and revokes#
import { createVestingEngine } from "@tokenops/sdk/fhe-vesting";
const engine = createVestingEngine("v1");
const tokenFee = engine.tokenFeeTransfer(999n, 30n);
// { transfer: 996n, fee: 3n }
const split = engine.splitAmounts(1_000n, 300n, 2_500n, 10_000n);
// original keeps 25%; child receives the remainder.
// { original: 250n, child: 750n,
// originalSettled: 75n, childSettled: 225n }
const revoke = engine.revokeEffect(1_000n, 600n, 450n);
// { unvested: 400n } = allocation - max(vestedAtRevoke, settled)Use a capability registry as an adapter#
The SDK does not discover a manager's engine selector or feature flags. Store those facts in your deployment inventory, use capabilities such as split and pause to gate UI, and pass only the inventory's engine selector into the SDK. Check the frozen known-version list at the boundary and fail closed.
import {
EngineError,
KNOWN_MANAGER_VERSIONS,
createVestingEngine,
type ManagerVersion,
} from "@tokenops/sdk/fhe-vesting";
// This record belongs to your deployment inventory, not the SDK.
interface ManagerCapabilities {
engineVersion: ManagerVersion;
split: boolean;
pausable: boolean;
}
export function engineForManager(record: ManagerCapabilities) {
if (!KNOWN_MANAGER_VERSIONS.includes(record.engineVersion)) {
throw new EngineError("UnsupportedManagerVersion", {
version: record.engineVersion,
});
}
return createVestingEngine(record.engineVersion);
}import { EngineError, createVestingEngine } from "@tokenops/sdk/fhe-vesting";
declare const versionFromInventory: string;
declare function quarantineManager(version: string | undefined): void;
try {
createVestingEngine(versionFromInventory);
} catch (error) {
if (error instanceof EngineError && error.code === "UnsupportedManagerVersion") {
quarantineManager(error.version); // do not guess or fall back
}
throw error;
}Existing compute helpers remain public#
The earlier computeVestedAmount, computeClaimState, computeClaimTransfer, computeSplitVestingAmounts and related helpers still ship from @tokenops/sdk/fhe-vesting. They accept VestingInfo or VestingParams directly and expose conveniences such as phases and next-release calculations. Use the versioned engine when stored manager versions must select an exact parity implementation; use the compute* surface when its client-shaped inputs and richer timeline results suit the call site.