From 4e2b4868a2c3206c40184c0fcb73fdb9a7eb1024 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Wed, 31 Jul 2024 21:24:41 -0400 Subject: [PATCH 1/4] feat: restake example contract - A contract that allows calls to make an OrchestrationAccount, and subsequently schedule a restake (claim and stake rewards) on an interval. - Leverages existing invitation makers like Delegate and combines them with Restake and CancelRestake. --- .../src/examples/restake.contract.js | 95 +++++++ .../src/examples/restake.flows.js | 53 ++++ .../orchestration/src/examples/restake.kit.js | 260 ++++++++++++++++++ .../src/utils/orchestrationAccount.js | 18 ++ .../test/examples/restake.contract.test.ts | 170 ++++++++++++ 5 files changed, 596 insertions(+) create mode 100644 packages/orchestration/src/examples/restake.contract.js create mode 100644 packages/orchestration/src/examples/restake.flows.js create mode 100644 packages/orchestration/src/examples/restake.kit.js create mode 100644 packages/orchestration/test/examples/restake.contract.test.ts diff --git a/packages/orchestration/src/examples/restake.contract.js b/packages/orchestration/src/examples/restake.contract.js new file mode 100644 index 00000000000..1e9cdb48812 --- /dev/null +++ b/packages/orchestration/src/examples/restake.contract.js @@ -0,0 +1,95 @@ +import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; +import { M } from '@endo/patterns'; +import { withOrchestration } from '../utils/start-helper.js'; +import * as flows from './restake.flows.js'; +import { + prepareRestakeHolderKit, + prepareRestakeWaker, + restakeInvitaitonGuardShape, +} from './restake.kit.js'; +import { prepareCombineInvitationMakers } from '../exos/combine-invitation-makers.js'; + +/** + * @import {Zone} from '@agoric/zone'; + * @import {OrchestrationPowers} from '../utils/start-helper.js'; + * @import {OrchestrationTools} from '../utils/start-helper.js'; + */ + +/** + * XXX consider moving to privateArgs / creatorFacet, as terms are immutable + * + * @typedef {{ + * minimumDelay: bigint; + * minimumInterval: bigint; + * }} RestakeContractTerms + */ + +/** + * A contract that allows calls to make an OrchestrationAccount, and + * subsequently schedule a restake (claim and stake rewards) on an interval. + * + * Leverages existing invitation makers like Delegate and combines them with + * Restake and CancelRestake. + * + * @param {ZCF} zcf + * @param {OrchestrationPowers & { + * marshaller: Marshaller; + * }} privateArgs + * @param {Zone} zone + * @param {OrchestrationTools} tools + */ +const contract = async ( + zcf, + { timerService }, + zone, + { orchestrateAll, vowTools }, +) => { + const makeRestakeWaker = prepareRestakeWaker( + zone.subZone('restakeWaker'), + vowTools, + ); + const makeCombineInvitationMakers = prepareCombineInvitationMakers( + zone, + restakeInvitaitonGuardShape, + ); + + const { minimumDelay, minimumInterval } = zcf.getTerms(); + + const makeRestakeHolderKit = prepareRestakeHolderKit( + zone.subZone('restakeHolder'), + { + vowTools, + zcf, + timer: timerService, + makeRestakeWaker, + params: harden({ minimumDelay, minimumInterval }), + }, + ); + + const orchFns = orchestrateAll(flows, { + makeRestakeHolderKit, + makeCombineInvitationMakers, + }); + + const publicFacet = zone.exo( + 'Restake Public Facet', + M.interface('Restake PF', { + makeRestakeAccountInvitation: M.callWhen().returns(InvitationShape), + }), + { + makeRestakeAccountInvitation() { + return zcf.makeInvitation( + orchFns.makeRestakeAccount, + 'Make a Restake Account', + ); + }, + }, + ); + + return { publicFacet }; +}; + +export const start = withOrchestration(contract); +harden(start); + +/** @typedef {typeof start} RestakeSF */ diff --git a/packages/orchestration/src/examples/restake.flows.js b/packages/orchestration/src/examples/restake.flows.js new file mode 100644 index 00000000000..44d9be027bb --- /dev/null +++ b/packages/orchestration/src/examples/restake.flows.js @@ -0,0 +1,53 @@ +/** + * @file Example contract that allows users to do different things with rewards + */ +import { M, mustMatch } from '@endo/patterns'; + +/** + * @import {OrchestrationAccount, OrchestrationFlow, Orchestrator, StakingAccountActions} from '@agoric/orchestration'; + * @import {MakeRestakeHolderKit} from './restake.kit.js'; + * @import {MakeCombineInvitationMakers} from '../exos/combine-invitation-makers.js'; + */ + +/** + * Create an OrchestrationAccount for a specific chain and return a continuing + * offer with invitations makers for Delegate, WithdrawRewards, Transfer, etc. + * + * @satisfies {OrchestrationFlow} + * @param {Orchestrator} orch + * @param {{ + * makeRestakeHolderKit: MakeRestakeHolderKit; + * makeCombineInvitationMakers: MakeCombineInvitationMakers; + * }} ctx + * @param {ZCFSeat} seat + * @param {{ chainName: string }} offerArgs + */ +export const makeRestakeAccount = async ( + orch, + { makeRestakeHolderKit, makeCombineInvitationMakers }, + seat, + { chainName }, +) => { + seat.exit(); // no funds exchanged + mustMatch(chainName, M.string()); + const remoteChain = await orch.getChain(chainName); + const orchAccount = + /** @type {OrchestrationAccount & StakingAccountActions} */ ( + await remoteChain.makeAccount() + ); + const restakeHolderKit = makeRestakeHolderKit(orchAccount); + const { invitationMakers: orchInvitationMakers, publicSubscribers } = + await orchAccount.asContinuingOffer(); + + const combinedInvitationMakers = makeCombineInvitationMakers( + // `orchInvitationMakers` currently lying about its type + orchInvitationMakers, + // @ts-expect-error update `makeCombineInvitationMakers` to accept Guarded... + restakeHolderKit.invitationMakers, + ); + + return harden({ + invitationMakers: combinedInvitationMakers, + publicSubscribers, + }); +}; diff --git a/packages/orchestration/src/examples/restake.kit.js b/packages/orchestration/src/examples/restake.kit.js new file mode 100644 index 00000000000..3b75707335d --- /dev/null +++ b/packages/orchestration/src/examples/restake.kit.js @@ -0,0 +1,260 @@ +import { M, mustMatch } from '@endo/patterns'; +import { E } from '@endo/far'; +import { Fail } from '@endo/errors'; +import { TimestampRecordShape } from '@agoric/time'; +import { VowShape } from '@agoric/vow'; +import { makeTracer } from '@agoric/internal'; +import { EmptyProposalShape } from '@agoric/zoe/src/typeGuards.js'; +import { ChainAddressShape, DenomAmountShape } from '../typeGuards.js'; + +const trace = makeTracer('RestakeKit'); + +/** + * @import {Vow, VowTools} from '@agoric/vow'; + * @import {Zone} from '@agoric/zone'; + * @import {CosmosValidatorAddress, DenomAmount, OrchestrationAccount, StakingAccountActions} from '@agoric/orchestration'; + * @import {Remote} from '@agoric/internal'; + * @import {TimestampRecord, TimerService, TimerRepeater} from '@agoric/time'; + */ + +/** + * @typedef {{ + * orchAccount: OrchestrationAccount & StakingAccountActions; + * validator: CosmosValidatorAddress; + * }} RestakeWakerState + */ + +/** + * @param {Zone} zone + * @param {VowTools} vowTools + */ +const prepareRestakeWakerKit = (zone, { watch }) => { + return zone.exoClassKit( + 'RestakeWakerKit', + { + waker: M.interface('RestakeWaker', { + wake: M.call(TimestampRecordShape).returns(M.any()), + }), + withdrawRewardsHandler: M.interface('WithdrawRewardsHandle', { + onFulfilled: M.call(M.arrayOf(DenomAmountShape)) + .optional(M.arrayOf(M.undefined())) + .returns(VowShape), + }), + }, + /** + * @param {OrchestrationAccount & StakingAccountActions} orchAccount + * @param {CosmosValidatorAddress} validator + * @returns {RestakeWakerState} + */ + (orchAccount, validator) => harden({ orchAccount, validator }), + { + waker: { + /** @param {TimestampRecord} timestampRecord */ + wake(timestampRecord) { + trace('Wake Received', timestampRecord); + const { orchAccount, validator } = this.state; + return watch( + E(orchAccount).withdrawReward(validator), + this.facets.withdrawRewardsHandler, + ); + }, + }, + withdrawRewardsHandler: { + /** @param {DenomAmount[]} amounts */ + onFulfilled(amounts) { + trace('Withdrew Rewards', amounts); + if (amounts.length !== 1) { + // XXX post to vstorage, else this effectively dropped on the ground + throw Fail`Received ${amounts.length} amounts, only expected one.`; + } + if (!amounts[0].value) return; + const { orchAccount, validator } = this.state; + return watch(E(orchAccount).delegate(validator, amounts[0])); + }, + }, + }, + { + stateShape: { + orchAccount: M.remotable('OrchestrationAccount'), + validator: ChainAddressShape, + }, + }, + ); +}; + +/** + * @param {Zone} zone + * @param {VowTools} vowTools + * @returns {( + * ...args: Parameters> + * ) => ReturnType>['waker']} + */ +export const prepareRestakeWaker = (zone, vowTools) => { + const makeKit = prepareRestakeWakerKit(zone, vowTools); + return (...args) => makeKit(...args).waker; +}; + +/** + * @typedef {{ + * orchAccount: OrchestrationAccount & StakingAccountActions; + * restakeRepeater: TimerRepeater | undefined; + * }} RestakeHolderState + */ + +const RepeaterStateShape = { + orchAccount: M.remotable('OrchestrationAccount'), + restakeRepeater: M.or(M.remotable('TimerRepeater'), M.undefined()), +}; + +/** + * @typedef {{ + * delay: bigint; + * interval: bigint; + * }} RepeaterOpts + */ + +const RepeaterOptsShape = { + delay: M.nat(), + interval: M.nat(), +}; + +/** + * @typedef {{ + * minimumDelay: bigint; + * minimumInterval: bigint; + * }} RestakeParams + */ + +export const restakeInvitaitonGuardShape = { + Restake: M.call(ChainAddressShape, RepeaterOptsShape).returns(M.promise()), + CancelRestake: M.call().returns(M.promise()), +}; + +/** + * @param {Zone} zone + * @param {object} opts + * @param {VowTools} opts.vowTools + * @param {ZCF} opts.zcf + * @param {Remote} opts.timer + * @param {ReturnType} opts.makeRestakeWaker + * @param {RestakeParams} opts.params + */ +export const prepareRestakeHolderKit = ( + zone, + { + vowTools: { watch, asVow }, + zcf, + timer, + makeRestakeWaker, + params: { minimumDelay, minimumInterval }, + }, +) => { + return zone.exoClassKit( + 'RestakeHolderKit', + { + invitationMakers: M.interface('InvitationMakers', { + ...restakeInvitaitonGuardShape, + }), + holder: M.interface('Holder', { + restake: M.call(ChainAddressShape, RepeaterOptsShape).returns(VowShape), + cancelRestake: M.call().returns(VowShape), + }), + }, + /** + * @param {OrchestrationAccount & StakingAccountActions} orchAccount + */ + orchAccount => { + return /** @type {RestakeHolderState} */ ( + harden({ + orchAccount, + restakeRepeater: undefined, + }) + ); + }, + { + invitationMakers: { + /** + * @param {CosmosValidatorAddress} validator + * @param {RepeaterOpts} opts + * @returns {Promise} + */ + Restake(validator, opts) { + trace('Restake', validator, opts); + return zcf.makeInvitation( + seat => { + seat.exit(); + return watch(this.facets.holder.restake(validator, opts)); + }, + 'Restake', + undefined, + EmptyProposalShape, + ); + }, + CancelRestake() { + trace('Cancel Restake'); + return zcf.makeInvitation( + seat => { + seat.exit(); + return watch(this.facets.holder.cancelRestake()); + }, + 'Cancel Restake', + undefined, + EmptyProposalShape, + ); + }, + }, + holder: { + /** + * @param {CosmosValidatorAddress} validator + * @param {RepeaterOpts} opts + * @returns {Vow} + */ + restake(validator, opts) { + trace('restake', validator, opts); + return asVow(() => { + mustMatch( + validator, + ChainAddressShape, + 'invalid validator address', + ); + mustMatch(opts, RepeaterOptsShape, 'invalid repeater options'); + const { delay, interval } = opts; + delay >= minimumDelay || + Fail`delay must be at least ${minimumDelay}`; + interval >= minimumInterval || + Fail`interval must be at least ${minimumInterval}`; + if (this.state.restakeRepeater) { + // TODO block on this + E(this.state.restakeRepeater.disable()); + } + const restakeWaker = makeRestakeWaker( + this.state.orchAccount, + validator, + ); + return watch( + E.when(E(timer).makeRepeater(delay, interval), repeater => { + this.state.restakeRepeater = repeater; + console.debug('new repeater', repeater); + return E(repeater).schedule(restakeWaker); + // XXX on success, post to vstorage + // XXX orch-acct needs to expose its recorder kit writer + }), + ); + }); + }, + cancelRestake() { + if (!this.state.restakeRepeater) { + throw Fail`Restake not active.`; + } + return watch(E(this.state.restakeRepeater).disable()); + }, + }, + }, + { + stateShape: RepeaterStateShape, + }, + ); +}; + +/** @typedef {ReturnType} MakeRestakeHolderKit */ +/** @typedef {ReturnType} RestakeHolderKit */ diff --git a/packages/orchestration/src/utils/orchestrationAccount.js b/packages/orchestration/src/utils/orchestrationAccount.js index 242482bce60..6c74a591733 100644 --- a/packages/orchestration/src/utils/orchestrationAccount.js +++ b/packages/orchestration/src/utils/orchestrationAccount.js @@ -6,6 +6,7 @@ import { M } from '@endo/patterns'; import { AmountArgShape, ChainAddressShape, + DelegationShape, DenomAmountShape, IBCTransferOptionsShape, } from '../typeGuards.js'; @@ -38,3 +39,20 @@ export const orchestrationAccountMethods = { ), getPublicTopics: M.call().returns(Vow$(TopicsRecordShape)), }; + +export const orchestrationAccountInvitationMakers = { + Delegate: M.call(ChainAddressShape, AmountArgShape).returns(M.promise()), + Redelegate: M.call( + ChainAddressShape, + ChainAddressShape, + AmountArgShape, + ).returns(M.promise()), + WithdrawReward: M.call(ChainAddressShape).returns(M.promise()), + Undelegate: M.call(M.arrayOf(DelegationShape)).returns(M.promise()), + DeactivateAccount: M.call().returns(M.promise()), + ReactivateAccount: M.call().returns(M.promise()), + TransferAccount: M.call().returns(M.promise()), + Send: M.call().returns(M.promise()), + SendAll: M.call().returns(M.promise()), + Transfer: M.call().returns(M.promise()), +}; diff --git a/packages/orchestration/test/examples/restake.contract.test.ts b/packages/orchestration/test/examples/restake.contract.test.ts new file mode 100644 index 00000000000..d48627f40a1 --- /dev/null +++ b/packages/orchestration/test/examples/restake.contract.test.ts @@ -0,0 +1,170 @@ +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import type { TestFn } from 'ava'; +import { setUpZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; +import type { Instance } from '@agoric/zoe/src/zoeService/utils.js'; +import { E } from '@endo/far'; +import path from 'path'; +import { + MsgWithdrawDelegatorReward, + MsgWithdrawDelegatorRewardResponse, +} from '@agoric/cosmic-proto/cosmos/distribution/v1beta1/tx.js'; +import { IBCEvent } from '@agoric/vats'; +import { MsgDelegateResponse } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; +import { commonSetup } from '../supports.js'; +import { + buildMsgResponseString, + buildTxPacketString, +} from '../../tools/ibc-mocks.js'; +import { CosmosValidatorAddress } from '../../src/cosmos-api.js'; +import { defaultMockAckMap } from '../ibc-mocks.js'; + +const dirname = path.dirname(new URL(import.meta.url).pathname); + +const contractName = 'restake'; +const contractFile = `${dirname}/../../src/examples/${contractName}.contract.js`; +type StartFn = typeof import('../../src/examples/restake.contract.js').start; + +type TestContext = Awaited> & { + zoe: ZoeService; + instance: Instance; +}; + +const test = anyTest as TestFn; + +test.before(async t => { + const setupContext = await commonSetup(t); + const { + bootstrap: { storage }, + commonPrivateArgs, + mocks: { ibcBridge }, + } = setupContext; + + const { zoe, bundleAndInstall } = await setUpZoeForTest(); + + t.log('contract coreEval', contractName); + const installation = await bundleAndInstall(contractFile); + + const storageNode = await E(storage.rootNode).makeChildNode(contractName); + const { instance } = await E(zoe).startInstance( + installation, + undefined, + { + minimumDelay: 1n, + minimumInterval: 5n, // for testing only + }, + { ...commonPrivateArgs, storageNode }, + ); + + const buildMocks = () => { + const withdrawReward = buildTxPacketString([ + MsgWithdrawDelegatorReward.toProtoMsg({ + delegatorAddress: 'cosmos1test', + validatorAddress: 'cosmosvaloper1test', + }), + ]); + const wdRewardResp = buildMsgResponseString( + MsgWithdrawDelegatorRewardResponse, + { + amount: [{ denom: 'uatom', amount: '10' }], + }, + ); + return { ...defaultMockAckMap, [withdrawReward]: wdRewardResp }; + }; + await E(ibcBridge).setMockAck(buildMocks()); + + t.context = { + ...setupContext, + zoe, + instance, + }; +}); + +const chainConfigs = { + agoric: { addressPrefix: 'agoric1fakeLCAAddress' }, + cosmoshub: { addressPrefix: 'cosmos1test' }, +}; + +const defaultValidator: CosmosValidatorAddress = { + value: 'cosmosvaloper1test', + chainId: 'cosmoshub-test', + encoding: 'bech32', +}; + +test('restake contract schedules claimRewards() and delegate() on an interval', async t => { + const { + bootstrap: { vowTools: vt, timer }, + zoe, + instance, + utils: { inspectDibcBridge }, + } = t.context; + const publicFacet = await E(zoe).getPublicFacet(instance); + const inv = E(publicFacet).makeRestakeAccountInvitation(); + + const userSeat = E(zoe).offer(inv, {}, undefined, { chainName: 'cosmoshub' }); + const { invitationMakers, publicSubscribers } = await vt.when( + E(userSeat).getOfferResult(), + ); + t.is( + publicSubscribers.account.storagePath, + 'mockChainStorageRoot.restake.cosmos1test', + ); + + // Delegate, so we have some rewards to claim + const delegateInv = await E(invitationMakers).Delegate(defaultValidator, { + denom: 'uatom', + value: 10n, + }); + const delegateSeat = E(zoe).offer(delegateInv, {}); + await vt.when(E(delegateSeat).getOfferResult()); + + const restakeInv = await E(invitationMakers).Restake(defaultValidator, { + delay: 10n, + interval: 20n, + }); + const restakeSeat = await E(zoe).offer(restakeInv); + await vt.when(E(restakeSeat).getOfferResult()); + + timer.advanceTo(32n); + + await eventLoopIteration(); + const messages = await inspectDibcBridge(); + + // ChannelOpen, Delegate, WithdrawReward, Delegate + t.is(messages.length, 4); + + const verifyWithdrawAndDelegate = (msgs, label) => { + const withdrawRewardAck = msgs.at(-2) as IBCEvent<'acknowledgementPacket'>; + t.is( + withdrawRewardAck.acknowledgement, + buildMsgResponseString(MsgWithdrawDelegatorRewardResponse, { + amount: [{ denom: 'uatom', amount: '10' }], + }), + `Account withdrew rewards - ${label}`, + ); + + const delegateAck = msgs.at(-1) as IBCEvent<'acknowledgementPacket'>; + t.is( + delegateAck.acknowledgement, + buildMsgResponseString(MsgDelegateResponse, {}), + `Account delegated the claimed rewards - ${label} `, + ); + }; + + verifyWithdrawAndDelegate(messages, 'first iteration'); + + // repeater continues to fire + timer.advanceTo(64n); + await eventLoopIteration(); + const newMessages = await inspectDibcBridge(); + t.is(newMessages.length, 6); + verifyWithdrawAndDelegate(newMessages, 'second iteration'); +}); + +test.todo('CancelRestake stops the repeater'); +test.todo( + 'Restake with an active repeater stops the old one and starts a new one', +); +test.todo( + 'interval interval and delay need to be greater than minimumDelay and minimumInterval', +); From 582993c762f33fe54a58bb3e25b7f9b79f91b0e0 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Wed, 31 Jul 2024 23:01:06 -0400 Subject: [PATCH 2/4] refactor: restake as flows - interact with timerService and repeater in `contract` context (using E, promises) - invoke flow in wake handler --- .../src/examples/restake.contract.js | 160 ++++++++--- .../src/examples/restake.flows.js | 76 +++-- .../orchestration/src/examples/restake.kit.js | 260 ------------------ .../test/examples/restake.contract.test.ts | 103 ++++--- 4 files changed, 253 insertions(+), 346 deletions(-) delete mode 100644 packages/orchestration/src/examples/restake.kit.js diff --git a/packages/orchestration/src/examples/restake.contract.js b/packages/orchestration/src/examples/restake.contract.js index 1e9cdb48812..e8fcb50350c 100644 --- a/packages/orchestration/src/examples/restake.contract.js +++ b/packages/orchestration/src/examples/restake.contract.js @@ -1,20 +1,30 @@ -import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; +import { + EmptyProposalShape, + InvitationShape, +} from '@agoric/zoe/src/typeGuards.js'; import { M } from '@endo/patterns'; +import { E } from '@endo/far'; +import { makeTracer } from '@agoric/internal'; +import { TimestampRecordShape } from '@agoric/time'; +import { Fail } from '@endo/errors'; import { withOrchestration } from '../utils/start-helper.js'; import * as flows from './restake.flows.js'; -import { - prepareRestakeHolderKit, - prepareRestakeWaker, - restakeInvitaitonGuardShape, -} from './restake.kit.js'; import { prepareCombineInvitationMakers } from '../exos/combine-invitation-makers.js'; +import { CosmosOrchestrationInvitationMakersInterface } from '../exos/cosmos-orchestration-account.js'; +import { ChainAddressShape } from '../typeGuards.js'; /** + * @import {GuestInterface} from '@agoric/async-flow'; + * @import {TimerRepeater, TimestampRecord} from '@agoric/time'; * @import {Zone} from '@agoric/zone'; * @import {OrchestrationPowers} from '../utils/start-helper.js'; * @import {OrchestrationTools} from '../utils/start-helper.js'; + * @import {CosmosOrchestrationAccount} from '../exos/cosmos-orchestration-account.js'; + * @import {CosmosValidatorAddress} from '../types.js'; */ +const trace = makeTracer('RestakeContract'); + /** * XXX consider moving to privateArgs / creatorFacet, as terms are immutable * @@ -24,6 +34,21 @@ import { prepareCombineInvitationMakers } from '../exos/combine-invitation-maker * }} RestakeContractTerms */ +/** + * TODO use RelativeTimeRecord + * + * @typedef {{ + * delay: bigint; + * interval: bigint; + * }} RepeaterOpts + */ + +const RepeaterOptsShape = { + delay: M.nat(), + interval: M.nat(), +}; +harden(RepeaterOptsShape); + /** * A contract that allows calls to make an OrchestrationAccount, and * subsequently schedule a restake (claim and stake rewards) on an interval. @@ -34,41 +59,114 @@ import { prepareCombineInvitationMakers } from '../exos/combine-invitation-maker * @param {ZCF} zcf * @param {OrchestrationPowers & { * marshaller: Marshaller; - * }} privateArgs + * }} _privateArgs * @param {Zone} zone * @param {OrchestrationTools} tools */ -const contract = async ( - zcf, - { timerService }, - zone, - { orchestrateAll, vowTools }, -) => { - const makeRestakeWaker = prepareRestakeWaker( - zone.subZone('restakeWaker'), - vowTools, - ); - const makeCombineInvitationMakers = prepareCombineInvitationMakers( - zone, - restakeInvitaitonGuardShape, - ); - - const { minimumDelay, minimumInterval } = zcf.getTerms(); +const contract = async (zcf, { timerService }, zone, { orchestrateAll }) => { + const RestakeInvitationMakersI = M.interface('RestakeInvitationMakers', { + Restake: M.call(ChainAddressShape, RepeaterOptsShape).returns(M.promise()), + CancelRestake: M.call().returns(M.promise()), + }); - const makeRestakeHolderKit = prepareRestakeHolderKit( - zone.subZone('restakeHolder'), + const makeRestakeKit = zone.exoClassKit( + 'RestakeExtraInvitationMaker', { - vowTools, - zcf, - timer: timerService, - makeRestakeWaker, - params: harden({ minimumDelay, minimumInterval }), + invitationMakers: RestakeInvitationMakersI, + waker: M.interface('Waker', { + wake: M.call(TimestampRecordShape).returns(M.any()), + }), + }, + /** + * @param {GuestInterface} account + */ + account => + /** + * @type {{ + * account: GuestInterface; + * repeater: TimerRepeater | undefined; + * validator: CosmosValidatorAddress | undefined; + * }} + */ ({ account, repeater: undefined, validator: undefined }), + { + invitationMakers: { + /** + * @param {CosmosValidatorAddress} validator + * @param {RepeaterOpts} opts + * @returns {Promise} + */ + Restake(validator, opts) { + trace('Restake', validator); + const { delay, interval } = opts; + const { minimumDelay, minimumInterval } = zcf.getTerms(); + // TODO use AmounthMath + delay >= minimumDelay || Fail`delay must be at least ${minimumDelay}`; + interval >= minimumInterval || + Fail`interval must be at least ${minimumInterval}`; + + return zcf.makeInvitation( + async () => { + await null; + this.state.validator = validator; + if (this.state.repeater) { + await E(this.state.repeater).disable(); + this.state.repeater = undefined; + } + const repeater = await E(timerService).makeRepeater( + delay, + interval, + ); + this.state.repeater = repeater; + await E(repeater).schedule(this.facets.waker); + }, + 'Restake', + undefined, + EmptyProposalShape, + ); + }, + CancelRestake() { + trace('Cancel Restake'); + return zcf.makeInvitation( + async () => { + const { repeater } = this.state; + if (!repeater) throw Fail`No active repeater.`; + await E(repeater).disable(); + this.state.repeater = undefined; + }, + 'Cancel Restake', + undefined, + EmptyProposalShape, + ); + }, + }, + waker: { + /** @param {TimestampRecord} timestampRecord */ + wake(timestampRecord) { + trace('Waker Fired', timestampRecord); + const { account, validator } = this.state; + try { + if (!account) throw Fail`No account`; + if (!validator) throw Fail`No validator`; + // eslint-disable-next-line no-use-before-define -- defined by orchestrateAll, necessarily after this + orchFns.wakerHandler(account, validator, timestampRecord); + } catch (e) { + trace('Wake handler failed', e); + } + }, + }, }, ); + /** @type {any} XXX async membrane */ + const makeCombineInvitationMakers = prepareCombineInvitationMakers( + zone, + CosmosOrchestrationInvitationMakersInterface, + RestakeInvitationMakersI, + ); + const orchFns = orchestrateAll(flows, { - makeRestakeHolderKit, makeCombineInvitationMakers, + makeRestakeKit, }); const publicFacet = zone.exo( diff --git a/packages/orchestration/src/examples/restake.flows.js b/packages/orchestration/src/examples/restake.flows.js index 44d9be027bb..75f33293b88 100644 --- a/packages/orchestration/src/examples/restake.flows.js +++ b/packages/orchestration/src/examples/restake.flows.js @@ -2,11 +2,18 @@ * @file Example contract that allows users to do different things with rewards */ import { M, mustMatch } from '@endo/patterns'; +import { Fail } from '@endo/errors'; +import { makeTracer } from '@agoric/internal'; + +const trace = makeTracer('RestakeFlows'); /** - * @import {OrchestrationAccount, OrchestrationFlow, Orchestrator, StakingAccountActions} from '@agoric/orchestration'; - * @import {MakeRestakeHolderKit} from './restake.kit.js'; + * @import {CosmosValidatorAddress, OrchestrationAccount, OrchestrationFlow, Orchestrator, StakingAccountActions} from '@agoric/orchestration'; + * @import {TimestampRecord} from '@agoric/time'; + * @import {GuestInterface} from '@agoric/async-flow'; + * @import {InvitationMakers} from '@agoric/smart-wallet/src/types.js'; * @import {MakeCombineInvitationMakers} from '../exos/combine-invitation-makers.js'; + * @import {CosmosOrchestrationAccount} from '../exos/cosmos-orchestration-account.js'; */ /** @@ -16,38 +23,69 @@ import { M, mustMatch } from '@endo/patterns'; * @satisfies {OrchestrationFlow} * @param {Orchestrator} orch * @param {{ - * makeRestakeHolderKit: MakeRestakeHolderKit; * makeCombineInvitationMakers: MakeCombineInvitationMakers; + * makeRestakeKit: ( + * account: OrchestrationAccount & StakingAccountActions, + * ) => unknown; * }} ctx * @param {ZCFSeat} seat * @param {{ chainName: string }} offerArgs */ export const makeRestakeAccount = async ( orch, - { makeRestakeHolderKit, makeCombineInvitationMakers }, + { makeRestakeKit, makeCombineInvitationMakers }, seat, { chainName }, ) => { - seat.exit(); // no funds exchanged + trace('MakeRestakeAccount', chainName); + seat.exit(); mustMatch(chainName, M.string()); const remoteChain = await orch.getChain(chainName); - const orchAccount = + const account = /** @type {OrchestrationAccount & StakingAccountActions} */ ( await remoteChain.makeAccount() ); - const restakeHolderKit = makeRestakeHolderKit(orchAccount); - const { invitationMakers: orchInvitationMakers, publicSubscribers } = - await orchAccount.asContinuingOffer(); - - const combinedInvitationMakers = makeCombineInvitationMakers( - // `orchInvitationMakers` currently lying about its type - orchInvitationMakers, - // @ts-expect-error update `makeCombineInvitationMakers` to accept Guarded... - restakeHolderKit.invitationMakers, + const restakeKit = /** @type {{ invitationMakers: InvitationMakers }} */ ( + makeRestakeKit(account) ); + const result = await account.asContinuingOffer(); + + return { + ...result, + invitationMakers: makeCombineInvitationMakers( + restakeKit.invitationMakers, + result.invitationMakers, + ), + }; +}; +harden(makeRestakeAccount); + +/** + * A flow that can be provided to `makeFlowWaker` to act as the handler for a + * `TimerWaker`. This handler withdraws rewards from a validator and delegates + * to them. + * + * @satisfies {OrchestrationFlow} + * @param {Orchestrator} _orch + * @param {object} _ctx + * @param {GuestInterface} account + * @param {CosmosValidatorAddress} validator + * @param {TimestampRecord} timestampRecord + */ +export const wakerHandler = async ( + _orch, + _ctx, + account, + validator, + timestampRecord, +) => { + trace('Restake Waker Fired', timestampRecord); + const amounts = await account.withdrawReward(validator); + if (amounts.length !== 1) { + throw Fail`Received ${amounts.length} amounts, only expected one.`; + } + if (!amounts[0].value) return; - return harden({ - invitationMakers: combinedInvitationMakers, - publicSubscribers, - }); + return account.delegate(validator, amounts[0]); }; +harden(wakerHandler); diff --git a/packages/orchestration/src/examples/restake.kit.js b/packages/orchestration/src/examples/restake.kit.js deleted file mode 100644 index 3b75707335d..00000000000 --- a/packages/orchestration/src/examples/restake.kit.js +++ /dev/null @@ -1,260 +0,0 @@ -import { M, mustMatch } from '@endo/patterns'; -import { E } from '@endo/far'; -import { Fail } from '@endo/errors'; -import { TimestampRecordShape } from '@agoric/time'; -import { VowShape } from '@agoric/vow'; -import { makeTracer } from '@agoric/internal'; -import { EmptyProposalShape } from '@agoric/zoe/src/typeGuards.js'; -import { ChainAddressShape, DenomAmountShape } from '../typeGuards.js'; - -const trace = makeTracer('RestakeKit'); - -/** - * @import {Vow, VowTools} from '@agoric/vow'; - * @import {Zone} from '@agoric/zone'; - * @import {CosmosValidatorAddress, DenomAmount, OrchestrationAccount, StakingAccountActions} from '@agoric/orchestration'; - * @import {Remote} from '@agoric/internal'; - * @import {TimestampRecord, TimerService, TimerRepeater} from '@agoric/time'; - */ - -/** - * @typedef {{ - * orchAccount: OrchestrationAccount & StakingAccountActions; - * validator: CosmosValidatorAddress; - * }} RestakeWakerState - */ - -/** - * @param {Zone} zone - * @param {VowTools} vowTools - */ -const prepareRestakeWakerKit = (zone, { watch }) => { - return zone.exoClassKit( - 'RestakeWakerKit', - { - waker: M.interface('RestakeWaker', { - wake: M.call(TimestampRecordShape).returns(M.any()), - }), - withdrawRewardsHandler: M.interface('WithdrawRewardsHandle', { - onFulfilled: M.call(M.arrayOf(DenomAmountShape)) - .optional(M.arrayOf(M.undefined())) - .returns(VowShape), - }), - }, - /** - * @param {OrchestrationAccount & StakingAccountActions} orchAccount - * @param {CosmosValidatorAddress} validator - * @returns {RestakeWakerState} - */ - (orchAccount, validator) => harden({ orchAccount, validator }), - { - waker: { - /** @param {TimestampRecord} timestampRecord */ - wake(timestampRecord) { - trace('Wake Received', timestampRecord); - const { orchAccount, validator } = this.state; - return watch( - E(orchAccount).withdrawReward(validator), - this.facets.withdrawRewardsHandler, - ); - }, - }, - withdrawRewardsHandler: { - /** @param {DenomAmount[]} amounts */ - onFulfilled(amounts) { - trace('Withdrew Rewards', amounts); - if (amounts.length !== 1) { - // XXX post to vstorage, else this effectively dropped on the ground - throw Fail`Received ${amounts.length} amounts, only expected one.`; - } - if (!amounts[0].value) return; - const { orchAccount, validator } = this.state; - return watch(E(orchAccount).delegate(validator, amounts[0])); - }, - }, - }, - { - stateShape: { - orchAccount: M.remotable('OrchestrationAccount'), - validator: ChainAddressShape, - }, - }, - ); -}; - -/** - * @param {Zone} zone - * @param {VowTools} vowTools - * @returns {( - * ...args: Parameters> - * ) => ReturnType>['waker']} - */ -export const prepareRestakeWaker = (zone, vowTools) => { - const makeKit = prepareRestakeWakerKit(zone, vowTools); - return (...args) => makeKit(...args).waker; -}; - -/** - * @typedef {{ - * orchAccount: OrchestrationAccount & StakingAccountActions; - * restakeRepeater: TimerRepeater | undefined; - * }} RestakeHolderState - */ - -const RepeaterStateShape = { - orchAccount: M.remotable('OrchestrationAccount'), - restakeRepeater: M.or(M.remotable('TimerRepeater'), M.undefined()), -}; - -/** - * @typedef {{ - * delay: bigint; - * interval: bigint; - * }} RepeaterOpts - */ - -const RepeaterOptsShape = { - delay: M.nat(), - interval: M.nat(), -}; - -/** - * @typedef {{ - * minimumDelay: bigint; - * minimumInterval: bigint; - * }} RestakeParams - */ - -export const restakeInvitaitonGuardShape = { - Restake: M.call(ChainAddressShape, RepeaterOptsShape).returns(M.promise()), - CancelRestake: M.call().returns(M.promise()), -}; - -/** - * @param {Zone} zone - * @param {object} opts - * @param {VowTools} opts.vowTools - * @param {ZCF} opts.zcf - * @param {Remote} opts.timer - * @param {ReturnType} opts.makeRestakeWaker - * @param {RestakeParams} opts.params - */ -export const prepareRestakeHolderKit = ( - zone, - { - vowTools: { watch, asVow }, - zcf, - timer, - makeRestakeWaker, - params: { minimumDelay, minimumInterval }, - }, -) => { - return zone.exoClassKit( - 'RestakeHolderKit', - { - invitationMakers: M.interface('InvitationMakers', { - ...restakeInvitaitonGuardShape, - }), - holder: M.interface('Holder', { - restake: M.call(ChainAddressShape, RepeaterOptsShape).returns(VowShape), - cancelRestake: M.call().returns(VowShape), - }), - }, - /** - * @param {OrchestrationAccount & StakingAccountActions} orchAccount - */ - orchAccount => { - return /** @type {RestakeHolderState} */ ( - harden({ - orchAccount, - restakeRepeater: undefined, - }) - ); - }, - { - invitationMakers: { - /** - * @param {CosmosValidatorAddress} validator - * @param {RepeaterOpts} opts - * @returns {Promise} - */ - Restake(validator, opts) { - trace('Restake', validator, opts); - return zcf.makeInvitation( - seat => { - seat.exit(); - return watch(this.facets.holder.restake(validator, opts)); - }, - 'Restake', - undefined, - EmptyProposalShape, - ); - }, - CancelRestake() { - trace('Cancel Restake'); - return zcf.makeInvitation( - seat => { - seat.exit(); - return watch(this.facets.holder.cancelRestake()); - }, - 'Cancel Restake', - undefined, - EmptyProposalShape, - ); - }, - }, - holder: { - /** - * @param {CosmosValidatorAddress} validator - * @param {RepeaterOpts} opts - * @returns {Vow} - */ - restake(validator, opts) { - trace('restake', validator, opts); - return asVow(() => { - mustMatch( - validator, - ChainAddressShape, - 'invalid validator address', - ); - mustMatch(opts, RepeaterOptsShape, 'invalid repeater options'); - const { delay, interval } = opts; - delay >= minimumDelay || - Fail`delay must be at least ${minimumDelay}`; - interval >= minimumInterval || - Fail`interval must be at least ${minimumInterval}`; - if (this.state.restakeRepeater) { - // TODO block on this - E(this.state.restakeRepeater.disable()); - } - const restakeWaker = makeRestakeWaker( - this.state.orchAccount, - validator, - ); - return watch( - E.when(E(timer).makeRepeater(delay, interval), repeater => { - this.state.restakeRepeater = repeater; - console.debug('new repeater', repeater); - return E(repeater).schedule(restakeWaker); - // XXX on success, post to vstorage - // XXX orch-acct needs to expose its recorder kit writer - }), - ); - }); - }, - cancelRestake() { - if (!this.state.restakeRepeater) { - throw Fail`Restake not active.`; - } - return watch(E(this.state.restakeRepeater).disable()); - }, - }, - }, - { - stateShape: RepeaterStateShape, - }, - ); -}; - -/** @typedef {ReturnType} MakeRestakeHolderKit */ -/** @typedef {ReturnType} RestakeHolderKit */ diff --git a/packages/orchestration/test/examples/restake.contract.test.ts b/packages/orchestration/test/examples/restake.contract.test.ts index d48627f40a1..863d1fa399f 100644 --- a/packages/orchestration/test/examples/restake.contract.test.ts +++ b/packages/orchestration/test/examples/restake.contract.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable jsdoc/require-param */ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import type { TestFn } from 'ava'; import { setUpZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; @@ -11,6 +12,7 @@ import { import { IBCEvent } from '@agoric/vats'; import { MsgDelegateResponse } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; +import { ContinuingOfferResult } from '@agoric/smart-wallet/src/types.js'; import { commonSetup } from '../supports.js'; import { buildMsgResponseString, @@ -50,8 +52,8 @@ test.before(async t => { installation, undefined, { - minimumDelay: 1n, - minimumInterval: 5n, // for testing only + minimumDelay: 10n, + minimumInterval: 20n, // for testing only }, { ...commonPrivateArgs, storageNode }, ); @@ -80,11 +82,6 @@ test.before(async t => { }; }); -const chainConfigs = { - agoric: { addressPrefix: 'agoric1fakeLCAAddress' }, - cosmoshub: { addressPrefix: 'cosmos1test' }, -}; - const defaultValidator: CosmosValidatorAddress = { value: 'cosmosvaloper1test', chainId: 'cosmoshub-test', @@ -101,10 +98,12 @@ test('restake contract schedules claimRewards() and delegate() on an interval', const publicFacet = await E(zoe).getPublicFacet(instance); const inv = E(publicFacet).makeRestakeAccountInvitation(); - const userSeat = E(zoe).offer(inv, {}, undefined, { chainName: 'cosmoshub' }); - const { invitationMakers, publicSubscribers } = await vt.when( + const userSeat = E(zoe).offer(inv, {}, undefined, { + chainName: 'cosmoshub', + }); + const { invitationMakers, publicSubscribers } = (await vt.when( E(userSeat).getOfferResult(), - ); + )) as ContinuingOfferResult; t.is( publicSubscribers.account.storagePath, 'mockChainStorageRoot.restake.cosmos1test', @@ -125,16 +124,15 @@ test('restake contract schedules claimRewards() and delegate() on an interval', const restakeSeat = await E(zoe).offer(restakeInv); await vt.when(E(restakeSeat).getOfferResult()); - timer.advanceTo(32n); - - await eventLoopIteration(); - const messages = await inspectDibcBridge(); - - // ChannelOpen, Delegate, WithdrawReward, Delegate - t.is(messages.length, 4); - - const verifyWithdrawAndDelegate = (msgs, label) => { - const withdrawRewardAck = msgs.at(-2) as IBCEvent<'acknowledgementPacket'>; + /** verify WithdrawReward and Delegate acknowledgements */ + const verifyWithdrawAndDelegate = ( + events: IBCEvent[], + label: string, + ) => { + const withdrawRewardAck = events.at( + -2, + ) as IBCEvent<'acknowledgementPacket'>; + console.log('withdrawRewardAck', withdrawRewardAck); t.is( withdrawRewardAck.acknowledgement, buildMsgResponseString(MsgWithdrawDelegatorRewardResponse, { @@ -143,7 +141,7 @@ test('restake contract schedules claimRewards() and delegate() on an interval', `Account withdrew rewards - ${label}`, ); - const delegateAck = msgs.at(-1) as IBCEvent<'acknowledgementPacket'>; + const delegateAck = events.at(-1) as IBCEvent<'acknowledgementPacket'>; t.is( delegateAck.acknowledgement, buildMsgResponseString(MsgDelegateResponse, {}), @@ -151,20 +149,53 @@ test('restake contract schedules claimRewards() and delegate() on an interval', ); }; - verifyWithdrawAndDelegate(messages, 'first iteration'); - - // repeater continues to fire - timer.advanceTo(64n); - await eventLoopIteration(); - const newMessages = await inspectDibcBridge(); - t.is(newMessages.length, 6); - verifyWithdrawAndDelegate(newMessages, 'second iteration'); + { + t.log('Advance timer past first wake'); + timer.advanceTo(32n); + await eventLoopIteration(); + const { bridgeEvents } = await inspectDibcBridge(); + // ChannelOpen, Delegate, WithdrawReward, Delegate + t.is(bridgeEvents.length, 4); + verifyWithdrawAndDelegate(bridgeEvents, 'first iteration'); + } + + { + // repeater continues to fire + timer.advanceTo(64n); + await eventLoopIteration(); + const { bridgeEvents } = await inspectDibcBridge(); + t.is(bridgeEvents.length, 6); + verifyWithdrawAndDelegate(bridgeEvents, 'second iteration'); + } + + t.log('Cancel the restake repeater'); + const cancelInv = await E(invitationMakers).CancelRestake(); + const cancelSeat = await E(zoe).offer(cancelInv); + await vt.when(E(cancelSeat).getOfferResult()); + + { + timer.advanceTo(96n); + await eventLoopIteration(); + const { bridgeEvents } = await inspectDibcBridge(); + t.is(bridgeEvents.length, 6, 'no more events after cancel'); + } + + t.log('contract enforces minimum intervals'); + await t.throwsAsync( + E(invitationMakers).Restake(defaultValidator, { + delay: 1n, + interval: 2n, + }), + { message: 'delay must be at least "[10n]"' }, + ); + await t.throwsAsync( + E(invitationMakers).Restake(defaultValidator, { + delay: 10n, + interval: 2n, + }), + { message: 'interval must be at least "[20n]"' }, + ); }); -test.todo('CancelRestake stops the repeater'); -test.todo( - 'Restake with an active repeater stops the old one and starts a new one', -); -test.todo( - 'interval interval and delay need to be greater than minimumDelay and minimumInterval', -); +test.todo('if wake handler fails, repeater still continues'); +test.todo('contract restart behavior'); From 50f97f00f7aa622ca33ae3e35eea59f1f297de7c Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 9 Sep 2024 18:28:52 -0400 Subject: [PATCH 3/4] fixup! refactor: restake as flows --- .../src/examples/restake.contract.js | 19 +++++++++++++++ .../src/examples/restake.flows.js | 24 ++++++++++--------- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/packages/orchestration/src/examples/restake.contract.js b/packages/orchestration/src/examples/restake.contract.js index e8fcb50350c..0f231669bc0 100644 --- a/packages/orchestration/src/examples/restake.contract.js +++ b/packages/orchestration/src/examples/restake.contract.js @@ -69,6 +69,25 @@ const contract = async (zcf, { timerService }, zone, { orchestrateAll }) => { CancelRestake: M.call().returns(M.promise()), }); + /** + * A durable-ready exo that's returned to callers of `makeRestakeAccount`. + * Each caller will have their own instance with isolated state. + * + * It serves three main purposes: + * + * - persist state for our custom invitationMakers + * - provide invitationMakers and offer handlers for our restaking actions + * - provide a TimerWaker handler for the repeater logic + * + * The invitationMaker offer handlers interface with `TimerService` using `E` + * and remote calls. Since calls to `chainTimerService` are not cross-chain, + * it's safe to write them using Promises. + * + * When we need to perform cross-chain actions, like in the `wake` handler, we + * should use resumable code. Since our restake logic performs cross-chain + * actions via `.withdrawReward()` and `delegate()`, `orchFns.wakerHandler()` + * is written as an async-flow in {@link file://./restake.flows.js} + */ const makeRestakeKit = zone.exoClassKit( 'RestakeExtraInvitationMaker', { diff --git a/packages/orchestration/src/examples/restake.flows.js b/packages/orchestration/src/examples/restake.flows.js index 75f33293b88..8204e0e5560 100644 --- a/packages/orchestration/src/examples/restake.flows.js +++ b/packages/orchestration/src/examples/restake.flows.js @@ -11,14 +11,16 @@ const trace = makeTracer('RestakeFlows'); * @import {CosmosValidatorAddress, OrchestrationAccount, OrchestrationFlow, Orchestrator, StakingAccountActions} from '@agoric/orchestration'; * @import {TimestampRecord} from '@agoric/time'; * @import {GuestInterface} from '@agoric/async-flow'; - * @import {InvitationMakers} from '@agoric/smart-wallet/src/types.js'; + * @import {ContinuingOfferResult, InvitationMakers} from '@agoric/smart-wallet/src/types.js'; * @import {MakeCombineInvitationMakers} from '../exos/combine-invitation-makers.js'; * @import {CosmosOrchestrationAccount} from '../exos/cosmos-orchestration-account.js'; */ /** - * Create an OrchestrationAccount for a specific chain and return a continuing - * offer with invitations makers for Delegate, WithdrawRewards, Transfer, etc. + * Create an OrchestrationAccount for a specific chain and return a + * {@link ContinuingOfferResult} that combines the invitationMakers from the orch + * account (`Delegate`, `WithdrawRewards`, `Transfer`, etc.) with our custom + * invitationMakers (`Restake`, `CancelRestake`) from `RestakeKit`. * * @satisfies {OrchestrationFlow} * @param {Orchestrator} orch @@ -48,22 +50,22 @@ export const makeRestakeAccount = async ( const restakeKit = /** @type {{ invitationMakers: InvitationMakers }} */ ( makeRestakeKit(account) ); - const result = await account.asContinuingOffer(); + const { publicSubscribers, invitationMakers } = + await account.asContinuingOffer(); - return { - ...result, + return /** @type {ContinuingOfferResult} */ ({ + publicSubscribers, invitationMakers: makeCombineInvitationMakers( restakeKit.invitationMakers, - result.invitationMakers, + invitationMakers, ), - }; + }); }; harden(makeRestakeAccount); /** - * A flow that can be provided to `makeFlowWaker` to act as the handler for a - * `TimerWaker`. This handler withdraws rewards from a validator and delegates - * to them. + * A resumable async-flow that's provided to the `TimerWaker` waker handler in + * `RestakeKit`. It withdraws rewards from a validator and delegates them. * * @satisfies {OrchestrationFlow} * @param {Orchestrator} _orch From 7e01722ab53b991ceada2cb40b3e2fe8f6706861 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 9 Sep 2024 18:59:01 -0400 Subject: [PATCH 4/4] fixup! refactor: restake as flows --- packages/orchestration/src/examples/restake.flows.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/orchestration/src/examples/restake.flows.js b/packages/orchestration/src/examples/restake.flows.js index 8204e0e5560..e1a78c6a751 100644 --- a/packages/orchestration/src/examples/restake.flows.js +++ b/packages/orchestration/src/examples/restake.flows.js @@ -41,7 +41,6 @@ export const makeRestakeAccount = async ( ) => { trace('MakeRestakeAccount', chainName); seat.exit(); - mustMatch(chainName, M.string()); const remoteChain = await orch.getChain(chainName); const account = /** @type {OrchestrationAccount & StakingAccountActions} */ (