Skip to content

Commit

Permalink
feat: ChainAccountKit returns vows (#9562)
Browse files Browse the repository at this point in the history
refs: #9449

## Description

- More returning of vows, using `asVow` helper
- Adjusts **resumable** custom lint rules to ignore `onOpen` and `onClose` when they are properties of `connectionHandler`
- removes unused `onReceive` handler or connectionHandler for `chainAccountKit` and `icqConnectionKit`

### Security Considerations
none
### Scaling Considerations
none

### Documentation Considerations
none

### Testing Considerations
CI for now

### Upgrade Considerations
not yet deployed
  • Loading branch information
mergify[bot] committed Jun 26, 2024
2 parents 4390d8c + 9bc7afb commit 23e1921
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 68 deletions.
4 changes: 3 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ const deprecatedTerminology = Object.fromEntries(
*/
const resumable = [
{
selector: 'FunctionExpression[async=true]',
// all async function expressions, except `onOpen` and `onClose` when they are properties of `connectionHandler`
selector:
'FunctionExpression[async=true]:not(Property[key.name="connectionHandler"] > ObjectExpression > Property[key.name=/^(onOpen|onClose)$/] > FunctionExpression[async=true])',
message: 'Non-immediate functions must return vows, not promises',
},
{
Expand Down
8 changes: 5 additions & 3 deletions packages/orchestration/src/examples/sendAnywhere.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,16 @@ export const start = async (zcf, privateArgs, baggage) => {
vowTools,
});

/** @type {{ account: OrchestrationAccount<any> | undefined }} */
const contractState = makeStateRecord({ account: undefined });
const contractState = makeStateRecord(
/** @type {{ account: OrchestrationAccount<any> | undefined }} */ {
account: undefined,
},
);

/** @type {OfferHandler} */
const sendIt = orchestrate(
'sendIt',
{ zcf, agoricNamesTools, contractState },
// eslint-disable-next-line no-shadow -- this `zcf` is enclosed in a membrane
sendItFn,
);

Expand Down
85 changes: 44 additions & 41 deletions packages/orchestration/src/exos/chain-account-kit.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/** @file ChainAccount exo */
import { NonNullish } from '@agoric/assert';
import { PurseShape } from '@agoric/ertp';
import { makeTracer } from '@agoric/internal';
import { VowShape } from '@agoric/vow';
import { E } from '@endo/far';
import { M } from '@endo/patterns';
import {
ChainAddressShape,
ConnectionHandlerI,
OutboundConnectionHandlerI,
Proto3Shape,
} from '../typeGuards.js';
import { findAddressField } from '../utils/address.js';
Expand All @@ -15,7 +15,7 @@ import { makeTxPacket, parseTxPacket } from '../utils/packet.js';
/**
* @import {Zone} from '@agoric/base-zone';
* @import {Connection, Port} from '@agoric/network';
* @import {Remote, VowTools} from '@agoric/vow';
* @import {Remote, Vow, VowTools} from '@agoric/vow';
* @import {AnyJson} from '@agoric/cosmic-proto';
* @import {TxBody} from '@agoric/cosmic-proto/cosmos/tx/v1beta1/tx.js';
* @import {LocalIbcAddress, RemoteIbcAddress} from '@agoric/vats/tools/ibc-utils.js';
Expand All @@ -30,17 +30,17 @@ const UNPARSABLE_CHAIN_ADDRESS = 'UNPARSABLE_CHAIN_ADDRESS';

export const ChainAccountI = M.interface('ChainAccount', {
getAddress: M.call().returns(ChainAddressShape),
getBalance: M.callWhen(M.string()).returns(M.any()),
getBalances: M.callWhen().returns(M.any()),
getBalance: M.call(M.string()).returns(VowShape),
getBalances: M.call().returns(VowShape),
getLocalAddress: M.call().returns(M.string()),
getRemoteAddress: M.call().returns(M.string()),
getPort: M.call().returns(M.remotable('Port')),
executeTx: M.call(M.arrayOf(M.record())).returns(M.promise()),
executeTx: M.call(M.arrayOf(M.record())).returns(VowShape),
executeEncodedTx: M.call(M.arrayOf(Proto3Shape))
.optional(M.record())
.returns(M.promise()),
close: M.callWhen().returns(M.undefined()),
getPurse: M.callWhen().returns(PurseShape),
.returns(VowShape),
close: M.call().returns(VowShape),
getPurse: M.call().returns(VowShape),
});

/**
Expand All @@ -59,12 +59,12 @@ export const ChainAccountI = M.interface('ChainAccount', {
* @param {Zone} zone
* @param {VowTools} vowTools
*/
export const prepareChainAccountKit = (zone, { watch, when }) =>
export const prepareChainAccountKit = (zone, { watch, asVow }) =>
zone.exoClassKit(
'ChainAccountKit',
{
account: ChainAccountI,
connectionHandler: ConnectionHandlerI,
connectionHandler: OutboundConnectionHandlerI,
parseTxPacketWatcher: M.interface('ParseTxPacketWatcher', {
onFulfilled: M.call(M.string())
.optional(M.arrayOf(M.undefined())) // does not need watcherContext
Expand Down Expand Up @@ -103,11 +103,11 @@ export const prepareChainAccountKit = (zone, { watch, when }) =>
},
getBalance(_denom) {
// UNTIL https://github.com/Agoric/agoric-sdk/issues/9326
throw new Error('not yet implemented');
return asVow(() => Fail`'not yet implemented'`);
},
getBalances() {
// UNTIL https://github.com/Agoric/agoric-sdk/issues/9326
throw new Error('not yet implemented');
return asVow(() => Fail`'not yet implemented'`);
},
getLocalAddress() {
return NonNullish(
Expand All @@ -125,49 +125,52 @@ export const prepareChainAccountKit = (zone, { watch, when }) =>
return this.state.port;
},
executeTx() {
throw new Error('not yet implemented');
return asVow(() => Fail`'not yet implemented'`);
},
/**
* Submit a transaction on behalf of the remote account for execution on
* the remote chain.
*
* @param {AnyJson[]} msgs
* @param {Omit<TxBody, 'messages'>} [opts]
* @returns {Promise<string>} - base64 encoded bytes string. Can be
* decoded using the corresponding `Msg*Response` object.
* @returns {Vow<string>} - base64 encoded bytes string. Can be decoded
* using the corresponding `Msg*Response` object.
* @throws {Error} if packet fails to send or an error is returned
*/
async executeEncodedTx(msgs, opts) {
const { connection } = this.state;
// TODO #9281 do not throw synchronously when returning a promise; return a rejected Vow
/// see https://github.com/Agoric/agoric-sdk/pull/9454#discussion_r1626898694
if (!connection) throw Fail`connection not available`;
return when(
watch(
executeEncodedTx(msgs, opts) {
return asVow(() => {
const { connection } = this.state;
if (!connection) throw Fail`connection not available`;
return watch(
E(connection).send(makeTxPacket(msgs, opts)),
this.facets.parseTxPacketWatcher,
),
);
);
});
},
/** Close the remote account */
async close() {
/// TODO #9192 what should the behavior be here? and `onClose`?
// - retrieve assets?
// - revoke the port?
const { connection } = this.state;
// TODO #9281 do not throw synchronously when returning a promise; return a rejected Vow
/// see https://github.com/Agoric/agoric-sdk/pull/9454#discussion_r1626898694
if (!connection) throw Fail`connection not available`;
return when(watch(E(connection).close()));
/**
* Close the remote account
*
* @returns {Vow<void>}
* @throws {Error} if connection is not available or already closed
*/
close() {
return asVow(() => {
/// TODO #9192 what should the behavior be here? and `onClose`?
// - retrieve assets?
// - revoke the port?
const { connection } = this.state;
if (!connection) throw Fail`connection not available`;
return E(connection).close();
});
},
/**
* get Purse for a brand to .withdraw() a Payment from the account
*
* @param {Brand} brand
*/
async getPurse(brand) {
getPurse(brand) {
console.log('getPurse got', brand);
throw new Error('not yet implemented');
return asVow(() => Fail`'not yet implemented'`);
},
},
connectionHandler: {
Expand All @@ -189,15 +192,15 @@ export const prepareChainAccountKit = (zone, { watch, when }) =>
addressEncoding: 'bech32',
});
},
/**
* @param {Remote<Connection>} _connection
* @param {unknown} reason
*/
async onClose(_connection, reason) {
trace(`ICA Channel closed. Reason: ${reason}`);
// FIXME handle connection closing https://github.com/Agoric/agoric-sdk/issues/9192
// 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 '';
},
},
},
);
Expand Down
8 changes: 2 additions & 6 deletions packages/orchestration/src/exos/icq-connection-kit.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { makeTracer } from '@agoric/internal';
import { E } from '@endo/far';
import { M } from '@endo/patterns';
import { makeQueryPacket, parseQueryPacket } from '../utils/packet.js';
import { ConnectionHandlerI } from '../typeGuards.js';
import { OutboundConnectionHandlerI } from '../typeGuards.js';

/**
* @import {Zone} from '@agoric/base-zone';
Expand Down Expand Up @@ -59,7 +59,7 @@ export const prepareICQConnectionKit = (zone, { watch, when }) =>
'ICQConnectionKit',
{
connection: ICQConnectionI,
connectionHandler: ConnectionHandlerI,
connectionHandler: OutboundConnectionHandlerI,
parseQueryPacketWatcher: M.interface('ParseQueryPacketWatcher', {
onFulfilled: M.call(M.string())
.optional(M.arrayOf(M.undefined())) // does not need watcherContext
Expand Down Expand Up @@ -127,10 +127,6 @@ export const prepareICQConnectionKit = (zone, { watch, when }) =>
async onClose(_connection, reason) {
trace(`ICQ Channel closed. Reason: ${reason}`);
},
async onReceive(connection, bytes) {
trace(`ICQ Channel onReceive`, connection, bytes);
return '';
},
},
},
);
Expand Down
19 changes: 14 additions & 5 deletions packages/orchestration/src/typeGuards.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@ import { AmountShape } from '@agoric/ertp';
import { VowShape } from '@agoric/vow';
import { M } from '@endo/patterns';

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()),
});
/**
* Used for IBC Channel Connections that only send outgoing transactions. If
* your channel expects incoming transactions, please extend this interface to
* include the `onReceive` handler.
*/
export const OutboundConnectionHandlerI = M.interface(
'OutboundConnectionHandler',
{
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()),
},
);

export const ChainAddressShape = {
address: M.string(),
Expand Down
23 changes: 13 additions & 10 deletions packages/orchestration/test/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { QueryBalanceRequest } from '@agoric/cosmic-proto/cosmos/bank/v1beta1/qu
import { MsgDelegate } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js';
import { Any } from '@agoric/cosmic-proto/google/protobuf/any.js';
import { matches } from '@endo/patterns';
import { heapVowTools } from '@agoric/vow/vat.js';
import { commonSetup } from './supports.js';
import { ChainAddressShape } from '../src/typeGuards.js';

Expand Down Expand Up @@ -45,14 +46,16 @@ test('makeICQConnection returns an ICQConnection', async t => {
t.is(localAddr, localAddr2, 'provideICQConnection is idempotent');

await t.throwsAsync(
E(icqConnection).query([
toRequestQueryJson(
QueryBalanceRequest.toProtoMsg({
address: 'cosmos1test',
denom: 'uatom',
}),
),
]),
heapVowTools.when(
E(icqConnection).query([
toRequestQueryJson(
QueryBalanceRequest.toProtoMsg({
address: 'cosmos1test',
denom: 'uatom',
}),
),
]),
),
{ message: /"data":"(.*)"memo":""/ },
'TODO do not use echo connection',
);
Expand Down Expand Up @@ -114,14 +117,14 @@ test('makeAccount returns a ChainAccount', async t => {
}),
);
await t.throwsAsync(
E(account).executeEncodedTx([delegateMsg]),
heapVowTools.when(E(account).executeEncodedTx([delegateMsg])),
{ message: /"type":1(.*)"data":"(.*)"memo":""/ },
'TODO do not use echo connection',
);

await E(account).close();
await t.throwsAsync(
E(account).executeEncodedTx([delegateMsg]),
heapVowTools.when(E(account).executeEncodedTx([delegateMsg])),
{
message: 'Connection closed',
},
Expand Down
4 changes: 2 additions & 2 deletions packages/vow/src/vow-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { M, matches } from '@endo/patterns';

/**
* @import {PassableCap} from '@endo/pass-style';
* @import {VowPayload, Vow} from './types.js';
* @import {VowPayload, Vow, PromiseVow} from './types.js';
* @import {MakeVowKit} from './vow.js';
*/

Expand Down Expand Up @@ -81,7 +81,7 @@ export const makeAsVow = makeVowKit => {
* Helper function that coerces the result of a function to a Vow. Helpful
* for scenarios like a synchronously thrown error.
* @template {any} T
* @param {(...args: any[]) => Vow<Awaited<T>> | Awaited<T>} fn
* @param {(...args: any[]) => Vow<Awaited<T>> | Awaited<T> | PromiseVow<T>} fn
* @returns {Vow<Awaited<T>>}
*/
const asVow = fn => {
Expand Down

0 comments on commit 23e1921

Please sign in to comment.