diff --git a/packages/orchestration/src/examples/stakeBld.contract.js b/packages/orchestration/src/examples/stakeBld.contract.js index 1652ae606fd1..82952d356058 100644 --- a/packages/orchestration/src/examples/stakeBld.contract.js +++ b/packages/orchestration/src/examples/stakeBld.contract.js @@ -12,6 +12,7 @@ import { deeplyFulfilled } from '@endo/marshal'; import { M } from '@endo/patterns'; import { prepareLocalOrchestrationAccountKit } from '../exos/local-orchestration-account.js'; import { makeChainHub } from '../exos/chain-hub.js'; +import { makeTimestampHelper } from '../exos/time.js'; /** * @import {NameHub} from '@agoric/vats'; @@ -49,6 +50,7 @@ export const start = async (zcf, privateArgs, baggage) => { privateArgs.timerService, vowTools, makeChainHub(privateArgs.agoricNames), + makeTimestampHelper(privateArgs.timerService), ); // ---------------- diff --git a/packages/orchestration/src/exos/cosmos-orchestration-account.js b/packages/orchestration/src/exos/cosmos-orchestration-account.js index 46ddd1e6bcbd..b142a684b08a 100644 --- a/packages/orchestration/src/exos/cosmos-orchestration-account.js +++ b/packages/orchestration/src/exos/cosmos-orchestration-account.js @@ -28,9 +28,12 @@ import { CoinShape, DelegationShape, } from '../typeGuards.js'; -import { maxClockSkew, tryDecodeResponse } from '../utils/cosmos.js'; +import { + maxClockSkew, + tryDecodeResponse, + dateInSeconds, +} from '../utils/cosmos.js'; import { orchestrationAccountMethods } from '../utils/orchestrationAccount.js'; -import { dateInSeconds } from '../utils/time.js'; /** * @import {AmountArg, IcaAccount, ChainAddress, CosmosValidatorAddress, ICQConnection, StakingAccountActions, DenomAmount, OrchestrationAccountI, DenomArg} from '../types.js'; diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index bce4acc52351..00a214c321a5 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -13,9 +13,8 @@ import { ChainAmountShape, IBCTransferOptionsShape, } from '../typeGuards.js'; -import { maxClockSkew } from '../utils/cosmos.js'; +import { maxClockSkew, dateInSeconds } from '../utils/cosmos.js'; import { orchestrationAccountMethods } from '../utils/orchestrationAccount.js'; -import { dateInSeconds, makeTimestampHelper } from '../utils/time.js'; /** * @import {LocalChainAccount} from '@agoric/vats/src/localchain.js'; @@ -27,6 +26,7 @@ import { dateInSeconds, makeTimestampHelper } from '../utils/time.js'; * @import {PromiseVow, VowTools} from '@agoric/vow'; * @import {TypedJson} from '@agoric/cosmic-proto'; * @import {ChainHub} from './chain-hub.js'; + * @import {TimestampHelper} from './time.js'; */ const trace = makeTracer('LOA'); @@ -66,6 +66,7 @@ const PUBLIC_TOPICS = { * @param {Remote} timerService * @param {VowTools} vowTools * @param {ChainHub} chainHub + * @param {TimestampHelper} timestampHelper */ export const prepareLocalOrchestrationAccountKit = ( zone, @@ -74,11 +75,10 @@ export const prepareLocalOrchestrationAccountKit = ( timerService, { watch, when, allVows }, chainHub, -) => { - const timestampHelper = makeTimestampHelper(timerService); - + timestampHelper, +) => /** Make an object wrapping an LCA with Zoe interfaces. */ - const makeLocalOrchestrationAccountKit = zone.exoClassKit( + zone.exoClassKit( 'Local Orchestration Account Kit', { holder: HolderI, @@ -423,7 +423,6 @@ export const prepareLocalOrchestrationAccountKit = ( }, }, ); - return makeLocalOrchestrationAccountKit; -}; + /** @typedef {ReturnType} MakeLocalOrchestrationAccountKit */ /** @typedef {ReturnType} LocalOrchestrationAccountKit */ diff --git a/packages/orchestration/src/exos/time.js b/packages/orchestration/src/exos/time.js index 8e8dc062f6e3..9fdb955a4f40 100644 --- a/packages/orchestration/src/exos/time.js +++ b/packages/orchestration/src/exos/time.js @@ -1,63 +1,76 @@ +import { RelativeTimeRecordShape, TimeMath } from '@agoric/time'; +import { VowShape } from '@agoric/vow'; +import { watch, allVows } from '@agoric/vow/vat.js'; +import { makeHeapZone } from '@agoric/zone'; import { E } from '@endo/far'; -import { TimeMath } from '@agoric/time'; +import { M } from '@endo/patterns'; /** - * @import {RelativeTimeRecord, TimerBrand, TimerService} from '@agoric/time'; - * @import {Remote} from '@agoric/internal'; + * @import {Remote} from '@agoric/internal';* + * @import {RelativeTimeRecord, TimerBrand, TimerService, TimestampRecord} from '@agoric/time'; + * @import {Vow} from '@agoric/vow'; + * @import {Zone} from '@agoric/zone'; */ export const SECONDS_PER_MINUTE = 60n; export const NANOSECONDS_PER_SECOND = 1_000_000_000n; /** - * XXX should this be durable? resumable? - * - * @param {Remote} timer + * @param {Remote} timerService + * @param {Zone} [zone] */ -export function makeTimestampHelper(timer) { + +export const makeTimestampHelper = (timerService, zone = makeHeapZone()) => { /** @type {TimerBrand | undefined} */ let brandCache; const getBrand = async () => { if (brandCache) return brandCache; - brandCache = await E(timer).getTimerBrand(); - return brandCache; + return watch(E(timerService).getTimerBrand(), { + onFulfilled: timerBrand => { + return timerBrand; + }, + }); }; - return harden({ - /** - * XXX do this need to be resumable / use Vows? - * - * Takes the current time from ChainTimerService and adds a relative time to - * determine a timeout timestamp in nanoseconds. Useful for - * {@link MsgTransfer.timeoutTimestamp}. - * - * @param {RelativeTimeRecord} [relativeTime] defaults to 5 minutes - * @returns {Promise} Timeout timestamp in absolute nanoseconds - * since unix epoch - */ - async getTimeoutTimestampNS(relativeTime) { - const currentTime = await E(timer).getCurrentTimestamp(); - const timeout = - relativeTime || - TimeMath.coerceRelativeTimeRecord( - SECONDS_PER_MINUTE * 5n, - await getBrand(), + return zone.exo( + 'Timestamp Helper', + M.interface('TimeStampHelperI', { + getTimeoutTimestampNS: M.call() + .optional(RelativeTimeRecordShape) + .returns(VowShape), + }), + { + /** + * Takes the current time from ChainTimerService and adds a relative time + * to determine a timeout timestamp in nanoseconds. Useful for + * {@link MsgTransfer.timeoutTimestamp}. + * + * @param {RelativeTimeRecord} [relativeTime] defaults to 5 minutes + * @returns {Vow} Timeout timestamp in absolute nanoseconds since + * unix epoch + */ + getTimeoutTimestampNS(relativeTime) { + return watch( + allVows([E(timerService).getCurrentTimestamp(), getBrand()]), + { + /** @param {[TimestampRecord, TimerBrand]} r */ + onFulfilled([currentTime, timerBrand]) { + const timeout = + relativeTime || + TimeMath.coerceRelativeTimeRecord( + SECONDS_PER_MINUTE * 5n, + timerBrand, + ); + return ( + TimeMath.addAbsRel(currentTime, timeout).absValue * + NANOSECONDS_PER_SECOND + ); + }, + }, ); - return ( - TimeMath.addAbsRel(currentTime, timeout).absValue * - NANOSECONDS_PER_SECOND - ); + }, }, - }); -} + ); +}; /** @typedef {Awaited>} TimestampHelper */ - -/** - * Convert a Date from a Cosmos message, which has millisecond precision, to a - * BigInt for number of seconds since epoch, for use in a timer. - * - * @param {Date} date - * @returns {bigint} - */ -export const dateInSeconds = date => BigInt(Math.floor(date.getTime() / 1000)); diff --git a/packages/orchestration/src/utils/cosmos.js b/packages/orchestration/src/utils/cosmos.js index d84a2e5f033b..22ae769b1c15 100644 --- a/packages/orchestration/src/utils/cosmos.js +++ b/packages/orchestration/src/utils/cosmos.js @@ -34,3 +34,12 @@ export const tryDecodeResponse = (ackStr, fromProtoMsg) => { throw assert.error(`bad response: ${ackStr}`, undefined, { cause }); } }; + +/** + * Convert a Date from a Cosmos message, which has millisecond precision, to a + * BigInt for number of seconds since epoch, for use in a timer. + * + * @param {Date} date + * @returns {bigint} + */ +export const dateInSeconds = date => BigInt(Math.floor(date.getTime() / 1000)); diff --git a/packages/orchestration/src/utils/start-helper.js b/packages/orchestration/src/utils/start-helper.js index 6b7db2acc963..bbc57260136a 100644 --- a/packages/orchestration/src/utils/start-helper.js +++ b/packages/orchestration/src/utils/start-helper.js @@ -8,6 +8,7 @@ import { makeChainHub } from '../exos/chain-hub.js'; import { prepareRemoteChainFacade } from '../exos/remote-chain-facade.js'; import { prepareCosmosOrchestrationAccount } from '../exos/cosmos-orchestration-account.js'; import { prepareLocalChainFacade } from '../exos/local-chain-facade.js'; +import { makeTimestampHelper } from '../exos/time.js'; /** * @import {PromiseKit} from '@endo/promise-kit' @@ -47,6 +48,7 @@ export const provideOrchestration = ( const zone = makeDurableZone(baggage); const chainHub = makeChainHub(remotePowers.agoricNames); + const timestampHelper = makeTimestampHelper(remotePowers.timerService); const vowTools = prepareVowTools(zone.subZone('vows')); @@ -58,6 +60,7 @@ export const provideOrchestration = ( remotePowers.timerService, vowTools, chainHub, + timestampHelper, ); const asyncFlowTools = prepareAsyncFlowTools(zone.subZone('asyncFlow'), { diff --git a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts index 21ba90fcd683..00dae4c6764f 100644 --- a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts +++ b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts @@ -8,7 +8,10 @@ import { Far } from '@endo/far'; import { prepareLocalOrchestrationAccountKit } from '../../src/exos/local-orchestration-account.js'; import { ChainAddress } from '../../src/orchestration-api.js'; import { makeChainHub } from '../../src/exos/chain-hub.js'; -import { NANOSECONDS_PER_SECOND } from '../../src/utils/time.js'; +import { + makeTimestampHelper, + NANOSECONDS_PER_SECOND, +} from '../../src/exos/time.js'; import { commonSetup } from '../supports.js'; test('deposit, withdraw', async t => { @@ -34,6 +37,7 @@ test('deposit, withdraw', async t => { timer, vowTools, makeChainHub(bootstrap.agoricNames), + makeTimestampHelper(timer), ); t.log('request account from vat-localchain'); @@ -105,6 +109,7 @@ test('delegate, undelegate', async t => { timer, vowTools, makeChainHub(bootstrap.agoricNames), + makeTimestampHelper(timer), ); t.log('request account from vat-localchain'); @@ -157,6 +162,7 @@ test('transfer', async t => { timer, vowTools, makeChainHub(bootstrap.agoricNames), + makeTimestampHelper(timer), ); t.log('request account from vat-localchain'); diff --git a/packages/orchestration/test/utils/time.test.ts b/packages/orchestration/test/exos/time.test.ts similarity index 61% rename from packages/orchestration/test/utils/time.test.ts rename to packages/orchestration/test/exos/time.test.ts index 01f496814c35..768869e493f9 100644 --- a/packages/orchestration/test/utils/time.test.ts +++ b/packages/orchestration/test/exos/time.test.ts @@ -1,29 +1,31 @@ +/* eslint-disable @jessie.js/safe-await-separator */ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { V } from '@agoric/vow/vat.js'; import { buildZoeManualTimer } from '@agoric/zoe/tools/manualTimer.js'; import { TimeMath } from '@agoric/time'; import { - dateInSeconds, makeTimestampHelper, NANOSECONDS_PER_SECOND, SECONDS_PER_MINUTE, -} from '../../src/utils/time.js'; +} from '../../src/exos/time.js'; test('makeTimestampHelper - getCurrentTimestamp', async t => { const timer = buildZoeManualTimer(t.log); const timerBrand = timer.getTimerBrand(); t.is(timer.getCurrentTimestamp().absValue, 0n, 'current time is 0n'); - const { getTimeoutTimestampNS } = makeTimestampHelper(timer); - await null; + const timestampHelper = makeTimestampHelper(timer); t.is( - await getTimeoutTimestampNS(), + await V.when(timestampHelper.getTimeoutTimestampNS()), 5n * SECONDS_PER_MINUTE * NANOSECONDS_PER_SECOND, 'default timestamp is 5 minutes from current time, in nanoseconds', ); t.is( - await getTimeoutTimestampNS( - TimeMath.coerceRelativeTimeRecord(1n, timerBrand), + await V.when( + timestampHelper.getTimeoutTimestampNS( + TimeMath.coerceRelativeTimeRecord(1n, timerBrand), + ), ), 1n * NANOSECONDS_PER_SECOND, 'timestamp is 1 second since unix epoch, in nanoseconds', @@ -32,18 +34,12 @@ test('makeTimestampHelper - getCurrentTimestamp', async t => { // advance timer by 3 seconds await timer.tickN(3); t.is( - await getTimeoutTimestampNS( - TimeMath.coerceRelativeTimeRecord(1n, timerBrand), + await V.when( + timestampHelper.getTimeoutTimestampNS( + TimeMath.coerceRelativeTimeRecord(1n, timerBrand), + ), ), (1n + 3n) * NANOSECONDS_PER_SECOND, 'timestamp is 4 seconds since unix epoch, in nanoseconds', ); }); - -test('dateInSeconds', t => { - t.is(dateInSeconds(new Date(1)), 0n); - t.is(dateInSeconds(new Date(999)), 0n); - t.is(dateInSeconds(new Date(1000)), 1n); - - t.is(dateInSeconds(new Date('2025-12-17T12:23:45Z')), 1765974225n); -}); diff --git a/packages/orchestration/test/utils/cosmos.test.ts b/packages/orchestration/test/utils/cosmos.test.ts new file mode 100644 index 000000000000..44647e400659 --- /dev/null +++ b/packages/orchestration/test/utils/cosmos.test.ts @@ -0,0 +1,10 @@ +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { dateInSeconds } from '../../src/utils/cosmos.js'; + +test('dateInSeconds', t => { + t.is(dateInSeconds(new Date(1)), 0n); + t.is(dateInSeconds(new Date(999)), 0n); + t.is(dateInSeconds(new Date(1000)), 1n); + + t.is(dateInSeconds(new Date('2025-12-17T12:23:45Z')), 1765974225n); +});