Skip to content

Commit

Permalink
feat(orchestration): stakeAtom query balance
Browse files Browse the repository at this point in the history
  • Loading branch information
0xpatrickdev committed May 3, 2024
1 parent d4687b1 commit 06d745e
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 41 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
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
18 changes: 12 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,12 +31,15 @@ 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;

const zone = makeDurableZone(baggage);

Expand All @@ -52,12 +56,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
96 changes: 69 additions & 27 deletions packages/orchestration/src/exos/stakingAccountKit.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,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 { Any } from '@agoric/cosmic-proto/google/protobuf/any.js';
import { AmountShape } from '@agoric/ertp';
import { makeTracer } from '@agoric/internal';
Expand All @@ -16,11 +20,13 @@ 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 { toRequestQueryJson } from '@agoric/cosmic-proto';
import { ChainAddressShape, CoinShape } from '../typeGuards.js';

/**
* @import { ChainAccount, ChainAddress, ChainAmount, CosmosValidatorAddress } from '../types.js';
* @import { RecorderKit, MakeRecorderKit } from '@agoric/zoe/src/contractSupport/recorder.js';
* @import { Baggage } from '@agoric/swingset-liveslots';
* @import {ChainAccount, ChainAddress, ChainAmount, 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';
*/

Expand All @@ -37,13 +43,17 @@ const { Fail } = assert;
* topicKit: RecorderKit<StakingAccountNotification>;
* account: ChainAccount;
* chainAddress: ChainAddress;
* icqConnection: ICQConnection;
* bondDenom: string;
* }} State
*/

const HolderI = M.interface('holder', {
export const ChainAccountHolderI = M.interface('ChainAccountHolder', {
getPublicTopics: M.call().returns(TopicsRecordShape),
delegate: M.callWhen(M.string(), AmountShape).returns(M.record()),
withdrawReward: M.callWhen(M.string()).returns(M.array()),
getAddress: M.call().returns(ChainAddressShape),
getBalance: M.callWhen().optional(M.string()).returns(CoinShape),
delegate: M.callWhen(ChainAddressShape, AmountShape).returns(M.record()),
withdrawReward: M.callWhen(ChainAddressShape).returns(M.arrayOf(CoinShape)),
});

/** @type {{ [name: string]: [description: string, valueShape: Pattern] }} */
Expand Down Expand Up @@ -89,10 +99,10 @@ export const prepareStakingAccountKit = (baggage, makeRecorderKit, zcf) => {
'Staking Account Holder',
{
helper: UnguardedHelperI,
holder: HolderI,
holder: ChainAccountHolderI,
invitationMakers: M.interface('invitationMakers', {
Delegate: M.call(M.string(), AmountShape).returns(M.promise()),
WithdrawReward: M.call(M.string()).returns(M.promise()),
Delegate: M.call(ChainAddressShape, AmountShape).returns(M.promise()),
WithdrawReward: M.call(ChainAddressShape).returns(M.promise()),
CloseAccount: M.call().returns(M.promise()),
TransferAccount: M.call().returns(M.promise()),
}),
Expand All @@ -101,13 +111,15 @@ export const prepareStakingAccountKit = (baggage, makeRecorderKit, zcf) => {
* @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 @@ -126,24 +138,24 @@ export const prepareStakingAccountKit = (baggage, makeRecorderKit, zcf) => {
invitationMakers: {
/**
*
* @param {string} validatorAddress
* @param {CosmosValidatorAddress} validator
* @param {Amount<'nat'>} amount
*/
Delegate(validatorAddress, amount) {
trace('Delegate', validatorAddress, amount);
Delegate(validator, amount) {
trace('Delegate', validator, amount);

return zcf.makeInvitation(async seat => {
seat.exit();
return this.facets.holder.delegate(validatorAddress, amount);
return this.facets.holder.delegate(validator, amount);
}, 'Delegate');
},
/** @param {string} validatorAddress */
WithdrawReward(validatorAddress) {
trace('WithdrawReward', validatorAddress);
/** @param {CosmosValidatorAddress} validator */
WithdrawReward(validator) {
trace('WithdrawReward', validator);

return zcf.makeInvitation(async seat => {
seat.exit();
return this.facets.holder.withdrawReward(validatorAddress);
return this.facets.holder.withdrawReward(validator);
}, 'WithdrawReward');
},
CloseAccount() {
Expand All @@ -170,13 +182,17 @@ export const prepareStakingAccountKit = (baggage, makeRecorderKit, zcf) => {
},
// TODO move this beneath the Orchestration abstraction,
// to the OrchestrationAccount provided by makeAccount()
/** @returns {ChainAddress} */
getAddress() {
return this.state.chainAddress;
},
/**
* _Assumes users has already sent funds to their ICA, until #9193
* @param {string} validatorAddress
* @param {CosmosValidatorAddress} validator
* @param {Amount<'nat'>} ertpAmount
*/
async delegate(validatorAddress, ertpAmount) {
trace('delegate', validatorAddress, ertpAmount);
async delegate(validator, ertpAmount) {
trace('delegate', validator, ertpAmount);

// FIXME get values from proposal or args
// FIXME brand handling and amount scaling
Expand All @@ -193,7 +209,7 @@ export const prepareStakingAccountKit = (baggage, makeRecorderKit, zcf) => {
toAnyJSON(
MsgDelegate.toProtoMsg({
delegatorAddress,
validatorAddress,
validatorAddress: validator.address,
amount,
}),
),
Expand All @@ -204,15 +220,15 @@ export const prepareStakingAccountKit = (baggage, makeRecorderKit, zcf) => {
},

/**
* @param {string} validatorAddress
* @param {CosmosValidatorAddress} validator
* @returns {Promise<ChainAmount[]>}
*/
async withdrawReward(validatorAddress) {
async withdrawReward(validator) {
const { chainAddress } = this.state;
assert.typeof(validatorAddress, 'string');
assert.typeof(validator.address, 'string');
const msg = MsgWithdrawDelegatorReward.toProtoMsg({
delegatorAddress: chainAddress.address,
validatorAddress,
validatorAddress: validator.address,
});
const account = this.facets.helper.owned();
const result = await E(account).executeEncodedTx([toAnyJSON(msg)]);
Expand All @@ -222,9 +238,35 @@ export const prepareStakingAccountKit = (baggage, makeRecorderKit, zcf) => {
);
return harden(coins.map(toChainAmount));
},
/**
* @param {ChainAmount['denom']} [denom] - defaults to bondDenom
* @returns {Promise<ChainAmount>}
*/
async getBalance(denom) {
const { chainAddress, icqConnection, bondDenom } = this.state;
denom ||= bondDenom;
assert.typeof(denom, 'string');

const [result] = await E(icqConnection).query([
toRequestQueryJson(
QueryBalanceRequest.toProtoMsg({
address: chainAddress.address,
denom,
}),
),
]);
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}`;
return harden(toChainAmount(balance));
},
},
},
);
return makeStakingAccountKit;
};

/** @typedef {ReturnType<ReturnType<typeof prepareStakingAccountKit>>} StakingAccountKit */
/** @typedef {StakingAccountKit['holder']} StakingAccounHolder */
19 changes: 15 additions & 4 deletions packages/orchestration/src/proposals/start-stakeAtom.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const startStakeAtom = async (
agoricNames,
board,
chainStorage,
orchestration,
orchestration: orchestrationP,
startUpgradable,
},
installation: {
Expand All @@ -27,17 +27,26 @@ 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);
const marshaller = await E(board).getPublishingMarshaller();
const atomIssuer = await E(agoricNames).lookup('issuer', 'ATOM');
trace('ATOM Issuer', atomIssuer);

const orchestration = await orchestrationP;
const icqConnection = await E(orchestration).provideICQConnection(
controllerConnectionId,
);

/** @type {StartUpgradableOpts<StakeAtomSF>} */
const startOpts = {
label: 'stakeAtom',
Expand All @@ -46,9 +55,11 @@ export const startStakeAtom = async (
terms: {
hostConnectionId,
controllerConnectionId,
bondDenom,
},
privateArgs: {
orchestration: await orchestration,
orchestration,
icqConnection,
storageNode,
marshaller,
},
Expand Down

0 comments on commit 06d745e

Please sign in to comment.