Skip to content

Commit

Permalink
9693 lint flow (Agoric#9726)
Browse files Browse the repository at this point in the history
closes: Agoric#9692, Agoric#9693

## Description

Sets up a convention for `flows` modules and document in the orchestration README. 

Also makes lint config to test them. The lint config is exported as a shared config that developers can use. I've included it in the same PR to make sure it works with the new convention.

### Security Considerations
none

### Scaling Considerations
none

### Documentation Considerations
Documented the convention in the README. I expect it'll make its way to docs.agoric.com as needed.

### Testing Considerations
nothing special

### Upgrade Considerations
not yet deployed
  • Loading branch information
mergify[bot] authored Jul 18, 2024
2 parents 81ac381 + 1a91e7b commit c3009d4
Show file tree
Hide file tree
Showing 16 changed files with 149 additions and 87 deletions.
2 changes: 1 addition & 1 deletion packages/async-flow/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,6 @@
"workerThreads": false
},
"typeCoverage": {
"atLeast": 76.94
"atLeast": 77.01
}
}
4 changes: 3 additions & 1 deletion packages/async-flow/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ export type GuestAsyncFunc = (
...activationArgs: Guest[]
) => Guest<Promise<any>>;

export type HostAsyncFuncWrapper = (...activationArgs: Host[]) => HostVow;
export type HostAsyncFuncWrapper = (
...activationArgs: Host<any>[]
) => HostVow<any>;

/**
* The function from the host as it will be available in the guest.
Expand Down
19 changes: 19 additions & 0 deletions packages/eslint-config/eslint-config.cjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
/* eslint-env node */

const orchestrationFlowRestrictions = [
{
selector: "Identifier[name='heapVowE']",
message: 'Eventual send is not yet supported within an orchestration flow',
},
{
selector: "Identifier[name='E']",
message: 'Eventual send is not yet supported within an orchestration flow',
},
];

module.exports = {
extends: [
'airbnb-base',
Expand Down Expand Up @@ -95,5 +107,12 @@ module.exports = {
'no-unused-vars': 'off',
},
},
{
// Orchestration flows
files: ['**/*.flows.js'],
rules: {
'no-restricted-syntax': ['error', ...orchestrationFlowRestrictions],
},
},
],
};
15 changes: 15 additions & 0 deletions packages/orchestration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,18 @@
Build feature-rich applications that can orchestrate assets and services across the interchain.

Usage examples can be found under [src/examples](https://github.com/Agoric/agoric-sdk/tree/master/packages/orchestration/src/examples). They are exported for integration testing with other packages.

## Orchestration flows

Flows to orchestrate are regular Javascript functions but have some constraints to fulfill the requirements of resumability after termination of the enclosing vat. Some requirements for each orchestration flow:
- must not close over any values that could change between invocations
- must satisfy the `OrchestrationFlow` interface
- must be hardened
- must not use `E()` (eventual send)

The call to `orchestrate` using a flow function in reincarnations of the vat must have the same `durableName` as before. To help enforce these constraints, we recommend:

- keeping flows in a `.flows.js` module
- importing them all with `import * as flows` to get a single object keyed by the export name
- using `orchestrateAll` to treat each export name as the `durableName` of the flow
- adopting `@agoric/eslint-config` that has rules to help detect problems
2 changes: 1 addition & 1 deletion packages/orchestration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,6 @@
"access": "public"
},
"typeCoverage": {
"atLeast": 98.09
"atLeast": 98.05
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { preparePortfolioHolder } from '../exos/portfolio-holder-kit.js';
* @import {Remote} from '@agoric/vow';
* @import {Zone} from '@agoric/zone';
* @import {GuestInterface} from '@agoric/async-flow';
* @import {CosmosValidatorAddress, Orchestrator, CosmosInterchainService, Denom, OrchestrationAccount, StakingAccountActions} from '@agoric/orchestration';
* @import {CosmosValidatorAddress, Orchestrator, CosmosInterchainService, Denom, OrchestrationAccount, StakingAccountActions, OrchestrationFlow} from '@agoric/orchestration';
* @import {MakeStakingTap} from './auto-stake-it-tap-kit.js';
* @import {MakePortfolioHolder} from '../exos/portfolio-holder-kit.js';
* @import {ChainHub} from '../exos/chain-hub.js';
Expand All @@ -35,6 +35,7 @@ import { preparePortfolioHolder } from '../exos/portfolio-holder-kit.js';
*/

/**
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
* @param {{
* makeStakingTap: MakeStakingTap;
Expand Down
4 changes: 3 additions & 1 deletion packages/orchestration/src/examples/basic-flows.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { preparePortfolioHolder } from '../exos/portfolio-holder-kit.js';

/**
* @import {Zone} from '@agoric/zone';
* @import {OrchestrationAccount, Orchestrator} from '@agoric/orchestration';
* @import {OrchestrationAccount, OrchestrationFlow, Orchestrator} from '@agoric/orchestration';
* @import {ResolvedPublicTopic} from '@agoric/zoe/src/contractSupport/topics.js';
* @import {OrchestrationPowers} from '../utils/start-helper.js';
* @import {MakePortfolioHolder} from '../exos/portfolio-holder-kit.js';
Expand All @@ -20,6 +20,7 @@ import { preparePortfolioHolder } from '../exos/portfolio-holder-kit.js';
* Create an account on a Cosmos chain and return a continuing offer with
* invitations makers for Delegate, WithdrawRewards, Transfer, etc.
*
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
* @param {undefined} _ctx
* @param {ZCFSeat} seat
Expand All @@ -39,6 +40,7 @@ const makeOrchAccountHandler = async (orch, _ctx, seat, { chainName }) => {
* Calls to the underlying invitationMakers are proxied through the
* `MakeInvitation` invitation maker.
*
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
* @param {MakePortfolioHolder} makePortfolioHolder
* @param {ZCFSeat} seat
Expand Down
4 changes: 2 additions & 2 deletions packages/orchestration/src/examples/sendAnywhere.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Fail } from '@endo/errors';
import { E } from '@endo/far';
import { M } from '@endo/patterns';
import { withOrchestration } from '../utils/start-helper.js';
import { orchestrationFns } from './sendAnywhereFlows.js';
import * as flows from './sendAnywhere.flows.js';
import { prepareChainHubAdmin } from '../exos/chain-hub-admin.js';

/**
Expand Down Expand Up @@ -76,7 +76,7 @@ const contract = async (
);

// orchestrate uses the names on orchestrationFns to do a "prepare" of the associated behavior
const orchFns = orchestrateAll(orchestrationFns, {
const orchFns = orchestrateAll(flows, {
zcf,
contractState,
localTransfer: zoeTools.localTransfer,
Expand Down
59 changes: 59 additions & 0 deletions packages/orchestration/src/examples/sendAnywhere.flows.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { M, mustMatch } from '@endo/patterns';

/**
* @import {GuestOf} from '@agoric/async-flow';
* @import {VBankAssetDetail} from '@agoric/vats/tools/board-utils.js';
* @import {ZoeTools} from '../utils/zoe-tools.js';
* @import {Orchestrator, LocalAccountMethods, OrchestrationAccountI, OrchestrationFlow} from '../types.js';
*/

const { entries } = Object;

// in guest file (the orchestration functions)
// the second argument is all the endowments provided

/**
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
* @param {object} ctx
* @param {{ account?: OrchestrationAccountI & LocalAccountMethods }} ctx.contractState
* @param {GuestOf<ZoeTools['localTransfer']>} ctx.localTransfer
* @param {(brand: Brand) => Promise<VBankAssetDetail>} ctx.findBrandInVBank
* @param {ZCFSeat} seat
* @param {{ chainName: string; destAddr: string }} offerArgs
*/
export async function sendIt(
orch,
{ contractState, localTransfer, findBrandInVBank },
seat,
offerArgs,
) {
mustMatch(offerArgs, harden({ chainName: M.scalar(), destAddr: M.string() }));
const { chainName, destAddr } = offerArgs;
// NOTE the proposal shape ensures that the `give` is a single asset
const { give } = seat.getProposal();
const [[_kw, amt]] = entries(give);
const { denom } = await findBrandInVBank(amt.brand);
const chain = await orch.getChain(chainName);

if (!contractState.account) {
const agoricChain = await orch.getChain('agoric');
contractState.account = await agoricChain.makeAccount();
}

const info = await chain.getChainInfo();
const { chainId } = info;
assert(typeof chainId === 'string', 'bad chainId');

await localTransfer(seat, contractState.account, give);

await contractState.account.transfer(
{ denom, value: amt.value },
{
value: destAddr,
encoding: 'bech32',
chainId,
},
);
}
harden(sendIt);
62 changes: 0 additions & 62 deletions packages/orchestration/src/examples/sendAnywhereFlows.js

This file was deleted.

3 changes: 2 additions & 1 deletion packages/orchestration/src/examples/swapExample.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { withOrchestration } from '../utils/start-helper.js';

/**
* @import {LocalTransfer} from '../utils/zoe-tools.js';
* @import {Orchestrator, CosmosValidatorAddress} from '../types.js'
* @import {Orchestrator, CosmosValidatorAddress, OrchestrationFlow} from '../types.js'
* @import {TimerService} from '@agoric/time';
* @import {LocalChain} from '@agoric/vats/src/localchain.js';
* @import {Remote} from '@agoric/internal';
Expand All @@ -17,6 +17,7 @@ import { withOrchestration } from '../utils/start-helper.js';
*/

/**
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
* @param {object} ctx
* @param {LocalTransfer} ctx.localTransfer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { M } from '@endo/patterns';
import { withOrchestration } from '../utils/start-helper.js';

/**
* @import {Orchestrator, IcaAccount, CosmosValidatorAddress} from '../types.js'
* @import {Orchestrator, OrchestrationFlow} from '../types.js'
* @import {TimerService} from '@agoric/time';
* @import {LocalChain} from '@agoric/vats/src/localchain.js';
* @import {NameHub} from '@agoric/vats';
Expand All @@ -13,6 +13,7 @@ import { withOrchestration } from '../utils/start-helper.js';
*/

/**
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
* @param {object} ctx
* @param {ZCF} ctx.zcf
Expand Down
23 changes: 7 additions & 16 deletions packages/orchestration/src/facade.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import { assertAllDefined } from '@agoric/internal';
* @import {HostOrchestrator} from './exos/orchestrator.js';
* @import {Remote} from '@agoric/internal';
* @import {CosmosInterchainService} from './exos/cosmos-interchain-service.js';
* @import {Chain, ChainInfo, CosmosChainInfo, IBCConnectionInfo, OrchestrationAccount, Orchestrator} from './types.js';
* @import {Chain, ChainInfo, CosmosChainInfo, IBCConnectionInfo, OrchestrationAccount, OrchestrationFlow, Orchestrator} from './types.js';
*/

/**
* For a given guest passed to orchestrate(), return the host-side form.
*
* @template {(orc: Orchestrator, ctx: any, ...args: any[]) => Promise<any>} GF
* @template {OrchestrationFlow} GF
* @typedef {GF extends (
* orc: Orchestrator,
* ctx: any,
Expand Down Expand Up @@ -66,18 +66,13 @@ export const makeOrchestrationFacade = ({
const { prepareEndowment, asyncFlow, adminAsyncFlow } = asyncFlowTools;

/**
* @template GR - return type
* @template HC - host context
* @template {any[]} GA - guest args
* @template {OrchestrationFlow<GuestInterface<HC>>} GF guest fn
* @param {string} durableName - the orchestration flow identity in the zone
* (to resume across upgrades)
* @param {HC} hostCtx - values to pass through the async flow membrane
* @param {(
* guestOrc: Orchestrator,
* guestCtx: GuestInterface<HC>,
* ...args: GA
* ) => Promise<GR>} guestFn
* @returns {(...args: HostArgs<GA>) => Vow<GR>}
* @param {GF} guestFn
* @returns {HostForGuest<GF>}
*/
const orchestrate = (durableName, hostCtx, guestFn) => {
const subZone = zone.subZone(durableName);
Expand All @@ -92,7 +87,7 @@ export const makeOrchestrationFacade = ({
const hostFn = asyncFlow(subZone, 'asyncFlow', guestFn);

// cast because return could be arbitrary subtype
const orcFn = /** @type {(...args: HostArgs<GA>) => Vow<GR>} */ (
const orcFn = /** @type {HostForGuest<GF>} */ (
(...args) => hostFn(wrappedOrc, wrappedCtx, ...args)
);

Expand All @@ -106,11 +101,7 @@ export const makeOrchestrationFacade = ({
*
* @template HC - host context
* @template {{
* [durableName: string]: (
* orc: Orchestrator,
* ctx: GuestInterface<HC>,
* ...args: any[]
* ) => Promise<any>;
* [durableName: string]: OrchestrationFlow<GuestInterface<HC>>;
* }} GFM
* guest fn map
* @param {GFM} guestFns
Expand Down
5 changes: 5 additions & 0 deletions packages/orchestration/src/orchestration-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { CurrentWalletRecord } from '@agoric/smart-wallet/src/smartWallet.j
import type { Timestamp } from '@agoric/time';
import type { LocalChainAccount } from '@agoric/vats/src/localchain.js';
import type { ResolvedPublicTopic } from '@agoric/zoe/src/contractSupport/topics.js';
import type { Passable } from '@endo/marshal';
import type {
ChainInfo,
CosmosChainAccountMethods,
Expand Down Expand Up @@ -192,6 +193,10 @@ export interface OrchestrationAccountI {
getPublicTopics: () => Promise<Record<string, ResolvedPublicTopic<unknown>>>;
}

export interface OrchestrationFlow<CT = unknown> {
(orc: Orchestrator, ctx: CT, ...args: Passable[]): Promise<unknown>;
}

/**
* Internal structure for TransferMsgs.
* The type must be able to express transfers across different chains and transports.
Expand Down
25 changes: 25 additions & 0 deletions packages/orchestration/test/examples/bad.flows.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @file Module with linting errors, to verify the linting config detects them.
* This assumes `reportUnusedDisableDirectives` is enabled in the local
* config.
*/

// TODO error on exports that:
// - aren't hardened (probably a new rule in @endo/eslint-plugin )
// - don't satisfy orchestration flow type

// eslint-disable-next-line no-restricted-syntax -- intentional for test
import { E } from '@endo/far';

export function notFlow() {
console.log('This function is not a flow');
}

export async function notHardened() {
console.log('This function is the most minimal flow, but it’s not hardened');
}

export async function usesE(orch, { someEref }) {
// eslint-disable-next-line no-restricted-syntax -- intentional for test
await E(someEref).foo();
}
Loading

0 comments on commit c3009d4

Please sign in to comment.