Skip to content

Commit

Permalink
calling flows from flows and other support for 9796 (#10024)
Browse files Browse the repository at this point in the history
refs: #9796 

## Description

#10023 started with a series of refactors and cleanup. This separates those into another PR to be reviewed more rapidly.

It also includes a new feature to orchestrateAll to ease calling flows from flows. Each flow is available on the context object under `flows`.

### Security Considerations
Every flow can call another flow. In the event a developer wants isolate between flows, they won't put them in the same `orchestrateAll`.

### Scaling Considerations
n/a

### Documentation Considerations
none

### Testing Considerations
new tests

### Upgrade Considerations
not yet deployed
  • Loading branch information
mergify[bot] authored Sep 5, 2024
2 parents 80f5cb1 + 84d3ea6 commit bd19a6f
Show file tree
Hide file tree
Showing 24 changed files with 637 additions and 371 deletions.
2 changes: 1 addition & 1 deletion packages/async-flow/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './src/async-flow.js';
export * from './src/types.js';
export { makeStateRecord } from './src/endowments.js';
export { makeSharedStateRecord } from './src/endowments.js';
2 changes: 1 addition & 1 deletion packages/async-flow/src/endowments.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const forwardingMethods = rem => {
* @param {R} dataRecord
* @returns {R}
*/
export const makeStateRecord = dataRecord =>
export const makeSharedStateRecord = dataRecord =>
harden(
create(
objectPrototype,
Expand Down
8 changes: 4 additions & 4 deletions packages/boot/test/bootstrapTests/orchestration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ test.serial('basic-flows - portfolio holder', async t => {
invitationSpec: {
source: 'continuing',
previousOffer: 'request-portfolio-acct',
invitationMakerName: 'MakeInvitation',
invitationMakerName: 'Proxying',
invitationArgs: [
'cosmoshub',
'Delegate',
Expand All @@ -550,7 +550,7 @@ test.serial('basic-flows - portfolio holder', async t => {
invitationSpec: {
source: 'continuing',
previousOffer: 'request-portfolio-acct',
invitationMakerName: 'MakeInvitation',
invitationMakerName: 'Proxying',
invitationArgs: [
'agoric',
'Delegate',
Expand All @@ -570,7 +570,7 @@ test.serial('basic-flows - portfolio holder', async t => {
invitationSpec: {
source: 'continuing',
previousOffer: 'request-portfolio-acct',
invitationMakerName: 'MakeInvitation',
invitationMakerName: 'Proxying',
invitationArgs: [
'cosmoshub',
'Delegate',
Expand All @@ -590,7 +590,7 @@ test.serial('basic-flows - portfolio holder', async t => {
invitationSpec: {
source: 'continuing',
previousOffer: 'request-portfolio-acct',
invitationMakerName: 'MakeInvitation',
invitationMakerName: 'Proxying',
invitationArgs: [
'agoric',
'Delegate',
Expand Down
49 changes: 49 additions & 0 deletions packages/internal/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,55 @@ export const deeplyFulfilledObject = async obj => {
return deeplyFulfilled(obj);
};

/**
* @param {any} value
* @param {string | undefined} name
* @param {object | undefined} container
* @param {(value: any, name: string, record: object) => any} mapper
* @returns {any}
*/
const deepMapObjectInternal = (value, name, container, mapper) => {
if (container && typeof name === 'string') {
const mapped = mapper(value, name, container);
if (mapped !== value) {
return mapped;
}
}

if (typeof value !== 'object' || !value) {
return value;
}

let wasMapped = false;
const mappedEntries = Object.entries(value).map(([innerName, innerValue]) => {
const mappedInnerValue = deepMapObjectInternal(
innerValue,
innerName,
value,
mapper,
);
wasMapped ||= mappedInnerValue !== innerValue;
return [innerName, mappedInnerValue];
});

return wasMapped ? Object.fromEntries(mappedEntries) : value;
};

/**
* Traverses a record object structure deeply, calling a replacer for each
* enumerable string property values of an object. If none of the values are
* changed, the original object is used as-is, maintaining its identity.
*
* When an object is found as a property value, the replacer is first called on
* it. If not replaced, the object is then traversed.
*
* @param {object} obj
* @param {(value: any, name: string, record: object) => any} mapper
* @returns {object}
*/
export const deepMapObject = (obj, mapper) =>
deepMapObjectInternal(obj, undefined, undefined, mapper);

/**
* Returns a function that uses a millisecond-based time-since-epoch capability
* (such as `performance.now`) to measure execution time of an async function
Expand Down
93 changes: 93 additions & 0 deletions packages/internal/test/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
untilTrue,
forever,
deeplyFulfilledObject,
deepMapObject,
synchronizedTee,
} from '../src/utils.js';

Expand All @@ -30,6 +31,98 @@ test('deeplyFulfilledObject', async t => {
});
});

/**
* @typedef {object} DeepMapObjectTestParams
* @property {any} input
* @property {[any, any][]} replacements
* @property {string[][]} unchangedPaths
* @property {any} [expectedOutput]
*/

/** @type {import('ava').Macro<[DeepMapObjectTestParams]>} */
const deepMapObjectTest = test.macro({
title(providedTitle, { input }) {
return `deepMapObject - ${providedTitle || JSON.stringify(input)}`;
},
exec(t, { input, replacements, unchangedPaths, expectedOutput }) {
const replacementMap = new Map(replacements);
const output = deepMapObject(input, val =>
replacementMap.has(val) ? replacementMap.get(val) : val,
);

for (const unchangedPath of unchangedPaths) {
/** @type {any} */
let inputVal = input;
/** @type {any} */
let outputVal = output;
for (const pathPart of unchangedPath) {
inputVal = inputVal[pathPart];
outputVal = outputVal[pathPart];
}
t.is(
outputVal,
inputVal,
`${['obj', ...unchangedPath].join('.')} is unchanged`,
);
}

if (expectedOutput) {
t.deepEqual(output, expectedOutput);
}
},
});

test('identity', deepMapObjectTest, {
input: { foo: 42 },
replacements: [],
unchangedPaths: [[]],
});
test('non object', deepMapObjectTest, {
input: 'not an object',
replacements: [['not an object', 'not replaced']],
unchangedPaths: [[]],
expectedOutput: 'not an object',
});
test('one level deep', deepMapObjectTest, {
input: { replace: 'replace me', notChanged: {} },
replacements: [['replace me', 'replaced']],
unchangedPaths: [['notChanged']],
expectedOutput: { replace: 'replaced', notChanged: {} },
});

const testRecord = { maybeReplace: 'replace me' };
test('replace first before deep map', deepMapObjectTest, {
input: { replace: testRecord, notChanged: {} },
replacements: [
[testRecord, { different: 'something new' }],
['replace me', 'should not be replaced'],
],
unchangedPaths: [['notChanged']],
expectedOutput: { replace: { different: 'something new' }, notChanged: {} },
});

test('not mapping top level container', deepMapObjectTest, {
input: testRecord,
replacements: [
[testRecord, { different: 'should not be different' }],
['replace me', 'replaced'],
],
unchangedPaths: [],
expectedOutput: { maybeReplace: 'replaced' },
});
test('deep mapping', deepMapObjectTest, {
input: {
one: { two: { three: 'replace me' }, notChanged: {} },
another: 'replace me',
},
replacements: [['replace me', 'replaced']],
unchangedPaths: [['one', 'notChanged']],
expectedOutput: {
one: { two: { three: 'replaced' }, notChanged: {} },
another: 'replaced',
},
});

test('makeMeasureSeconds', async t => {
const times = [1000.25, 2000.75, NaN];
/** @type {() => number} */
Expand Down
4 changes: 2 additions & 2 deletions packages/orchestration/src/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This directory contains sample contracts showcasing the Orchestration API. Each
The following contracts are a work in progress as they contain bindings that need to be promptly updated.

- **stakeIca.contract.js**: Interchain account creation for remote staking
- **unbondExample.contract.js**: Cross-chain unbonding and liquid staking
- **swapExample.contract.js**: Token swapping and remote staking
- **unbond.contract.js**: Cross-chain unbonding and liquid staking
- **swap.contract.js**: Token swapping and remote staking
- **stakeBld.contract.js**: BLD token staking on Agoric

4 changes: 2 additions & 2 deletions packages/orchestration/src/examples/basic-flows.flows.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ harden(makeOrchAccount);
/**
* Create accounts on multiple chains and return them in a single continuing
* offer with invitations makers for Delegate, WithdrawRewards, Transfer, etc.
* Calls to the underlying invitationMakers are proxied through the
* `MakeInvitation` invitation maker.
* Calls to the underlying invitationMakers are proxied through the `Proxying`
* invitation maker.
*
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
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
@@ -1,4 +1,4 @@
import { makeStateRecord } from '@agoric/async-flow';
import { makeSharedStateRecord } from '@agoric/async-flow';
import { AmountShape } from '@agoric/ertp';
import { InvitationShape } from '@agoric/zoe/src/typeGuards.js';
import { M } from '@endo/patterns';
Expand Down Expand Up @@ -51,7 +51,7 @@ const contract = async (
zone,
{ chainHub, orchestrateAll, zoeTools },
) => {
const contractState = makeStateRecord(
const contractState = makeSharedStateRecord(
/** @type {{ account: OrchestrationAccount<any> | undefined }} */ {
localAccount: undefined,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { StorageNodeShape } from '@agoric/internal';
import { TimerServiceShape } from '@agoric/time';
import { M } from '@endo/patterns';
import { orcUtils } from '../utils/orc.js';
import { withOrchestration } from '../utils/start-helper.js';
import * as flows from './swap.flows.js';

/**
* @import {LocalTransfer} from '../utils/zoe-tools.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 @@ -16,50 +14,6 @@ import { withOrchestration } from '../utils/start-helper.js';
* @import {OrchestrationTools} from '../utils/start-helper.js';
*/

/**
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
* @param {object} ctx
* @param {LocalTransfer} ctx.localTransfer
* @param {ZCFSeat} seat
* @param {object} offerArgs
* @param {Amount<'nat'>} offerArgs.staked
* @param {CosmosValidatorAddress} offerArgs.validator
*/
const stakeAndSwapFn = async (orch, { localTransfer }, seat, offerArgs) => {
const { give } = seat.getProposal();

const omni = await orch.getChain('omniflixhub');
const agoric = await orch.getChain('agoric');

const [omniAccount, localAccount] = await Promise.all([
omni.makeAccount(),
agoric.makeAccount(),
]);

const omniAddress = omniAccount.getAddress();

// deposit funds from user seat to LocalChainAccount
await localTransfer(seat, localAccount, give);
seat.exit();

// build swap instructions with orcUtils library
const transferMsg = orcUtils.makeOsmosisSwap({
destChain: 'omniflixhub',
destAddress: omniAddress,
amountIn: give.Stable,
brandOut: /** @type {any} */ ('FIXME'),
slippage: 0.03,
});

try {
await localAccount.transferSteps(give.Stable, transferMsg);
await omniAccount.delegate(offerArgs.validator, offerArgs.staked);
} catch (e) {
console.error(e);
}
};

/** @type {ContractMeta<typeof start>} */
export const meta = {
privateArgsShape: {
Expand Down Expand Up @@ -99,20 +53,23 @@ harden(makeNatAmountShape);
* @param {Zone} zone
* @param {OrchestrationTools} tools
*/
const contract = async (zcf, privateArgs, zone, { orchestrate, zoeTools }) => {
const contract = async (
zcf,
privateArgs,
zone,
{ orchestrateAll, zoeTools },
) => {
const { brands } = zcf.getTerms();

/** deprecated historical example */
const swapAndStakeHandler = orchestrate(
'LSTTia',
{ zcf, localTransfer: zoeTools.localTransfer },
stakeAndSwapFn,
);
const { stakeAndSwap } = orchestrateAll(flows, {
zcf,
localTransfer: zoeTools.localTransfer,
});

const publicFacet = zone.exo('publicFacet', undefined, {
makeSwapAndStakeInvitation() {
return zcf.makeInvitation(
swapAndStakeHandler,
stakeAndSwap,
'Swap for TIA and stake',
undefined,
harden({
Expand Down
Loading

0 comments on commit bd19a6f

Please sign in to comment.