diff --git a/packages/orchestration/src/examples/auto-stake-it-tap-kit.js b/packages/orchestration/src/examples/auto-stake-it-tap-kit.js deleted file mode 100644 index 7092c704343..00000000000 --- a/packages/orchestration/src/examples/auto-stake-it-tap-kit.js +++ /dev/null @@ -1,157 +0,0 @@ -import { M, mustMatch } from '@endo/patterns'; -import { E } from '@endo/far'; -import { VowShape } from '@agoric/vow'; -import { makeTracer } from '@agoric/internal'; -import { atob } from '@endo/base64'; -import { ChainAddressShape } from '../typeGuards.js'; - -const trace = makeTracer('AutoStakeItTap'); - -/** - * @import {IBCChannelID, VTransferIBCEvent} from '@agoric/vats'; - * @import {VowTools} from '@agoric/vow'; - * @import {Zone} from '@agoric/zone'; - * @import {TargetApp} from '@agoric/vats/src/bridge-target.js'; - * @import {ChainAddress, CosmosValidatorAddress, Denom, OrchestrationAccount, StakingAccountActions} from '@agoric/orchestration'; - * @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js'; - * @import {TypedPattern} from '@agoric/internal'; - */ - -/** - * @typedef {{ - * stakingAccount: ERef & StakingAccountActions>; - * localAccount: ERef>; - * validator: CosmosValidatorAddress; - * localChainAddress: ChainAddress; - * remoteChainAddress: ChainAddress; - * sourceChannel: IBCChannelID; - * remoteDenom: Denom; - * localDenom: Denom; - * }} StakingTapState - */ - -/** @type {TypedPattern} */ -const StakingTapStateShape = { - stakingAccount: M.remotable('CosmosOrchestrationAccount'), - localAccount: M.remotable('LocalOrchestrationAccount'), - validator: ChainAddressShape, - localChainAddress: ChainAddressShape, - remoteChainAddress: ChainAddressShape, - sourceChannel: M.string(), - remoteDenom: M.string(), - localDenom: M.string(), -}; -harden(StakingTapStateShape); - -/** - * @param {Zone} zone - * @param {VowTools} vowTools - */ -const prepareStakingTapKit = (zone, { watch }) => { - return zone.exoClassKit( - 'StakingTapKit', - { - tap: M.interface('AutoStakeItTap', { - receiveUpcall: M.call(M.record()).returns( - M.or(VowShape, M.undefined()), - ), - }), - transferWatcher: M.interface('TransferWatcher', { - onFulfilled: M.call(M.undefined()) - .optional(M.bigint()) - .returns(VowShape), - }), - }, - /** @param {StakingTapState} initialState */ - initialState => { - mustMatch(initialState, StakingTapStateShape); - return harden(initialState); - }, - { - tap: { - /** - * Transfers from localAccount to stakingAccount, then delegates from - * the stakingAccount to `validator` if the expected token (remoteDenom) - * is received. - * - * @param {VTransferIBCEvent} event - */ - receiveUpcall(event) { - trace('receiveUpcall', event); - - // ignore packets from unknown channels - if (event.packet.source_channel !== this.state.sourceChannel) { - return; - } - - const tx = /** @type {FungibleTokenPacketData} */ ( - JSON.parse(atob(event.packet.data)) - ); - trace('receiveUpcall packet data', tx); - - const { remoteDenom, localChainAddress } = this.state; - // ignore outgoing transfers - if (tx.receiver !== localChainAddress.value) { - return; - } - // only interested in transfers of `remoteDenom` - if (tx.denom !== remoteDenom) { - return; - } - - const { localAccount, localDenom, remoteChainAddress } = this.state; - return watch( - E(localAccount).transfer( - { - denom: localDenom, - value: BigInt(tx.amount), - }, - remoteChainAddress, - ), - this.facets.transferWatcher, - BigInt(tx.amount), - ); - }, - }, - transferWatcher: { - /** - * @param {void} _result - * @param {bigint} value the qty of uatom to delegate - */ - onFulfilled(_result, value) { - const { stakingAccount, validator, remoteDenom } = this.state; - return watch( - E(stakingAccount).delegate(validator, { - denom: remoteDenom, - value, - }), - ); - }, - }, - }, - ); -}; - -/** - * Provides a {@link TargetApp} that reacts to an incoming IBC transfer by: - * - * 1. transferring the funds to the staking account specified at initialization - * 2. delegating the funds to the validator specified at initialization - * - * XXX consider a facet with a method for changing the validator - * - * XXX consider logic for multiple stakingAccounts + denoms - * - * @param {Zone} zone - * @param {VowTools} vowTools - * @returns {( - * ...args: Parameters> - * ) => ReturnType>['tap']} - */ -export const prepareStakingTap = (zone, vowTools) => { - const makeKit = prepareStakingTapKit(zone, vowTools); - return (...args) => makeKit(...args).tap; -}; - -/** @typedef {ReturnType} MakeStakingTap */ -/** @typedef {ReturnType} StakingTap */ diff --git a/packages/orchestration/src/examples/auto-stake-it.contract.js b/packages/orchestration/src/examples/auto-stake-it.contract.js index d08781e47ff..7ebdcba9d49 100644 --- a/packages/orchestration/src/examples/auto-stake-it.contract.js +++ b/packages/orchestration/src/examples/auto-stake-it.contract.js @@ -2,18 +2,55 @@ import { EmptyProposalShape, InvitationShape, } from '@agoric/zoe/src/typeGuards.js'; +import { makeTracer } from '@agoric/internal'; import { M } from '@endo/patterns'; import { prepareChainHubAdmin } from '../exos/chain-hub-admin.js'; import { preparePortfolioHolder } from '../exos/portfolio-holder-kit.js'; import { withOrchestration } from '../utils/start-helper.js'; -import { prepareStakingTap } from './auto-stake-it-tap-kit.js'; import * as flows from './auto-stake-it.flows.js'; +import { ChainAddressShape } from '../typeGuards.js'; + +const trace = makeTracer('AutoStakeIt'); /** + * @import {GuestInterface} from '@agoric/async-flow'; * @import {Zone} from '@agoric/zone'; + * @import {IBCChannelID, VTransferIBCEvent} from '@agoric/vats'; + * @import {TargetApp} from '@agoric/vats/src/bridge-target.js'; + * @import {ChainAddress, CosmosValidatorAddress, Denom} from '@agoric/orchestration'; + * @import {CosmosOrchestrationAccount} from '../exos/cosmos-orchestration-account.js'; + * @import {LocalOrchestrationAccount} from '../exos/local-orchestration-account.js'; * @import {OrchestrationPowers, OrchestrationTools} from '../utils/start-helper.js'; */ +/** + * @typedef {{ + * stakingAccount: GuestInterface; + * localAccount: GuestInterface; + * config: { + * validator: CosmosValidatorAddress; + * localChainAddress: ChainAddress; + * remoteChainAddress: ChainAddress; + * sourceChannel: IBCChannelID; + * remoteDenom: Denom; + * localDenom: Denom; + * }; + * }} StakingTapState + */ + +const StakingTapStateShape = harden({ + stakingAccount: M.remotable('CosmosOrchestrationAccount'), + localAccount: M.remotable('LocalOrchestrationAccount'), + config: { + validator: ChainAddressShape, + localChainAddress: ChainAddressShape, + remoteChainAddress: ChainAddressShape, + sourceChannel: M.string(), + remoteDenom: M.string(), + localDenom: M.string(), + }, +}); + /** * AutoStakeIt allows users to to create an auto-forwarding address that * transfers and stakes tokens on a remote chain when received. @@ -33,16 +70,42 @@ const contract = async ( zone, { chainHub, orchestrateAll, vowTools }, ) => { - const makeStakingTap = prepareStakingTap( - zone.subZone('stakingTap'), - vowTools, - ); const makePortfolioHolder = preparePortfolioHolder( zone.subZone('portfolio'), vowTools, ); - const { makeAccounts } = orchestrateAll(flows, { + /** + * Provides a {@link TargetApp} that reacts to an incoming IBC transfer. + */ + const makeStakingTap = zone.exoClass( + 'StakingTap', + M.interface('AutoStakeItTap', { + receiveUpcall: M.call(M.record()).returns(M.undefined()), + }), + /** @param {StakingTapState} initialState */ + initialState => harden(initialState), + { + /** + * Transfers from localAccount to stakingAccount, then delegates from the + * stakingAccount to `validator` if the expected token (remoteDenom) is + * received. + * + * @param {VTransferIBCEvent} event + */ + receiveUpcall(event) { + trace('receiveUpcall', event); + const { localAccount, stakingAccount, config } = this.state; + // eslint-disable-next-line no-use-before-define -- defined by orchestrateAll, necessarily after this + orchFns.autoStake(localAccount, stakingAccount, config, event); + }, + }, + { + stateShape: StakingTapStateShape, + }, + ); + + const orchFns = orchestrateAll(flows, { makeStakingTap, makePortfolioHolder, chainHub, @@ -56,7 +119,7 @@ const contract = async ( { makeAccountsInvitation() { return zcf.makeInvitation( - makeAccounts, + orchFns.makeAccounts, 'Make Accounts', undefined, EmptyProposalShape, diff --git a/packages/orchestration/src/examples/auto-stake-it.flows.js b/packages/orchestration/src/examples/auto-stake-it.flows.js index 74d1fc839a7..403e5fcdf1d 100644 --- a/packages/orchestration/src/examples/auto-stake-it.flows.js +++ b/packages/orchestration/src/examples/auto-stake-it.flows.js @@ -1,20 +1,29 @@ +import { makeTracer } from '@agoric/internal'; +import { atob } from '@endo/base64'; import { Fail } from '@endo/errors'; import { denomHash } from '../utils/denomHash.js'; +const trace = makeTracer('AutoStakeItFlows'); + /** * @import {ResolvedPublicTopic} from '@agoric/zoe/src/contractSupport/topics.js'; * @import {GuestInterface} from '@agoric/async-flow'; - * @import {CosmosValidatorAddress, Orchestrator, CosmosInterchainService, Denom, OrchestrationAccount, StakingAccountActions, OrchestrationFlow} from '@agoric/orchestration'; - * @import {MakeStakingTap} from './auto-stake-it-tap-kit.js'; + * @import {VTransferIBCEvent} from '@agoric/vats'; + * @import {CosmosValidatorAddress, Orchestrator, OrchestrationAccount, StakingAccountActions, OrchestrationFlow} from '@agoric/orchestration'; + * @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js'; + * @import {Guarded} from '@endo/exo'; * @import {MakePortfolioHolder} from '../exos/portfolio-holder-kit.js'; * @import {ChainHub} from '../exos/chain-hub.js'; + * @import {StakingTapState} from './auto-stake-it.contract.js'; */ /** * @satisfies {OrchestrationFlow} * @param {Orchestrator} orch * @param {{ - * makeStakingTap: MakeStakingTap; + * makeStakingTap: ( + * initialState: StakingTapState, + * ) => Guarded<{ receiveUpcall: (event: VTransferIBCEvent) => void }>; * makePortfolioHolder: MakePortfolioHolder; * chainHub: GuestInterface; * }} ctx @@ -64,14 +73,18 @@ export const makeAccounts = async ( // Every time the `localAccount` receives `remoteDenom` over IBC, delegate it. const tap = makeStakingTap({ + // @ts-expect-error LocalOrchestrationAccount vs. OrchestrationAccount localAccount, + // @ts-expect-error CosmosOrchestrationAccount vs. OrchestrationAccount stakingAccount, - validator, - localChainAddress, - remoteChainAddress, - sourceChannel: transferChannel.counterPartyChannelId, - remoteDenom, - localDenom, + config: { + validator, + localChainAddress, + remoteChainAddress, + sourceChannel: transferChannel.counterPartyChannelId, + remoteDenom, + localDenom, + }, }); // XXX consider storing appRegistration, so we can .revoke() or .updateTargetApp() // @ts-expect-error tap.receiveUpcall: 'Vow | undefined' not assignable to 'Promise' @@ -100,3 +113,55 @@ export const makeAccounts = async ( return portfolioHolder.asContinuingOffer(); }; harden(makeAccounts); + +/** + * @satisfies {OrchestrationFlow} + * @param {Orchestrator} orch + * @param {object} ctx + * @param {StakingTapState['localAccount']} localAccount + * @param {StakingTapState['stakingAccount']} stakingAccount + * @param {StakingTapState['config']} config + * @param {VTransferIBCEvent} event + */ +export const autoStake = async ( + orch, + ctx, + localAccount, + stakingAccount, + config, + event, +) => { + // ignore packets from unknown channels + if (event.packet.source_channel !== config.sourceChannel) { + return; + } + const tx = /** @type {FungibleTokenPacketData} */ ( + JSON.parse(atob(event.packet.data)) + ); + trace('receiveUpcall packet data', tx); + const { remoteDenom, localChainAddress } = config; + // ignore outgoing transfers + if (tx.receiver !== localChainAddress.value) { + return; + } + // only interested in transfers of `remoteDenom` + if (tx.denom !== remoteDenom) { + return; + } + + const { localDenom, remoteChainAddress, validator } = config; + + await localAccount.transfer( + { + denom: localDenom, + value: BigInt(tx.amount), + }, + remoteChainAddress, + ); + + await stakingAccount.delegate(validator, { + denom: remoteDenom, + value: BigInt(tx.amount), + }); +}; +harden(autoStake); diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index a17c4d4063c..3b727cee21c 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -664,3 +664,4 @@ export const prepareLocalOrchestrationAccountKit = ( /** @typedef {ReturnType} MakeLocalOrchestrationAccountKit */ /** @typedef {ReturnType} LocalOrchestrationAccountKit */ +/** @typedef {LocalOrchestrationAccountKit['holder']} LocalOrchestrationAccount */