From 99c4cae34ce70b48b96d78f6c3905eb7be771e2b Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 18 Jun 2024 10:09:45 -0700 Subject: [PATCH] feat(orchestration): localChainAccountKit returns unwrapped vows --- .../src/examples/stakeBld.contract.js | 4 +- .../src/exos/local-chain-account-kit.js | 257 +++++++++++++----- packages/orchestration/src/service.js | 6 +- .../orchestration/src/utils/start-helper.js | 4 +- packages/orchestration/src/utils/time.js | 2 + .../test/exos/local-chain-account-kit.test.ts | 12 +- 6 files changed, 212 insertions(+), 73 deletions(-) diff --git a/packages/orchestration/src/examples/stakeBld.contract.js b/packages/orchestration/src/examples/stakeBld.contract.js index 17fddccf346..6eb8fca3408 100644 --- a/packages/orchestration/src/examples/stakeBld.contract.js +++ b/packages/orchestration/src/examples/stakeBld.contract.js @@ -6,7 +6,7 @@ import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/record import { withdrawFromSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js'; import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; import { makeDurableZone } from '@agoric/zone/durable.js'; -import { V } from '@agoric/vow/vat.js'; +import { prepareVowTools, V } from '@agoric/vow/vat.js'; import { E } from '@endo/far'; import { deeplyFulfilled } from '@endo/marshal'; import { M } from '@endo/patterns'; @@ -40,12 +40,14 @@ export const start = async (zcf, privateArgs, baggage) => { baggage, privateArgs.marshaller, ); + const vowTools = prepareVowTools(zone.subZone('vows')); const makeLocalChainAccountKit = prepareLocalChainAccountKit( zone, makeRecorderKit, zcf, privateArgs.timerService, + vowTools, makeChainHub(privateArgs.agoricNames), ); diff --git a/packages/orchestration/src/exos/local-chain-account-kit.js b/packages/orchestration/src/exos/local-chain-account-kit.js index 3c5fd73f6ac..384f6709767 100644 --- a/packages/orchestration/src/exos/local-chain-account-kit.js +++ b/packages/orchestration/src/exos/local-chain-account-kit.js @@ -6,11 +6,11 @@ import { makeTracer } from '@agoric/internal'; import { M } from '@agoric/vat-data'; import { TopicsRecordShape } from '@agoric/zoe/src/contractSupport/index.js'; import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; -import { V } from '@agoric/vow/vat.js'; import { E } from '@endo/far'; import { AmountArgShape, ChainAddressShape, + ChainAmountShape, IBCTransferOptionsShape, } from '../typeGuards.js'; import { maxClockSkew } from '../utils/cosmos.js'; @@ -18,11 +18,13 @@ import { dateInSeconds, makeTimestampHelper } from '../utils/time.js'; /** * @import {LocalChainAccount} from '@agoric/vats/src/localchain.js'; - * @import {AmountArg, ChainAddress, DenomAmount, IBCMsgTransferOptions, CosmosChainInfo} from '@agoric/orchestration'; + * @import {AmountArg, ChainAddress, DenomAmount, IBCMsgTransferOptions, CosmosChainInfo, ChainInfo, Chain, IBCConnectionInfo} from '@agoric/orchestration'; * @import {RecorderKit, MakeRecorderKit} from '@agoric/zoe/src/contractSupport/recorder.js'. * @import {Zone} from '@agoric/zone'; * @import {Remote} from '@agoric/internal'; - * @import {TimerService, TimerBrand} from '@agoric/time'; + * @import {TimerService, TimerBrand, TimestampRecord} from '@agoric/time'; + * @import {PromiseVow, VowTools} from '@agoric/vow'; + * @import {TypedJson} from '@agoric/cosmic-proto'; * @import {ChainHub} from '../utils/chainHub.js'; */ @@ -65,6 +67,7 @@ const PUBLIC_TOPICS = { * @param {MakeRecorderKit} makeRecorderKit * @param {ZCF} zcf * @param {Remote} timerService + * @param {VowTools} vowTools * @param {ChainHub} chainHub */ export const prepareLocalChainAccountKit = ( @@ -72,6 +75,7 @@ export const prepareLocalChainAccountKit = ( makeRecorderKit, zcf, timerService, + { watch, when, allVows }, chainHub, ) => { const timestampHelper = makeTimestampHelper(timerService); @@ -81,6 +85,40 @@ export const prepareLocalChainAccountKit = ( 'LCA Kit', { holder: HolderI, + undelegateWatcher: M.interface('undelegateWatcher', { + onFulfilled: M.call(M.arrayOf(M.record())) // XXX consider specifying `completionTime` + .optional(M.arrayOf(M.undefined())) // empty context + .returns(M.promise()), + }), + getChainInfoWatcher: M.interface('getChainInfoWatcher', { + onFulfilled: M.call(M.record()) // agoric chain info + .optional({ destination: ChainAddressShape }) // empty context + .returns(M.promise()), // transfer channel + }), + getTimeoutTimestampWatcher: M.interface('getTimeoutTimestampWatcher', { + onFulfilled: M.call(M.bigint()) + .optional(IBCTransferOptionsShape) + .returns(M.bigint()), + }), + transferWatcher: M.interface('transferWatcher', { + onFulfilled: M.call(M.any()) + .optional({ + destination: ChainAddressShape, + opts: M.or(M.undefined(), IBCTransferOptionsShape), + amount: ChainAmountShape, + }) + .returns(M.promise()), + }), + extractFirstResultWatcher: M.interface('extractFirstResultWatcher', { + onFulfilled: M.call(M.arrayOf(M.record())) + .optional(M.arrayOf(M.undefined())) + .returns(M.record()), + }), + returnVoidWatcher: M.interface('extractFirstResultWatcher', { + onFulfilled: M.call(M.arrayOf(M.record())) + .optional(M.arrayOf(M.undefined())) + .returns(M.undefined()), + }), invitationMakers: M.interface('invitationMakers', { Delegate: M.callWhen(M.string(), AmountShape).returns(InvitationShape), Undelegate: M.callWhen(M.string(), AmountShape).returns( @@ -137,6 +175,110 @@ export const prepareLocalChainAccountKit = ( throw Error('not yet implemented'); }, }, + undelegateWatcher: { + /** + * @param {[ + * TypedJson<'/cosmos.staking.v1beta1.MsgUndelegateResponse'>, + * ]} response + */ + onFulfilled(response) { + const { completionTime } = response[0]; + return E(timerService).wakeAt( + // TODO clean up date handling once we have real data + dateInSeconds(new Date(completionTime)) + maxClockSkew, + ); + }, + }, + getChainInfoWatcher: { + /** + * @param {ChainInfo} agoricChainInfo + * @param {{ destination: ChainAddress }} ctx + */ + onFulfilled(agoricChainInfo, { destination }) { + return chainHub.getConnectionInfo( + agoricChainInfo.chainId, + destination.chainId, + ); + }, + }, + getTimeoutTimestampWatcher: { + /** + * @param {bigint} timeoutTimestamp + * @param {{ opts: IBCMsgTransferOptions }} ctx + */ + onFulfilled(timeoutTimestamp, { opts }) { + // FIXME: do not call `getTimeoutTimestampNS` if `opts.timeoutTimestamp` or `opts.timeoutHeight` is provided + return ( + opts?.timeoutTimestamp ?? + (opts?.timeoutHeight ? 0n : timeoutTimestamp) + ); + }, + }, + transferWatcher: { + /** + * @param {[ + * { transferChannel: IBCConnectionInfo['transferChannel'] }, + * bigint, + * ]} params + * @param {{ + * destination: ChainAddress; + * opts: IBCMsgTransferOptions; + * amount: DenomAmount; + * }} ctx + */ + onFulfilled( + [{ transferChannel }, timeoutTimestamp], + { opts, amount, destination }, + ) { + return E(this.state.account).executeTx([ + typedJson('/ibc.applications.transfer.v1.MsgTransfer', { + sourcePort: transferChannel.portId, + sourceChannel: transferChannel.channelId, + token: { + amount: String(amount.value), + denom: amount.denom, + }, + sender: this.state.address, + receiver: destination.address, + timeoutHeight: opts?.timeoutHeight ?? { + revisionHeight: 0n, + revisionNumber: 0n, + }, + timeoutTimestamp, + memo: opts?.memo ?? '', + }), + ]); + }, + }, + /** + * takes an array of results (from `executeEncodedTx`) and returns the + * first result + */ + extractFirstResultWatcher: { + /** + * @param {Record[]} results + */ + onFulfilled(results) { + results.length === 1 || + Fail`expected exactly one result; got ${results}`; + return results[0]; + }, + }, + /** + * takes an array of results (from `executeEncodedTx`) and returns void + * since we are not interested in the result + */ + returnVoidWatcher: { + /** + * @param {Record[]} results + */ + onFulfilled(results) { + results.length === 1 || + Fail`expected exactly one result; got ${results}`; + trace('Result', results[0]); + return undefined; + }, + }, holder: { getPublicTopics() { const { topicKit } = this.state; @@ -159,23 +301,24 @@ export const prepareLocalChainAccountKit = ( denom: 'ubld', }; const { account: lca } = this.state; - trace('lca', lca); - const delegatorAddress = await V(lca).getAddress(); - trace('delegatorAddress', delegatorAddress); - const [result] = await V(lca).executeTx([ - typedJson('/cosmos.staking.v1beta1.MsgDelegate', { - amount, - validatorAddress, - delegatorAddress, - }), - ]); - trace('got result', result); - return result; + + return when( + watch( + E(lca).executeTx([ + typedJson('/cosmos.staking.v1beta1.MsgDelegate', { + amount, + validatorAddress, + delegatorAddress: this.state.address, + }), + ]), + this.facets.extractFirstResultWatcher, + ), + ); }, /** * @param {string} validatorAddress * @param {Amount<'nat'>} ertpAmount - * @returns {Promise} + * @returns {PromiseVow} */ async undelegate(validatorAddress, ertpAmount) { // TODO #9211 lookup denom from brand @@ -184,22 +327,18 @@ export const prepareLocalChainAccountKit = ( denom: 'ubld', }; const { account: lca } = this.state; - trace('lca', lca); - const delegatorAddress = await V(lca).getAddress(); - trace('delegatorAddress', delegatorAddress); - const [response] = await V(lca).executeTx([ - typedJson('/cosmos.staking.v1beta1.MsgUndelegate', { - amount, - validatorAddress, - delegatorAddress, - }), - ]); - trace('undelegate response', response); - const { completionTime } = response; - - await E(timerService).wakeAt( - // TODO clean up date handling once we have real data - dateInSeconds(new Date(completionTime)) + maxClockSkew, + return when( + watch( + E(lca).executeTx([ + typedJson('/cosmos.staking.v1beta1.MsgUndelegate', { + amount, + validatorAddress, + delegatorAddress: this.state.address, + }), + ]), + // @ts-expect-error Type 'JsonSafe' is not assignable to type 'MsgUndelegateResponse'. + this.facets.undelegateWatcher, + ), ); }, /** @@ -209,16 +348,17 @@ export const prepareLocalChainAccountKit = ( */ /** @type {LocalChainAccount['deposit']} */ async deposit(payment, optAmountShape) { - return V(this.state.account).deposit(payment, optAmountShape); + return when( + watch(E(this.state.account).deposit(payment, optAmountShape)), + ); }, /** @type {LocalChainAccount['withdraw']} */ async withdraw(amount) { - return V(this.state.account).withdraw(amount); + return when(watch(E(this.state.account).withdraw(amount))); }, /** @type {LocalChainAccount['executeTx']} */ async executeTx(messages) { - // @ts-expect-error subtype - return V(this.state.account).executeTx(messages); + return when(watch(E(this.state.account).executeTx(messages))); }, /** @returns {ChainAddress['address']} */ getAddress() { @@ -238,40 +378,27 @@ export const prepareLocalChainAccountKit = ( // TODO #9211 lookup denom from brand if ('brand' in amount) throw Fail`ERTP Amounts not yet supported`; - const agoricChainInfo = await chainHub.getChainInfo('agoric'); - const { transferChannel } = await chainHub.getConnectionInfo( - agoricChainInfo.chainId, - destination.chainId, + const connectionInfoV = watch( + chainHub.getChainInfo('agoric'), + this.facets.getChainInfoWatcher, + { destination }, ); - await null; // set a `timeoutTimestamp` if caller does not supply either `timeoutHeight` or `timeoutTimestamp` // TODO #9324 what's a reasonable default? currently 5 minutes - const timeoutTimestamp = - opts?.timeoutTimestamp ?? - (opts?.timeoutHeight - ? 0n - : await timestampHelper.getTimeoutTimestampNS()); + // FIXME: do not call `getTimeoutTimestampNS` if `opts.timeoutTimestamp` or `opts.timeoutHeight` is provided + const timeoutTimestampV = watch( + timestampHelper.getTimeoutTimestampNS(), + this.facets.getTimeoutTimestampWatcher, + { opts }, + ); - const [result] = await V(this.state.account).executeTx([ - typedJson('/ibc.applications.transfer.v1.MsgTransfer', { - sourcePort: transferChannel.portId, - sourceChannel: transferChannel.channelId, - token: { - amount: String(amount.value), - denom: amount.denom, - }, - sender: this.state.address, - receiver: destination.address, - timeoutHeight: opts?.timeoutHeight ?? { - revisionHeight: 0n, - revisionNumber: 0n, - }, - timeoutTimestamp, - memo: opts?.memo ?? '', - }), - ]); - trace('MsgTransfer result', result); + const transferV = watch( + allVows([connectionInfoV, timeoutTimestampV]), + this.facets.transferWatcher, + { opts, amount, destination }, + ); + return when(watch(transferV, this.facets.returnVoidWatcher)); }, }, }, diff --git a/packages/orchestration/src/service.js b/packages/orchestration/src/service.js index 635edafed22..2fb26ee227e 100644 --- a/packages/orchestration/src/service.js +++ b/packages/orchestration/src/service.js @@ -194,7 +194,7 @@ const prepareOrchestrationKit = ( * @param {IBCConnectionID} controllerConnectionId self connection_id * @returns {Promise} */ - makeAccount(chainId, hostConnectionId, controllerConnectionId) { + async makeAccount(chainId, hostConnectionId, controllerConnectionId) { const remoteConnAddr = makeICAChannelAddress( hostConnectionId, controllerConnectionId, @@ -213,9 +213,9 @@ const prepareOrchestrationKit = ( }, /** * @param {IBCConnectionID} controllerConnectionId - * @returns {ICQConnection | Promise} + * @returns {Promise} */ - provideICQConnection(controllerConnectionId) { + async provideICQConnection(controllerConnectionId) { if (this.state.icqConnections.has(controllerConnectionId)) { // TODO #9281 do not return synchronously. see https://github.com/Agoric/agoric-sdk/pull/9454#discussion_r1626898694 return this.state.icqConnections.get(controllerConnectionId) diff --git a/packages/orchestration/src/utils/start-helper.js b/packages/orchestration/src/utils/start-helper.js index 31e2b79b914..f0f653ac864 100644 --- a/packages/orchestration/src/utils/start-helper.js +++ b/packages/orchestration/src/utils/start-helper.js @@ -45,16 +45,18 @@ export const provideOrchestration = ( const chainHub = makeChainHub(remotePowers.agoricNames); + const vowTools = prepareVowTools(zone.subZone('vows')); + const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller); const makeLocalChainAccountKit = prepareLocalChainAccountKit( zone, makeRecorderKit, zcf, remotePowers.timerService, + vowTools, chainHub, ); - const vowTools = prepareVowTools(zone.subZone('vows')); const asyncFlowTools = prepareAsyncFlowTools(zone.subZone('asyncFlow'), { vowTools, }); diff --git a/packages/orchestration/src/utils/time.js b/packages/orchestration/src/utils/time.js index 975069f4efa..a7e63504a12 100644 --- a/packages/orchestration/src/utils/time.js +++ b/packages/orchestration/src/utils/time.js @@ -21,6 +21,8 @@ export function makeTimestampHelper(timer) { return harden({ /** + * XXX do this need to be resumable / use Vows? + * * Takes the current time from ChainTimerService and adds a relative time to * determine a timeout timestamp in nanoseconds. Useful for * {@link MsgTransfer.timeoutTimestamp}. diff --git a/packages/orchestration/test/exos/local-chain-account-kit.test.ts b/packages/orchestration/test/exos/local-chain-account-kit.test.ts index 88547d4f22f..26ffa6c9956 100644 --- a/packages/orchestration/test/exos/local-chain-account-kit.test.ts +++ b/packages/orchestration/test/exos/local-chain-account-kit.test.ts @@ -15,7 +15,8 @@ test('deposit, withdraw', async t => { const { bld: stake } = brands; - const { timer, localchain, marshaller, rootZone, storage } = bootstrap; + const { timer, localchain, marshaller, rootZone, storage, vowTools } = + bootstrap; t.log('chainInfo mocked via `prepareMockChainInfo` until #8879'); @@ -30,6 +31,7 @@ test('deposit, withdraw', async t => { // @ts-expect-error mocked zcf. use `stake-bld.contract.test.ts` to test LCA with offer Far('MockZCF', {}), timer, + vowTools, makeChainHub(bootstrap.agoricNames), ); @@ -81,7 +83,8 @@ test('delegate, undelegate', async t => { const { bld } = brands; - const { timer, localchain, marshaller, rootZone, storage } = bootstrap; + const { timer, localchain, marshaller, rootZone, storage, vowTools } = + bootstrap; t.log('exo setup - prepareLocalChainAccountKit'); const { makeRecorderKit } = prepareRecorderKitMakers( @@ -94,6 +97,7 @@ test('delegate, undelegate', async t => { // @ts-expect-error mocked zcf. use `stake-bld.contract.test.ts` to test LCA with offer Far('MockZCF', {}), timer, + vowTools, makeChainHub(bootstrap.agoricNames), ); @@ -128,7 +132,8 @@ test('transfer', async t => { const { bld: stake } = brands; - const { timer, localchain, marshaller, rootZone, storage } = bootstrap; + const { timer, localchain, marshaller, rootZone, storage, vowTools } = + bootstrap; t.log('exo setup - prepareLocalChainAccountKit'); const { makeRecorderKit } = prepareRecorderKitMakers( @@ -141,6 +146,7 @@ test('transfer', async t => { // @ts-expect-error mocked zcf. use `stake-bld.contract.test.ts` to test LCA with offer Far('MockZCF', {}), timer, + vowTools, makeChainHub(bootstrap.agoricNames), );