-
Notifications
You must be signed in to change notification settings - Fork 208
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(orchestration): create ChainAccount
- Loading branch information
1 parent
39b73d5
commit ba75ed6
Showing
9 changed files
with
480 additions
and
1 deletion.
There are no files selected for viewing
89 changes: 89 additions & 0 deletions
89
packages/boot/test/bootstrapTests/test-vat-orchestration.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 */ |
Oops, something went wrong.