From e538811561737fd34c2a10fd4b077ca41afa68a5 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 30 Apr 2024 16:58:14 -0400 Subject: [PATCH] feat(orchestration): add queryBalance to stakeAtom - refs: #9042 --- .../test/bootstrapTests/test-orchestration.ts | 11 +++ .../scripts/orchestration/init-stakeAtom.js | 2 + .../src/examples/stakeAtom.contract.js | 12 ++- .../queryConnectionKit.js} | 4 +- .../src/exos/stakingAccountKit.js | 90 ++++++++++++++++--- .../src/proposals/start-stakeAtom.js | 9 +- packages/orchestration/src/service.js | 2 +- packages/orchestration/src/types.d.ts | 2 +- 8 files changed, 111 insertions(+), 21 deletions(-) rename packages/orchestration/src/{queryConnection.js => exos/queryConnectionKit.js} (96%) diff --git a/packages/boot/test/bootstrapTests/test-orchestration.ts b/packages/boot/test/bootstrapTests/test-orchestration.ts index 6c67392d6831..25f3b4396e6e 100644 --- a/packages/boot/test/bootstrapTests/test-orchestration.ts +++ b/packages/boot/test/bootstrapTests/test-orchestration.ts @@ -126,6 +126,17 @@ test.serial('stakeAtom - repl-style', async t => { const res = await EV(account).delegate('cosmosvaloper1test', atomAmount); t.is(res, 'Success', 'delegate returns Success'); + + const queryRes = await EV(account).queryBalance(); + t.deepEqual(queryRes, { amount: '0', denom: 'uatom' }); + + const queryUnknownDenom = + await EV(account).queryBalance('some-invalid-denom'); + t.deepEqual( + queryUnknownDenom, + { amount: '0', denom: 'some-invalid-denom' }, + 'queryBalance for unknown denom returns amount: 0', + ); }); test.serial('stakeAtom - smart wallet', async t => { diff --git a/packages/builders/scripts/orchestration/init-stakeAtom.js b/packages/builders/scripts/orchestration/init-stakeAtom.js index 9d96b937d01b..3ab10b627e21 100644 --- a/packages/builders/scripts/orchestration/init-stakeAtom.js +++ b/packages/builders/scripts/orchestration/init-stakeAtom.js @@ -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', @@ -21,6 +22,7 @@ export const defaultProposalBuilder = async ( }, hostConnectionId, controllerConnectionId, + bondDenom, }, ], }); diff --git a/packages/orchestration/src/examples/stakeAtom.contract.js b/packages/orchestration/src/examples/stakeAtom.contract.js index 614061cde5b4..3ac7b62de19a 100644 --- a/packages/orchestration/src/examples/stakeAtom.contract.js +++ b/packages/orchestration/src/examples/stakeAtom.contract.js @@ -20,6 +20,7 @@ const trace = makeTracer('StakeAtom'); * @typedef {{ * hostConnectionId: IBCConnectionID; * controllerConnectionId: IBCConnectionID; + * bondDenom: string; * }} StakeAtomTerms */ @@ -34,7 +35,9 @@ const trace = makeTracer('StakeAtom'); * @param {Baggage} baggage */ export const start = async (zcf, privateArgs, baggage) => { - const { hostConnectionId, controllerConnectionId } = zcf.getTerms(); + // TODO #9063 this roughly matches what we'll get from Chain.getChainInfo() + const { hostConnectionId, controllerConnectionId, bondDenom } = + zcf.getTerms(); const { orchestration, marshaller, storageNode } = privateArgs; const zone = makeDurableZone(baggage); @@ -52,12 +55,19 @@ export const start = async (zcf, privateArgs, baggage) => { hostConnectionId, controllerConnectionId, ); + // TODO #9063 reference existing QueryConn from `Chain` object versus creating + // a new one for every user + const queryConnection = await E(orchestration).createQueryConnection( + controllerConnectionId, + ); const accountAddress = await E(account).getAccountAddress(); trace('account address', accountAddress); const { holder, invitationMakers } = makeStakingAccountKit( account, storageNode, accountAddress, + queryConnection, + bondDenom, ); return { publicSubscribers: holder.getPublicTopics(), diff --git a/packages/orchestration/src/queryConnection.js b/packages/orchestration/src/exos/queryConnectionKit.js similarity index 96% rename from packages/orchestration/src/queryConnection.js rename to packages/orchestration/src/exos/queryConnectionKit.js index 16d3b492f670..314cfc3b1846 100644 --- a/packages/orchestration/src/queryConnection.js +++ b/packages/orchestration/src/exos/queryConnectionKit.js @@ -4,8 +4,8 @@ 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 { makeQueryPacket, parseQueryPacket } from './utils/packet.js'; -import { ConnectionHandlerI } from './typeGuards.js'; +import { makeQueryPacket, parseQueryPacket } from '../utils/packet.js'; +import { ConnectionHandlerI } from '../typeGuards.js'; /** * @import { Zone } from '@agoric/base-zone'; diff --git a/packages/orchestration/src/exos/stakingAccountKit.js b/packages/orchestration/src/exos/stakingAccountKit.js index 3df1c3019b2e..ed8fc03c8433 100644 --- a/packages/orchestration/src/exos/stakingAccountKit.js +++ b/packages/orchestration/src/exos/stakingAccountKit.js @@ -4,6 +4,12 @@ import { MsgDelegate, MsgDelegateResponse, } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; +import { + QueryBalanceRequest, + QueryBalanceResponse, +} from '@agoric/cosmic-proto/cosmos/bank/v1beta1/query.js'; +import { Any } from '@agoric/cosmic-proto/google/protobuf/any'; +import { RequestQuery } from '@agoric/cosmic-proto/tendermint/abci/types.js'; import { AmountShape } from '@agoric/ertp'; import { makeTracer } from '@agoric/internal'; import { UnguardedHelperI } from '@agoric/internal/src/typeGuards.js'; @@ -11,13 +17,12 @@ import { M, prepareExoClassKit } from '@agoric/vat-data'; import { TopicsRecordShape } from '@agoric/zoe/src/contractSupport/index.js'; import { decodeBase64 } from '@endo/base64'; import { E } from '@endo/far'; -import { Any } from '@agoric/cosmic-proto/google/protobuf/any'; /** - * @import { ChainAccount, ChainAddress } from '../types.js'; + * @import { ChainAccountKit, ChainAddress, CosmosValidatorAddress, QueryConnection } from '../types.js'; * @import { RecorderKit, MakeRecorderKit } from '@agoric/zoe/src/contractSupport/recorder.js'; * @import { Baggage } from '@agoric/swingset-liveslots'; - * @import {AnyJson} from '@agoric/cosmic-proto'; + * @import { AnyJson, RequestQueryJson } from '@agoric/cosmic-proto'; */ const trace = makeTracer('StakingAccountHolder'); @@ -31,17 +36,23 @@ const { Fail } = assert; /** * @typedef {{ * topicKit: RecorderKit; - * account: ChainAccount; - * chainAddress: string; + * account: ChainAccountKit['account']; + * chainAddress: ChainAddress['address']; + * queryConnection: QueryConnection; + * bondDenom: string; * }} State */ +const BalanceShape = { amount: M.string(), denom: M.string() }; + const HolderI = M.interface('holder', { getPublicTopics: M.call().returns(TopicsRecordShape), makeDelegateInvitation: M.call(M.string(), AmountShape).returns(M.promise()), makeCloseAccountInvitation: M.call().returns(M.promise()), makeTransferAccountInvitation: M.call().returns(M.promise()), delegate: M.callWhen(M.string(), AmountShape).returns(M.string()), + queryBalance: M.callWhen().optional(M.string()).returns(BalanceShape), + getAccountAddress: M.call().returns(M.string()), }); /** @type {{ [name: string]: [description: string, valueShape: Pattern] }} */ @@ -69,16 +80,18 @@ export const prepareStakingAccountKit = (baggage, makeRecorderKit, zcf) => { }), }, /** - * @param {ChainAccount} account + * @param {ChainAccountKit['account']} account * @param {StorageNode} storageNode - * @param {string} chainAddress + * @param {ChainAddress['address']} chainAddress + * @param {QueryConnection} queryConnection + * @param {string} bondDenom e.g. 'uatom' * @returns {State} */ - (account, storageNode, chainAddress) => { + (account, storageNode, chainAddress, queryConnection, bondDenom) => { // must be the fully synchronous maker because the kit is held in durable state const topicKit = makeRecorderKit(storageNode, PUBLIC_TOPICS.account[1]); - return { account, chainAddress, topicKit }; + return { account, chainAddress, topicKit, queryConnection, bondDenom }; }, { helper: { @@ -95,17 +108,49 @@ export const prepareStakingAccountKit = (baggage, makeRecorderKit, zcf) => { }, // TODO move this beneath the Orchestration abstraction, // to the OrchestrationAccount provided by createAccount() + /** + * @param {string} [denom] - defaults to bondDenom + * @returns {Promise<{ amount: string; denom: string; }>} + */ + async queryBalance(denom) { + const { chainAddress, queryConnection, bondDenom } = this.state; + + denom ||= bondDenom; + + const [result] = await E(queryConnection).query([ + /** @type {RequestQueryJson} */ ( + RequestQuery.toJSON( + RequestQuery.fromPartial({ + path: '/cosmos.bank.v1beta1.Query/Balance', + data: QueryBalanceRequest.encode( + QueryBalanceRequest.fromPartial({ + address: chainAddress, + denom, + }), + ).finish(), + }), + ) + ), + ]); + if (!result?.key) throw Fail`Error parsing result ${result}`; + const { balance } = QueryBalanceResponse.decode( + decodeBase64(result.key), + ); + if (!balance) throw Fail`Error parsing result ${result}`; + // TODO, return Amount? cast amount to bigint? #9211 + return balance; + }, /** * _Assumes users has already sent funds to their ICA, until #9193 - * @param {string} validatorAddress + * @param {CosmosValidatorAddress} validatorAddress * @param {Amount<'nat'>} ertpAmount */ async delegate(validatorAddress, ertpAmount) { - // FIXME get values from proposal or args + // FIXME get values from proposal or args #9211 // FIXME brand handling and amount scaling const amount = { amount: String(ertpAmount.value), - denom: 'uatom', + denom: this.state.bondDenom, // TODO use ertpAmount.brand #9211 }; const account = this.facets.helper.owned(); @@ -116,6 +161,7 @@ export const prepareStakingAccountKit = (baggage, makeRecorderKit, zcf) => { Any.toJSON( MsgDelegate.toProtoMsg({ delegatorAddress, + // @ts-expect-error Type 'CosmosValidatorAddress' is not assignable to type 'string'. validatorAddress, amount, }), @@ -134,6 +180,10 @@ export const prepareStakingAccountKit = (baggage, makeRecorderKit, zcf) => { }, }, invitationMakers: { + /** + * @param {CosmosValidatorAddress} validatorAddress + * @param {Amount<'nat'>} amount + */ Delegate(validatorAddress, amount) { return this.facets.holder.makeDelegateInvitation( validatorAddress, @@ -160,16 +210,28 @@ export const prepareStakingAccountKit = (baggage, makeRecorderKit, zcf) => { }, /** * - * @param {string} validatorAddress + * @param {CosmosValidatorAddress} validatorAddress * @param {Amount<'nat'>} ertpAmount */ async delegate(validatorAddress, ertpAmount) { trace('delegate', validatorAddress, ertpAmount); return this.facets.helper.delegate(validatorAddress, ertpAmount); }, + getAccountAddress() { + return this.state.chainAddress; + }, + /** + * @param {string} [denom] - defaults to bondDenom + * @returns {Promise<{ amount: string; denom: string; }>} + */ + async queryBalance(denom) { + denom ||= this.state.bondDenom; + trace('queryBalance', this.state.chainAddress, denom); + return this.facets.helper.queryBalance(denom); + }, /** * - * @param {string} validatorAddress + * @param {CosmosValidatorAddress} validatorAddress * @param {Amount<'nat'>} ertpAmount */ makeDelegateInvitation(validatorAddress, ertpAmount) { diff --git a/packages/orchestration/src/proposals/start-stakeAtom.js b/packages/orchestration/src/proposals/start-stakeAtom.js index 13aa78b15801..888765fd18ea 100644 --- a/packages/orchestration/src/proposals/start-stakeAtom.js +++ b/packages/orchestration/src/proposals/start-stakeAtom.js @@ -27,10 +27,14 @@ export const startStakeAtom = async ( produce: { stakeAtom: produceInstance }, }, }, - { options: { hostConnectionId, controllerConnectionId } }, + { options: { hostConnectionId, controllerConnectionId, bondDenom } }, ) => { const VSTORAGE_PATH = 'stakeAtom'; - trace('startStakeAtom', { hostConnectionId, controllerConnectionId }); + trace('startStakeAtom', { + hostConnectionId, + controllerConnectionId, + bondDenom, + }); await null; const storageNode = await makeStorageNodeChild(chainStorage, VSTORAGE_PATH); @@ -46,6 +50,7 @@ export const startStakeAtom = async ( terms: { hostConnectionId, controllerConnectionId, + bondDenom, }, privateArgs: { orchestration: await orchestration, diff --git a/packages/orchestration/src/service.js b/packages/orchestration/src/service.js index 9a2b0c458fb1..f3bfa87cac3a 100644 --- a/packages/orchestration/src/service.js +++ b/packages/orchestration/src/service.js @@ -9,7 +9,7 @@ import { PaymentShape, PurseShape } from '@agoric/ertp'; import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; import { Shape as NetworkShape } from '@agoric/network'; import { ConnectionHandlerI } from './typeGuards.js'; -import { prepareQueryConnectionKit } from './queryConnection.js'; +import { prepareQueryConnectionKit } from './exos/queryConnectionKit.js'; import { makeTxPacket, parseTxPacket } from './utils/packet.js'; import { makeICAChannelAddress, diff --git a/packages/orchestration/src/types.d.ts b/packages/orchestration/src/types.d.ts index 46d1895d43c7..9a7147a8d200 100644 --- a/packages/orchestration/src/types.d.ts +++ b/packages/orchestration/src/types.d.ts @@ -16,7 +16,7 @@ import type { export type * from './service.js'; export type * from './vat-orchestration.js'; export type * from './utils/packet.js'; -export type * from './queryConnection.js'; +export type * from './exos/queryConnectionKit.js'; /** * static declaration of known chain types will allow type support for