Skip to content

Commit

Permalink
feat(orchestration): add stakeAtom example contract
Browse files Browse the repository at this point in the history
  • Loading branch information
0xpatrickdev committed Apr 4, 2024
1 parent 9a1368f commit cca6961
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 21 deletions.
36 changes: 36 additions & 0 deletions packages/boot/test/bootstrapTests/test-orchestration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnType<typeof makeWalletFactoryContext>>;
Expand All @@ -31,6 +32,7 @@ test('stakeBld', async t => {
await evalProposal(
buildProposal('@agoric/builders/scripts/orchestration/init-stakeBld.js'),
);

// update now that stakeBld is instantiated
refreshAgoricNamesRemotes();
const stakeBld = agoricNamesRemotes.instance.stakeBld as Instance<
Expand Down Expand Up @@ -86,3 +88,37 @@ test('stakeBld', async t => {
},
});
});

test('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',
);
});
34 changes: 34 additions & 0 deletions packages/builders/scripts/orchestration/init-stakeAtom.js
Original file line number Diff line number Diff line change
@@ -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);
};
44 changes: 44 additions & 0 deletions packages/orchestration/src/contracts/stakeAtom.contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// @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';

/**
* @typedef {{
* hostConnectionId: import('../types').ConnectionId;
* controllerConnectionId: import('../types').ConnectionId;
* }} StakeAtomTerms
*/

/**
*
* @param {ZCF<StakeAtomTerms>} zcf
* @param {{
* orchestration: import('@agoric/vats/src/orchestration').Orchestration;
* }} privateArgs
* @param {import('@agoric/vat-data').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', undefined, {
/**
* @param {Port} [port] if the user has a bound port and wants to reuse it
*/
async createAccount(port) {
return E(orchestration).createAccount(
hostConnectionId,
controllerConnectionId,
port,
);
},
});

return { publicFacet };
};
68 changes: 68 additions & 0 deletions packages/orchestration/src/proposals/start-stakeAtom.js
Original file line number Diff line number Diff line change
@@ -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<import('../contracts/stakeAtom.contract.js').start>}}}} 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<import('../contracts/stakeAtom.contract.js').start>} */
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,
};
};
16 changes: 9 additions & 7 deletions packages/orchestration/src/utils/address.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-check
import { Fail } from '@agoric/assert';

/**
Expand Down Expand Up @@ -27,18 +28,19 @@ export const makeICAConnectionAddress = (
};

/**
* Parse a chain address from a remote address string
* Parse a chain address from a remote address string.
* Assumes the address string is in a JSON format and contains an "address" field.
* This function is designed to be safe against malformed inputs and unexpected data types, and will return `undefined` in those cases.
* @param {string} remoteAddressString - remote address string, including version
* @returns {string} chain address
* @throws {Error} if address cannot be parsed
* @returns {string | undefined} returns undefined on error
*/
export const parseAddress = remoteAddressString => {
try {
// Extract JSON version string assuming it's always surrounded by {}
const jsonStr = remoteAddressString.match(/{.*?}/)[0];
const jsonObj = JSON.parse(jsonStr);
return jsonObj.address || '';
const jsonStr = remoteAddressString?.match(/{.*?}/)?.[0];
const jsonObj = jsonStr ? JSON.parse(jsonStr) : undefined;
return jsonObj?.address ?? undefined;
} catch (error) {
Fail`Error parsing address`;
return undefined;
}
};
22 changes: 10 additions & 12 deletions packages/orchestration/test/utils/address.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,32 @@ import {

test('makeICAConnectionAddress', t => {
t.throws(() => makeICAConnectionAddress(), {
message: 'controllerConnectionId is required',
message: 'hostConnectionId is required',
});
t.throws(() => makeICAConnectionAddress('connection-0'), {
message: 'hostConnectionId is required',
message: 'controllerConnectionId is required',
});
t.is(
makeICAConnectionAddress('connection-0', 'connection-1'),
makeICAConnectionAddress('connection-1', 'connection-0'),
'/ibc-hop/connection-0/ibc-port/icahost/ordered/{"version":"ics27-1","controllerConnectionId":"connection-0","hostConnectionId":"connection-1","address":"","encoding":"proto3","txType":"sdk_multi_msg"}',
'returns connection string when controllerConnectionId and hostConnectionId are provided',
);
t.is(
makeICAConnectionAddress('connection-0', 'connection-1', {
makeICAConnectionAddress('connection-1', 'connection-0', {
version: 'ics27-0',
}),
'/ibc-hop/connection-0/ibc-port/icahost/ordered/{"version":"ics27-0","controllerConnectionId":"connection-0","hostConnectionId":"connection-1","address":"","encoding":"proto3","txType":"sdk_multi_msg"}',
'accepts custom version',
);
t.is(
makeICAConnectionAddress('connection-0', 'connection-1', {
makeICAConnectionAddress('connection-1', 'connection-0', {
encoding: 'test',
}),
'/ibc-hop/connection-0/ibc-port/icahost/ordered/{"version":"ics27-1","controllerConnectionId":"connection-0","hostConnectionId":"connection-1","address":"","encoding":"test","txType":"sdk_multi_msg"}',
'accepts custom encoding',
);
t.is(
makeICAConnectionAddress('connection-0', 'connection-1', {
makeICAConnectionAddress('connection-1', 'connection-0', {
ordering: 'unordered',
}),
'/ibc-hop/connection-0/ibc-port/icahost/unordered/{"version":"ics27-1","controllerConnectionId":"connection-0","hostConnectionId":"connection-1","address":"","encoding":"proto3","txType":"sdk_multi_msg"}',
Expand All @@ -40,12 +40,10 @@ test('makeICAConnectionAddress', t => {
});

test('parseAddress', t => {
t.throws(
() => parseAddress('/ibc-hop/'),
{
message: 'Error parsing address',
},
'throws when version json is missing',
t.is(
parseAddress('/ibc-hop/'),
undefined,
'returns undefined when version json is missing',
);
t.is(
parseAddress(
Expand Down
5 changes: 5 additions & 0 deletions packages/vats/src/core/types-ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ type WellKnownName = {
| 'reserve'
| 'psm'
| 'scaledPriceAuthority'
| 'stakeAtom' // test contract
| 'stakeBld' // test contract
| 'econCommitteeCharter'
| 'priceAggregator';
Expand All @@ -193,6 +194,7 @@ type WellKnownName = {
| 'provisionPool'
| 'reserve'
| 'reserveGovernor'
| 'stakeAtom' // test contract
| 'stakeBld' // test contract
| 'Pegasus';
oracleBrand: 'USD';
Expand Down Expand Up @@ -341,9 +343,12 @@ type ChainBootstrapSpaceT = {
* Vault Factory. ONLY FOR DISASTER RECOVERY
*/
instancePrivateArgs: Map<Instance, unknown>;
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/vats/src/orchestration').Orchestration;
pegasusConnections: import('@agoric/vats').NameHubKit;
pegasusConnectionsAdmin: import('@agoric/vats').NameAdmin;
priceAuthorityVat: Awaited<PriceAuthorityVat>;
Expand Down
2 changes: 2 additions & 0 deletions packages/vats/src/core/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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: {
Expand Down
4 changes: 2 additions & 2 deletions packages/vats/src/orchestration.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const trace = makeTracer('Orchestration');
*/

/**
* PowerStore is used so additional powers can be added on upgrade.
*
* @typedef {MapStore<
* keyof OrchestrationPowers,
* OrchestrationPowers[keyof OrchestrationPowers]
Expand Down Expand Up @@ -231,11 +233,9 @@ const prepareOrchestration = (zone, createChainAccount) =>
hostConnectionId,
controllerConnectionId,
);
// @ts-expect-error Fail does not satisfy possibly undefined
const chainAccount = createChainAccount(port, remoteConnAddr);

// await so we do not return a ChainAccount before it successfully instantiates
// @ts-expect-error Fail does not satisfy possibly undefined
await E(port)
.connect(remoteConnAddr, chainAccount.connectionHandler)
// XXX if we fail, should we close the port (if it was created in this flow)?
Expand Down

0 comments on commit cca6961

Please sign in to comment.