@arkiv-network/sdk
Version:
TypeScript client library for Arkiv's blockchains interactions
147 lines (144 loc) • 8.02 kB
JavaScript
const require_consts = require('./consts-DKlf1_lP.cjs');
const require_errors = require('./errors-CVkdB3AT.cjs');
//#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 require_errors.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 require_errors.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 > require_consts.MAX_LIFETIME) throw new require_errors.InvalidExpiryError(`a lifetime of ${minLifetime} blocks is longer than the chain's maximum lifetime of ${require_consts.MAX_LIFETIME} blocks (about 182 days). Use ExpirationTime.max() for the longest lifetime allowed.`);
const latestDeadline = currentBlock + 1n + require_consts.MAX_LIFETIME;
if (absolute > latestDeadline) throw new require_errors.InvalidExpiryError(`expiresAt resolves to block ${absolute}, past block ${latestDeadline}: the chain's maximum lifetime is ${require_consts.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 require_errors.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 require_errors.InvalidExpiryError(`a lifetime must be a positive whole number of seconds, got ${seconds}. Use the ExpirationTime helpers, e.g. ExpirationTime.fromDays(30).`);
if (seconds % require_consts.BLOCK_TIME !== 0) throw new require_errors.InvalidExpiryError(`a lifetime must be a multiple of the ${require_consts.BLOCK_TIME}s block time, got ${seconds}, which is ${seconds / require_consts.BLOCK_TIME} blocks. Try ${seconds - seconds % require_consts.BLOCK_TIME} or ${seconds + require_consts.BLOCK_TIME - seconds % require_consts.BLOCK_TIME}, or ExpirationTime.fromBlocks(n).`);
return BigInt(seconds / require_consts.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 require_errors.InvalidExpiryError(`the deadline ${date.toISOString()} is in the past, so the entity would be dead on arrival`);
return currentBlock + BigInt(Math.ceil(secondsAway / require_consts.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 require_errors.InvalidExpiryError(`a lifetime must be a positive whole number of blocks, got ${blocks}`);
return makeExpiry(BigInt(blocks), 0n);
},
max: () => makeExpiry(require_consts.MAX_LIFETIME, 0n),
atBlock: (block, options) => {
if (typeof block !== "bigint" || block < 0n) throw new require_errors.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 require_errors.InvalidExpiryError(`a deadline must be a valid Date, got ${String(date)}`);
return deadlineOf(date, options);
}
};
//#endregion
Object.defineProperty(exports, 'ExpirationTime', {
enumerable: true,
get: function () {
return ExpirationTime;
}
});
Object.defineProperty(exports, 'resolveExpiry', {
enumerable: true,
get: function () {
return resolveExpiry;
}
});