diff --git a/packages/async-flow/index.js b/packages/async-flow/index.js index 7c6e499a9c0..9ea1e5a6f19 100644 --- a/packages/async-flow/index.js +++ b/packages/async-flow/index.js @@ -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'; diff --git a/packages/async-flow/src/endowments.js b/packages/async-flow/src/endowments.js index 209eae0c8f2..1aba97efaaf 100644 --- a/packages/async-flow/src/endowments.js +++ b/packages/async-flow/src/endowments.js @@ -64,7 +64,7 @@ export const forwardingMethods = rem => { * @param {R} dataRecord * @returns {R} */ -export const makeStateRecord = dataRecord => +export const makeSharedStateRecord = dataRecord => harden( create( objectPrototype, diff --git a/packages/boot/test/bootstrapTests/orchestration.test.ts b/packages/boot/test/bootstrapTests/orchestration.test.ts index c619b911af4..a6628c1d775 100644 --- a/packages/boot/test/bootstrapTests/orchestration.test.ts +++ b/packages/boot/test/bootstrapTests/orchestration.test.ts @@ -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', @@ -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', @@ -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', @@ -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', diff --git a/packages/internal/src/utils.js b/packages/internal/src/utils.js index ed82db385b6..adde6849d6e 100644 --- a/packages/internal/src/utils.js +++ b/packages/internal/src/utils.js @@ -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 diff --git a/packages/internal/test/utils.test.js b/packages/internal/test/utils.test.js index 27d391ca488..7d1b9ba5092 100644 --- a/packages/internal/test/utils.test.js +++ b/packages/internal/test/utils.test.js @@ -9,6 +9,7 @@ import { untilTrue, forever, deeplyFulfilledObject, + deepMapObject, synchronizedTee, } from '../src/utils.js'; @@ -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} */ diff --git a/packages/orchestration/src/examples/README.md b/packages/orchestration/src/examples/README.md index a9dac0b0259..33623c46adc 100644 --- a/packages/orchestration/src/examples/README.md +++ b/packages/orchestration/src/examples/README.md @@ -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 diff --git a/packages/orchestration/src/examples/basic-flows.flows.js b/packages/orchestration/src/examples/basic-flows.flows.js index 2948d3c6c49..0d01c4a7696 100644 --- a/packages/orchestration/src/examples/basic-flows.flows.js +++ b/packages/orchestration/src/examples/basic-flows.flows.js @@ -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 diff --git a/packages/orchestration/src/examples/sendAnywhere.contract.js b/packages/orchestration/src/examples/sendAnywhere.contract.js index f7dc456b135..11a79bb6498 100644 --- a/packages/orchestration/src/examples/sendAnywhere.contract.js +++ b/packages/orchestration/src/examples/sendAnywhere.contract.js @@ -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'; @@ -51,7 +51,7 @@ const contract = async ( zone, { chainHub, orchestrateAll, zoeTools }, ) => { - const contractState = makeStateRecord( + const contractState = makeSharedStateRecord( /** @type {{ account: OrchestrationAccount | undefined }} */ { localAccount: undefined, }, diff --git a/packages/orchestration/src/examples/swapExample.contract.js b/packages/orchestration/src/examples/swap.contract.js similarity index 57% rename from packages/orchestration/src/examples/swapExample.contract.js rename to packages/orchestration/src/examples/swap.contract.js index 32f5de5c042..b4af770f7b0 100644 --- a/packages/orchestration/src/examples/swapExample.contract.js +++ b/packages/orchestration/src/examples/swap.contract.js @@ -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'; @@ -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} */ export const meta = { privateArgsShape: { @@ -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({ diff --git a/packages/orchestration/src/examples/swap.flows.js b/packages/orchestration/src/examples/swap.flows.js new file mode 100644 index 00000000000..7514683440f --- /dev/null +++ b/packages/orchestration/src/examples/swap.flows.js @@ -0,0 +1,57 @@ +import { orcUtils } from '../utils/orc.js'; + +/** + * @import {LocalTransfer} from '../utils/zoe-tools.js'; + * @import {Orchestrator, CosmosValidatorAddress, OrchestrationFlow} from '../types.js' + */ + +// XXX does not actually work. An early illustration that needs to be fixed. +/** + * @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 + */ +export const stakeAndSwap = 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); + } +}; +harden(stakeAndSwap); diff --git a/packages/orchestration/src/examples/unbond.contract.js b/packages/orchestration/src/examples/unbond.contract.js new file mode 100644 index 00000000000..c3768146660 --- /dev/null +++ b/packages/orchestration/src/examples/unbond.contract.js @@ -0,0 +1,53 @@ +import { M } from '@endo/patterns'; +import { withOrchestration } from '../utils/start-helper.js'; +import * as flows from './unbond.flows.js'; + +/** + * @import {TimerService} from '@agoric/time'; + * @import {LocalChain} from '@agoric/vats/src/localchain.js'; + * @import {NameHub} from '@agoric/vats'; + * @import {Remote} from '@agoric/internal'; + * @import {Zone} from '@agoric/zone'; + * @import {CosmosInterchainService} from '../exos/cosmos-interchain-service.js'; + * @import {OrchestrationTools} from '../utils/start-helper.js'; + */ + +/** + * Orchestration contract to be wrapped by withOrchestration for Zoe + * + * @param {ZCF} zcf + * @param {{ + * agoricNames: Remote; + * localchain: Remote; + * orchestrationService: Remote; + * storageNode: Remote; + * marshaller: Marshaller; + * timerService: Remote; + * }} privateArgs + * @param {Zone} zone + * @param {OrchestrationTools} tools + */ +const contract = async (zcf, privateArgs, zone, { orchestrateAll }) => { + const { unbondAndLiquidStake } = orchestrateAll(flows, { zcf }); + + const publicFacet = zone.exo('publicFacet', undefined, { + makeUnbondAndLiquidStakeInvitation() { + return zcf.makeInvitation( + unbondAndLiquidStake, + 'Unbond and liquid stake', + undefined, + harden({ + // Nothing to give; the funds come from undelegating + give: {}, + want: {}, // XXX ChainAccount Ownable? + exit: M.any(), + }), + ); + }, + }); + + return harden({ publicFacet }); +}; + +export const start = withOrchestration(contract); +harden(start); diff --git a/packages/orchestration/src/examples/unbond.flows.js b/packages/orchestration/src/examples/unbond.flows.js new file mode 100644 index 00000000000..c936d93b630 --- /dev/null +++ b/packages/orchestration/src/examples/unbond.flows.js @@ -0,0 +1,47 @@ +/** + * @import {Orchestrator, OrchestrationFlow} from '../types.js' + * @import {TimerService} from '@agoric/time'; + * @import {LocalChain} from '@agoric/vats/src/localchain.js'; + * @import {NameHub} from '@agoric/vats'; + * @import {Remote} from '@agoric/internal'; + * @import {Zone} from '@agoric/zone'; + * @import {CosmosInterchainService} from '../exos/cosmos-interchain-service.js'; + * @import {OrchestrationTools} from '../utils/start-helper.js'; + */ + +/** + * @satisfies {OrchestrationFlow} + * @param {Orchestrator} orch + * @param {object} ctx + * @param {ZCF} ctx.zcf + * @param {ZCFSeat} _seat + * @param {undefined} _offerArgs + */ +export const unbondAndLiquidStake = async ( + orch, + { zcf }, + _seat, + _offerArgs, +) => { + console.log('zcf within the membrane', zcf); + // Osmosis is one of the few chains with icqEnabled + const osmosis = await orch.getChain('osmosis'); + // In a real world scenario, accounts would be re-used across invokations of the handler + const osmoAccount = await osmosis.makeAccount(); + + // TODO https://github.com/Agoric/agoric-sdk/issues/10016 + // const delegations = await celestiaAccount.getDelegations(); + // // wait for the undelegations to be complete (may take weeks) + // await celestiaAccount.undelegate(delegations); + // ??? should this be synchronous? depends on how names are resolved. + const stride = await orch.getChain('stride'); + const strideAccount = await stride.makeAccount(); + + // TODO the `TIA` string actually needs to be the Brand from AgoricNames + // const tiaAmt = await celestiaAccount.getBalance('TIA'); + // await celestiaAccount.transfer(tiaAmt, strideAccount.getAddress()); + // TODO https://github.com/Agoric/agoric-sdk/issues/10017 + // await strideAccount.liquidStake(tiaAmt); + console.log(osmoAccount, strideAccount); +}; +harden(unbondAndLiquidStake); diff --git a/packages/orchestration/src/examples/unbondExample.contract.js b/packages/orchestration/src/examples/unbondExample.contract.js deleted file mode 100644 index e2d09a1117f..00000000000 --- a/packages/orchestration/src/examples/unbondExample.contract.js +++ /dev/null @@ -1,88 +0,0 @@ -import { M } from '@endo/patterns'; -import { withOrchestration } from '../utils/start-helper.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'; - * @import {Remote} from '@agoric/internal'; - * @import {Zone} from '@agoric/zone'; - * @import {CosmosInterchainService} from '../exos/cosmos-interchain-service.js'; - * @import {OrchestrationTools} from '../utils/start-helper.js'; - */ - -/** - * @satisfies {OrchestrationFlow} - * @param {Orchestrator} orch - * @param {object} ctx - * @param {ZCF} ctx.zcf - * @param {ZCFSeat} _seat - * @param {undefined} _offerArgs - */ -const unbondAndLiquidStakeFn = async (orch, { zcf }, _seat, _offerArgs) => { - console.log('zcf within the membrane', zcf); - // We would actually alreaady have the account from the orchestrator - // ??? could these be passed in? It would reduce the size of this handler, - // keeping it focused on long-running operations. - const omni = await orch.getChain('omniflixhub'); - const omniAccount = await omni.makeAccount(); - - // TODO implement these - // const delegations = await celestiaAccount.getDelegations(); - // // wait for the undelegations to be complete (may take weeks) - // await celestiaAccount.undelegate(delegations); - // ??? should this be synchronous? depends on how names are resolved. - const stride = await orch.getChain('stride'); - const strideAccount = await stride.makeAccount(); - - // TODO the `TIA` string actually needs to be the Brand from AgoricNames - // const tiaAmt = await celestiaAccount.getBalance('TIA'); - // await celestiaAccount.transfer(tiaAmt, strideAccount.getAddress()); - // await strideAccount.liquidStake(tiaAmt); - console.log(omniAccount, strideAccount); -}; - -/** - * Orchestration contract to be wrapped by withOrchestration for Zoe - * - * @param {ZCF} zcf - * @param {{ - * agoricNames: Remote; - * localchain: Remote; - * orchestrationService: Remote; - * storageNode: Remote; - * marshaller: Marshaller; - * timerService: Remote; - * }} privateArgs - * @param {Zone} zone - * @param {OrchestrationTools} tools - */ -const contract = async (zcf, privateArgs, zone, { orchestrate }) => { - const unbondAndLiquidStake = orchestrate( - 'LSTTia', - { zcf }, - unbondAndLiquidStakeFn, - ); - - const publicFacet = zone.exo('publicFacet', undefined, { - makeUnbondAndLiquidStakeInvitation() { - return zcf.makeInvitation( - unbondAndLiquidStake, - 'Unbond and liquid stake', - undefined, - harden({ - // Nothing to give; the funds come from undelegating - give: {}, - want: {}, // XXX ChainAccount Ownable? - exit: M.any(), - }), - ); - }, - }); - - return harden({ publicFacet }); -}; - -export const start = withOrchestration(contract); -harden(start); diff --git a/packages/orchestration/src/exos/cosmos-orchestration-account.js b/packages/orchestration/src/exos/cosmos-orchestration-account.js index 576532eaf8b..66087ad86fa 100644 --- a/packages/orchestration/src/exos/cosmos-orchestration-account.js +++ b/packages/orchestration/src/exos/cosmos-orchestration-account.js @@ -209,7 +209,7 @@ export const prepareCosmosOrchestrationAccountKit = ( * @returns {State} */ ({ chainAddress, bondDenom, localAddress, remoteAddress }, io) => { - const { storageNode, ...rest } = io; + const { storageNode } = io; // must be the fully synchronous maker because the kit is held in durable state const topicKit = makeRecorderKit(storageNode, PUBLIC_TOPICS.account[1]); // TODO determine what goes in vstorage https://github.com/Agoric/agoric-sdk/issues/9066 @@ -223,13 +223,16 @@ export const prepareCosmosOrchestrationAccountKit = ( }), ); + const { account, icqConnection, timer } = io; return { - chainAddress, + account, bondDenom, + chainAddress, + icqConnection, localAddress, remoteAddress, + timer, topicKit, - ...rest, }; }, { diff --git a/packages/orchestration/src/exos/portfolio-holder-kit.js b/packages/orchestration/src/exos/portfolio-holder-kit.js index 176b61ce916..059eebe03e1 100644 --- a/packages/orchestration/src/exos/portfolio-holder-kit.js +++ b/packages/orchestration/src/exos/portfolio-holder-kit.js @@ -43,7 +43,7 @@ const preparePortfolioHolderKit = (zone, { asVow, when }) => { 'PortfolioHolderKit', { invitationMakers: M.interface('InvitationMakers', { - MakeInvitation: M.call( + Proxying: M.call( ChainNameShape, M.string(), M.arrayOf(M.any()), @@ -95,7 +95,7 @@ const preparePortfolioHolderKit = (zone, { asVow, when }) => { * @param {IA} invitationArgs * @returns {Promise>} */ - MakeInvitation(chainName, action, invitationArgs) { + Proxying(chainName, action, invitationArgs) { const { accounts } = this.state; accounts.has(chainName) || Fail`no account found for ${chainName}`; const account = accounts.get(chainName); diff --git a/packages/orchestration/src/facade.js b/packages/orchestration/src/facade.js index 6be7807c248..71a11ad430e 100644 --- a/packages/orchestration/src/facade.js +++ b/packages/orchestration/src/facade.js @@ -1,6 +1,5 @@ /** @file Orchestration facade */ - -import { assertAllDefined } from '@agoric/internal'; +import { assertAllDefined, deepMapObject } from '@agoric/internal'; /** * @import {AsyncFlowTools, GuestInterface, HostArgs, HostOf} from '@agoric/async-flow'; @@ -95,9 +94,12 @@ export const makeOrchestrationFacade = ({ /** * Orchestrate all the guest functions. * + * If the `guestFns` object is provided as a property of `hostCtx` the + * functions will be available within the other guests. + * * NOTE multiple calls to this with the same guestFn name will fail * - * @template HC - host context + * @template {Record} HC - host context * @template {{ * [durableName: string]: OrchestrationFlow>; * }} GFM @@ -106,16 +108,32 @@ export const makeOrchestrationFacade = ({ * @param {HC} hostCtx * @returns {{ [N in keyof GFM]: HostForGuest }} */ - const orchestrateAll = (guestFns, hostCtx) => - /** @type {{ [N in keyof GFM]: HostForGuest }} */ ( + const orchestrateAll = (guestFns, hostCtx) => { + const mappedFlows = new Map( + Object.entries(guestFns).map(([name, guestFn]) => [ + guestFn, + // eslint-disable-next-line no-use-before-define + (...args) => orcFns[name](...args), + ]), + ); + + const mappedContext = deepMapObject( + hostCtx, + val => mappedFlows.get(val) || val, + ); + + const orcFns = /** @type {{ [N in keyof GFM]: HostForGuest }} */ ( Object.fromEntries( Object.entries(guestFns).map(([name, guestFn]) => [ name, - orchestrate(name, hostCtx, guestFn), + orchestrate(name, mappedContext, guestFn), ]), ) ); + return { ...orcFns }; + }; + return harden({ orchestrate, orchestrateAll, diff --git a/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.md b/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.md similarity index 89% rename from packages/orchestration/test/examples/snapshots/unbondExample.test.ts.md rename to packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.md index 98c8b7a6295..dc4bf4aa5a3 100644 --- a/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.md +++ b/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.md @@ -1,6 +1,6 @@ -# Snapshot report for `test/examples/unbondExample.test.ts` +# Snapshot report for `test/examples/unbond.contract.test.ts` -The actual snapshot is saved in `unbondExample.test.ts.snap`. +The actual snapshot is saved in `unbond.contract.test.ts.snap`. Generated by [AVA](https://avajs.dev). @@ -26,7 +26,7 @@ Generated by [AVA](https://avajs.dev). }, contract: { orchestration: { - LSTTia: { + unbondAndLiquidStake: { asyncFlow_kindHandle: 'Alleged: kind', }, }, @@ -40,7 +40,7 @@ Generated by [AVA](https://avajs.dev). Orchestrator_kindHandle: 'Alleged: kind', RemoteChainFacade_kindHandle: 'Alleged: kind', chainName: { - omniflixhub: 'Alleged: RemoteChainFacade public', + osmosis: 'Alleged: RemoteChainFacade public', stride: 'Alleged: RemoteChainFacade public', }, ibcTools: { diff --git a/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.snap b/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.snap new file mode 100644 index 00000000000..26978e4b049 Binary files /dev/null and b/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.snap differ diff --git a/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.snap b/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.snap deleted file mode 100644 index 92e7e65fe76..00000000000 Binary files a/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.snap and /dev/null differ diff --git a/packages/orchestration/test/examples/swapExample.test.ts b/packages/orchestration/test/examples/swap.contract.test.ts similarity index 91% rename from packages/orchestration/test/examples/swapExample.test.ts rename to packages/orchestration/test/examples/swap.contract.test.ts index 2c59723510b..7eb8869eb38 100644 --- a/packages/orchestration/test/examples/swapExample.test.ts +++ b/packages/orchestration/test/examples/swap.contract.test.ts @@ -8,9 +8,9 @@ import { commonSetup } from '../supports.js'; const dirname = path.dirname(new URL(import.meta.url).pathname); -const contractFile = `${dirname}/../../src/examples/swapExample.contract.js`; +const contractFile = `${dirname}/../../src/examples/swap.contract.js`; type StartFn = - typeof import('@agoric/orchestration/src/examples/swapExample.contract.js').start; + typeof import('@agoric/orchestration/src/examples/swap.contract.js').start; test('start', async t => { const { diff --git a/packages/orchestration/test/examples/unbondExample.test.ts b/packages/orchestration/test/examples/unbond.contract.test.ts similarity index 89% rename from packages/orchestration/test/examples/unbondExample.test.ts rename to packages/orchestration/test/examples/unbond.contract.test.ts index 830353380f4..74266f0b62c 100644 --- a/packages/orchestration/test/examples/unbondExample.test.ts +++ b/packages/orchestration/test/examples/unbond.contract.test.ts @@ -8,9 +8,9 @@ import { commonSetup } from '../supports.js'; const dirname = path.dirname(new URL(import.meta.url).pathname); -const contractFile = `${dirname}/../../src/examples/unbondExample.contract.js`; +const contractFile = `${dirname}/../../src/examples/unbond.contract.js`; type StartFn = - typeof import('@agoric/orchestration/src/examples/unbondExample.contract.js').start; + typeof import('@agoric/orchestration/src/examples/unbond.contract.js').start; test('start', async t => { const { diff --git a/packages/orchestration/test/exos/portfolio-holder-kit.test.ts b/packages/orchestration/test/exos/portfolio-holder-kit.test.ts index 5151873f908..62f48203f25 100644 --- a/packages/orchestration/test/exos/portfolio-holder-kit.test.ts +++ b/packages/orchestration/test/exos/portfolio-holder-kit.test.ts @@ -78,7 +78,7 @@ test('portfolio holder kit behaviors', async t => { const { invitationMakers } = await E(holder).asContinuingOffer(); - const delegateInv = await E(invitationMakers).MakeInvitation( + const delegateInv = await E(invitationMakers).Proxying( 'cosmoshub', 'Delegate', [ @@ -99,7 +99,7 @@ test('portfolio holder kit behaviors', async t => { // note: mocked zcf (we are not in a contract) returns inv description // @ts-expect-error Argument of type 'string' is not assignable to parameter of type 'Vow' 'Delegate', - 'any invitation maker accessible via MakeInvitation', + 'any invitation maker accessible via Proxying', ); const osmosisAccount = await makeCosmosAccount({ diff --git a/packages/orchestration/test/facade-durability.test.ts b/packages/orchestration/test/facade-durability.test.ts new file mode 100644 index 00000000000..ffc4d06f20d --- /dev/null +++ b/packages/orchestration/test/facade-durability.test.ts @@ -0,0 +1,234 @@ +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import { makeIssuerKit } from '@agoric/ertp'; +import { reincarnate } from '@agoric/swingset-liveslots/tools/setup-vat-data.js'; +import { prepareSwingsetVowTools } from '@agoric/vow/vat.js'; +import { setupZCFTest } from '@agoric/zoe/test/unitTests/zcf/setupZcfTest.js'; +import type { CosmosChainInfo, IBCConnectionInfo } from '../src/cosmos-api.js'; +import fetchedChainInfo from '../src/fetched-chain-info.js'; // Refresh with scripts/refresh-chain-info.ts +import type { Chain } from '../src/orchestration-api.js'; +import { denomHash } from '../src/utils/denomHash.js'; +import { provideOrchestration } from '../src/utils/start-helper.js'; +import { commonSetup, provideDurableZone } from './supports.js'; + +const test = anyTest; + +const mockChainInfo: CosmosChainInfo = harden({ + chainId: 'mock-1', + icaEnabled: false, + icqEnabled: false, + pfmEnabled: false, + ibcHooksEnabled: false, + stakingTokens: [{ denom: 'umock' }], +}); +const mockChainConnection: IBCConnectionInfo = { + id: 'connection-0', + client_id: '07-tendermint-2', + counterparty: { + client_id: '07-tendermint-2', + connection_id: 'connection-1', + prefix: { + key_prefix: '', + }, + }, + state: 3 /* IBCConnectionState.STATE_OPEN */, + transferChannel: { + portId: 'transfer', + channelId: 'channel-1', + counterPartyChannelId: 'channel-1', + counterPartyPortId: 'transfer', + ordering: 1 /* Order.ORDER_UNORDERED */, + state: 3 /* IBCConnectionState.STATE_OPEN */, + version: 'ics20-1', + }, +}; + +test.serial('chain info', async t => { + const { bootstrap, facadeServices, commonPrivateArgs } = await commonSetup(t); + + const { zcf } = await setupZCFTest(); + + // After setupZCFTest because this disables relaxDurabilityRules + // which breaks Zoe test setup's fakeVatAdmin + const zone = provideDurableZone('test'); + const vt = prepareSwingsetVowTools(zone); + + const orchKit = provideOrchestration( + zcf, + zone.mapStore('test'), + { + agoricNames: facadeServices.agoricNames, + timerService: facadeServices.timerService, + storageNode: commonPrivateArgs.storageNode, + orchestrationService: facadeServices.orchestrationService, + localchain: facadeServices.localchain, + }, + commonPrivateArgs.marshaller, + ); + + const { chainHub, orchestrate } = orchKit; + + chainHub.registerChain('mock', mockChainInfo); + chainHub.registerConnection( + 'agoric-3', + mockChainInfo.chainId, + mockChainConnection, + ); + + const handle = orchestrate('mock', {}, async orc => { + return orc.getChain('mock'); + }); + + const result = (await vt.when(handle())) as Chain; + t.deepEqual(await vt.when(result.getChainInfo()), mockChainInfo); +}); + +test.serial('faulty chain info', async t => { + const { facadeServices, commonPrivateArgs } = await commonSetup(t); + + // XXX relax again so setupZCFTest can run. This is also why the tests are serial. + reincarnate({ relaxDurabilityRules: true }); + const { zcf } = await setupZCFTest(); + + // After setupZCFTest because this disables relaxDurabilityRules + // which breaks Zoe test setup's fakeVatAdmin + const zone = provideDurableZone('test'); + const vt = prepareSwingsetVowTools(zone); + + const orchKit = provideOrchestration( + zcf, + zone.mapStore('test'), + { + agoricNames: facadeServices.agoricNames, + timerService: facadeServices.timerService, + storageNode: commonPrivateArgs.storageNode, + orchestrationService: facadeServices.orchestrationService, + localchain: facadeServices.localchain, + }, + commonPrivateArgs.marshaller, + ); + + const { chainHub, orchestrate } = orchKit; + + const { stakingTokens, ...sansStakingTokens } = mockChainInfo; + + chainHub.registerChain('mock', sansStakingTokens); + chainHub.registerConnection( + 'agoric-3', + mockChainInfo.chainId, + mockChainConnection, + ); + + const handle = orchestrate('mock', {}, async orc => { + const chain = await orc.getChain('mock'); + const account = await chain.makeAccount(); + return account; + }); + + await t.throwsAsync(vt.when(handle()), { + message: 'chain info lacks staking denom', + }); +}); + +test.serial('asset / denom info', async t => { + const { facadeServices, commonPrivateArgs } = await commonSetup(t); + + // XXX relax again + reincarnate({ relaxDurabilityRules: true }); + const { zcf } = await setupZCFTest(); + const zone = provideDurableZone('test'); + const vt = prepareSwingsetVowTools(zone); + const orchKit = provideOrchestration( + zcf, + zone.mapStore('test'), + { + agoricNames: facadeServices.agoricNames, + timerService: facadeServices.timerService, + storageNode: commonPrivateArgs.storageNode, + orchestrationService: facadeServices.orchestrationService, + localchain: facadeServices.localchain, + }, + commonPrivateArgs.marshaller, + ); + const { chainHub, orchestrate } = orchKit; + + chainHub.registerChain('agoric', fetchedChainInfo.agoric); + chainHub.registerChain(mockChainInfo.chainId, mockChainInfo); + chainHub.registerConnection( + 'agoric-3', + mockChainInfo.chainId, + mockChainConnection, + ); + + chainHub.registerAsset('utoken1', { + chainName: mockChainInfo.chainId, + baseName: mockChainInfo.chainId, + baseDenom: 'utoken1', + }); + + const { channelId } = mockChainConnection.transferChannel; + const agDenom = `ibc/${denomHash({ denom: 'utoken1', channelId })}`; + const { brand } = makeIssuerKit('Token1'); + t.log(`utoken1 over ${channelId}: ${agDenom}`); + chainHub.registerAsset(agDenom, { + chainName: 'agoric', + baseName: mockChainInfo.chainId, + baseDenom: 'utoken1', + brand, + }); + + const handle = orchestrate( + 'useDenoms', + { brand }, + // eslint-disable-next-line no-shadow + async (orc, { brand }) => { + const c1 = await orc.getChain(mockChainInfo.chainId); + + { + const actual = orc.getDenomInfo('utoken1'); + console.log('actual', actual); + const info = await actual.chain.getChainInfo(); + t.deepEqual(info, mockChainInfo); + + t.deepEqual(actual, { + base: c1, + chain: c1, + baseDenom: 'utoken1', + brand: undefined, + }); + } + + const ag = await orc.getChain('agoric'); + { + const actual = orc.getDenomInfo(agDenom); + + t.deepEqual(actual, { + chain: ag, + base: c1, + baseDenom: 'utoken1', + brand, + }); + } + }, + ); + + await vt.when(handle()); + + chainHub.registerChain('anotherChain', mockChainInfo); + chainHub.registerConnection('agoric-3', 'anotherChain', mockChainConnection); + chainHub.registerAsset('utoken2', { + chainName: 'anotherChain', + baseName: 'anotherChain', + baseDenom: 'utoken2', + }); + + const missingGetChain = orchestrate('missing getChain', {}, async orc => { + const actual = orc.getDenomInfo('utoken2'); + }); + + await t.throwsAsync(vt.when(missingGetChain()), { + message: 'use getChain("anotherChain") before getDenomInfo("utoken2")', + }); +}); + +test.todo('contract upgrade'); diff --git a/packages/orchestration/test/facade.test.ts b/packages/orchestration/test/facade.test.ts index 99c685433a3..8ee3e4aaa45 100644 --- a/packages/orchestration/test/facade.test.ts +++ b/packages/orchestration/test/facade.test.ts @@ -1,56 +1,21 @@ +/* eslint-disable @jessie.js/safe-await-separator */ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import type { VowTools } from '@agoric/vow'; import { prepareSwingsetVowTools } from '@agoric/vow/vat.js'; import { setupZCFTest } from '@agoric/zoe/test/unitTests/zcf/setupZcfTest.js'; -import { reincarnate } from '@agoric/swingset-liveslots/tools/setup-vat-data.js'; -import { makeIssuerKit } from '@agoric/ertp'; -import type { CosmosChainInfo, IBCConnectionInfo } from '../src/cosmos-api.js'; -import type { Chain } from '../src/orchestration-api.js'; +import { makeHeapZone } from '@agoric/zone'; +import type { TestFn } from 'ava'; +import type { OrchestrationFlow } from '../src/orchestration-api.js'; import { provideOrchestration } from '../src/utils/start-helper.js'; -import { commonSetup, provideDurableZone } from './supports.js'; -import { denomHash } from '../src/utils/denomHash.js'; -import fetchedChainInfo from '../src/fetched-chain-info.js'; // Refresh with scripts/refresh-chain-info.ts +import { commonSetup } from './supports.js'; -const test = anyTest; - -const mockChainInfo: CosmosChainInfo = harden({ - chainId: 'mock-1', - icaEnabled: false, - icqEnabled: false, - pfmEnabled: false, - ibcHooksEnabled: false, - stakingTokens: [{ denom: 'umock' }], -}); -const mockChainConnection: IBCConnectionInfo = { - id: 'connection-0', - client_id: '07-tendermint-2', - counterparty: { - client_id: '07-tendermint-2', - connection_id: 'connection-1', - prefix: { - key_prefix: '', - }, - }, - state: 3 /* IBCConnectionState.STATE_OPEN */, - transferChannel: { - portId: 'transfer', - channelId: 'channel-1', - counterPartyChannelId: 'channel-1', - counterPartyPortId: 'transfer', - ordering: 1 /* Order.ORDER_UNORDERED */, - state: 3 /* IBCConnectionState.STATE_OPEN */, - version: 'ics20-1', - }, -}; - -test.serial('chain info', async t => { - const { bootstrap, facadeServices, commonPrivateArgs } = await commonSetup(t); +const test = anyTest as TestFn<{ vt: VowTools; orchestrateAll: any; zcf: ZCF }>; +test.beforeEach(async t => { + const { facadeServices, commonPrivateArgs } = await commonSetup(t); const { zcf } = await setupZCFTest(); - - // After setupZCFTest because this disables relaxDurabilityRules - // which breaks Zoe test setup's fakeVatAdmin - const zone = provideDurableZone('test'); + const zone = makeHeapZone(); const vt = prepareSwingsetVowTools(zone); const orchKit = provideOrchestration( @@ -66,169 +31,47 @@ test.serial('chain info', async t => { commonPrivateArgs.marshaller, ); - const { chainHub, orchestrate } = orchKit; - - chainHub.registerChain('mock', mockChainInfo); - chainHub.registerConnection( - 'agoric-3', - mockChainInfo.chainId, - mockChainConnection, - ); - - const handle = orchestrate('mock', {}, async orc => { - return orc.getChain('mock'); - }); - - const result = (await vt.when(handle())) as Chain; - t.deepEqual(await vt.when(result.getChainInfo()), mockChainInfo); + const { orchestrateAll } = orchKit; + t.context = { vt, orchestrateAll, zcf }; }); -test.serial('faulty chain info', async t => { - const { facadeServices, commonPrivateArgs } = await commonSetup(t); - - // XXX relax again so setupZCFTest can run. This is also why the tests are serial. - reincarnate({ relaxDurabilityRules: true }); - const { zcf } = await setupZCFTest(); - - // After setupZCFTest because this disables relaxDurabilityRules - // which breaks Zoe test setup's fakeVatAdmin - const zone = provideDurableZone('test'); - const vt = prepareSwingsetVowTools(zone); +test('calls between flows', async t => { + const { vt, orchestrateAll, zcf } = t.context; - const orchKit = provideOrchestration( - zcf, - zone.mapStore('test'), - { - agoricNames: facadeServices.agoricNames, - timerService: facadeServices.timerService, - storageNode: commonPrivateArgs.storageNode, - orchestrationService: facadeServices.orchestrationService, - localchain: facadeServices.localchain, + const flows = { + outer(orch, ctx, ...recipients) { + return ctx.peerFlows.inner('Hello', ...recipients); }, - commonPrivateArgs.marshaller, - ); - - const { chainHub, orchestrate } = orchKit; - - const { stakingTokens, ...sansStakingTokens } = mockChainInfo; - - chainHub.registerChain('mock', sansStakingTokens); - chainHub.registerConnection( - 'agoric-3', - mockChainInfo.chainId, - mockChainConnection, - ); + inner(orch, ctx, ...strs) { + return Promise.resolve(strs.join(' ')); + }, + } as Record>; - const handle = orchestrate('mock', {}, async orc => { - const chain = await orc.getChain('mock'); - const account = await chain.makeAccount(); - return account; + const { outer, outer2, inner } = orchestrateAll(flows, { + peerFlows: flows, + zcf, }); - await t.throwsAsync(vt.when(handle()), { - message: 'chain info lacks staking denom', - }); + t.deepEqual(await vt.when(inner('a', 'b', 'c')), 'a b c'); + t.deepEqual(await vt.when(outer('a', 'b', 'c')), 'Hello a b c'); }); -test.serial('asset / denom info', async t => { - const { facadeServices, commonPrivateArgs } = await commonSetup(t); +test('context mapping individual flows', async t => { + const { vt, orchestrateAll, zcf } = t.context; - // XXX relax again - reincarnate({ relaxDurabilityRules: true }); - const { zcf } = await setupZCFTest(); - const zone = provideDurableZone('test'); - const vt = prepareSwingsetVowTools(zone); - const orchKit = provideOrchestration( - zcf, - zone.mapStore('test'), - { - agoricNames: facadeServices.agoricNames, - timerService: facadeServices.timerService, - storageNode: commonPrivateArgs.storageNode, - orchestrationService: facadeServices.orchestrationService, - localchain: facadeServices.localchain, + const flows = { + outer(orch, ctx, ...recipients) { + return ctx.peerFlows.inner('Hello', ...recipients); }, - commonPrivateArgs.marshaller, - ); - const { chainHub, orchestrate } = orchKit; - - chainHub.registerChain('agoric', fetchedChainInfo.agoric); - chainHub.registerChain(mockChainInfo.chainId, mockChainInfo); - chainHub.registerConnection( - 'agoric-3', - mockChainInfo.chainId, - mockChainConnection, - ); - - chainHub.registerAsset('utoken1', { - chainName: mockChainInfo.chainId, - baseName: mockChainInfo.chainId, - baseDenom: 'utoken1', - }); - - const { channelId } = mockChainConnection.transferChannel; - const agDenom = `ibc/${denomHash({ denom: 'utoken1', channelId })}`; - const { brand } = makeIssuerKit('Token1'); - t.log(`utoken1 over ${channelId}: ${agDenom}`); - chainHub.registerAsset(agDenom, { - chainName: 'agoric', - baseName: mockChainInfo.chainId, - baseDenom: 'utoken1', - brand, - }); - - const handle = orchestrate( - 'useDenoms', - { brand }, - // eslint-disable-next-line no-shadow - async (orc, { brand }) => { - const c1 = await orc.getChain(mockChainInfo.chainId); - - { - const actual = orc.getDenomInfo('utoken1'); - console.log('actual', actual); - const info = await actual.chain.getChainInfo(); - t.deepEqual(info, mockChainInfo); - - t.deepEqual(actual, { - base: c1, - chain: c1, - baseDenom: 'utoken1', - brand: undefined, - }); - } - - const ag = await orc.getChain('agoric'); - { - const actual = orc.getDenomInfo(agDenom); - - t.deepEqual(actual, { - chain: ag, - base: c1, - baseDenom: 'utoken1', - brand, - }); - } + inner(orch, ctx, ...strs) { + return Promise.resolve(strs.join(' ')); }, - ); - - await vt.when(handle()); - - chainHub.registerChain('anotherChain', mockChainInfo); - chainHub.registerConnection('agoric-3', 'anotherChain', mockChainConnection); - chainHub.registerAsset('utoken2', { - chainName: 'anotherChain', - baseName: 'anotherChain', - baseDenom: 'utoken2', - }); + } as Record>; - const missingGetChain = orchestrate('missing getChain', {}, async orc => { - const actual = orc.getDenomInfo('utoken2'); + const { outer } = orchestrateAll(flows, { + peerFlows: { inner: flows.inner }, + zcf, }); - await t.throwsAsync(vt.when(missingGetChain()), { - message: 'use getChain("anotherChain") before getDenomInfo("utoken2")', - }); + t.deepEqual(await vt.when(outer('a', 'b', 'c')), 'Hello a b c'); }); - -test.todo('contract upgrade');