forked from Agoric/agoric-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f48f5bb
commit 9a1368f
Showing
8 changed files
with
478 additions
and
0 deletions.
There are no files selected for viewing
58 changes: 58 additions & 0 deletions
58
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,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, | ||
}); | ||
}); |
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/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); | ||
}; |
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 +1,7 @@ | ||
import type { RouterProtocol } from '@agoric/network/src/router'; | ||
|
||
export type ConnectionId = `connection-${number}`; | ||
|
||
export type AttenuatedNetwork = { | ||
bind: RouterProtocol['bind']; | ||
}; |
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,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 | ||
*/ |
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,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 */ |
Oops, something went wrong.