UNPKG

@arkiv-network/sdk

Version:

TypeScript client library for Arkiv's blockchains interactions

137 lines (134 loc) • 7.53 kB
import { a as MAX_LIFETIME, r as BLOCK_TIME } from "./consts-Lp55G1Qc.js"; import { n as InvalidExpiryError } from "./errors-BLeoFbFG.js"; //#region src/entity/expiry.ts /** * Builds a branded {@link Expiry}. * * Internal seam for {@link ExpirationTime}, which is the only thing that should call it — it is not * part of the package's public surface. */ function makeExpiry(minLifetime, expiresAt) { return { minLifetime, expiresAt }; } /** * Resolves an expiry into the wire pair and the block the entity will expire at. * * A duration arrives here already in blocks — {@link ExpirationTime} converted it at the nominal * `BLOCK_TIME` (2 seconds) through {@link toBlocks} — and a `Date` deadline is placed against * `currentBlock` at that same block time here. Both are **approximate**: block production is not a * clock, so a lifetime asked for in days drifts if the chain runs fast or slow. The block the * expiry resolves to against `currentBlock` comes back as {@link ResolvedExpiry.target}, so the * approximation is always observable rather than assumed. * * @throws {InvalidExpiryError} If the expiry is malformed, would outlive the chain's * {@link MAX_LIFETIME}, or would leave the entity dead on arrival. */ function resolveExpiry(expiry, context) { const { currentBlock } = context; if (typeof expiry?.minLifetime !== "bigint" || expiry.expiresAt === void 0) throw new InvalidExpiryError("expiration must be built with ExpirationTime, e.g. ExpirationTime.fromDays(30) or ExpirationTime.atBlock(1_200_000n)"); const { minLifetime } = expiry; const absolute = typeof expiry.expiresAt === "bigint" ? expiry.expiresAt : blockAtDate(expiry.expiresAt, currentBlock); if (minLifetime === 0n && absolute === 0n) throw new InvalidExpiryError("no expiry given. Pass ExpirationTime.fromDays(30) for a lifetime, ExpirationTime.atBlock(n) for an absolute deadline, or a deadline with { atLeast } to take whichever is later."); if (minLifetime > MAX_LIFETIME) throw new InvalidExpiryError(`a lifetime of ${minLifetime} blocks is longer than the chain's maximum lifetime of ${MAX_LIFETIME} blocks (about 182 days). Use ExpirationTime.max() for the longest lifetime allowed.`); const latestDeadline = currentBlock + 1n + MAX_LIFETIME; if (absolute > latestDeadline) throw new InvalidExpiryError(`expiresAt resolves to block ${absolute}, past block ${latestDeadline}: the chain's maximum lifetime is ${MAX_LIFETIME} blocks (about 182 days) after the block the transaction lands in, and the current block is ${currentBlock}. Use ExpirationTime.max() for the longest lifetime allowed.`); const relative = minLifetime === 0n ? 0n : currentBlock + minLifetime; const target = absolute > relative ? absolute : relative; if (target <= currentBlock) throw new InvalidExpiryError(`the entity would expire at block ${target}, which is not after the current block ${currentBlock}, so it would be dead on arrival`); return { expiresAt: absolute, minLifetime, target }; } /** * A duration in seconds as a whole number of blocks. * * Arkiv counts lifetimes in blocks, so a duration that is not a whole number of them does not * describe anything the chain can store. Rather than silently round a caller's `3` up to two blocks * — four seconds, not what they asked for — this rejects it: a duration must be a positive multiple * of `BLOCK_TIME` (2 seconds). The {@link ExpirationTime} helpers all produce such multiples. * * @throws {InvalidExpiryError} If the duration is not a positive whole number of blocks' seconds. */ function toBlocks(seconds) { if (!Number.isInteger(seconds) || seconds <= 0) throw new InvalidExpiryError(`a lifetime must be a positive whole number of seconds, got ${seconds}. Use the ExpirationTime helpers, e.g. ExpirationTime.fromDays(30).`); if (seconds % BLOCK_TIME !== 0) throw new InvalidExpiryError(`a lifetime must be a multiple of the ${BLOCK_TIME}s block time, got ${seconds}, which is ${seconds / BLOCK_TIME} blocks. Try ${seconds - seconds % BLOCK_TIME} or ${seconds + BLOCK_TIME - seconds % BLOCK_TIME}, or ExpirationTime.fromBlocks(n).`); return BigInt(seconds / BLOCK_TIME); } /** * The block a wall-clock instant lands on. * * Rounds **up**: a date almost never falls on a block boundary, and unlike a duration — which the * caller states and can state exactly — there is nothing here to reject, so the entity lives until * at least the instant asked for. */ function blockAtDate(date, currentBlock) { const secondsAway = (date.getTime() - Date.now()) / 1e3; if (secondsAway <= 0) throw new InvalidExpiryError(`the deadline ${date.toISOString()} is in the past, so the entity would be dead on arrival`); return currentBlock + BigInt(Math.ceil(secondsAway / BLOCK_TIME)); } //#endregion //#region src/utils/expirationTime.ts function lifetimeOf(seconds) { return makeExpiry(toBlocks(seconds), 0n); } function deadlineOf(expiresAt, options) { return makeExpiry(options?.atLeast?.minLifetime ?? 0n, expiresAt); } /** * Builds the `expiration` of a create or an extension. * * Every helper returns the `{ minLifetime, expiresAt }` pair the ABI carries, so what you write is * what goes on the wire. The `from*` helpers fill `minLifetime` and leave `expiresAt` at `0n`; * {@link ExpirationTime.atBlock} and {@link ExpirationTime.atDate} do the reverse. The engine takes * whichever comes later, which is why a deadline can also carry a floor. * * Values are plain data — no methods, no prototype — so they log, clone and compare like any other * object. * * @example The 90% case: a duration, and nothing else to think about. * await client.createEntity(Posts, values, { expiration: ExpirationTime.fromDays(30) }) * * @example An exact block, or a wall-clock date. * expiration: ExpirationTime.atBlock(1_200_000n) * expiration: ExpirationTime.atDate(new Date("2027-01-01")) * * @example A deadline that still guarantees a minimum life. * expiration: ExpirationTime.atBlock(1_200_000n, { atLeast: ExpirationTime.fromDays(1) }) * * @example The longest life the chain allows. * expiration: ExpirationTime.max() * * No entity may live more than {@link MAX_LIFETIME} blocks (about 182 days) past the block its * create or extension lands in. A longer expiry, such as `fromYears(1)`, is refused before the * transaction is sent. */ const ExpirationTime = { fromSeconds: (seconds) => lifetimeOf(seconds), fromMinutes: (minutes) => lifetimeOf(minutes * 60), fromHours: (hours) => lifetimeOf(hours * 60 * 60), fromDays: (days) => lifetimeOf(days * 24 * 60 * 60), fromWeeks: (weeks) => lifetimeOf(weeks * 7 * 24 * 60 * 60), fromMonths: (months) => lifetimeOf(months * 30 * 24 * 60 * 60), fromYears: (years) => lifetimeOf(years * 365 * 24 * 60 * 60), fromBlocks: (blocks) => { if (!Number.isInteger(blocks) || blocks <= 0) throw new InvalidExpiryError(`a lifetime must be a positive whole number of blocks, got ${blocks}`); return makeExpiry(BigInt(blocks), 0n); }, max: () => makeExpiry(MAX_LIFETIME, 0n), atBlock: (block, options) => { if (typeof block !== "bigint" || block < 0n) throw new InvalidExpiryError(`a deadline must be a non-negative block height, got ${block}`); return deadlineOf(block, options); }, atDate: (date, options) => { if (!(date instanceof Date) || Number.isNaN(date.getTime())) throw new InvalidExpiryError(`a deadline must be a valid Date, got ${String(date)}`); return deadlineOf(date, options); } }; //#endregion export { resolveExpiry as n, ExpirationTime as t }; //# sourceMappingURL=expirationTime-C1gcuh7y.js.map