Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

calling flows from flows and other support for 9796 #10024

Merged
merged 10 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 =>
turadg marked this conversation as resolved.
Show resolved Hide resolved
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
turadg marked this conversation as resolved.
Show resolved Hide resolved
* @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
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
Loading