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

9796 continuing ica #10023

Merged
merged 4 commits into from
Sep 6, 2024
Merged
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
138 changes: 138 additions & 0 deletions packages/orchestration/src/examples/staking-combinations.contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* @file This contract demonstrates the continuing invitation pattern with async
* flows.
*
* The primary offer result is a power for invitation makers that can perform
* actions with an ICA account.
*/
import { AmountShape } from '@agoric/ertp';
import { VowShape } from '@agoric/vow';
import { M } from '@endo/patterns';
import { prepareCombineInvitationMakers } from '../exos/combine-invitation-makers.js';
import { CosmosOrchestrationInvitationMakersInterface } from '../exos/cosmos-orchestration-account.js';
import { withOrchestration } from '../utils/start-helper.js';
import * as flows from './staking-combinations.flows.js';

/**
* @import {GuestInterface} from '@agoric/async-flow';
* @import {Delegation} from '@agoric/cosmic-proto/cosmos/staking/v1beta1/staking.js';
* @import {ContinuingOfferResult} from '@agoric/smart-wallet/src/types.js';
* @import {TimerService} from '@agoric/time';
* @import {LocalChain} from '@agoric/vats/src/localchain.js';
* @import {NameHub} from '@agoric/vats';
* @import {Vow} from '@agoric/vow';
* @import {Remote} from '@agoric/internal';
* @import {Zone} from '@agoric/zone';
* @import {CosmosInterchainService} from '../exos/cosmos-interchain-service.js';
* @import {OrchestrationTools} from '../utils/start-helper.js';
* @import {CosmosOrchestrationAccount} from '../exos/cosmos-orchestration-account.js';
*/

const emptyOfferShape = harden({
// Nothing to give; the funds are deposited offline
give: {},
want: {}, // UNTIL https://github.com/Agoric/agoric-sdk/issues/2230
exit: M.any(),
});

/**
* Orchestration contract to be wrapped by withOrchestration for Zoe.
*
* @param {ZCF} zcf
* @param {{
* agoricNames: Remote<NameHub>;
* localchain: Remote<LocalChain>;
* orchestrationService: Remote<CosmosInterchainService>;
* storageNode: Remote<StorageNode>;
* marshaller: Marshaller;
* timerService: Remote<TimerService>;
* }} privateArgs
* @param {Zone} zone
* @param {OrchestrationTools} tools
*/
const contract = async (
zcf,
privateArgs,
zone,
{ orchestrateAll, vowTools },
) => {
const ExtraInvitationMakerInterface = M.interface('', {
DepositAndDelegate: M.call(M.array()).returns(VowShape),
UndelegateAndTransfer: M.call(M.array()).returns(VowShape),
});
/** @type {any} XXX async membrane */
const makeExtraInvitationMaker = zone.exoClass(
'ContinuingInvitationExampleInvitationMakers',
ExtraInvitationMakerInterface,
/** @param {GuestInterface<CosmosOrchestrationAccount>} account */
account => {
return { account };
},
{
DepositAndDelegate() {
const { account } = this.state;

const invP = zcf.makeInvitation(
(seat, validatorAddr, amountArg) =>
// eslint-disable-next-line no-use-before-define -- defined by orchestrateAll, necessarily after this
orchFns.depositAndDelegate(account, seat, validatorAddr, amountArg),
'Deposit and delegate',
undefined,
{
give: {
Stake: AmountShape,
},
},
);

return vowTools.watch(invP);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking on this more, I don't think we need vowTools.watch here. We can expect the promise for an Invitation to resolve promptly - it's just the offer handler that needs to return a vow.

If we feel we should return a vow here, we should also make this change in the orchAccount exos. Currently, we're always returning a promise.

The only instance coming to mind where it'd be helpful to return a vow is if a developer wanted to call an invitationMaker from inside a flow - it'd need to be a vow to cross the membrane. I'm not sure how common this will be - we don't have any examples that need this currently.

Copy link
Member Author

@turadg turadg Sep 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wanted to call an invitationMaker from inside a flow

I think that's what motivated the vow: an earlier version of this branch was calling between guest fns. But definitely not needed now. Thanks for pointing it out.

We can expect the promise for an Invitation to resolve promptly

I believe "promptly" means in "in the same run". zcf.makeInvitation calls out to another vat so I don't think it can be in the same run.

const invitationHandle = storeOfferHandler(offerHandler);
const invitationP = E(zoeInstanceAdmin).makeInvitation(
invitationHandle,

},
/**
* @param {Omit<Delegation, 'delegatorAddress'>[]} delegations
*/
UndelegateAndTransfer(delegations) {
const { account } = this.state;

const invP = zcf.makeInvitation(
// eslint-disable-next-line no-use-before-define -- defined by orchestrateAll, necessarily after this
() => orchFns.undelegateAndTransfer(account, delegations),
'Undelegate and transfer',
undefined,
emptyOfferShape,
);

return vowTools.watch(invP);
},
},
);

/** @type {any} XXX async membrane */
const makeCombineInvitationMakers = prepareCombineInvitationMakers(
zone,
CosmosOrchestrationInvitationMakersInterface,
ExtraInvitationMakerInterface,
);

const orchFns = orchestrateAll(flows, {
makeCombineInvitationMakers,
makeExtraInvitationMaker,
flows,
zcf,
});

const publicFacet = zone.exo('publicFacet', undefined, {
makeAccount() {
return zcf.makeInvitation(
orchFns.makeAccount,
'Make an ICA account',
undefined,
emptyOfferShape,
);
},
});

return harden({ publicFacet });
};

export const start = withOrchestration(contract);
harden(start);
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* @import {GuestInterface} from '@agoric/async-flow';
* @import {Orchestrator, OrchestrationFlow, OrchestrationAccount, OrchestrationAccountI, StakingAccountActions, AmountArg, CosmosValidatorAddress} from '../types.js'
* @import {ContinuingOfferResult, InvitationMakers} from '@agoric/smart-wallet/src/types.js';
* @import {MakeCombineInvitationMakers} from '../exos/combine-invitation-makers.js';
* @import {Delegation} from '@agoric/cosmic-proto/cosmos/staking/v1beta1/staking.js';
* @import {CosmosOrchestrationAccount} from '../exos/cosmos-orchestration-account.js';
*/

/**
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
* @param {{
* makeCombineInvitationMakers: MakeCombineInvitationMakers;
* makeExtraInvitationMaker: (account: any) => InvitationMakers;
* }} ctx
* @param {ZCFSeat} _seat
* @param {{ chainName: string }} offerArgs
* @returns {Promise<ContinuingOfferResult>}
*/
export const makeAccount = async (orch, ctx, _seat, { chainName }) => {
const chain = await orch.getChain(chainName);
const account = await chain.makeAccount();

const extraMakers = ctx.makeExtraInvitationMaker(account);

/** @type {ContinuingOfferResult} */
const result = await account.asContinuingOffer();

return {
...result,
invitationMakers: ctx.makeCombineInvitationMakers(
extraMakers,
result.invitationMakers,
),
};
};
harden(makeAccount);

/**
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
* @param {object} ctx
* @param {GuestInterface<CosmosOrchestrationAccount>} account
* @param {ZCFSeat} seat
* @param {CosmosValidatorAddress} validator
* @param {AmountArg} amount
* @returns {Promise<string>}
*/
export const depositAndDelegate = async (
orch,
ctx,
account,
seat,
validator,
amount,
) => {
console.log('depositAndDelegate', account, seat, validator, amount);
// TODO deposit the amount
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ticket # would be nice, but thanks for including this!

await account.delegate(validator, amount);
return 'guest depositAndDelegate complete';
};
harden(depositAndDelegate);

/**
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
* @param {object} ctx
* @param {GuestInterface<CosmosOrchestrationAccount>} account
* @param {Omit<Delegation, 'delegatorAddress'>[]} delegations
* @returns {Promise<string>}
*/
export const undelegateAndTransfer = async (
orch,
ctx,
account,
delegations,
) => {
await account.undelegate(delegations);
// TODO transfer something
return 'guest undelegateAndTransfer complete';
};
harden(undelegateAndTransfer);
53 changes: 53 additions & 0 deletions packages/orchestration/src/exos/combine-invitation-makers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { M } from '@endo/patterns';
import {
prepareGuardedAttenuator,
makeSyncMethodCallback,
} from '@agoric/internal/src/callback.js';
import { getMethodNames } from '@agoric/internal';

/**
* @import {InvitationMakers} from '@agoric/smart-wallet/src/types.js';
* @import {Zone} from '@agoric/zone';
*/

// TODO use a helper from Endo https://github.com/endojs/endo/issues/2448
/**
* Takes two or more InvitationMaker exos and combines them into a new one.
*
* @param {Zone} zone
* @param {import('@endo/patterns').InterfaceGuard[]} interfaceGuards
*/
export const prepareCombineInvitationMakers = (zone, ...interfaceGuards) => {
const methodGuards = interfaceGuards.map(ig => ig.payload.methodGuards);
const CombinedInterfaceGuard = M.interface(
'CombinedInvitationMakers interface',
Object.assign({}, ...methodGuards),
);

const mixin = prepareGuardedAttenuator(zone, CombinedInterfaceGuard, {
tag: 'CombinedInvitationMakers',
});

/**
* @template {InvitationMakers[]} IM
* @param {IM} invitationMakers
* @returns {IM[number]}
*/
const combineInvitationMakers = (...invitationMakers) => {
const overrides = {};
for (const invMakers of invitationMakers) {
// remove '__getInterfaceGuard__', '__getMethodNames__'
const names = getMethodNames(invMakers).filter(n => !n.startsWith('__'));
Copy link
Member

@0xpatrickdev 0xpatrickdev Sep 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nb: should we move this to @agoric/internal and call it getPublicMethodNames?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no opinion here. WDYT @mhofman ?

Copy link
Member Author

@turadg turadg Sep 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for (const key of names) {
overrides[key] = makeSyncMethodCallback(invMakers, key);
}
}
return mixin({
overrides,
});
};

return combineInvitationMakers;
};

/** @typedef {ReturnType<typeof prepareCombineInvitationMakers>} MakeCombineInvitationMakers */
40 changes: 22 additions & 18 deletions packages/orchestration/src/exos/cosmos-orchestration-account.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,27 @@ const PUBLIC_TOPICS = {
account: ['Staking Account holder status', M.any()],
};

export const CosmosOrchestrationInvitationMakersInterface = M.interface(
'invitationMakers',
{
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()),
},
);
harden(CosmosOrchestrationInvitationMakersInterface);

/**
* @param {Zone} zone
* @param {object} powers
Expand Down Expand Up @@ -177,24 +198,7 @@ export const prepareCosmosOrchestrationAccountKit = (
.returns(Vow$(M.record())),
}),
holder: IcaAccountHolderI,
invitationMakers: M.interface('invitationMakers', {
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()),
}),
invitationMakers: CosmosOrchestrationInvitationMakersInterface,
},
/**
* @param {object} info
Expand Down
Loading
Loading