Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(orchestration): icq balance query #9198

Merged
merged 10 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

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
Loading