From 5f50d9208ccf6fac95c70a7f7eb565816175fbab Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Wed, 11 Sep 2024 12:34:16 -0400 Subject: [PATCH 1/2] feat: use orchFns.autoStake as target for receiveUpcall - uses an async-flow function as the handler for a `.monitorTransfers()` tap - allows guest develoeprs to write handlers as flows instead of with vows and watchers --- .../src/examples/auto-stake-it-tap-kit.js | 154 ------------------ .../src/examples/auto-stake-it.contract.js | 79 ++++++++- .../src/examples/auto-stake-it.flows.js | 81 ++++++++- .../src/exos/local-orchestration-account.js | 1 + 4 files changed, 144 insertions(+), 171 deletions(-) delete mode 100644 packages/orchestration/src/examples/auto-stake-it-tap-kit.js 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 8e73fc74f61..00000000000 --- a/packages/orchestration/src/examples/auto-stake-it-tap-kit.js +++ /dev/null @@ -1,154 +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(remoteChainAddress, { - denom: localDenom, - value: BigInt(tx.amount), - }), - 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 667a6ebc72f..4b4722fe1de 100644 --- a/packages/orchestration/src/examples/auto-stake-it.contract.js +++ b/packages/orchestration/src/examples/auto-stake-it.contract.js @@ -2,20 +2,57 @@ 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 { registerChainsAndAssets } from '../utils/chain-hub-helper.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, CosmosChainInfo, DenomDetail} from '@agoric/orchestration'; + * @import {Passable} from '@endo/marshal'; + * @import {CosmosOrchestrationAccount} from '../exos/cosmos-orchestration-account.js'; + * @import {LocalOrchestrationAccount} from '../exos/local-orchestration-account.js'; * @import {OrchestrationPowers, OrchestrationTools} from '../utils/start-helper.js'; - * @import {CosmosChainInfo, Denom, DenomDetail} from '../types.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. @@ -37,16 +74,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 & Passable} event + */ + receiveUpcall(event) { + trace('receiveUpcall', event); + const { localAccount, stakingAccount, config } = this.state; + + orchFns.autoStake(localAccount, stakingAccount, config, event); + }, + }, + { + stateShape: StakingTapStateShape, + }, + ); + + const orchFns = orchestrateAll(flows, { makeStakingTap, makePortfolioHolder, chainHub, @@ -60,7 +123,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..13a38c9a29b 100644 --- a/packages/orchestration/src/examples/auto-stake-it.flows.js +++ b/packages/orchestration/src/examples/auto-stake-it.flows.js @@ -1,20 +1,30 @@ +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 {Passable} from '@endo/marshal'; * @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 +74,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 +114,52 @@ 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 & Passable} 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(remoteChainAddress, { + denom: localDenom, + value: BigInt(tx.amount), + }); + + 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 9a35b866133..0ee25a42494 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -748,3 +748,4 @@ export const prepareLocalOrchestrationAccountKit = ( /** @typedef {ReturnType} MakeLocalOrchestrationAccountKit */ /** @typedef {ReturnType} LocalOrchestrationAccountKit */ +/** @typedef {LocalOrchestrationAccountKit['holder']} LocalOrchestrationAccount */ From 937c55bda2727371a30abe9a8753940e7737aa2f Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Wed, 12 Feb 2025 13:24:06 -0500 Subject: [PATCH 2/2] chore: `OrchestrationAccount` types --- .../src/examples/auto-stake-it.contract.js | 12 ++++++------ .../src/examples/auto-stake-it.flows.js | 3 --- packages/orchestration/src/orchestration-api.ts | 13 ++++++------- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/packages/orchestration/src/examples/auto-stake-it.contract.js b/packages/orchestration/src/examples/auto-stake-it.contract.js index 4b4722fe1de..2c4ac33201f 100644 --- a/packages/orchestration/src/examples/auto-stake-it.contract.js +++ b/packages/orchestration/src/examples/auto-stake-it.contract.js @@ -14,21 +14,21 @@ 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, CosmosChainInfo, DenomDetail} from '@agoric/orchestration'; + * @import {ChainAddress, CosmosValidatorAddress, CosmosChainInfo, Denom, DenomDetail, OrchestrationAccount} from '@agoric/orchestration'; * @import {Passable} from '@endo/marshal'; - * @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; + * stakingAccount: OrchestrationAccount<{ + * chainId: 'osmosis-1'; + * stakingTokens: [{ denom: 'uosmo' }]; + * }>; + * localAccount: OrchestrationAccount<{ chainId: 'agoric-3' }>; * config: { * validator: CosmosValidatorAddress; * localChainAddress: ChainAddress; diff --git a/packages/orchestration/src/examples/auto-stake-it.flows.js b/packages/orchestration/src/examples/auto-stake-it.flows.js index 13a38c9a29b..8f484762826 100644 --- a/packages/orchestration/src/examples/auto-stake-it.flows.js +++ b/packages/orchestration/src/examples/auto-stake-it.flows.js @@ -74,9 +74,7 @@ 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, config: { validator, @@ -88,7 +86,6 @@ export const makeAccounts = async ( }, }); // XXX consider storing appRegistration, so we can .revoke() or .updateTargetApp() - // @ts-expect-error tap.receiveUpcall: 'Vow | undefined' not assignable to 'Promise' await localAccount.monitorTransfers(tap); const accountEntries = harden( diff --git a/packages/orchestration/src/orchestration-api.ts b/packages/orchestration/src/orchestration-api.ts index d258b56aa62..b51ba9d2f63 100644 --- a/packages/orchestration/src/orchestration-api.ts +++ b/packages/orchestration/src/orchestration-api.ts @@ -69,13 +69,12 @@ export type ChainAddress = { * * The methods available depend on the chain and its capabilities. */ -export type OrchestrationAccount = - OrchestrationAccountCommon & - (CI extends CosmosChainInfo - ? CI['chainId'] extends `agoric${string}` - ? LocalAccountMethods - : CosmosChainAccountMethods - : object); +export type OrchestrationAccount = OrchestrationAccountCommon & + (CI extends CosmosChainInfo + ? CI['chainId'] extends `agoric${string}` + ? LocalAccountMethods & Passable + : CosmosChainAccountMethods & Passable + : Passable); /** * An object for access the core functions of a remote chain.