Skip to content

Commit

Permalink
feat(orchestration): add support for queries (icq/v1)
Browse files Browse the repository at this point in the history
- refs: Agoric#9072
  • Loading branch information
0xpatrickdev committed May 2, 2024
1 parent 9637194 commit ce0f0fd
Show file tree
Hide file tree
Showing 16 changed files with 637 additions and 80 deletions.
106 changes: 100 additions & 6 deletions packages/boot/test/bootstrapTests/test-vat-orchestration.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js';
import type { ExecutionContext, TestFn } from 'ava';

import type { AnyJson } from '@agoric/cosmic-proto';
import type { AnyJson, RequestQueryJson } from '@agoric/cosmic-proto';
import {
QueryBalanceRequest,
QueryBalanceResponse,
} from '@agoric/cosmic-proto/cosmos/bank/v1beta1/query.js';
import { Any } from '@agoric/cosmic-proto/google/protobuf/any';
import {
MsgDelegate,
MsgDelegateResponse,
} from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js';
import { Any } from '@agoric/cosmic-proto/google/protobuf/any';
import { RequestQuery } from '@agoric/cosmic-proto/tendermint/abci/types.js';
import type { OrchestrationService } from '@agoric/orchestration';
import { decodeBase64 } from '@endo/base64';
import { M, matches } from '@endo/patterns';
Expand All @@ -19,9 +24,9 @@ type DefaultTestContext = Awaited<ReturnType<typeof makeTestContext>>;
const test: TestFn<DefaultTestContext> = anyTest;

/**
* To update, pass the message into `makeTxPacket` from `@agoric/orchestration`,
* and paste the resulting `data` key into `protoMsgMocks` in
* [mocks.js](../../tools/ibc/mocks.js).
* To update, pass the message into `makeTxPacket` or `makeQueryPacket` from
* `@agoric/orchestration`, and paste the resulting `data` key into `protoMsgMocks`
* in [mocks.js](../../tools/ibc/mocks.js).
* If adding a new msg, reference the mock in the `sendPacket` switch statement
* in [supports.ts](../../tools/supports.ts).
*/
Expand All @@ -32,6 +37,17 @@ const delegateMsgSuccess = Any.toJSON(
amount: { denom: 'uatom', amount: '10' },
}),
) as AnyJson;
const balanceQuery = RequestQuery.toJSON(
RequestQuery.fromPartial({
path: '/cosmos.bank.v1beta1.Query/Balance',
data: QueryBalanceRequest.encode(
QueryBalanceRequest.fromPartial({
address: 'cosmos1test',
denom: 'uatom',
}),
).finish(),
}),
) as RequestQueryJson;

test.before(async t => {
t.context = await makeTestContext(t);
Expand Down Expand Up @@ -133,7 +149,7 @@ test('ICA connection can send msg with proto3', async t => {
// @ts-expect-error intentional
await t.throwsAsync(EV(account).executeEncodedTx('malformed'), {
message:
'In "executeEncodedTx" method of (ChainAccount account): arg 0: string "malformed" - Must be a copyArray',
'In "executeEncodedTx" method of (ChainAccountKit account): arg 0: string "malformed" - Must be a copyArray',
});

const txSuccess = await EV(account).executeEncodedTx([delegateMsgSuccess]);
Expand Down Expand Up @@ -173,3 +189,81 @@ test('ICA connection can send msg with proto3', async t => {
message: 'ABCI code: 5: error handling packet: see events for details',
});
});

test('Query connection can be created', async t => {
const {
runUtils: { EV },
} = t.context;

type Powers = { orchestration: OrchestrationService };
const contract = async ({ orchestration }: Powers) => {
const connection =
await EV(orchestration).createQueryConnection('connection-0');
t.log('Query Connection', connection);
t.truthy(connection, 'createQueryConnection returns a connection');
t.truthy(
matches(connection, M.remotable('QueryConnection')),
'QueryConnection is a remotable',
);
};

// core eval context
{
const orchestration: OrchestrationService =
await EV.vat('bootstrap').consumeItem('orchestration');
await contract({ orchestration });
}
});

test('Query connection can send a query', async t => {
const {
runUtils: { EV },
} = t.context;

type Powers = { orchestration: OrchestrationService };
const contract = async ({ orchestration }: { Powers }) => {
const queryConnection: QueryConnection =
await EV(orchestration).createQueryConnection('connection-0');

const [result] = await EV(queryConnection).query([balanceQuery]);
t.is(result.code, 0);
t.is(typeof result.height, 'bigint');
t.deepEqual(QueryBalanceResponse.decode(decodeBase64(result.key)), {
balance: {
amount: '0',
denom: 'uatom',
},
});

const results = await EV(queryConnection).query([
balanceQuery,
balanceQuery,
]);
t.is(results.length, 2);
for (const { key } of results) {
t.deepEqual(QueryBalanceResponse.decode(decodeBase64(key)), {
balance: {
amount: '0',
denom: 'uatom',
},
});
}

await t.throwsAsync(
EV(queryConnection).query([
{ ...balanceQuery, path: '/cosmos.bank.v1beta1.QueryBalanceRequest' },
]),
{
message: 'ABCI code: 4: error handling packet: see events for details',
},
'Use gRPC method to query, not protobuf typeUrl',
);
};

// core eval context
{
const orchestration: OrchestrationService =
await EV.vat('bootstrap').consumeItem('orchestration');
await contract({ orchestration });
}
});
51 changes: 41 additions & 10 deletions packages/boot/tools/ibc/mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,22 @@ const responses = {
// {"result":"+/cosmos.staking.v1beta1.MsgDelegateResponse"}
delegate:
'eyJyZXN1bHQiOiJFaTBLS3k5amIzTnRiM011YzNSaGEybHVaeTUyTVdKbGRHRXhMazF6WjBSbGJHVm5ZWFJsVW1WemNHOXVjMlU9In0=',
// XXX what does code 5 mean? are there other codes?
// '{"result":{"data":{"balance":{"amount":"0","denom":"uatom"}}}}'
queryBalance:
'eyJyZXN1bHQiOiJleUprWVhSaElqb2lRMmMwZVVSQmIwdERaMVl4V1ZoU2RtSlNTVUpOUVQwOUluMD0ifQ==',
// {"result":{"data":[{"balance":{"amount":"0","denom":"uatom"}},{"balance":{"amount":"0","denom":"uatom"}}]}}
queryBalanceMulti:
'eyJyZXN1bHQiOiJleUprWVhSaElqb2lRMmMwZVVSQmIwdERaMVl4V1ZoU2RtSlNTVUpOUVc5UFRXZDNTME5uYjBaa1YwWXdZakl3VTBGVVFUMGlmUT09In0=',
// '{"result":{"data":{"balance":{"amount":"0","denom":"some-invalid-denom"}}}}' (does not result in an error)
// eyJkYXRhIjoiQ2hzeUdRb1hDaEp6YjIxbExXbHVkbUZzYVdRdFpHVnViMjBTQVRBPSJ9
queryBalanceUnknownDenom:
'eyJyZXN1bHQiOiJleUprWVhSaElqb2lRMmh6ZVVkUmIxaERhRXA2WWpJeGJFeFhiSFZrYlVaellWZFJkRnBIVm5WaU1qQlRRVlJCUFNKOSJ9',
// {"error":"ABCI code: 4: error handling packet: see events for details"}
error4:
'eyJlcnJvciI6IkFCQ0kgY29kZTogNDogZXJyb3IgaGFuZGxpbmcgcGFja2V0OiBzZWUgZXZlbnRzIGZvciBkZXRhaWxzIn0=',
// XXX what does code 5 mean? are there other codes? I have observed 1, 4, 5, 7
// {"error":"ABCI code: 5: error handling packet: see events for details"}
error:
error5:
'eyJlcnJvciI6IkFCQ0kgY29kZTogNTogZXJyb3IgaGFuZGxpbmcgcGFja2V0OiBzZWUgZXZlbnRzIGZvciBkZXRhaWxzIn0=',
};

Expand All @@ -18,13 +31,33 @@ export const protoMsgMocks = {
msg: 'eyJ0eXBlIjoxLCJkYXRhIjoiQ2xVS0l5OWpiM050YjNNdWMzUmhhMmx1Wnk1Mk1XSmxkR0V4TGsxelowUmxiR1ZuWVhSbEVpNEtDMk52YzIxdmN6RjBaWE4wRWhKamIzTnRiM04yWVd4dmNHVnlNWFJsYzNRYUN3b0ZkV0YwYjIwU0FqRXciLCJtZW1vIjoiIn0=',
ack: responses.delegate,
},
// QueryBalanceRequest (/cosmos.bank.v1beta1.Query/Balance) of uatom for cosmos1test
queryBalance: {
msg: 'eyJ0eXBlIjoxLCJkYXRhIjoiQ2pvS0ZBb0xZMjl6Ylc5ek1YUmxjM1FTQlhWaGRHOXRFaUl2WTI5emJXOXpMbUpoYm1zdWRqRmlaWFJoTVM1UmRXVnllUzlDWVd4aGJtTmwiLCJtZW1vIjoiIn0=',
ack: responses.queryBalance,
},
// QueryBalanceRequest of uatom for cosmos1test, repeated twice
queryBalanceMulti: {
msg: 'eyJ0eXBlIjoxLCJkYXRhIjoiQ2pvS0ZBb0xZMjl6Ylc5ek1YUmxjM1FTQlhWaGRHOXRFaUl2WTI5emJXOXpMbUpoYm1zdWRqRmlaWFJoTVM1UmRXVnllUzlDWVd4aGJtTmxDam9LRkFvTFkyOXpiVzl6TVhSbGMzUVNCWFZoZEc5dEVpSXZZMjl6Ylc5ekxtSmhibXN1ZGpGaVpYUmhNUzVSZFdWeWVTOUNZV3hoYm1ObCIsIm1lbW8iOiIifQ==',
ack: responses.queryBalanceMulti,
},
// QueryBalanceRequest of 'some-invalid-denom' for cosmos1test
queryBalanceUnknownDenom: {
msg: 'eyJ0eXBlIjoxLCJkYXRhIjoiQ2tjS0lRb0xZMjl6Ylc5ek1YUmxjM1FTRW5OdmJXVXRhVzUyWVd4cFpDMWtaVzV2YlJJaUwyTnZjMjF2Y3k1aVlXNXJMbll4WW1WMFlURXVVWFZsY25rdlFtRnNZVzVqWlE9PSIsIm1lbW8iOiIifQ==',
ack: responses.queryBalanceUnknownDenom,
},
// Query for /cosmos.bank.v1beta1.QueryBalanceRequest
queryUnknownPath: {
msg: 'eyJ0eXBlIjoxLCJkYXRhIjoiQ2tBS0ZBb0xZMjl6Ylc5ek1YUmxjM1FTQlhWaGRHOXRFaWd2WTI5emJXOXpMbUpoYm1zdWRqRmlaWFJoTVM1UmRXVnllVUpoYkdGdVkyVlNaWEYxWlhOMCIsIm1lbW8iOiIifQ==',
ack: responses.error4,
},
// MsgDelegate 10uatom from cosmos1test to cosmosvaloper1test with memo: 'TESTING' and timeoutHeight: 1_000_000_000n
delegateWithOpts: {
msg: 'eyJ0eXBlIjoxLCJkYXRhIjoiQ2xVS0l5OWpiM050YjNNdWMzUmhhMmx1Wnk1Mk1XSmxkR0V4TGsxelowUmxiR1ZuWVhSbEVpNEtDMk52YzIxdmN6RjBaWE4wRWhKamIzTnRiM04yWVd4dmNHVnlNWFJsYzNRYUN3b0ZkV0YwYjIwU0FqRXdFZ2RVUlZOVVNVNUhHSUNVNjl3RCIsIm1lbW8iOiIifQ==',
ack: responses.delegate,
},
error: {
ack: responses.error,
ack: responses.error5,
},
};

Expand Down Expand Up @@ -60,15 +93,13 @@ export const icaMocks = {
* @returns {IBCEvent<'channelOpenAck'>}
*/
channelOpenAck: obj => {
// Fake a channel IDs from port suffixes. _Ports have no relation to channels._
// Fake a channel IDs from port suffixes. _Ports have no relation to channels, and hosts
// and controllers will likely have different channel IDs for the same channel._
const mocklID = Number(obj.packet.source_port.split('-').at(-1));
/** @type {IBCChannelID} */
const mockLocalChannelID = `channel-${Number(
obj?.packet?.source_port?.split('-')?.at(-1),
)}`;
const mockLocalChannelID = `channel-${mocklID}`;
/** @type {IBCChannelID} */
const mockRemoteChannelID = `channel-${Number(
obj?.packet?.destination_port?.split('-')?.at(-1),
)}`;
const mockRemoteChannelID = `channel-${mocklID}`;

return {
type: 'IBC_EVENT',
Expand Down
37 changes: 27 additions & 10 deletions packages/boot/tools/supports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,9 @@ export const makeSwingsetTestKit = async (

const makeAckEvent = (obj: IBCMethod<'sendPacket'>, ack: string) => {
ibcSequenceNonce += 1;
return icaMocks.ackPacket(obj, ibcSequenceNonce, ack);
const msg = icaMocks.ackPacket(obj, ibcSequenceNonce, ack);
inbound(BridgeId.DIBC, msg);
return msg.packet;
};
/**
* Mock the bridge outbound handler. The real one is implemented in Golang so
Expand Down Expand Up @@ -376,22 +378,37 @@ export const makeSwingsetTestKit = async (
case 'sendPacket':
switch (obj.packet.data) {
case protoMsgMocks.delegate.msg: {
const msg = makeAckEvent(obj, protoMsgMocks.delegate.ack);
inbound(BridgeId.DIBC, msg);
return msg.packet;
return makeAckEvent(obj, protoMsgMocks.delegate.ack);
}
case protoMsgMocks.delegateWithOpts.msg: {
const msg = makeAckEvent(
return makeAckEvent(
obj,
protoMsgMocks.delegateWithOpts.ack,
);
inbound(BridgeId.DIBC, msg);
return msg.packet;
}
case protoMsgMocks.queryBalance.msg: {
return makeAckEvent(obj, protoMsgMocks.queryBalance.ack);
}
case protoMsgMocks.queryUnknownPath.msg: {
return makeAckEvent(
obj,
protoMsgMocks.queryUnknownPath.ack,
);
}
case protoMsgMocks.queryBalanceMulti.msg: {
return makeAckEvent(
obj,
protoMsgMocks.queryBalanceMulti.ack,
);
}
case protoMsgMocks.queryBalanceUnknownDenom.msg: {
return makeAckEvent(
obj,
protoMsgMocks.queryBalanceUnknownDenom.ack,
);
}
default: {
const msg = makeAckEvent(obj, protoMsgMocks.error.ack);
inbound(BridgeId.DIBC, msg);
return msg.packet;
return makeAckEvent(obj, protoMsgMocks.error.ack);
}
}
default:
Expand Down
12 changes: 12 additions & 0 deletions packages/cosmic-proto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@
"./cosmos/staking/v1beta1/tx.js": {
"types": "./dist/codegen/cosmos/staking/v1beta1/tx.d.ts",
"default": "./dist/codegen/cosmos/staking/v1beta1/tx.js"
},
"./cosmos/bank/v1beta1/query.js": {
"types": "./dist/codegen/cosmos/bank/v1beta1/query.d.ts",
"default": "./dist/codegen/cosmos/bank/v1beta1/query.js"
},
"./icq/v1/packet.js": {
"types": "./dist/codegen/icq/v1/packet.d.ts",
"default": "./dist/codegen/icq/v1/packet.js"
},
"./tendermint/abci/types.js": {
"types": "./dist/codegen/tendermint/abci/types.d.ts",
"default": "./dist/codegen/tendermint/abci/types.js"
}
},
"main": "dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/orchestration/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// eslint-disable-next-line import/export
export * from './src/types.js';
export * from './src/utils/address.js';
export * from './src/service.js';
export * from './src/typeGuards.js';
34 changes: 11 additions & 23 deletions packages/orchestration/src/exos/chainAccountKit.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,35 @@
// @ts-check
/** @file Orchestration service */
import { NonNullish } from '@agoric/assert';
import { makeTracer } from '@agoric/internal';
/** @file ChainAccount exo */

// XXX ambient types runtime imports until https://github.com/Agoric/agoric-sdk/issues/6512
import '@agoric/network/exported.js';

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 { PaymentShape, PurseShape } from '@agoric/ertp';
import { InvitationShape } from '@agoric/zoe/src/typeGuards.js';
import { findAddressField } from '../utils/address.js';
import {
ConnectionHandlerI,
ChainAddressShape,
Proto3Shape,
} from '../typeGuards.js';
import { makeTxPacket, parseTxPacket } from '../utils/packet.js';

/**
* @import { Zone } from '@agoric/base-zone';
* @import { Connection, Port } from '@agoric/network';
* @import { Remote } from '@agoric/vow';
* @import { Zone } from '@agoric/base-zone';
* @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';
* @import { ChainAddress } from '../types.js';
*/

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

export const Proto3Shape = {
typeUrl: M.string(),
value: M.string(),
};

export const ChainAddressShape = {
address: M.string(),
chainId: M.string(),
addressEncoding: M.string(),
};
const trace = makeTracer('ChainAccountKit');

/** @typedef {'UNPARSABLE_CHAIN_ADDRESS'} UnparsableChainAddress */
const UNPARSABLE_CHAIN_ADDRESS = 'UNPARSABLE_CHAIN_ADDRESS';
Expand All @@ -55,16 +49,10 @@ export const ChainAccountI = M.interface('ChainAccount', {
prepareTransfer: M.callWhen().returns(InvitationShape),
});

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 {Zone} zone */
export const prepareChainAccountKit = zone =>
zone.exoClassKit(
'ChainAccount',
'ChainAccountKit',
{ account: ChainAccountI, connectionHandler: ConnectionHandlerI },
/**
* @param {Port} port
Expand Down
Loading

0 comments on commit ce0f0fd

Please sign in to comment.