Skip to content

Commit

Permalink
feat(orchestration): localChainAccountKit returns unwrapped vows
Browse files Browse the repository at this point in the history
  • Loading branch information
0xpatrickdev committed Jun 18, 2024
1 parent d106f82 commit 99c4cae
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 73 deletions.
4 changes: 3 additions & 1 deletion packages/orchestration/src/examples/stakeBld.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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),
);

Expand Down
257 changes: 192 additions & 65 deletions packages/orchestration/src/exos/local-chain-account-kit.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,25 @@ 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';
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';
*/

Expand Down Expand Up @@ -65,13 +67,15 @@ const PUBLIC_TOPICS = {
* @param {MakeRecorderKit} makeRecorderKit
* @param {ZCF} zcf
* @param {Remote<TimerService>} timerService
* @param {VowTools} vowTools
* @param {ChainHub} chainHub
*/
export const prepareLocalChainAccountKit = (
zone,
makeRecorderKit,
zcf,
timerService,
{ watch, when, allVows },
chainHub,
) => {
const timestampHelper = makeTimestampHelper(timerService);
Expand All @@ -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(
Expand Down Expand Up @@ -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<unknown, unknown>[]} 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<unknown, unknown>[]} 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;
Expand All @@ -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<void>}
* @returns {PromiseVow<void | TimestampRecord>}
*/
async undelegate(validatorAddress, ertpAmount) {
// TODO #9211 lookup denom from brand
Expand All @@ -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<MsgUndelegateResponse & { '@type': "/cosmos.staking.v1beta1.MsgUndelegateResponse"; }>' is not assignable to type 'MsgUndelegateResponse'.
this.facets.undelegateWatcher,
),
);
},
/**
Expand All @@ -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() {
Expand All @@ -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));
},
},
},
Expand Down
6 changes: 3 additions & 3 deletions packages/orchestration/src/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ const prepareOrchestrationKit = (
* @param {IBCConnectionID} controllerConnectionId self connection_id
* @returns {Promise<IcaAccount>}
*/
makeAccount(chainId, hostConnectionId, controllerConnectionId) {
async makeAccount(chainId, hostConnectionId, controllerConnectionId) {
const remoteConnAddr = makeICAChannelAddress(
hostConnectionId,
controllerConnectionId,
Expand All @@ -213,9 +213,9 @@ const prepareOrchestrationKit = (
},
/**
* @param {IBCConnectionID} controllerConnectionId
* @returns {ICQConnection | Promise<ICQConnection>}
* @returns {Promise<ICQConnection>}
*/
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)
Expand Down
Loading

0 comments on commit 99c4cae

Please sign in to comment.