Skip to content

Commit

Permalink
feat(orchestration): icq balance query (#9198)
Browse files Browse the repository at this point in the history
<!-- < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < ☺
v                               ✰  Thanks for creating a PR! ✰
☺ > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > >  -->

<!-- Most PRs should close a specific Issue. All PRs should at least
reference one or more Issues. Edit and/or delete the following lines as
appropriate (note: you don't need both `refs` and `closes` for the same
one): -->

closes: #9072
refs: #9042 
refs: #9162 
## Description

<!-- Add a description of the changes that this PR introduces and the
files that
are the most critical to review.
-->

- Binds an `icqcontroller-*` port and sends a query packet using
`CosmosQuery` (`/icq/v1/packet.js`) and `RequestQuery`
(`/tendermint/abci/types.js`).
- Adds `.queryBalance([denom])` method to `StakingAccountHolder` exo in
`StakeAtom` contract, using `QueryBalanceRequest`
(`/cosmos/bank/v1beta1/query.js`)
- adds `icq/v1` protos from
[cosmos/ibc-apps#8e64543](https://github.com/cosmos/ibc-apps/tree/18248c35e913b3ccba99a2756612b1fcb3370a0d/modules/async-icq/proto/icq/v1)
- adds `JsonSafe`, `typeUrlToGrpcPath`, and `toRequestQueryJson` helpers
to `@agoric/cosmic-proto`
- ensures we are using `ChainAddress` objects in all orchestration code
(#9162)

Able to confirm port creation and successful balances queries in e2e
testing:
<details>
<summary>relayer logs</summary>

```log
2024-04-05T01:57:06.907563Z  INFO ThreadId(29) worker.batch{chain=agoriclocal}:supervisor.handle_batch{chain=agoriclocal}:supervisor.process_batch{chain=agoriclocal}:worker.channel{channel=channel::channel-0/icqcontroller-0:agoriclocal->osmosis-test}: 🎊  osmosis-test => OpenTryChannel(OpenTry { port_id: icqhost, channel_id: channel-1, connection_id: connection-1, counterparty_port_id: icqcontroller-0, counterparty_channel_id: channel-0 }) at height 0-470
2024-04-05T01:57:06.907586Z  INFO ThreadId(29) worker.batch{chain=agoriclocal}:supervisor.handle_batch{chain=agoriclocal}:supervisor.process_batch{chain=agoriclocal}:worker.channel{channel=channel::channel-0/icqcontroller-0:agoriclocal->osmosis-test}: channel handshake step completed with events: OpenTryChannel(OpenTry { port_id: icqhost, channel_id: channel-1, connection_id: connection-1, counterparty_port_id: icqcontroller-0, counterparty_channel_id: channel-0 })
2024-04-05T01:57:11.647420Z  INFO ThreadId(48) worker.batch{chain=osmosis-test}:supervisor.handle_batch{chain=osmosis-test}:supervisor.process_batch{chain=osmosis-test}:worker.channel{channel=channel::channel-1/icqhost:osmosis-test->agoriclocal}: 🎊  agoriclocal => OpenAckChannel(OpenAck { port_id: icqcontroller-0, channel_id: channel-0, connection_id: connection-0, counterparty_port_id: icqhost, counterparty_channel_id: channel-1 }) at height 0-52
2024-04-05T01:57:11.647448Z  INFO ThreadId(48) worker.batch{chain=osmosis-test}:supervisor.handle_batch{chain=osmosis-test}:supervisor.process_batch{chain=osmosis-test}:worker.channel{channel=channel::channel-1/icqhost:osmosis-test->agoriclocal}: channel handshake step completed with events: OpenAckChannel(OpenAck { port_id: icqcontroller-0, channel_id: channel-0, connection_id: connection-0, counterparty_port_id: icqhost, counterparty_channel_id: channel-1 })
```
</details>

<img width="1557" alt="Screenshot 2024-04-19 at 4 47 17 PM"
src="https://github.com/Agoric/agoric-sdk/assets/11021913/d569bcfb-f0e0-43dc-8a0a-b87ae601a731">


### Security Considerations

<!-- Does this change introduce new assumptions or dependencies that, if
violated, could introduce security vulnerabilities? How does this PR
change the boundaries between mutually-suspicious components? What new
authorities are introduced by this change, perhaps by new API calls?
-->

### Scaling Considerations

<!-- Does this change require or encourage significant increase in
consumption of CPU cycles, RAM, on-chain storage, message exchanges, or
other scarce resources? If so, can that be prevented or mitigated? -->

### Documentation Considerations

<!-- Give our docs folks some hints about what needs to be described to
downstream users.

Backwards compatibility: what happens to existing data or deployments
when this code is shipped? Do we need to instruct users to do something
to upgrade their saved data? If there is no upgrade path possible, how
bad will that be for users?

-->

### Testing Considerations

<!-- Every PR should of course come with tests of its own functionality.
What additional tests are still needed beyond those unit tests? How does
this affect CI, other test automation, or the testnet?
-->

### Upgrade Considerations

<!-- What aspects of this PR are relevant to upgrading live production
systems, and how should they be addressed? -->
  • Loading branch information
mergify[bot] authored May 6, 2024
2 parents 286302a + 9f0ae09 commit 8e94b2a
Show file tree
Hide file tree
Showing 48 changed files with 2,256 additions and 323 deletions.
33 changes: 29 additions & 4 deletions packages/boot/test/bootstrapTests/test-orchestration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AmountMath } from '@agoric/ertp';
import type { start as stakeBldStart } from '@agoric/orchestration/src/examples/stakeBld.contract.js';
import type { Instance } from '@agoric/zoe/src/zoeService/utils.js';
import { M, matches } from '@endo/patterns';
import type { CosmosValidatorAddress } from '@agoric/orchestration';
import { makeWalletFactoryContext } from './walletFactory.ts';

type DefaultTestContext = Awaited<ReturnType<typeof makeWalletFactoryContext>>;
Expand Down Expand Up @@ -124,8 +125,21 @@ test.serial('stakeAtom - repl-style', async t => {
const atomBrand = await EV(agoricNames).lookup('brand', 'ATOM');
const atomAmount = AmountMath.make(atomBrand, 10n);

await t.notThrowsAsync(
EV(account).delegate('cosmosvaloper1test', atomAmount),
const validatorAddress: CosmosValidatorAddress = {
address: 'cosmosvaloper1test',
chainId: 'gaiatest',
addressEncoding: 'bech32',
};
await t.notThrowsAsync(EV(account).delegate(validatorAddress, atomAmount));

const queryRes = await EV(account).getBalance();
t.deepEqual(queryRes, { value: 0n, denom: 'uatom' });

const queryUnknownDenom = await EV(account).getBalance('some-invalid-denom');
t.deepEqual(
queryUnknownDenom,
{ value: 0n, denom: 'some-invalid-denom' },
'getBalance for unknown denom returns value: 0n',
);
});

Expand Down Expand Up @@ -156,6 +170,11 @@ test.serial('stakeAtom - smart wallet', async t => {

const { ATOM } = agoricNamesRemotes.brand;
ATOM || Fail`ATOM missing from agoricNames`;
const validatorAddress: CosmosValidatorAddress = {
address: 'cosmosvaloper1test',
chainId: 'gaiatest',
addressEncoding: 'bech32',
};

await t.notThrowsAsync(
wd.executeOffer({
Expand All @@ -164,7 +183,7 @@ test.serial('stakeAtom - smart wallet', async t => {
source: 'continuing',
previousOffer: 'request-account',
invitationMakerName: 'Delegate',
invitationArgs: ['cosmosvaloper1test', { brand: ATOM, value: 10n }],
invitationArgs: [validatorAddress, { brand: ATOM, value: 10n }],
},
proposal: {},
}),
Expand All @@ -173,14 +192,20 @@ test.serial('stakeAtom - smart wallet', async t => {
status: { id: 'request-delegate-success', numWantsSatisfied: 1 },
});

const validatorAddressFail: CosmosValidatorAddress = {
address: 'cosmosvaloper1fail',
chainId: 'gaiatest',
addressEncoding: 'bech32',
};

await t.throwsAsync(
wd.executeOffer({
id: 'request-delegate-fail',
invitationSpec: {
source: 'continuing',
previousOffer: 'request-account',
invitationMakerName: 'Delegate',
invitationArgs: ['cosmosvaloper1fail', { brand: ATOM, value: 10n }],
invitationArgs: [validatorAddressFail, { brand: ATOM, value: 10n }],
},
proposal: {},
}),
Expand Down
130 changes: 124 additions & 6 deletions packages/boot/test/bootstrapTests/test-vat-orchestration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ 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 { toRequestQueryJson } 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.js';
import {
MsgDelegate,
MsgDelegateResponse,
} from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js';
import { Any } from '@agoric/cosmic-proto/google/protobuf/any.js';
import type { OrchestrationService } from '@agoric/orchestration';
import type {
OrchestrationService,
ICQConnection,
} from '@agoric/orchestration';
import { decodeBase64 } from '@endo/base64';
import { M, matches } from '@endo/patterns';
import { makeWalletFactoryContext } from './walletFactory.ts';
Expand All @@ -19,9 +27,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 +40,12 @@ const delegateMsgSuccess = Any.toJSON(
amount: { denom: 'uatom', amount: '10' },
}),
) as AnyJson;
const balanceQuery = toRequestQueryJson(
QueryBalanceRequest.toProtoMsg({
address: 'cosmos1test',
denom: 'uatom',
}),
);

test.before(async t => {
t.context = await makeTestContext(t);
Expand Down Expand Up @@ -133,7 +147,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 +187,107 @@ 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).provideICQConnection('connection-0');
t.log('Query Connection', connection);
t.truthy(connection, 'provideICQConnection returns a connection');
t.truthy(
matches(connection, M.remotable('ICQConnection')),
'ICQConnection 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: ICQConnection =
await EV(orchestration).provideICQConnection('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 });
}
});

test('provideICQConnection is idempotent', async t => {
const {
runUtils: { EV },
} = t.context;
const orchestration: OrchestrationService =
await EV.vat('bootstrap').consumeItem('orchestration');

const queryConn0 =
await EV(orchestration).provideICQConnection('connection-0');
const queryConn1 =
await EV(orchestration).provideICQConnection('connection-1');
const queryConn02 =
await EV(orchestration).provideICQConnection('connection-0');

const [addr0, addr1, addr02] = await Promise.all([
EV(queryConn0).getRemoteAddress(),
EV(queryConn1).getRemoteAddress(),
EV(queryConn02).getRemoteAddress(),
]);
t.is(addr0, addr02);
t.not(addr0, addr1);

const [result] = await EV(queryConn02).query([balanceQuery]);
t.is(result.code, 0, 'ICQConnectionKit from MapStore state can send queries');
});
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: 'eyJkYXRhIjoiQ2pvS0ZBb0xZMjl6Ylc5ek1YUmxjM1FTQlhWaGRHOXRFaUl2WTI5emJXOXpMbUpoYm1zdWRqRmlaWFJoTVM1UmRXVnllUzlDWVd4aGJtTmwiLCJtZW1vIjoiIn0=',
ack: responses.queryBalance,
},
// QueryBalanceRequest of uatom for cosmos1test, repeated twice
queryBalanceMulti: {
msg: 'eyJkYXRhIjoiQ2pvS0ZBb0xZMjl6Ylc5ek1YUmxjM1FTQlhWaGRHOXRFaUl2WTI5emJXOXpMbUpoYm1zdWRqRmlaWFJoTVM1UmRXVnllUzlDWVd4aGJtTmxDam9LRkFvTFkyOXpiVzl6TVhSbGMzUVNCWFZoZEc5dEVpSXZZMjl6Ylc5ekxtSmhibXN1ZGpGaVpYUmhNUzVSZFdWeWVTOUNZV3hoYm1ObCIsIm1lbW8iOiIifQ==',
ack: responses.queryBalanceMulti,
},
// QueryBalanceRequest of 'some-invalid-denom' for cosmos1test
queryBalanceUnknownDenom: {
msg: 'eyJkYXRhIjoiQ2tjS0lRb0xZMjl6Ylc5ek1YUmxjM1FTRW5OdmJXVXRhVzUyWVd4cFpDMWtaVzV2YlJJaUwyTnZjMjF2Y3k1aVlXNXJMbll4WW1WMFlURXVVWFZsY25rdlFtRnNZVzVqWlE9PSIsIm1lbW8iOiIifQ==',
ack: responses.queryBalanceUnknownDenom,
},
// Query for /cosmos.bank.v1beta1.QueryBalanceRequest
queryUnknownPath: {
msg: 'eyJkYXRhIjoiQ2tBS0ZBb0xZMjl6Ylc5ek1YUmxjM1FTQlhWaGRHOXRFaWd2WTI5emJXOXpMbUpoYm1zdWRqRmlaWFJoTVM1UmRXVnllVUpoYkdGdVkyVlNaWEYxWlhOMCIsIm1lbW8iOiIifQ==',
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
2 changes: 2 additions & 0 deletions packages/builders/scripts/orchestration/init-stakeAtom.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const defaultProposalBuilder = async (
const {
hostConnectionId = 'connection-1',
controllerConnectionId = 'connection-0',
bondDenom = 'uatom',
} = options;
return harden({
sourceSpec: '@agoric/orchestration/src/proposals/start-stakeAtom.js',
Expand All @@ -21,6 +22,7 @@ export const defaultProposalBuilder = async (
},
hostConnectionId,
controllerConnectionId,
bondDenom,
},
],
});
Expand Down
Loading

0 comments on commit 8e94b2a

Please sign in to comment.