diff --git a/packages/async-flow/package.json b/packages/async-flow/package.json index eeb9ae506de..90e41161022 100644 --- a/packages/async-flow/package.json +++ b/packages/async-flow/package.json @@ -61,6 +61,6 @@ "workerThreads": false }, "typeCoverage": { - "atLeast": 77.01 + "atLeast": 77.1 } } diff --git a/packages/async-flow/src/types.d.ts b/packages/async-flow/src/types.d.ts index 114ae65aa51..a2d28b6c7ef 100644 --- a/packages/async-flow/src/types.d.ts +++ b/packages/async-flow/src/types.d.ts @@ -46,11 +46,18 @@ export type GuestOf = F extends ( ? (...args: A) => Promise : F; +// from https://github.com/sindresorhus/type-fest/blob/main/source/simplify.d.ts +type Simplify = { [KeyType in keyof T]: T[KeyType] } & {}; + /** * Convert an entire Guest interface into what the host will implement. */ type HostInterface = { - [K in keyof T]: HostOf; + [K in keyof T]: T[K] extends CallableFunction + ? HostOf + : T[K] extends Record + ? Simplify> + : T[K]; }; /** @@ -71,8 +78,12 @@ export type GuestInterface = { * * Specifically, Promise return values are converted to Vows. */ -export type HostOf = F extends (...args: infer A) => Promise - ? (...args: A) => Vow> +export type HostOf = F extends ( + ...args: infer A +) => infer R + ? R extends Promise + ? (...args: A) => Vow> + : (...args: A) => HostInterface : F; export type HostArgs = { [K in keyof GA]: HostOf }; diff --git a/packages/notifier/src/notifier.js b/packages/notifier/src/notifier.js index 9439bbf528f..3c6ce35f19f 100644 --- a/packages/notifier/src/notifier.js +++ b/packages/notifier/src/notifier.js @@ -7,6 +7,7 @@ import { makePublishKit } from './publish-kit.js'; import { subscribeLatest } from './subscribe.js'; /** + * @import {Remote} from '@agoric/internal'; * @import {LatestTopic, Notifier, NotifierRecord, PublishKit, Subscriber, UpdateRecord} from './types.js'; */ @@ -41,7 +42,7 @@ export const makeNotifier = sharableInternalsP => { /** * @template T - * @param {ERef>} subscriber + * @param {ERef> | Remote>} subscriber * @returns {Notifier} */ export const makeNotifierFromSubscriber = subscriber => { diff --git a/packages/orchestration/package.json b/packages/orchestration/package.json index d2c37ec1d18..41c57cd6d3f 100644 --- a/packages/orchestration/package.json +++ b/packages/orchestration/package.json @@ -92,6 +92,6 @@ "access": "public" }, "typeCoverage": { - "atLeast": 98.05 + "atLeast": 98.1 } } diff --git a/packages/orchestration/src/examples/stakeIca.contract.js b/packages/orchestration/src/examples/stakeIca.contract.js index e40485d93ae..af8eeeac585 100644 --- a/packages/orchestration/src/examples/stakeIca.contract.js +++ b/packages/orchestration/src/examples/stakeIca.contract.js @@ -17,6 +17,7 @@ const trace = makeTracer('StakeIca'); * @import {Baggage} from '@agoric/vat-data'; * @import {IBCConnectionID} from '@agoric/vats'; * @import {TimerService} from '@agoric/time'; + * @import {ResolvedContinuingOfferResult} from '../utils/zoe-tools.js'; * @import {ICQConnection, CosmosInterchainService} from '../types.js'; */ @@ -129,10 +130,15 @@ export const start = async (zcf, privateArgs, baggage) => { makeAccountInvitationMaker() { trace('makeCreateAccountInvitation'); return zcf.makeInvitation( + // XXX use `orchestrate` membrane for vow? + /** + * @param {ZCFSeat} seat + * @returns {Promise} + */ async seat => { seat.exit(); const holder = await makeAccountKit(); - return holder.asContinuingOffer(); + return vowTools.when(holder.asContinuingOffer()); }, 'wantStakingAccount', undefined, diff --git a/packages/orchestration/src/exos/chain-hub.js b/packages/orchestration/src/exos/chain-hub.js index 24b280085b3..ea47e5f2f67 100644 --- a/packages/orchestration/src/exos/chain-hub.js +++ b/packages/orchestration/src/exos/chain-hub.js @@ -18,9 +18,11 @@ import { CosmosChainInfoShape, IBCConnectionInfoShape } from '../typeGuards.js'; */ /** + * If K matches a known chain, narrow the type from generic ChainInfo + * * @template {string} K * @typedef {K extends keyof KnownChains - * ? Omit + * ? ChainInfo & Omit * : ChainInfo} ActualChainInfo */ diff --git a/packages/orchestration/src/exos/cosmos-orchestration-account.js b/packages/orchestration/src/exos/cosmos-orchestration-account.js index 50293b4253a..08bac4861d9 100644 --- a/packages/orchestration/src/exos/cosmos-orchestration-account.js +++ b/packages/orchestration/src/exos/cosmos-orchestration-account.js @@ -312,6 +312,7 @@ export const prepareCosmosOrchestrationAccountKit = ( holder: { /** @type {HostOf} */ asContinuingOffer() { + // @ts-expect-error XXX invitationMakers // getPublicTopics resolves promptly (same run), so we don't need a watcher // eslint-disable-next-line no-restricted-syntax return asVow(async () => { diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index c25b675d5ac..450fbe3cbe4 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -293,6 +293,7 @@ export const prepareLocalOrchestrationAccountKit = ( holder: { /** @type {HostOf} */ asContinuingOffer() { + // @ts-expect-error XXX invitationMakers // getPublicTopics resolves promptly (same run), so we don't need a watcher // eslint-disable-next-line no-restricted-syntax return asVow(async () => { diff --git a/packages/orchestration/src/exos/orchestrator.js b/packages/orchestration/src/exos/orchestrator.js index 397e68a3442..10ed1daf665 100644 --- a/packages/orchestration/src/exos/orchestrator.js +++ b/packages/orchestration/src/exos/orchestrator.js @@ -16,7 +16,7 @@ import { /** * @import {Zone} from '@agoric/base-zone'; - * @import {ChainHub} from './chain-hub.js'; + * @import {ActualChainInfo, ChainHub} from './chain-hub.js'; * @import {AsyncFlowTools, HostInterface, HostOf} from '@agoric/async-flow'; * @import {Vow, VowTools} from '@agoric/vow'; * @import {TimerService} from '@agoric/time'; @@ -75,14 +75,14 @@ const prepareOrchestratorKit = ( { orchestrator: OrchestratorI, makeLocalChainFacadeWatcher: M.interface('makeLocalChainFacadeWatcher', { - onFulfilled: M.call(M.record(), M.string()).returns(M.any()), // FIXME narrow + onFulfilled: M.call(M.record()).returns(M.remotable()), }), makeRemoteChainFacadeWatcher: M.interface( 'makeRemoteChainFacadeWatcher', { onFulfilled: M.call(M.any(), M.string()) .optional(M.arrayOf(M.undefined())) // XXX needed? - .returns(M.any()), // FIXME narrow + .returns(M.remotable()), }, ), }, @@ -94,12 +94,11 @@ const prepareOrchestratorKit = ( /** Waits for `chainInfo` and returns a LocalChainFacade */ makeLocalChainFacadeWatcher: { /** - * @param {ChainInfo} agoricChainInfo - * @param {string} name + * @param {ActualChainInfo<'agoric'>} agoricChainInfo */ - onFulfilled(agoricChainInfo, name) { + onFulfilled(agoricChainInfo) { const it = makeLocalChainFacade(agoricChainInfo); - chainByName.init(name, it); + chainByName.init('agoric', it); return it; }, }, @@ -131,7 +130,6 @@ const prepareOrchestratorKit = ( return watch( chainHub.getChainInfo('agoric'), this.facets.makeLocalChainFacadeWatcher, - name, ); } return watch( @@ -153,7 +151,6 @@ const prepareOrchestratorKit = ( chainByName.has(baseName) || Fail`use getChain(${q(baseName)}) before getBrandInfo(${q(denom)})`; const base = chainByName.get(baseName); - // @ts-expect-error XXX HostOf<> not quite right? return harden({ chain, base, brand, baseDenom }); }, /** @type {HostOf} */ diff --git a/packages/orchestration/src/exos/portfolio-holder-kit.js b/packages/orchestration/src/exos/portfolio-holder-kit.js index 3177cdece3b..176b61ce916 100644 --- a/packages/orchestration/src/exos/portfolio-holder-kit.js +++ b/packages/orchestration/src/exos/portfolio-holder-kit.js @@ -8,7 +8,7 @@ import { VowShape } from '@agoric/vow'; const { fromEntries } = Object; /** - * @import {HostOf} from '@agoric/async-flow'; + * @import {HostInterface, HostOf} from '@agoric/async-flow'; * @import {MapStore} from '@agoric/store'; * @import {VowTools} from '@agoric/vow'; * @import {ResolvedPublicTopic} from '@agoric/zoe/src/contractSupport/topics.js'; @@ -18,7 +18,7 @@ const { fromEntries } = Object; /** * @typedef {{ - * accounts: MapStore>; + * accounts: MapStore>>; * publicTopics: MapStore>; * }} PortfolioHolderState */ @@ -99,6 +99,7 @@ const preparePortfolioHolderKit = (zone, { asVow, when }) => { const { accounts } = this.state; accounts.has(chainName) || Fail`no account found for ${chainName}`; const account = accounts.get(chainName); + // @ts-expect-error XXX invitationMakers return when(E(account).asContinuingOffer(), ({ invitationMakers }) => E(invitationMakers)[action](...invitationArgs), ); @@ -125,7 +126,7 @@ const preparePortfolioHolderKit = (zone, { asVow, when }) => { }, /** * @param {string} chainName key where the account is stored - * @param {OrchestrationAccount} account + * @param {HostInterface>} account * @param {ResolvedPublicTopic} publicTopic */ addAccount(chainName, account, publicTopic) { diff --git a/packages/orchestration/test/exos/portfolio-holder-kit.test.ts b/packages/orchestration/test/exos/portfolio-holder-kit.test.ts index 216764b64e1..5c215731212 100644 --- a/packages/orchestration/test/exos/portfolio-holder-kit.test.ts +++ b/packages/orchestration/test/exos/portfolio-holder-kit.test.ts @@ -70,7 +70,6 @@ test('portfolio holder kit behaviors', async t => { const cosmosAccount = await E(holder).getAccount('cosmoshub'); t.is( cosmosAccount, - // @ts-expect-error type mismatch between kit and OrchestrationAccountI accounts.cosmoshub, 'same account holder kit provided is returned', ); @@ -109,12 +108,15 @@ test('portfolio holder kit behaviors', async t => { const osmosisTopic = (await E(osmosisAccount).getPublicTopics()).account; - // @ts-expect-error type mismatch between kit and OrchestrationAccountI - await E(holder).addAccount('osmosis', osmosisAccount, osmosisTopic); + await E(holder).addAccount( + 'osmosis', + osmosisAccount, + // @ts-expect-error the promise from `subscriber.getUpdateSince` can't be used in a flow + osmosisTopic, + ); t.is( await E(holder).getAccount('osmosis'), - // @ts-expect-error type mismatch between kit and OrchestrationAccountI osmosisAccount, 'new accounts can be added', ); diff --git a/packages/orchestration/test/staking-ops.test.ts b/packages/orchestration/test/staking-ops.test.ts index 0a975f4e290..040f7ff5e74 100644 --- a/packages/orchestration/test/staking-ops.test.ts +++ b/packages/orchestration/test/staking-ops.test.ts @@ -239,6 +239,7 @@ test('makeAccount() writes to storage', async t => { }); const { publicSubscribers } = await E.when(holder.asContinuingOffer()); const accountNotifier = makeNotifierFromSubscriber( + // @ts-expect-error the promise from `subscriber.getUpdateSince` can't be used in a flow publicSubscribers.account.subscriber, ); const storageUpdate = await E(accountNotifier).getUpdateSince(); diff --git a/packages/orchestration/test/types.test-d.ts b/packages/orchestration/test/types.test-d.ts index 92f9c8de758..0c7512fd337 100644 --- a/packages/orchestration/test/types.test-d.ts +++ b/packages/orchestration/test/types.test-d.ts @@ -1,18 +1,22 @@ /** * @file pure types types, no runtime, ignored by Ava */ + import { expectNotType, expectType } from 'tsd'; import { typedJson } from '@agoric/cosmic-proto'; import type { MsgDelegateResponse } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; import type { QueryAllBalancesResponse } from '@agoric/cosmic-proto/cosmos/bank/v1beta1/query.js'; import type { Vow, VowTools } from '@agoric/vow'; import type { GuestAsyncFunc, HostInterface, HostOf } from '@agoric/async-flow'; +import type { ResolvedPublicTopic } from '@agoric/zoe/src/contractSupport/topics.js'; import type { ChainAddress, CosmosValidatorAddress, StakingAccountActions, OrchestrationAccount, Orchestrator, + Chain, + ChainInfo, } from '../src/types.js'; import type { LocalOrchestrationAccountKit } from '../src/exos/local-orchestration-account.js'; import { prepareCosmosOrchestrationAccount } from '../src/exos/cosmos-orchestration-account.js'; @@ -94,6 +98,30 @@ expectNotType(chainAddr); // Negative test expectNotType<() => Promise>(vowFn); + + const getBrandInfo: HostOf = null as any; + const chainHostOf = getBrandInfo('uatom').chain; + expectType>(chainHostOf.getChainInfo()); +} + +{ + // HostInterface + + const chain: Chain = null as any; + expectType>(chain.getChainInfo()); + const chainHostInterface: HostInterface> = null as any; + expectType>(chainHostInterface.getChainInfo()); + + const publicTopicRecord: HostInterface< + Record> + > = { + someTopic: { + subscriber: null as any, + storagePath: 'published.somewhere', + }, + }; + // @ts-expect-error the promise from `subscriber.getUpdateSince` can't be used in a flow + expectType>>(publicTopicRecord); } // HostOf with TransferSteps diff --git a/packages/vats/package.json b/packages/vats/package.json index d77dc771cfd..7535d1a1196 100644 --- a/packages/vats/package.json +++ b/packages/vats/package.json @@ -76,6 +76,6 @@ "workerThreads": false }, "typeCoverage": { - "atLeast": 91.41 + "atLeast": 91.52 } } diff --git a/packages/zoe/src/contractSupport/topics.js b/packages/zoe/src/contractSupport/topics.js index b2a93d83bb9..5c25b28976f 100644 --- a/packages/zoe/src/contractSupport/topics.js +++ b/packages/zoe/src/contractSupport/topics.js @@ -2,6 +2,10 @@ import { SubscriberShape } from '@agoric/notifier'; import { M } from '@agoric/store'; import { E } from '@endo/far'; +/** + * @import {Remote} from '@agoric/internal'; + */ + export { SubscriberShape }; export const PublicTopicShape = M.splitRecord( @@ -22,14 +26,14 @@ export const PublicTopicShape = M.splitRecord( */ /** - * A {PublicTopic} in which the `storagePath` is always a resolved string. + * A {PublicTopic} in which the `storagePath` is always a resolved string and the `subscriber is remote. * * Useful when working with Vows and async-flow. * * @template {object} T topic value * @typedef {{ * description?: string, - * subscriber: Subscriber, + * subscriber: Remote>, * storagePath: string, * }} ResolvedPublicTopic */