Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: restake example contract #9821

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions packages/orchestration/src/examples/restake.contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
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 { 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
*
* @typedef {{
* minimumDelay: bigint;
* minimumInterval: bigint;
* }} 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.
*
* Leverages existing invitation makers like Delegate and combines them with
* Restake and CancelRestake.
*
* @param {ZCF<RestakeContractTerms>} zcf
* @param {OrchestrationPowers & {
* marshaller: Marshaller;
* }} _privateArgs
* @param {Zone} zone
* @param {OrchestrationTools} tools
*/
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()),
});

/**
* 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',
{
invitationMakers: RestakeInvitationMakersI,
waker: M.interface('Waker', {
wake: M.call(TimestampRecordShape).returns(M.any()),
}),
},
/**
* @param {GuestInterface<CosmosOrchestrationAccount>} account
*/
account =>
/**
* @type {{
* account: GuestInterface<CosmosOrchestrationAccount>;
* repeater: TimerRepeater | undefined;
* validator: CosmosValidatorAddress | undefined;
* }}
*/ ({ account, repeater: undefined, validator: undefined }),
{
invitationMakers: {
/**
* @param {CosmosValidatorAddress} validator
* @param {RepeaterOpts} opts
* @returns {Promise<Invitation>}
*/
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, {
makeCombineInvitationMakers,
makeRestakeKit,
});

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 */
92 changes: 92 additions & 0 deletions packages/orchestration/src/examples/restake.flows.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* @file Example contract that allows users to do different things with rewards
*/
import { M, mustMatch } from '@endo/patterns';

Check failure on line 4 in packages/orchestration/src/examples/restake.flows.js

View workflow job for this annotation

GitHub Actions / lint-rest

'M' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 4 in packages/orchestration/src/examples/restake.flows.js

View workflow job for this annotation

GitHub Actions / lint-rest

'mustMatch' is defined but never used. Allowed unused vars must match /^_/u
import { Fail } from '@endo/errors';
import { makeTracer } from '@agoric/internal';

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 {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
* {@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
* @param {{
* makeCombineInvitationMakers: MakeCombineInvitationMakers;
* makeRestakeKit: (
* account: OrchestrationAccount<any> & StakingAccountActions,
* ) => unknown;
* }} ctx
* @param {ZCFSeat} seat
* @param {{ chainName: string }} offerArgs
*/
export const makeRestakeAccount = async (
orch,
{ makeRestakeKit, makeCombineInvitationMakers },
seat,
{ chainName },
) => {
trace('MakeRestakeAccount', chainName);
seat.exit();
const remoteChain = await orch.getChain(chainName);
const account =
/** @type {OrchestrationAccount<any> & StakingAccountActions} */ (
await remoteChain.makeAccount()
);
const restakeKit = /** @type {{ invitationMakers: InvitationMakers }} */ (
makeRestakeKit(account)
);
const { publicSubscribers, invitationMakers } =
await account.asContinuingOffer();

return /** @type {ContinuingOfferResult} */ ({
publicSubscribers,
invitationMakers: makeCombineInvitationMakers(
restakeKit.invitationMakers,
invitationMakers,
),
});
};
harden(makeRestakeAccount);

/**
* 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
* @param {object} _ctx
* @param {GuestInterface<CosmosOrchestrationAccount>} 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 account.delegate(validator, amounts[0]);
};
harden(wakerHandler);
18 changes: 18 additions & 0 deletions packages/orchestration/src/utils/orchestrationAccount.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { M } from '@endo/patterns';
import {
AmountArgShape,
ChainAddressShape,
DelegationShape,
DenomAmountShape,
IBCTransferOptionsShape,
} from '../typeGuards.js';
Expand Down Expand Up @@ -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()),
};
Loading
Loading