diff --git a/packages/orchestration/test/test-withdraw-reward.js b/packages/orchestration/test/test-withdraw-reward.js index 1fce513f570..f58d3fef533 100644 --- a/packages/orchestration/test/test-withdraw-reward.js +++ b/packages/orchestration/test/test-withdraw-reward.js @@ -1,7 +1,7 @@ // @ts-check import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { encodeBase64 } from '@endo/base64'; +import { decodeBase64, encodeBase64 } from '@endo/base64'; import { E, Far } from '@endo/far'; import * as pbjs from 'protobufjs'; import { @@ -9,6 +9,8 @@ import { MsgWithdrawDelegatorRewardResponse, } from '@agoric/cosmic-proto/cosmos/distribution/v1beta1/tx.js'; import { makeScalarBigMapStore } from '@agoric/vat-data'; +import { MsgDelegateResponse } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx'; +import { Any } from '@agoric/cosmic-proto/google/protobuf/any'; import { prepareStakingAccountKit } from '../src/exos/stakingAccountKit.js'; /** @import {ChainAccount} from '../src/types.js'; */ @@ -21,6 +23,32 @@ const { Fail } = assert; /** @type {typeof import('protobufjs').Writer} */ const Writer = pbjs.default.Writer; +const scenario1 = { + acct1: { + address: 'agoric1spy36ltduehs5dmszfrp792f0k2emcntrql3nx', + }, + validator: { address: 'agoric1valoper234', addressEncoding: 'bech32' }, + delegations: { + agoric1valoper234: { denom: 'ustake', amount: '200' }, + }, +}; + +test('DelegateResponse decoding', t => { + // executeEncodedTx() returns "acknowledge string" + const ackStr = + 'Ei0KKy9jb3Ntb3Muc3Rha2luZy52MWJldGExLk1zZ0RlbGVnYXRlUmVzcG9uc2U='; + // That's base64 protobuf of an Any + const any = Any.decode(decodeBase64(ackStr)); + + // use toJSON to get a ProtoMsg + // we happen to know that it's a MsgDelegateResponseProtoMsg + /** @import {MsgDelegateResponseProtoMsg} from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; */ + const msgA = /** @type {MsgDelegateResponseProtoMsg} */ (Any.toJSON(any)); + // now use fromProtoMsg to get a MsgDelegateResponse + const msgD = MsgDelegateResponse.fromProtoMsg(msgA); + t.deepEqual(msgD, {}); +}); + test('MsgWithdrawDelegatorReward: protobuf encoding reminder', t => { const actual = MsgWithdrawDelegatorReward.toProtoMsg({ delegatorAddress: 'abc', @@ -49,23 +77,33 @@ test('MsgWithdrawDelegatorReward: protobuf encoding reminder', t => { const mockAccount = (addr = 'agoric1234', delegations = {}) => { const calls = []; - const typeUrl = '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward'; + const simulate = { + '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward': _m => { + const response = { amount: Object.values(delegations) }; + const bytes = + MsgWithdrawDelegatorRewardResponse.encode(response).finish(); + const ackStr = encodeBase64(bytes); + return ackStr; + }, + + '/cosmos.staking.v1beta1.MsgDelegate': _m => { + return 'Ei0KKy9jb3Ntb3Muc3Rha2luZy52MWJldGExLk1zZ0RlbGVnYXRlUmVzcG9uc2U='; + // const m2 = Any.fromJSON(MsgDelegateResponse.toProtoMsg({})); + // return encodeBase64(Any.encode(m2).finish()); + }, + }; /** @type {ChainAccount} */ const account = Far('MockAccount', { getAccountAddress: () => addr, executeEncodedTx: async msgs => { assert.equal(msgs.length, 1); - assert.equal(msgs[0].typeUrl, typeUrl); + const { typeUrl } = msgs[0]; + const doMessage = simulate[typeUrl]; + assert(doMessage, `unknown ${typeUrl}`); await null; calls.push({ msgs }); - const wr = new Writer(); - for (const v of Object.values(delegations)) { - // @ts-expect-error BinaryWriter is not exported - MsgWithdrawDelegatorRewardResponse.encode({ amount: [v] }, wr); - } - const bs = wr.finish(); - return encodeBase64(bs); + return doMessage(msgs[0]); }, executeTx: () => Fail`mock`, close: () => Fail`mock`, @@ -76,52 +114,101 @@ const mockAccount = (addr = 'agoric1234', delegations = {}) => { return { account, calls }; }; -/** @returns {ZCF} */ -const mockZCF = () => - harden({ +const mockZCF = () => { + const toHandler = new Map(); + /** @type {ZCF} */ + const zcf = harden({ // @ts-expect-error mock makeInvitation: async (handler, desc, c = undefined, patt = undefined) => { - // const userSeat = harden({ - // getOfferResult: () => { - // const zcfSeat = {}; - // const r = handler(zcfSeat); - // return r; - // }, - // }); /** @type {Invitation} */ // @ts-expect-error mock const invitation = harden({}); + toHandler.set(invitation, handler); return invitation; }, }); + const zoe = harden({ + offer(invitation) { + const handler = toHandler.get(invitation); + const zcfSeat = harden({ + exit() {}, + }); + const result = Promise.resolve(null).then(() => handler(zcfSeat)); + const userSeat = harden({ + getOfferResult: () => result, + }); + return userSeat; + }, + }); + return { zcf, zoe }; +}; + +const makeRecorderKit = () => { + /** @type {any} */ + const kit = harden({}); + return kit; +}; test('withdraw rewards from staking account holder', async t => { - const makeRecorderKit = () => { - /** @type {any} */ - const kit = harden({}); - return kit; - }; const baggage = makeScalarBigMapStore('b1'); - const zcf = mockZCF(); + const { zcf } = mockZCF(); const make = prepareStakingAccountKit(baggage, makeRecorderKit, zcf); - const validator = { address: 'agoric1valoper234', addressEncoding: 'bech32' }; - const delegations = { - [validator.address]: { denom: 'ustake', amount: '200' }, - }; + const { validator, delegations } = scenario1; + const { account } = mockAccount(undefined, delegations); // const { rootNode } = makeFakeStorageKit('mockChainStorageRoot'); /** @type {StorageNode} */ // @ts-expect-error mock const storageNode = Far('StorageNode', {}); - const addr = 'agoric123'; // TODO: invitationMakers - const { helper } = make(account, storageNode, addr); + const { helper } = make(account, storageNode, account.getAccountAddress()); const actual = await E(helper).withdrawReward(validator); t.deepEqual(actual, [{ denom: 'ustake', value: 200n }]); }); +test(`delegate; undelegate`, async t => { + const baggage = makeScalarBigMapStore('b1'); + const { zcf, zoe } = mockZCF(); + const make = prepareStakingAccountKit(baggage, makeRecorderKit, zcf); + + const { validator, delegations } = scenario1; + + const { account } = mockAccount(undefined, delegations); + /** @type {StorageNode} */ + // @ts-expect-error mock + const storageNode = Far('StorageNode', {}); + + // TODO: invitationMakers + const { invitationMakers } = make( + account, + storageNode, + account.getAccountAddress(), + ); + + const toDelegate = await E(invitationMakers).Delegate(validator.address, { + brand: Far('Token'), + value: 2n, + }); + { + const seat = E(zoe).offer(toDelegate); + const result = await E(seat).getOfferResult(); + + t.deepEqual(result, {}); + } + + const toWithdraw = await E(invitationMakers).WithdrawReward( + validator.address, + ); + { + const seat = E(zoe).offer(toWithdraw); + const result = await E(seat).getOfferResult(); + + t.deepEqual(result, [{ denom: 'ustake', value: 200n }]); + } +}); + test.todo(`delegate; undelegate; collect rewards`); test.todo('undelegate uses a timer: begin; how long? wait; resolve'); test.todo('undelegate is cancellable - cosmos cancelUnbonding');