From 82f1901ec6ecf5a802a72023d033609deeb053e1 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 4 Apr 2024 10:09:22 -0400 Subject: [PATCH] feat(orchestration): add stakeAtom example contract --- .../test/bootstrapTests/test-orchestration.ts | 37 +++++++++- .../scripts/orchestration/init-stakeAtom.js | 34 ++++++++++ .../src/contracts/stakeAtom.contract.js | 52 ++++++++++++++ packages/orchestration/src/orchestration.js | 20 +++--- .../src/proposals/start-stakeAtom.js | 68 +++++++++++++++++++ packages/vats/src/core/types-ambient.d.ts | 5 ++ packages/vats/src/core/utils.js | 2 + 7 files changed, 209 insertions(+), 9 deletions(-) create mode 100644 packages/builders/scripts/orchestration/init-stakeAtom.js create mode 100644 packages/orchestration/src/contracts/stakeAtom.contract.js create mode 100644 packages/orchestration/src/proposals/start-stakeAtom.js diff --git a/packages/boot/test/bootstrapTests/test-orchestration.ts b/packages/boot/test/bootstrapTests/test-orchestration.ts index c443ff9ba94..92ff7b65abf 100644 --- a/packages/boot/test/bootstrapTests/test-orchestration.ts +++ b/packages/boot/test/bootstrapTests/test-orchestration.ts @@ -5,6 +5,7 @@ import type { TestFn } from 'ava'; import { Fail } from '@agoric/assert'; import type { start as stakeBldStart } from '@agoric/orchestration/src/contracts/stakeBld.contract.js'; import type { Instance } from '@agoric/zoe/src/zoeService/utils.js'; +import { M, matches } from '@endo/patterns'; import { makeWalletFactoryContext } from './walletFactory.ts'; type DefaultTestContext = Awaited>; @@ -14,7 +15,7 @@ const test: TestFn = anyTest; test.before(async t => (t.context = await makeWalletFactoryContext(t))); test.after.always(t => t.context.shutdown?.()); -test('stakeBld', async t => { +test.serial('stakeBld', async t => { const { agoricNamesRemotes, buildProposal, @@ -86,3 +87,37 @@ test('stakeBld', async t => { }, }); }); + +test.serial('stakeAtom', async t => { + const { + buildProposal, + evalProposal, + runUtils: { EV }, + } = t.context; + // TODO move into a vm-config for u15 + await evalProposal( + buildProposal('@agoric/builders/scripts/vats/init-network.js'), + ); + await evalProposal( + buildProposal('@agoric/builders/scripts/vats/init-orchestration.js'), + ); + await evalProposal( + buildProposal('@agoric/builders/scripts/orchestration/init-stakeAtom.js'), + ); + + const agoricNames = await EV.vat('bootstrap').consumeItem('agoricNames'); + const instance = await EV(agoricNames).lookup('instance', 'stakeAtom'); + t.truthy(instance, 'stakeAtom instance is available'); + + const zoe = await EV.vat('bootstrap').consumeItem('zoe'); + const publicFacet = await EV(zoe).getPublicFacet(instance); + t.truthy(publicFacet, 'stakeAtom publicFacet is available'); + + const account = await EV(publicFacet).createAccount(); + t.log('account', account); + t.truthy(account, 'createAccount returns an account on ATOM connection'); + t.truthy( + matches(account, M.remotable('ChainAccount')), + 'account is a remotable', + ); +}); diff --git a/packages/builders/scripts/orchestration/init-stakeAtom.js b/packages/builders/scripts/orchestration/init-stakeAtom.js new file mode 100644 index 00000000000..306834f34ab --- /dev/null +++ b/packages/builders/scripts/orchestration/init-stakeAtom.js @@ -0,0 +1,34 @@ +import { makeHelpers } from '@agoric/deploy-script-support'; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').ProposalBuilder} */ +export const defaultProposalBuilder = async ( + { publishRef, install }, + options = {}, +) => { + const { + hostConnectionId = 'connection-1', + controllerConnectionId = 'connection-0', + } = options; + return harden({ + sourceSpec: '@agoric/orchestration/src/proposals/start-stakeAtom.js', + getManifestCall: [ + 'getManifestForStakeAtom', + { + installKeys: { + stakeAtom: publishRef( + install( + '@agoric/orchestration/src/contracts/stakeAtom.contract.js', + ), + ), + }, + hostConnectionId, + controllerConnectionId, + }, + ], + }); +}; + +export default async (homeP, endowments) => { + const { writeCoreProposal } = await makeHelpers(homeP, endowments); + await writeCoreProposal('start-stakeAtom', defaultProposalBuilder); +}; diff --git a/packages/orchestration/src/contracts/stakeAtom.contract.js b/packages/orchestration/src/contracts/stakeAtom.contract.js new file mode 100644 index 00000000000..f730c54451b --- /dev/null +++ b/packages/orchestration/src/contracts/stakeAtom.contract.js @@ -0,0 +1,52 @@ +// @ts-check +/** + * @file Example contract that uses orchestration + */ + +import { makeDurableZone } from '@agoric/zone/durable.js'; +import { V as E } from '@agoric/vat-data/vow.js'; +import { M } from '@endo/patterns'; + +/** + * @import * as orchestration from '../types' + * @import * as vatData from '@agoric/vat-data' + */ + +/** + * @typedef {{ + * hostConnectionId: orchestration.ConnectionId; + * controllerConnectionId: orchestration.ConnectionId; + * }} StakeAtomTerms + */ + +/** + * + * @param {ZCF} zcf + * @param {{ + * orchestration: orchestration.Orchestration; + * }} privateArgs + * @param {vatData.Baggage} baggage + */ +export const start = async (zcf, privateArgs, baggage) => { + const { hostConnectionId, controllerConnectionId } = zcf.getTerms(); + const { orchestration } = privateArgs; + + const zone = makeDurableZone(baggage); + + const publicFacet = zone.exo( + 'StakeAtom', + M.interface('StakeAtomI', { + createAccount: M.callWhen().returns(M.remotable('ChainAccount')), + }), + { + async createAccount() { + return E(orchestration).createAccount( + hostConnectionId, + controllerConnectionId, + ); + }, + }, + ); + + return { publicFacet }; +}; diff --git a/packages/orchestration/src/orchestration.js b/packages/orchestration/src/orchestration.js index 2e4309e6620..37f4d83ac22 100644 --- a/packages/orchestration/src/orchestration.js +++ b/packages/orchestration/src/orchestration.js @@ -7,6 +7,11 @@ import { M } from '@endo/patterns'; import { makeICAConnectionAddress, parseAddress } from './utils/address.js'; import '@agoric/network/exported.js'; +/** + * @import { ConnectionId } from './types'; + * @import { Zone } from '@agoric/base-zone'; + */ + const { Fail, bare } = assert; const trace = makeTracer('Orchestration'); @@ -55,7 +60,7 @@ export const ConnectionHandlerI = M.interface('ConnectionHandler', { onReceive: M.callWhen(M.any(), M.string()).returns(M.any()), }); -/** @param {import('@agoric/base-zone').Zone} zone */ +/** @param {Zone} zone */ const prepareChainAccount = zone => zone.exoClassKit( 'ChainAccount', @@ -127,9 +132,8 @@ const prepareChainAccount = zone => * @param {Connection} connection * @param {string} localAddr * @param {string} remoteAddr - * @param {ConnectionHandler} _connectionHandler */ - async onOpen(connection, localAddr, remoteAddr, _connectionHandler) { + async onOpen(connection, localAddr, remoteAddr) { trace(`ICA Channel Opened for ${localAddr} at ${remoteAddr}`); this.state.connection = connection; this.state.remoteAddress = remoteAddr; @@ -137,7 +141,7 @@ const prepareChainAccount = zone => // XXX parseAddress currently throws, should it return '' instead? this.state.accountAddress = parseAddress(remoteAddr); }, - async onClose(_connection, reason, _connectionHandler) { + async onClose(_connection, reason) { trace(`ICA Channel closed. Reason: ${reason}`); // XXX handle connection closing // XXX is there a scenario where a connection will unexpectedly close? _I think yes_ @@ -157,7 +161,7 @@ export const OrchestrationI = M.interface('Orchestration', { }); /** - * @param {import('@agoric/base-zone').Zone} zone + * @param {Zone} zone * @param {ReturnType} createChainAccount */ const prepareOrchestration = (zone, createChainAccount) => @@ -193,9 +197,9 @@ const prepareOrchestration = (zone, createChainAccount) => }, public: { /** - * @param {import('@agoric/orchestration').ConnectionId} hostConnectionId + * @param {ConnectionId} hostConnectionId * the counterparty connection_id - * @param {import('@agoric/orchestration').ConnectionId} controllerConnectionId + * @param {ConnectionId} controllerConnectionId * self connection_id * @returns {Promise} */ @@ -220,7 +224,7 @@ const prepareOrchestration = (zone, createChainAccount) => }, ); -/** @param {import('@agoric/base-zone').Zone} zone */ +/** @param {Zone} zone */ export const prepareOrchestrationTools = zone => { const createChainAccount = prepareChainAccount(zone); const makeOrchestration = prepareOrchestration(zone, createChainAccount); diff --git a/packages/orchestration/src/proposals/start-stakeAtom.js b/packages/orchestration/src/proposals/start-stakeAtom.js new file mode 100644 index 00000000000..46dbb9c37ee --- /dev/null +++ b/packages/orchestration/src/proposals/start-stakeAtom.js @@ -0,0 +1,68 @@ +// @ts-check +import { makeTracer } from '@agoric/internal'; +import { E } from '@endo/far'; + +const trace = makeTracer('StartStakeAtom', true); + +/** + * @param {BootstrapPowers & { installation: {consume: {stakeAtom: Installation}}}} powers + * @param {{options: import('../contracts/stakeAtom.contract.js').StakeAtomTerms}} options + */ +export const startStakeAtom = async ( + { + consume: { orchestration, startUpgradable }, + installation: { + consume: { stakeAtom }, + }, + instance: { + produce: { stakeAtom: produceInstance }, + }, + }, + { options: { hostConnectionId, controllerConnectionId } }, +) => { + trace('startStakeAtom', { hostConnectionId, controllerConnectionId }); + await null; + + /** @type {StartUpgradableOpts} */ + const startOpts = { + label: 'stakeAtom', + installation: stakeAtom, + terms: { + hostConnectionId, + controllerConnectionId, + }, + privateArgs: { + orchestration: await orchestration, + }, + }; + + const { instance } = await E(startUpgradable)(startOpts); + produceInstance.resolve(instance); +}; +harden(startStakeAtom); + +export const getManifestForStakeAtom = ( + { restoreRef }, + { installKeys, ...options }, +) => { + return { + manifest: { + [startStakeAtom.name]: { + consume: { + orchestration: true, + startUpgradable: true, + }, + installation: { + consume: { stakeAtom: true }, + }, + instance: { + produce: { stakeAtom: true }, + }, + }, + }, + installations: { + stakeAtom: restoreRef(installKeys.stakeAtom), + }, + options, + }; +}; diff --git a/packages/vats/src/core/types-ambient.d.ts b/packages/vats/src/core/types-ambient.d.ts index 65d4e18edc7..f3bee5d95ed 100644 --- a/packages/vats/src/core/types-ambient.d.ts +++ b/packages/vats/src/core/types-ambient.d.ts @@ -179,6 +179,7 @@ type WellKnownName = { | 'reserve' | 'psm' | 'scaledPriceAuthority' + | 'stakeAtom' // test contract | 'stakeBld' // test contract | 'econCommitteeCharter' | 'priceAggregator'; @@ -193,6 +194,7 @@ type WellKnownName = { | 'provisionPool' | 'reserve' | 'reserveGovernor' + | 'stakeAtom' // test contract | 'stakeBld' // test contract | 'Pegasus'; oracleBrand: 'USD'; @@ -341,9 +343,12 @@ type ChainBootstrapSpaceT = { * Vault Factory. ONLY FOR DISASTER RECOVERY */ instancePrivateArgs: Map; + localchain: import('@agoric/vats/src/localchain.js').LocalChain; mints?: MintsVat; namesByAddress: import('../types.js').NameHub; namesByAddressAdmin: import('../types.js').NamesByAddressAdmin; + networkVat: NetworkVat; + orchestration: import('@agoric/orchestration/src/orchestration.js').Orchestration; pegasusConnections: import('@agoric/vats').NameHubKit; pegasusConnectionsAdmin: import('@agoric/vats').NameAdmin; priceAuthorityVat: Awaited; diff --git a/packages/vats/src/core/utils.js b/packages/vats/src/core/utils.js index 25c364b5e46..4256abf1d13 100644 --- a/packages/vats/src/core/utils.js +++ b/packages/vats/src/core/utils.js @@ -56,6 +56,7 @@ export const agoricNamesReserved = harden({ econCommitteeCharter: 'Charter for Econ Governance questions', priceAggregator: 'simple price aggregator', scaledPriceAuthority: 'scaled price authority', + stakeAtom: 'example ATOM staking contract', stakeBld: 'example BLD staking contract', }, instance: { @@ -70,6 +71,7 @@ export const agoricNamesReserved = harden({ econCommitteeCharter: 'Charter for Econ Governance questions', provisionPool: 'Account Provision Pool', walletFactory: 'Smart Wallet Factory', + stakeAtom: 'example ATOM staking contract', stakeBld: 'example BLD staking contract', }, oracleBrand: {