Skip to content

Commit

Permalink
feat(orchestration): service.js returns unwrapped vows
Browse files Browse the repository at this point in the history
- refs: #9449
  • Loading branch information
0xpatrickdev committed Jun 5, 2024
1 parent 5d7e4b8 commit 4735d0d
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 61 deletions.
192 changes: 137 additions & 55 deletions packages/orchestration/src/service.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** @file Orchestration service */

import { V as E } from '@agoric/vow/vat.js';
import { E } from '@endo/far';
import { M } from '@endo/patterns';
import { Shape as NetworkShape } from '@agoric/network';
import { prepareChainAccountKit } from './exos/chainAccountKit.js';
Expand All @@ -13,10 +13,11 @@ import {
/**
* @import {Zone} from '@agoric/base-zone';
* @import {Remote} from '@agoric/internal';
* @import {Port, PortAllocator} from '@agoric/network';
* @import {Connection, Port, PortAllocator} from '@agoric/network';
* @import {IBCConnectionID} from '@agoric/vats';
* @import {RemoteIbcAddress} from '@agoric/vats/tools/ibc-utils.js';
* @import {VowTools} from '@agoric/vow';
* @import {ICQConnection, IcaAccount, ICQConnectionKit} from './types.js';
* @import {ICQConnection, IcaAccount, ICQConnectionKit, ChainAccountKit} from './types.js';
*/

const { Fail, bare } = assert;
Expand All @@ -37,9 +38,9 @@ const { Fail, bare } = assert;
* >} PowerStore
*/

/**
* @typedef {MapStore<IBCConnectionID, ICQConnectionKit>} ICQConnectionStore
*/
/** @typedef {MapStore<IBCConnectionID, ICQConnectionKit>} ICQConnectionStore */

/** @typedef {ChainAccountKit | ICQConnectionKit} ConnectionKit */

/**
* @template {keyof OrchestrationPowers} K
Expand All @@ -52,37 +53,63 @@ const getPower = (powers, name) => {
};

export const OrchestrationI = M.interface('Orchestration', {
makeAccount: M.callWhen(M.string(), M.string()).returns(
M.remotable('ChainAccount'),
),
provideICQConnection: M.callWhen(M.string()).returns(
M.remotable('Connection'),
makeAccount: M.call(M.string(), M.string()).returns(M.promise()),
provideICQConnection: M.call(M.string()).returns(
M.or(M.promise(), M.remotable('ICQConnection')),
),
});

const requestICAConnectionWatcherI = M.interface(
'requestICAConnectionWatcher',
{
onFulfilled: M.call(M.remotable('Port'))
.optional({ remoteConnAddr: M.string() })
.returns(NetworkShape.Vow$(NetworkShape.Connection)),
},
);
const requestICQConnectionWatcherI = M.interface(
'requestICQConnectionWatcher',
{
onFulfilled: M.call(M.remotable('Port'))
.optional({
remoteConnAddr: M.string(),
controllerConnectionId: M.string(),
})
.returns(NetworkShape.Vow$(NetworkShape.Connection)),
},
);
const channelOpenWatcherI = M.interface('channelOpenWatcher', {
/// XXX Vow.connection
onFulfilled: M.call(M.any())
.optional(
M.splitRecord(
{ connectionKit: M.record(), returnFacet: M.string() },
{ saveICQConnection: M.string() },
),
)
.returns(M.remotable('ConnectionKit holder facet')),
});

/** @typedef {{ powers: PowerStore; icqConnections: ICQConnectionStore }} OrchestrationState */

/**
* @param {Zone} zone
* @param {VowTools} vowTools
* @param {ReturnType<typeof prepareChainAccountKit>} makeChainAccountKit
* @param {ReturnType<typeof prepareICQConnectionKit>} makeICQConnectionKit
*/
const prepareOrchestrationKit = (
zone,
{ when, watch },
makeChainAccountKit,
makeICQConnectionKit,
) =>
zone.exoClassKit(
'Orchestration',
{
self: M.interface('OrchestrationSelf', {
allocateICAControllerPort: M.callWhen().returns(
NetworkShape.Vow$(NetworkShape.Port),
),
allocateICQControllerPort: M.callWhen().returns(
NetworkShape.Vow$(NetworkShape.Port),
),
}),
requestICAConnectionWatcher: requestICAConnectionWatcherI,
requestICQConnectionWatcher: requestICQConnectionWatcherI,
channelOpenWatcher: channelOpenWatcherI,
public: OrchestrationI,
},
/** @param {Partial<OrchestrationPowers>} [initialPowers] */
Expand All @@ -98,15 +125,72 @@ const prepareOrchestrationKit = (
return /** @type {OrchestrationState} */ ({ powers, icqConnections });
},
{
self: {
async allocateICAControllerPort() {
const portAllocator = getPower(this.state.powers, 'portAllocator');
return E(portAllocator).allocateICAControllerPort();
requestICAConnectionWatcher: {
/**
* @param {Port} port
* @param {{
* remoteConnAddr: RemoteIbcAddress;
* }} watchContext
*/
onFulfilled(port, { remoteConnAddr }) {
const chainAccountKit = makeChainAccountKit(port, remoteConnAddr);
return watch(
E(port).connect(remoteConnAddr, chainAccountKit.connectionHandler),
this.facets.channelOpenWatcher,
{ returnFacet: 'account', connectionKit: chainAccountKit },
);
},
async allocateICQControllerPort() {
const portAllocator = getPower(this.state.powers, 'portAllocator');
return E(portAllocator).allocateICQControllerPort();
},
requestICQConnectionWatcher: {
/**
* @param {Port} port
* @param {{
* remoteConnAddr: RemoteIbcAddress;
* controllerConnectionId: IBCConnectionID;
* }} watchContext
*/
onFulfilled(port, { remoteConnAddr, controllerConnectionId }) {
const connectionKit = makeICQConnectionKit(port);
/** @param {ICQConnectionKit} kit */
return watch(
E(port).connect(remoteConnAddr, connectionKit.connectionHandler),
this.facets.channelOpenWatcher,
{
connectionKit,
returnFacet: 'connection',
saveICQConnection: controllerConnectionId,
},
);
},
},
/**
* Waits for a channel (ICA, ICQ) to open and returns the consumer-facing
* facet of the ConnectionKit, specified by `returnFacet`. Saves the
* ConnectionKit if `saveICQConnection` is provided.
*/
channelOpenWatcher: {
/**
* @param {Connection} _connection
* @param {{
* connectionKit: ConnectionKit;
* returnFacet: string;
* saveICQConnection?: IBCConnectionID;
* }} watchContext
*/
onFulfilled(
_connection,
{ connectionKit, returnFacet, saveICQConnection },
) {
if (saveICQConnection) {
this.state.icqConnections.init(
saveICQConnection,
/** @type {ICQConnectionKit} */ (connectionKit),
);
}
return connectionKit[returnFacet];
},
// TODO #9317 if we fail, should we revoke the port (if it was created in this flow)?
// onRejected() {}
},
public: {
/**
Expand All @@ -115,50 +199,47 @@ const prepareOrchestrationKit = (
* @param {IBCConnectionID} controllerConnectionId self connection_id
* @returns {Promise<IcaAccount>}
*/
async makeAccount(hostConnectionId, controllerConnectionId) {
const port = await this.facets.self.allocateICAControllerPort();

makeAccount(hostConnectionId, controllerConnectionId) {
const remoteConnAddr = makeICAChannelAddress(
hostConnectionId,
controllerConnectionId,
);
const chainAccountKit = makeChainAccountKit(port, remoteConnAddr);

// await so we do not return a ChainAccount before it successfully instantiates
await E(port).connect(
remoteConnAddr,
chainAccountKit.connectionHandler,
const portAllocator = getPower(this.state.powers, 'portAllocator');
return when(
watch(
E(portAllocator).allocateICAControllerPort(),
this.facets.requestICAConnectionWatcher,
{
remoteConnAddr,
},
),
);
// XXX if we fail, should we close the port (if it was created in this flow)?
return chainAccountKit.account;
},
/**
* @param {IBCConnectionID} controllerConnectionId
* @returns {Promise<ICQConnection>}
* @returns {ICQConnection | Promise<ICQConnection>}
*/
async provideICQConnection(controllerConnectionId) {
provideICQConnection(controllerConnectionId) {
if (this.state.icqConnections.has(controllerConnectionId)) {
return this.state.icqConnections.get(controllerConnectionId)
.connection;
}
// allocate a new Port for every Connection
// TODO #9317 optimize ICQ port allocation
const port = await this.facets.self.allocateICQControllerPort();
const remoteConnAddr = makeICQChannelAddress(controllerConnectionId);
const icqConnectionKit = makeICQConnectionKit(port);

// await so we do not return/save a ICQConnection before it successfully instantiates
await E(port).connect(
remoteConnAddr,
icqConnectionKit.connectionHandler,
const remoteConnAddr = harden(
makeICQChannelAddress(controllerConnectionId),
);

this.state.icqConnections.init(
controllerConnectionId,
icqConnectionKit,
const portAllocator = getPower(this.state.powers, 'portAllocator');
return when(
watch(
// allocate a new Port for every Connection
// TODO #9317 optimize ICQ port allocation
E(portAllocator).allocateICQControllerPort(),
this.facets.requestICQConnectionWatcher,
{
remoteConnAddr,
controllerConnectionId,
},
),
);

return icqConnectionKit.connection;
},
},
},
Expand All @@ -173,6 +254,7 @@ export const prepareOrchestrationTools = (zone, vowTools) => {
const makeICQConnectionKit = prepareICQConnectionKit(zone, vowTools);
const makeOrchestrationKit = prepareOrchestrationKit(
zone,
vowTools,
makeChainAccountKit,
makeICQConnectionKit,
);
Expand Down
67 changes: 61 additions & 6 deletions packages/orchestration/test/service.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js';
import { E } from '@endo/far';
import { toRequestQueryJson } from '@agoric/cosmic-proto';
import { QueryBalanceRequest } from '@agoric/cosmic-proto/cosmos/bank/v1beta1/query.js';
import { E } from '@endo/far';
import { MsgDelegate } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js';
import { Any } from '@agoric/cosmic-proto/google/protobuf/any.js';
import { commonSetup } from './supports.js';

test('makeICQConnection returns an ICQConnection', async t => {
const {
bootstrap: { orchestration },
} = await commonSetup(t);

const CONNECTION_ID = 'connection-0';
const CONTROLLER_CONNECTION_ID = 'connection-0';

const icqConnection =
await E(orchestration).provideICQConnection(CONNECTION_ID);
const icqConnection = await E(orchestration).provideICQConnection(
CONTROLLER_CONNECTION_ID,
);
const [localAddr, remoteAddr] = await Promise.all([
E(icqConnection).getLocalAddress(),
E(icqConnection).getRemoteAddress(),
Expand All @@ -24,7 +27,7 @@ test('makeICQConnection returns an ICQConnection', async t => {
t.regex(localAddr, /ibc-port\/icqcontroller-\d+/);
t.regex(
remoteAddr,
new RegExp(`/ibc-hop/${CONNECTION_ID}`),
new RegExp(`/ibc-hop/${CONTROLLER_CONNECTION_ID}`),
'remote address contains provided connectionId',
);
t.regex(
Expand All @@ -47,4 +50,56 @@ test('makeICQConnection returns an ICQConnection', async t => {
);
});

test.todo('makeAccount');
test('makeAccount returns a ChainAccount', async t => {
const {
bootstrap: { orchestration },
} = await commonSetup(t);

const HOST_CONNECTION_ID = 'connection-0';
const CONTROLLER_CONNECTION_ID = 'connection-1';

const account = await E(orchestration).makeAccount(
HOST_CONNECTION_ID,
CONTROLLER_CONNECTION_ID,
);
const [localAddr, remoteAddr, chainAddr] = await Promise.all([
E(account).getLocalAddress(),
E(account).getRemoteAddress(),
E(account).getAddress(),
]);
t.log(account, {
localAddr,
remoteAddr,
chainAddr,
});
t.regex(localAddr, /ibc-port\/icacontroller-\d+/);
t.regex(
remoteAddr,
new RegExp(`/ibc-hop/${CONTROLLER_CONNECTION_ID}`),
'remote address contains provided connectionId',
);
t.regex(
remoteAddr,
/icahost\/ordered/,
'remote address contains icahost port, ordered ordering',
);
t.regex(
remoteAddr,
/"version":"ics27-1"(.*)"encoding":"proto3"/,
'remote address contains version and encoding in version string',
);

await t.throwsAsync(
E(account).executeEncodedTx([
Any.toJSON(
MsgDelegate.toProtoMsg({
delegatorAddress: 'cosmos1test',
validatorAddress: 'cosmosvaloper1test',
amount: { denom: 'uatom', amount: '10' },
}),
),
]),
{ message: /"type":1(.*)"data":"(.*)"memo":""/ },
'TODO do not use echo connection',
);
});

0 comments on commit 4735d0d

Please sign in to comment.