From 1ffc1bedfa53cf176913d195a4f16b95ae775f6e Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 22 Apr 2024 13:18:25 -0400 Subject: [PATCH] feat(stakeAtom): implement queryBalance --- .../test/bootstrapTests/test-orchestration.ts | 11 ++++ .../scripts/orchestration/init-stakeAtom.js | 2 + .../src/contracts/stakeAtom.contract.js | 11 +++- .../src/contracts/stakingAccountHolder.js | 59 +++++++++++++++++-- .../src/proposals/start-stakeAtom.js | 9 ++- 5 files changed, 85 insertions(+), 7 deletions(-) diff --git a/packages/boot/test/bootstrapTests/test-orchestration.ts b/packages/boot/test/bootstrapTests/test-orchestration.ts index a7d2cff9b55c..ebfdfc841823 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 306834f34abd..0d4f17b70424 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', + baseDenom = 'uatom', } = options; return harden({ sourceSpec: '@agoric/orchestration/src/proposals/start-stakeAtom.js', @@ -23,6 +24,7 @@ export const defaultProposalBuilder = async ( }, hostConnectionId, controllerConnectionId, + baseDenom, }, ], }); diff --git a/packages/orchestration/src/contracts/stakeAtom.contract.js b/packages/orchestration/src/contracts/stakeAtom.contract.js index e93b56385fc2..cc02e6b7f469 100644 --- a/packages/orchestration/src/contracts/stakeAtom.contract.js +++ b/packages/orchestration/src/contracts/stakeAtom.contract.js @@ -20,6 +20,7 @@ const trace = makeTracer('StakeAtom'); * @typedef {{ * hostConnectionId: IBCConnectionID; * controllerConnectionId: IBCConnectionID; + * baseDenom: string; * }} StakeAtomTerms */ @@ -34,7 +35,8 @@ const trace = makeTracer('StakeAtom'); * @param {Baggage} baggage */ export const start = async (zcf, privateArgs, baggage) => { - const { hostConnectionId, controllerConnectionId } = zcf.getTerms(); + const { hostConnectionId, controllerConnectionId, baseDenom } = + zcf.getTerms(); const { orchestration, marshaller, storageNode } = privateArgs; const zone = makeDurableZone(baggage); @@ -52,12 +54,19 @@ export const start = async (zcf, privateArgs, baggage) => { hostConnectionId, controllerConnectionId, ); + // TODO, 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 } = makeStakingAccountHolder( account, storageNode, accountAddress, + queryConnection, + baseDenom, ); return { publicSubscribers: holder.getPublicTopics(), diff --git a/packages/orchestration/src/contracts/stakingAccountHolder.js b/packages/orchestration/src/contracts/stakingAccountHolder.js index fb7ff5be7806..456a89d58590 100644 --- a/packages/orchestration/src/contracts/stakingAccountHolder.js +++ b/packages/orchestration/src/contracts/stakingAccountHolder.js @@ -4,6 +4,10 @@ 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 { AmountShape } from '@agoric/ertp'; import { makeTracer } from '@agoric/internal'; import { UnguardedHelperI } from '@agoric/internal/src/typeGuards.js'; @@ -11,7 +15,7 @@ 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 { txToBase64 } from '../utils/packet.js'; +import { queryToBase64, txToBase64 } from '../utils/packet.js'; /** * @import { ChainAccount, ChainAddress } from '../types.js'; @@ -32,6 +36,8 @@ const { Fail } = assert; * topicKit: RecorderKit; * account: ChainAccount; * chainAddress: ChainAddress; + * queryConnection: import('../queryConnection.js').QueryConnection; + * baseDenom: string; * }} State */ @@ -41,6 +47,8 @@ const HolderI = M.interface('holder', { 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(M.any()), + getAccountAddress: M.call().returns(M.string()), }); /** @type {{ [name: string]: [description: string, valueShape: Pattern] }} */ @@ -71,13 +79,15 @@ export const prepareStakingAccountHolder = (baggage, makeRecorderKit, zcf) => { * @param {ChainAccount} account * @param {StorageNode} storageNode * @param {ChainAddress} chainAddress + * @param {import('../queryConnection.js').QueryConnection} queryConnection + * @param {string} baseDenom e.g. 'uatom' * @returns {State} */ - (account, storageNode, chainAddress) => { + (account, storageNode, chainAddress, queryConnection, baseDenom) => { // 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, baseDenom }; }, { helper: { @@ -92,6 +102,32 @@ export const prepareStakingAccountHolder = (baggage, makeRecorderKit, zcf) => { getUpdater() { return this.state.topicKit.recorder; }, + /** + * @param {string} [denom] - defaults to baseDenom + * @returns {Promise<{ amount: string; denom: string; }>} + */ + async queryBalance(denom) { + const { chainAddress, queryConnection } = this.state; + + const [result] = await E(queryConnection).query([ + queryToBase64({ + path: '/cosmos.bank.v1beta1.Query/Balance', + data: QueryBalanceRequest.encode( + QueryBalanceRequest.fromPartial({ + address: chainAddress, + denom: denom || this.state.baseDenom, + }), + ).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? + return balance; + }, /** * _Assumes users has already sent funds to their ICA, until #9193 * @param {string} validatorAddress @@ -102,7 +138,7 @@ export const prepareStakingAccountHolder = (baggage, makeRecorderKit, zcf) => { // FIXME brand handling and amount scaling const amount = { amount: String(ertpAmount.value), - denom: 'uatom', + denom: this.state.baseDenom, // TODO use ertpAmount.brand }; const account = this.facets.helper.owned(); @@ -162,6 +198,21 @@ export const prepareStakingAccountHolder = (baggage, makeRecorderKit, zcf) => { trace('delegate', validatorAddress, ertpAmount); return this.facets.helper.delegate(validatorAddress, ertpAmount); }, + getAccountAddress() { + return this.state.chainAddress; + }, + /** + * @param {string} [denom] - defaults to baseDenom + * @returns {Promise<{ amount: string; denom: string; }>} + */ + async queryBalance(denom) { + trace( + 'queryBalance', + this.state.chainAddress, + denom || this.state.baseDenom, + ); + return this.facets.helper.queryBalance(denom); + }, /** * * @param {string} validatorAddress diff --git a/packages/orchestration/src/proposals/start-stakeAtom.js b/packages/orchestration/src/proposals/start-stakeAtom.js index c41e07b486da..5b33e807f2a1 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, baseDenom } }, ) => { const VSTORAGE_PATH = 'stakeAtom'; - trace('startStakeAtom', { hostConnectionId, controllerConnectionId }); + trace('startStakeAtom', { + hostConnectionId, + controllerConnectionId, + baseDenom, + }); await null; const storageNode = await makeStorageNodeChild(chainStorage, VSTORAGE_PATH); @@ -46,6 +50,7 @@ export const startStakeAtom = async ( terms: { hostConnectionId, controllerConnectionId, + baseDenom, }, privateArgs: { orchestration: await orchestration,