Skip to content

Commit

Permalink
feat: plumb ICQConnection through RemoteChainFacade
Browse files Browse the repository at this point in the history
- ensure CosmosOrchestrationAccountKit is given icqConnection if chainConfig has icqEnabled
- ensure Chain.query() sends queries if chainConfig has icqEnabled
- lazily provideICQConnection on first request and cache result in state
- refs: #9890
  • Loading branch information
0xpatrickdev committed Aug 21, 2024
1 parent 8878ada commit 26fb182
Show file tree
Hide file tree
Showing 7 changed files with 345 additions and 61 deletions.
15 changes: 15 additions & 0 deletions packages/orchestration/src/examples/basic-flows.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ const contract = async (
M.interface('Basic Flows PF', {
makeOrchAccountInvitation: M.callWhen().returns(InvitationShape),
makePortfolioAccountInvitation: M.callWhen().returns(InvitationShape),
makeSendICQQueryInvitation: M.callWhen().returns(InvitationShape),
makeAccountAndSendBalanceQueryInvitation:
M.callWhen().returns(InvitationShape),
}),
{
makeOrchAccountInvitation() {
Expand All @@ -54,6 +57,18 @@ const contract = async (
'Make an Orchestration Account',
);
},
makeSendICQQueryInvitation() {
return zcf.makeInvitation(
orchFns.sendQuery,
'Submit a query to a remote chain',
);
},
makeAccountAndSendBalanceQueryInvitation() {
return zcf.makeInvitation(
orchFns.makeAccountAndSendBalanceQuery,
'Make an account and submit a balance query',
);
},
},
);

Expand Down
59 changes: 57 additions & 2 deletions packages/orchestration/src/examples/basic-flows.flows.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
* @file Primarily a testing fixture, but also serves as an example of how to
* leverage basic functionality of the Orchestration API with async-flow.
*/
import { Fail } from '@endo/errors';
import { M, mustMatch } from '@endo/patterns';

/**
* @import {Zone} from '@agoric/zone';
* @import {OrchestrationAccount, OrchestrationFlow, Orchestrator} from '@agoric/orchestration';
* @import {DenomArg, OrchestrationAccount, OrchestrationFlow, Orchestrator} from '@agoric/orchestration';
* @import {ResolvedPublicTopic} from '@agoric/zoe/src/contractSupport/topics.js';
* @import {JsonSafe} from '@agoric/cosmic-proto';
* @import {RequestQuery} from '@agoric/cosmic-proto/tendermint/abci/types.js';
* @import {OrchestrationPowers} from '../utils/start-helper.js';
* @import {MakePortfolioHolder} from '../exos/portfolio-holder-kit.js';
* @import {OrchestrationTools} from '../utils/start-helper.js';
Expand Down Expand Up @@ -79,3 +81,56 @@ export const makePortfolioAccount = async (
return portfolioHolder.asContinuingOffer();
};
harden(makePortfolioAccount);

/**
* Send a query and get the response back in an offer result. This invitation is
* for testing only. In a real scenario it's better to use an RPC or API client
* and vstorage to retrieve data for a frontend. Queries should only be
* leveraged if contract logic requires it.
*
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
* @param {any} _ctx
* @param {ZCFSeat} seat
* @param {{ chainName: string; msgs: JsonSafe<RequestQuery>[] }} offerArgs
*/
export const sendQuery = async (orch, _ctx, seat, { chainName, msgs }) => {
seat.exit(); // no funds exchanged
mustMatch(chainName, M.string());
if (chainName === 'agoric') throw Fail`ICQ not supported on local chain`;
const remoteChain = await orch.getChain(chainName);
// @ts-expect-error FIXME implement Chain.query
const queryResponse = await remoteChain.query(msgs);
console.debug('sendQuery response:', queryResponse);
return queryResponse;
};
harden(sendQuery);

/**
* Create an account and send a query and get the response back in an offer
* result. Like `sendQuery`, this invitation is for testing only. In a real
* scenario it doesn't make much sense to send a query immediately after the
* account is created - it won't have any funds.
*
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
* @param {any} _ctx
* @param {ZCFSeat} seat
* @param {{ chainName: string; denom: DenomArg }} offerArgs
*/
export const makeAccountAndSendBalanceQuery = async (
orch,
_ctx,
seat,
{ chainName, denom },
) => {
seat.exit(); // no funds exchanged
mustMatch(chainName, M.string());
if (chainName === 'agoric') throw Fail`ICQ not supported on local chain`;
const remoteChain = await orch.getChain(chainName);
const orchAccount = await remoteChain.makeAccount();
const queryResponse = await orchAccount.getBalance(denom);
console.debug('getBalance response:', queryResponse);
return queryResponse;
};
harden(makeAccountAndSendBalanceQuery);
7 changes: 1 addition & 6 deletions packages/orchestration/src/exos/icq-connection-kit.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { M } from '@endo/patterns';
import { VowShape } from '@agoric/vow';
import { NonNullish, makeTracer } from '@agoric/internal';
import { makeQueryPacket, parseQueryPacket } from '../utils/packet.js';
import { OutboundConnectionHandlerI } from '../typeGuards.js';
import { ICQMsgShape, OutboundConnectionHandlerI } from '../typeGuards.js';

/**
* @import {Zone} from '@agoric/base-zone';
Expand All @@ -18,11 +18,6 @@ import { OutboundConnectionHandlerI } from '../typeGuards.js';

const trace = makeTracer('Orchestration:ICQConnection');

export const ICQMsgShape = M.splitRecord(
{ path: M.string(), data: M.string() },
{ height: M.string(), prove: M.boolean() },
);

export const ICQConnectionI = M.interface('ICQConnection', {
getLocalAddress: M.call().returns(M.string()),
getRemoteAddress: M.call().returns(M.string()),
Expand Down
4 changes: 4 additions & 0 deletions packages/orchestration/src/exos/local-chain-facade.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { M } from '@endo/patterns';
import { pickFacet } from '@agoric/vat-data';
import { VowShape } from '@agoric/vow';

import { Fail } from '@endo/errors';
import { chainFacadeMethods } from '../typeGuards.js';

/**
Expand Down Expand Up @@ -107,6 +108,9 @@ const prepareLocalChainFacadeKit = (
this.facets.makeAccountWatcher,
);
},
query() {
return asVow(() => Fail`not yet implemented`);
},
/** @type {HostOf<AgoricChainMethods['getVBankAssetInfo']>} */
getVBankAssetInfo() {
return asVow(() => {
Expand Down
165 changes: 116 additions & 49 deletions packages/orchestration/src/exos/remote-chain-facade.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,24 @@ import { E } from '@endo/far';
import { M } from '@endo/patterns';
import { pickFacet } from '@agoric/vat-data';
import { VowShape } from '@agoric/vow';
import { ChainAddressShape, ChainFacadeI } from '../typeGuards.js';
import { ChainAddressShape, ChainFacadeI, ICQMsgShape } from '../typeGuards.js';

/**
* @import {HostInterface, HostOf} from '@agoric/async-flow';
* @import {HostOf} from '@agoric/async-flow';
* @import {Zone} from '@agoric/base-zone';
* @import {JsonSafe} from '@agoric/cosmic-proto';
* @import {RequestQuery, ResponseQuery} from '@agoric/cosmic-proto/tendermint/abci/types.js';
* @import {TimerService} from '@agoric/time';
* @import {Remote} from '@agoric/internal';
* @import {Vow, VowTools} from '@agoric/vow';
* @import {CosmosInterchainService} from './cosmos-interchain-service.js';
* @import {prepareCosmosOrchestrationAccount} from './cosmos-orchestration-account.js';
* @import {ChainInfo, CosmosChainInfo, IBCConnectionInfo, OrchestrationAccount, ChainAddress, IcaAccount, Denom, Chain} from '../types.js';
* @import {CosmosChainInfo, IBCConnectionInfo, ChainAddress, IcaAccount, Chain, ICQConnection} from '../types.js';
*/

const { Fail } = assert;
const trace = makeTracer('RemoteChainFacade');

/** @type {any} */
const anyVal = null;

/**
* @typedef {{
* makeCosmosOrchestrationAccount: ReturnType<
Expand All @@ -35,6 +34,14 @@ const anyVal = null;
* }} RemoteChainFacadePowers
*/

/**
* @typedef {{
* remoteChainInfo: CosmosChainInfo;
* connectionInfo: IBCConnectionInfo;
* icqConnection: ICQConnection | undefined;
* }} RemoteChainFacadeState
*/

/**
* @param {Zone} zone
* @param {RemoteChainFacadePowers} powers
Expand All @@ -48,30 +55,38 @@ const prepareRemoteChainFacadeKit = (
// consider making an `accounts` childNode
storageNode,
timer,
vowTools: { asVow, watch },
vowTools: { allVows, asVow, watch },
},
) =>
zone.exoClassKit(
'RemoteChainFacade',
{
public: ChainFacadeI,
makeAccountWatcher: M.interface('makeAccountWatcher', {
onFulfilled: M.call(M.remotable())
.optional(M.arrayOf(M.undefined())) // empty context
.returns(VowShape),
}),
makeICQConnectionQueryWatcher: M.interface(
'makeICQConnectionQueryWatcher',
{
onFulfilled: M.call(M.remotable(), M.arrayOf(ICQMsgShape)).returns(
VowShape,
),
},
),
makeAccountAndProvideQueryConnWatcher: M.interface(
'makeAccountAndProvideQueryConnWatcher',
{
onFulfilled: M.call([
M.remotable(),
M.or(M.remotable(), M.undefined()),
]).returns(VowShape),
},
),
getAddressWatcher: M.interface('getAddressWatcher', {
onFulfilled: M.call(M.record())
.optional(M.remotable())
.returns(VowShape),
onFulfilled: M.call(ChainAddressShape, M.remotable()).returns(VowShape),
}),
makeChildNodeWatcher: M.interface('makeChildNodeWatcher', {
onFulfilled: M.call(M.remotable())
.optional({
account: M.remotable(),
chainAddress: ChainAddressShape,
})
.returns(M.remotable()),
onFulfilled: M.call(M.remotable(), {
account: M.remotable(),
chainAddress: ChainAddressShape,
}).returns(M.remotable()),
}),
},
/**
Expand All @@ -80,7 +95,11 @@ const prepareRemoteChainFacadeKit = (
*/
(remoteChainInfo, connectionInfo) => {
trace('making a RemoteChainFacade');
return { remoteChainInfo, connectionInfo };
return /** @type {RemoteChainFacadeState} */ ({
remoteChainInfo,
connectionInfo,
icqConnection: undefined,
});
},
{
public: {
Expand All @@ -94,35 +113,82 @@ const prepareRemoteChainFacadeKit = (
return asVow(() => {
const { remoteChainInfo, connectionInfo } = this.state;
const stakingDenom = remoteChainInfo.stakingTokens?.[0]?.denom;
if (!stakingDenom) {
throw Fail`chain info lacks staking denom`;
}
if (!stakingDenom) throw Fail`chain info lacks staking denom`;

// icqConnection is ultimately retrieved from state, but let's
// create a connection if it doesn't exist
const icqConnOrUndefinedV =
remoteChainInfo.icqEnabled && !this.state.icqConnection
? E(orchestration).provideICQConnection(
connectionInfo.counterparty.connection_id,
)
: undefined;

const makeAccountV = E(orchestration).makeAccount(
remoteChainInfo.chainId,
connectionInfo.counterparty.connection_id,
connectionInfo.id,
);

return watch(
E(orchestration).makeAccount(
remoteChainInfo.chainId,
connectionInfo.counterparty.connection_id,
connectionInfo.id,
),
this.facets.makeAccountWatcher,
allVows([makeAccountV, icqConnOrUndefinedV]),
this.facets.makeAccountAndProvideQueryConnWatcher,
);
});
},
/** @type {ICQConnection['query']} */
query(msgs) {
return asVow(() => {
const {
remoteChainInfo: { icqEnabled, chainId },
connectionInfo,
} = this.state;
if (!icqEnabled) {
throw Fail`Queries not available for chain ${chainId}`;
}
// if none exists, make one + still send the query in the handler
if (!this.state.icqConnection) {
return watch(
E(orchestration).provideICQConnection(
connectionInfo.counterparty.connection_id,
),
this.facets.makeICQConnectionQueryWatcher,
msgs,
);
}
return watch(E(this.state.icqConnection).query(msgs));
});
},
},
makeAccountWatcher: {
makeAccountAndProvideQueryConnWatcher: {
/**
* XXX Pipeline vows allVows and E
*
* @param {IcaAccount} account
* @param {[IcaAccount, ICQConnection | undefined]} account
*/
onFulfilled(account) {
onFulfilled([account, icqConnection]) {
if (icqConnection && !this.state.icqConnection) {
this.state.icqConnection = icqConnection;
// no need to pass icqConnection in ctx; we can get it from state
}
return watch(
E(account).getAddress(),
this.facets.getAddressWatcher,
account,
);
},
},
makeICQConnectionQueryWatcher: {
/**
* @param {ICQConnection} icqConnection
* @param {JsonSafe<RequestQuery>[]} msgs
* @returns {Vow<JsonSafe<ResponseQuery>[]>}
*/
onFulfilled(icqConnection, msgs) {
if (!this.state.icqConnection) {
this.state.icqConnection = icqConnection;
}
return watch(E(icqConnection).query(msgs));
},
},
getAddressWatcher: {
/**
* @param {ChainAddress} chainAddress
Expand All @@ -145,20 +211,21 @@ const prepareRemoteChainFacadeKit = (
* }} ctx
*/
onFulfilled(childNode, { account, chainAddress }) {
const { remoteChainInfo } = this.state;
const { remoteChainInfo, icqConnection } = this.state;
const stakingDenom = remoteChainInfo.stakingTokens?.[0]?.denom;
if (!stakingDenom) {
throw Fail`chain info lacks staking denom`;
}
return makeCosmosOrchestrationAccount(chainAddress, stakingDenom, {
account,
// FIXME storage path https://github.com/Agoric/agoric-sdk/issues/9066
storageNode: childNode,
// FIXME provide real ICQ connection
// FIXME make Query Connection available via chain, not orchestrationAccount
icqConnection: anyVal,
timer,
});

return makeCosmosOrchestrationAccount(
chainAddress,
// @ts-expect-error stakingDenom already asserted
stakingDenom,
{
account,
// FIXME storage path https://github.com/Agoric/agoric-sdk/issues/9066
storageNode: childNode,
icqConnection,
timer,
},
);
},
},
},
Expand Down
Loading

0 comments on commit 26fb182

Please sign in to comment.