Skip to content

Commit

Permalink
feat(orchestration): create ChainAccount
Browse files Browse the repository at this point in the history
  • Loading branch information
0xpatrickdev committed Apr 4, 2024
1 parent 39b73d5 commit ba75ed6
Show file tree
Hide file tree
Showing 9 changed files with 480 additions and 1 deletion.
89 changes: 89 additions & 0 deletions packages/boot/test/bootstrapTests/test-vat-orchestration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js';
import type { ExecutionContext, TestFn } from 'ava';
import { M, matches } from '@endo/patterns';
import { makeWalletFactoryContext } from './walletFactory.ts';

const makeTestContext = async (t: ExecutionContext) =>
makeWalletFactoryContext(t);

type DefaultTestContext = Awaited<ReturnType<typeof makeTestContext>>;
const test: TestFn<DefaultTestContext> = anyTest;

test.before(async t => {
t.context = await makeTestContext(t);

async function setupDeps() {
const {
buildProposal,
evalProposal,
runUtils: { EV },
} = t.context;
/** ensure network, ibc, and orchestration are available */
await evalProposal(
buildProposal('@agoric/builders/scripts/vats/init-network.js'),
);
await evalProposal(
buildProposal('@agoric/builders/scripts/vats/init-orchestration.js'),
);
const vatStore = await EV.vat('bootstrap').consumeItem('vatStore');
t.true(await EV(vatStore).has('ibc'), 'ibc');
t.true(await EV(vatStore).has('network'), 'network');
t.true(await EV(vatStore).has('orchestration'), 'orchestration');
}

await setupDeps();
});

test.after.always(t => t.context.shutdown?.());

test('createAccount returns an ICA connection', async t => {
const {
runUtils: { EV },
} = t.context;

const orchestration = await EV.vat('bootstrap').consumeItem('orchestration');

const account = await EV(orchestration).createAccount(
'connection-0',
'connection-0',
);
t.truthy(account, 'createAccount returns an account');
t.truthy(
matches(account, M.remotable('ChainAccount')),
'account is a remotable',
);
const [remoteAddress, localAddress, accountAddress, port] = await Promise.all(
[
EV(account).getRemoteAddress(),
EV(account).getLocalAddress(),
EV(account).getAccountAddress(),
EV(account).getPort(),
],
);
t.regex(remoteAddress, /icahost/);
t.regex(localAddress, /icacontroller/);
t.regex(accountAddress, /osmo1/);
t.truthy(matches(port, M.remotable('Port')));
t.log('ICA Account Addresses', {
remoteAddress,
localAddress,
accountAddress,
});
});

test('ICA connection can be closed', async t => {
const {
runUtils: { EV },
} = t.context;

const orchestration = await EV.vat('bootstrap').consumeItem('orchestration');

const account = await EV(orchestration).createAccount(
'connection-0',
'connection-0',
);
t.truthy(account, 'createAccount returns an account');

const res = await EV(account).close();
t.is(res, 'Connection closed');
});
20 changes: 20 additions & 0 deletions packages/builders/scripts/vats/init-orchestration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { makeHelpers } from '@agoric/deploy-script-support';

/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').ProposalBuilder} */
export const defaultProposalBuilder = async ({ publishRef, install }) =>
harden({
sourceSpec: '@agoric/orchestration/src/proposals/orchestration-proposal.js',
getManifestCall: [
'getManifestForOrchestration',
{
orchestrationRef: publishRef(
install('@agoric/orchestration/src/vat-orchestration.js'),
),
},
],
});

export default async (homeP, endowments) => {
const { writeCoreProposal } = await makeHelpers(homeP, endowments);
await writeCoreProposal('gov-orchestration', defaultProposalBuilder);
};
1 change: 1 addition & 0 deletions packages/orchestration/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// eslint-disable-next-line import/export
export * from './src/types.js';
export * from './src/utils/address.js';
export * from './src/orchestration.js';
6 changes: 5 additions & 1 deletion packages/orchestration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@
},
"homepage": "https://github.com/Agoric/agoric-sdk#readme",
"dependencies": {
"@agoric/assert": "^0.6.0",
"@agoric/ertp": "^0.16.2",
"@agoric/internal": "^0.3.2",
"@agoric/network": "^0.1.0",
"@agoric/notifier": "^0.6.2",
"@agoric/store": "^0.9.2",
"@agoric/vat-data": "^0.5.2",
"@agoric/vats": "^0.15.1",
"@agoric/zoe": "^0.26.2",
Expand All @@ -41,9 +44,10 @@
"@endo/patterns": "^1.3.0"
},
"devDependencies": {
"@endo/ses-ava": "^1.2.0",
"@cosmjs/amino": "^0.32.3",
"@cosmjs/proto-signing": "^0.32.3",
"ava": "^5.3.0",
"ava": "^5.3.1",
"cosmjs-types": "^0.9.0"
},
"ava": {
Expand Down
236 changes: 236 additions & 0 deletions packages/orchestration/src/orchestration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
// @ts-check
/** @file Orchestration service */
import { NonNullish } from '@agoric/assert';
import { makeTracer } from '@agoric/internal';
import { V as E } from '@agoric/vat-data/vow.js';
import { M } from '@endo/patterns';
import { makeICAConnectionAddress, parseAddress } from './utils/address.js';
import '@agoric/network/exported.js';

const { Fail, bare } = assert;
const trace = makeTracer('Orchestration');

// TODO improve me
/** @typedef {string} ChainAddress */

/**
* @typedef {object} OrchestrationPowers
* @property {ERef<
* import('@agoric/orchestration/src/types').AttenuatedNetwork
* >} network
*/

/**
* PowerStore is used so additional powers can be added on upgrade. See
* [#7337](https://github.com/Agoric/agoric-sdk/issues/7337) for tracking on Exo
* state migrations.
*
* @typedef {MapStore<
* keyof OrchestrationPowers,
* OrchestrationPowers[keyof OrchestrationPowers]
* >} PowerStore
*/

/**
* @template {keyof OrchestrationPowers} K
* @param {PowerStore} powers
* @param {K} name
*/
const getPower = (powers, name) => {
powers.has(name) || Fail`need powers.${bare(name)} for this method`;
return /** @type {OrchestrationPowers[K]} */ (powers.get(name));
};

export const ChainAccountI = M.interface('ChainAccount', {
getAccountAddress: M.call().returns(M.string()),
getLocalAddress: M.call().returns(M.string()),
getRemoteAddress: M.call().returns(M.string()),
getPort: M.call().returns(M.remotable('Port')),
close: M.callWhen().returns(M.string()),
});

export const ConnectionHandlerI = M.interface('ConnectionHandler', {
onOpen: M.callWhen(M.any(), M.string(), M.string(), M.any()).returns(M.any()),
onClose: M.callWhen(M.any(), M.any(), M.any()).returns(M.any()),
onReceive: M.callWhen(M.any(), M.string()).returns(M.any()),
});

/** @param {import('@agoric/base-zone').Zone} zone */
const prepareChainAccount = zone =>
zone.exoClassKit(
'ChainAccount',
{ account: ChainAccountI, connectionHandler: ConnectionHandlerI },
/**
* @param {Port} port
* @param {string} requestedRemoteAddress
*/
(port, requestedRemoteAddress) =>
/**
* @type {{
* port: Port;
* connection: Connection | undefined;
* localAddress: string | undefined;
* requestedRemoteAddress: string;
* remoteAddress: string | undefined;
* accountAddress: ChainAddress | undefined;
* }}
*/ (
harden({
port,
connection: undefined,
requestedRemoteAddress,
remoteAddress: undefined,
accountAddress: undefined,
localAddress: undefined,
})
),
{
account: {
getAccountAddress() {
return NonNullish(
this.state.accountAddress,
'Error parsing account address from remote address',
);
},
getLocalAddress() {
return NonNullish(
this.state.localAddress,
'local address not available',
);
},
getRemoteAddress() {
return NonNullish(
this.state.remoteAddress,
'remote address not available',
);
},
getPort() {
return this.state.port;
},
async close() {
/// XXX what should the behavior be here? and `onClose`?
// - retrieve assets?
// - revoke the port?
const { connection } = this.state;
if (!connection) throw Fail`connection not available`;
await null;
try {
await E(connection).close();
} catch (e) {
throw Fail`Failed to close connection: ${e}`;
}
return 'Connection closed';
},
},
connectionHandler: {
/**
* @param {Connection} connection
* @param {string} localAddr
* @param {string} remoteAddr
* @param {ConnectionHandler} _connectionHandler
*/
async onOpen(connection, localAddr, remoteAddr, _connectionHandler) {
trace(`ICA Channel Opened for ${localAddr} at ${remoteAddr}`);
this.state.connection = connection;
this.state.remoteAddress = remoteAddr;
this.state.localAddress = localAddr;
// XXX parseAddress currently throws, should it return '' instead?
this.state.accountAddress = parseAddress(remoteAddr);
},
async onClose(_connection, reason, _connectionHandler) {
trace(`ICA Channel closed. Reason: ${reason}`);
// XXX handle connection closing
// XXX is there a scenario where a connection will unexpectedly close? _I think yes_
},
async onReceive(connection, bytes) {
trace(`ICA Channel onReceive`, connection, bytes);
return '';
},
},
},
);

export const OrchestrationI = M.interface('Orchestration', {
createAccount: M.callWhen(M.string(), M.string()).returns(
M.remotable('ChainAccount'),
),
});

/**
* @param {import('@agoric/base-zone').Zone} zone
* @param {ReturnType<typeof prepareChainAccount>} createChainAccount
*/
const prepareOrchestration = (zone, createChainAccount) =>
zone.exoClassKit(
'Orchestration',
{
self: M.interface('OrchestrationSelf', {
bindPort: M.callWhen().returns(M.remotable()),
}),
public: OrchestrationI,
},
/** @param {Partial<OrchestrationPowers>} [initialPowers] */
initialPowers => {
/** @type {PowerStore} */
const powers = zone.detached().mapStore('PowerStore');
if (initialPowers) {
for (const [name, power] of Object.entries(initialPowers)) {
powers.init(/** @type {keyof OrchestrationPowers} */ (name), power);
}
}
return { powers, icaControllerNonce: 0 };
},
{
self: {
async bindPort() {
const network = getPower(this.state.powers, 'network');
const port = await E(network)
.bind(`/ibc-port/icacontroller-${this.state.icaControllerNonce}`)
.catch(e => Fail`Failed to bind port: ${e}`);
this.state.icaControllerNonce += 1;
return port;
},
},
public: {
/**
* @param {import('@agoric/orchestration').ConnectionId} hostConnectionId
* the counterparty connection_id
* @param {import('@agoric/orchestration').ConnectionId} controllerConnectionId
* self connection_id
* @returns {Promise<ChainAccount>}
*/
async createAccount(hostConnectionId, controllerConnectionId) {
const port = await this.facets.self.bindPort();

const remoteConnAddr = makeICAConnectionAddress(
hostConnectionId,
controllerConnectionId,
);
const chainAccount = createChainAccount(port, remoteConnAddr);

// await so we do not return a ChainAccount before it successfully instantiates
await E(port)
.connect(remoteConnAddr, chainAccount.connectionHandler)
// XXX if we fail, should we close the port (if it was created in this flow)?
.catch(e => Fail`Failed to create ICA connection: ${bare(e)}`);

return chainAccount.account;
},
},
},
);

/** @param {import('@agoric/base-zone').Zone} zone */
export const prepareOrchestrationTools = zone => {
const createChainAccount = prepareChainAccount(zone);
const makeOrchestration = prepareOrchestration(zone, createChainAccount);

return harden({ makeOrchestration });
};
harden(prepareOrchestrationTools);

/** @typedef {ReturnType<ReturnType<typeof prepareChainAccount>>} ChainAccountKit */
/** @typedef {ChainAccountKit['account']} ChainAccount */
/** @typedef {ReturnType<typeof prepareOrchestrationTools>} OrchestrationTools */
/** @typedef {ReturnType<OrchestrationTools['makeOrchestration']>} OrchestrationKit */
/** @typedef {OrchestrationKit['public']} Orchestration */
Loading

0 comments on commit ba75ed6

Please sign in to comment.