Skip to content

Commit

Permalink
feat(orchestration): add queryBalance to stakeAtom
Browse files Browse the repository at this point in the history
- refs: #9042
  • Loading branch information
0xpatrickdev committed May 2, 2024
1 parent bf17425 commit c01f2f7
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 31 deletions.
34 changes: 31 additions & 3 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,24 @@ test.serial('stakeAtom - repl-style', async t => {
const atomBrand = await EV(agoricNames).lookup('brand', 'ATOM');
const atomAmount = AmountMath.make(atomBrand, 10n);

const res = await EV(account).delegate('cosmosvaloper1test', atomAmount);
const validatorAddress: CosmosValidatorAddress = {
address: 'cosmosvaloper1test',
chainId: 'gaiatest',
addressEncoding: 'bech32',
};
const res = await EV(account).delegate(validatorAddress, 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 => {
Expand Down Expand Up @@ -155,6 +172,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 @@ -163,7 +185,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 @@ -172,14 +194,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
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
19 changes: 13 additions & 6 deletions packages/orchestration/src/examples/stakeAtom.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ import { prepareStakingAccountKit } from '../exos/stakingAccountKit.js';

const trace = makeTracer('StakeAtom');
/**
* @import { OrchestrationService } from '../service.js'
* @import { Baggage } from '@agoric/vat-data';
* @import { IBCConnectionID } from '@agoric/vats';
* @import { ICQConnection, OrchestrationService } from '../types.js';
*/

/**
* @typedef {{
* hostConnectionId: IBCConnectionID;
* controllerConnectionId: IBCConnectionID;
* bondDenom: string;
* }} StakeAtomTerms
*/

Expand All @@ -30,13 +31,17 @@ const trace = makeTracer('StakeAtom');
* orchestration: OrchestrationService;
* storageNode: StorageNode;
* marshaller: Marshaller;
* icqConnection: ICQConnection
* }} privateArgs
* @param {Baggage} baggage
*/
export const start = async (zcf, privateArgs, baggage) => {
const { hostConnectionId, controllerConnectionId } = zcf.getTerms();
const { orchestration, marshaller, storageNode } = privateArgs;
// TODO #9063 this roughly matches what we'll get from Chain<C>.getChainInfo()
const { hostConnectionId, controllerConnectionId, bondDenom } =
zcf.getTerms();
const { orchestration, marshaller, storageNode, icqConnection } = privateArgs;

trace('@@@ICQCONN', icqConnection);
const zone = makeDurableZone(baggage);

const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller);
Expand All @@ -52,12 +57,14 @@ export const start = async (zcf, privateArgs, baggage) => {
hostConnectionId,
controllerConnectionId,
);
const address = await E(account).getAddress();
trace('chain address', address);
const accountAddress = await E(account).getAddress();
trace('account address', accountAddress);
const { holder, invitationMakers } = makeStakingAccountKit(
account,
storageNode,
address,
accountAddress,
icqConnection,
bondDenom,
);
return {
publicSubscribers: holder.getPublicTopics(),
Expand Down
102 changes: 84 additions & 18 deletions packages/orchestration/src/exos/stakingAccountKit.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,26 @@ 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.js';
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';
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.js';
import { ChainAddressShape } from '../typeGuards.js';

/**
* @import { ChainAccount, ChainAddress } from '../types.js';
* @import { ChainAccount, ChainAddress, CosmosValidatorAddress, ICQConnection } 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');
Expand All @@ -33,15 +39,21 @@ const { Fail } = assert;
* topicKit: RecorderKit<StakingAccountNotification>;
* account: ChainAccount;
* chainAddress: ChainAddress;
* icqConnection: ICQConnection;
* bondDenom: string;
* }} State
*/

const HolderI = M.interface('holder', {
export const BalanceShape = { amount: M.string(), denom: M.string() };

export const ChainAccountHolderI = M.interface('ChainAccountHolder', {
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()),
delegate: M.callWhen(ChainAddressShape, AmountShape).returns(M.string()),
queryBalance: M.callWhen().optional(M.string()).returns(BalanceShape),
getAddress: M.call().returns(ChainAddressShape),
});

/** @type {{ [name: string]: [description: string, valueShape: Pattern] }} */
Expand All @@ -60,25 +72,30 @@ export const prepareStakingAccountKit = (baggage, makeRecorderKit, zcf) => {
'Staking Account Holder',
{
helper: UnguardedHelperI,
holder: HolderI,
holder: ChainAccountHolderI,
invitationMakers: M.interface('invitationMakers', {
Delegate: HolderI.payload.methodGuards.makeDelegateInvitation,
CloseAccount: HolderI.payload.methodGuards.makeCloseAccountInvitation,
Delegate:
ChainAccountHolderI.payload.methodGuards.makeDelegateInvitation,
CloseAccount:
ChainAccountHolderI.payload.methodGuards.makeCloseAccountInvitation,
TransferAccount:
HolderI.payload.methodGuards.makeTransferAccountInvitation,
ChainAccountHolderI.payload.methodGuards
.makeTransferAccountInvitation,
}),
},
/**
* @param {ChainAccount} account
* @param {StorageNode} storageNode
* @param {ChainAddress} chainAddress
* @param {ICQConnection} icqConnection
* @param {string} bondDenom e.g. 'uatom'
* @returns {State}
*/
(account, storageNode, chainAddress) => {
(account, storageNode, chainAddress, icqConnection, 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, icqConnection, bondDenom };
},
{
helper: {
Expand All @@ -95,17 +112,49 @@ export const prepareStakingAccountKit = (baggage, makeRecorderKit, zcf) => {
},
// TODO move this beneath the Orchestration abstraction,
// to the OrchestrationAccount provided by makeAccount()
/**
* @param {string} [denom] - defaults to bondDenom
* @returns {Promise<{ amount: string; denom: string; }>}
*/
async queryBalance(denom) {
const { chainAddress, icqConnection, bondDenom } = this.state;

denom ||= bondDenom;

const [result] = await E(icqConnection).query([
/** @type {RequestQueryJson} */ (
RequestQuery.toJSON(
RequestQuery.fromPartial({
path: '/cosmos.bank.v1beta1.Query/Balance',
data: QueryBalanceRequest.encode(
QueryBalanceRequest.fromPartial({
address: chainAddress.address,
denom,
}),
).finish(),
}),
)
),
]);
if (!result?.key) throw Fail`Error parsing result ${result}`;
const { balance } = QueryBalanceResponse.decode(
decodeBase64(result.key),
);
if (!balance) throw Fail`Result lacked balance key: ${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} cosmosValidatorAddress
* @param {Amount<'nat'>} ertpAmount
*/
async delegate(validatorAddress, ertpAmount) {
// FIXME get values from proposal or args
async delegate(cosmosValidatorAddress, ertpAmount) {
// 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();
Expand All @@ -116,7 +165,7 @@ export const prepareStakingAccountKit = (baggage, makeRecorderKit, zcf) => {
Any.toJSON(
MsgDelegate.toProtoMsg({
delegatorAddress,
validatorAddress,
validatorAddress: cosmosValidatorAddress.address,
amount,
}),
)
Expand All @@ -134,6 +183,10 @@ export const prepareStakingAccountKit = (baggage, makeRecorderKit, zcf) => {
},
},
invitationMakers: {
/**
* @param {CosmosValidatorAddress} validatorAddress
* @param {Amount<'nat'>} amount
*/
Delegate(validatorAddress, amount) {
return this.facets.holder.makeDelegateInvitation(
validatorAddress,
Expand All @@ -160,16 +213,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);
},
getAddress() {
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.address, denom);
return this.facets.helper.queryBalance(denom);
},
/**
*
* @param {string} validatorAddress
* @param {CosmosValidatorAddress} validatorAddress
* @param {Amount<'nat'>} ertpAmount
*/
makeDelegateInvitation(validatorAddress, ertpAmount) {
Expand All @@ -195,4 +260,5 @@ export const prepareStakingAccountKit = (baggage, makeRecorderKit, zcf) => {
);
return makeStakingAccountKit;
};

/** @typedef {ReturnType<ReturnType<typeof prepareStakingAccountKit>>} StakingAccountKit */
Loading

0 comments on commit c01f2f7

Please sign in to comment.