Skip to content

Commit

Permalink
feat: create vat-orchestration
Browse files Browse the repository at this point in the history
  • Loading branch information
0xpatrickdev committed Apr 2, 2024
1 parent f48f5bb commit 9a1368f
Show file tree
Hide file tree
Showing 8 changed files with 478 additions and 0 deletions.
58 changes: 58 additions & 0 deletions packages/boot/test/bootstrapTests/test-vat-orchestration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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 type { start as stakeAtomStart } from '@agoric/orchestration/src/contracts/stakeAtom.contract.js';
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)));
test.after.always(t => t.context.shutdown?.());

test('createAccount returns an ICA connection', async t => {
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');

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] = await Promise.all([
EV(account).getRemoteAddress(),
EV(account).getLocalAddress(),
EV(account).getAccountAddress(),
]);
t.truthy(remoteAddress.includes('icahost'), 'remoteAddress is returned');
t.truthy(localAddress.includes('icacontroller'), 'localAddress is returned');
t.truthy(accountAddress.includes('osmo1'), 'accountAddress is returned');
t.log('ICA Account Addresses', {
remoteAddress,
localAddress,
accountAddress,
});
});
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/vats/src/proposals/orchestration-proposal.js',
getManifestCall: [
'getManifestForOrchestration',
{
orchestrationRef: publishRef(
install('@agoric/vats/src/vat-orchestration.js'),
),
},
],
});

export default async (homeP, endowments) => {
const { writeCoreProposal } = await makeHelpers(homeP, endowments);
await writeCoreProposal('gov-orchestration', defaultProposalBuilder);
};
6 changes: 6 additions & 0 deletions packages/orchestration/src/types.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
import type { RouterProtocol } from '@agoric/network/src/router';

export type ConnectionId = `connection-${number}`;

export type AttenuatedNetwork = {
bind: RouterProtocol['bind'];
};
6 changes: 6 additions & 0 deletions packages/orchestration/src/types.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
// @ts-check
import '@agoric/network/exported.js';
import '@agoric/vats/exported.js';
import '@agoric/zoe/exported.js';

export {};

/**
* // XXX FIXME, network/src/types.js' is not a module
* @typedef {import('@agoric/network/src/types').Port} Port
*/
1 change: 1 addition & 0 deletions packages/vats/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@agoric/internal": "^0.3.2",
"@agoric/network": "^0.1.0",
"@agoric/notifier": "^0.6.2",
"@agoric/orchestration": "^0.1.0",
"@agoric/store": "^0.9.2",
"@agoric/swingset-vat": "^0.32.2",
"@agoric/time": "^0.3.2",
Expand Down
268 changes: 268 additions & 0 deletions packages/vats/src/orchestration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
// @ts-check
/** @file Orchestration service */
import { makeTracer } from '@agoric/internal';
import { V as E } from '@agoric/vat-data/vow.js';
import { M, matches } from '@endo/patterns';
import { AmountShape, BrandShape } from '@agoric/ertp';
import { makeICAConnectionAddress, parseAddress } from '@agoric/orchestration';
import '@agoric/network/exported.js';

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

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

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

/**
* @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')),
executeTx: M.callWhen(M.arrayOf(M.record())).returns(M.any()),
executeEncodedTx: M.callWhen(M.string()).returns(M.any()),
deposit: M.callWhen(M.remotable('Payment'))
.optional(M.pattern())
.returns(AmountShape),
getPurse: M.callWhen(BrandShape).returns(M.remotable('VirtualPurse')),
close: M.callWhen().returns(M.promise()),
});

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.string()).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() {
const { accountAddress } = this.state;
accountAddress || Fail`account address not available`;
return accountAddress;
},
getLocalAddress() {
const { localAddress } = this.state;
localAddress || Fail`local address not available`;
return localAddress;
},
getRemoteAddress() {
const { remoteAddress } = this.state;
remoteAddress || Fail`remote address not available`;
return remoteAddress;
},
getPort() {
return this.state.port;
},
/** @param {Bytes} packetBytes */
async executeEncodedTx(packetBytes) {
const { connection } = this.state;
connection || Fail`connection not available`;
// @ts-expect-error Fail does not satisfy undefined
return E(connection).send(packetBytes);
},
/** @param {import('@agoric/vats/src/localchain').Proto3Jsonable[]} _msgs */
async executeTx(_msgs) {
// XXX encode to protobuf, and call this.executeEncodedTx(...)
Fail`executeTx not implemented yet`;
},
async deposit(_payment) {
// XXX deposit an ERTP payment to the remote account
Fail`deposit not implemented yet`;
},
async getPurse(_brand) {
Fail`getPurse not implemented yet`;
},
async close() {
const { connection } = this.state;
connection || Fail`connection not available`;
// XXX retrieve all assets first?
// XXX do we also revoke the port?
// @ts-expect-error Fail does not satisfy possibly undefined
return E(connection).close();
},
},
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) {
trace(`ICA Channel closed. Reason: ${reason}`);
// XXX handle connection closing
},
async onReceive(connection, bytes) {
trace(`ICA Channel onReceive`, connection, bytes);
return '';
},
},
},
);

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

/**
* @param {import('@agoric/base-zone').Zone} zone
* @param {ReturnType<typeof prepareChainAccount>} createChainAccount
*/
const prepareOrchestration = (zone, createChainAccount) =>
zone.exoClassKit(
'Orchestration',
{
admin: M.interface('OrchestrationAdmin', {
placeholder: M.call().returns(M.string()),
}),
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 };
},
{
admin: {
placeholder() {
return 'test';
},
},
public: {
/**
* @param {import('@agoric/orchestration').ConnectionId} hostConnectionId
* the counterparty connection_id
* @param {import('@agoric/orchestration').ConnectionId} controllerConnectionId
* self connection_id
* @param {Port} [currentPort] if a port is provided, it will be reused
* to create the account
* @returns {Promise<ChainAccount>}
*/
async createAccount(
hostConnectionId,
controllerConnectionId,
currentPort,
) {
await null;

let port;
if (!matches(currentPort, M.remotable('Port'))) {
if (currentPort) trace('Invalid Port provided, binding a new one.');
const network = getPower(this.state.powers, 'network');
port = await E(network)
.bind(`/ibc-port/icacontroller-${this.state.icaControllerNonce}`)
.catch(e => Fail`Failed to get remoteAddress: ${e}`);

this.state.icaControllerNonce += 1;
} else {
port = currentPort;
}
const remoteConnAddr = makeICAConnectionAddress(
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)?
.catch(e => Fail`Failed to create ICA connection: ${bare(e)}`);

return chainAccount.account;
},
/** @param {string} _chainName e.g. cosmos */
getChain(_chainName) {
Fail`getChain not implemented yet`;
},
},
},
);

/** @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['admin']} OrchestrationAdmin */
/** @typedef {OrchestrationKit['public']} Orchestration */
Loading

0 comments on commit 9a1368f

Please sign in to comment.