diff --git a/packages/cosmic-proto/package.json b/packages/cosmic-proto/package.json index 66fcf7a1c81..5d5cb8391ab 100644 --- a/packages/cosmic-proto/package.json +++ b/packages/cosmic-proto/package.json @@ -64,6 +64,14 @@ "types": "./dist/codegen/ibc/applications/interchain_accounts/v1/packet.d.ts", "default": "./dist/codegen/ibc/applications/interchain_accounts/v1/packet.js" }, + "./ibc/core/channel/v1/channel.js": { + "types": "./dist/codegen/ibc/core/channel/v1/channel.d.ts", + "default": "./dist/codegen/ibc/core/channel/v1/channel.js" + }, + "./ibc/core/connection/v1/connection.js": { + "types": "./dist/codegen/ibc/core/connection/v1/connection.d.ts", + "default": "./dist/codegen/ibc/core/connection/v1/connection.js" + }, "./icq/*.js": { "types": "./dist/codegen/icq/*.d.ts", "default": "./dist/codegen/icq/v1/*.js" diff --git a/packages/cosmic-proto/src/helpers.ts b/packages/cosmic-proto/src/helpers.ts index 242d164bc34..636c08e5f71 100644 --- a/packages/cosmic-proto/src/helpers.ts +++ b/packages/cosmic-proto/src/helpers.ts @@ -12,6 +12,10 @@ import type { } from './codegen/cosmos/staking/v1beta1/tx.js'; import { RequestQuery } from './codegen/tendermint/abci/types.js'; import type { Any } from './codegen/google/protobuf/any.js'; +import { + MsgTransfer, + MsgTransferResponse, +} from './codegen/ibc/applications/transfer/v1/tx.js'; /** * The result of Any.toJSON(). The type in cosms-types says it returns @@ -28,6 +32,8 @@ export type Proto3Shape = { '/cosmos.bank.v1beta1.QueryAllBalancesResponse': QueryAllBalancesResponse; '/cosmos.staking.v1beta1.MsgDelegate': MsgDelegate; '/cosmos.staking.v1beta1.MsgDelegateResponse': MsgDelegateResponse; + '/ibc.applications.transfer.v1.MsgTransfer': MsgTransfer; + '/ibc.applications.transfer.v1.MsgTransferResponse': MsgTransferResponse; }; // Often s/Request$/Response/ but not always @@ -35,6 +41,7 @@ type ResponseMap = { '/cosmos.bank.v1beta1.MsgSend': '/cosmos.bank.v1beta1.MsgSendResponse'; '/cosmos.bank.v1beta1.QueryAllBalancesRequest': '/cosmos.bank.v1beta1.QueryAllBalancesResponse'; '/cosmos.staking.v1beta1.MsgDelegate': '/cosmos.staking.v1beta1.MsgDelegateResponse'; + '/ibc.applications.transfer.v1.MsgTransfer': '/ibc.applications.transfer.v1.MsgTransferResponse'; }; /** diff --git a/packages/orchestration/src/cosmos-api.ts b/packages/orchestration/src/cosmos-api.ts index e83b668d548..9843f6a2bfb 100644 --- a/packages/orchestration/src/cosmos-api.ts +++ b/packages/orchestration/src/cosmos-api.ts @@ -11,6 +11,14 @@ import type { LocalIbcAddress, RemoteIbcAddress, } from '@agoric/vats/tools/ibc-utils.js'; +import { MsgTransfer } from '@agoric/cosmic-proto/ibc/applications/transfer/v1/tx.js'; +import type { State as IBCConnectionState } from '@agoric/cosmic-proto/ibc/core/connection/v1/connection.js'; +import type { + Order, + State as IBCChannelState, +} from '@agoric/cosmic-proto/ibc/core/channel/v1/channel.js'; +import { IBCChannelID, IBCConnectionID } from '@agoric/vats'; +import { MapStore } from '@agoric/store'; import type { AmountArg, ChainAddress, DenomAmount } from './types.js'; /** A helper type for type extensions. */ @@ -29,25 +37,37 @@ export type CosmosValidatorAddress = ChainAddress & { addressEncoding: 'bech32'; }; +/** Represents an IBC Connection between two chains, which can contain multiple Channels. */ +export type IBCConnectionInfo = { + id: IBCConnectionID; // e.g. connection-0 + client_id: string; // '07-tendermint-0' + state: IBCConnectionState; + counterparty: { + client_id: string; + connection_id: IBCConnectionID; + prefix: { + key_prefix: string; + }; + }; + versions: { identifier: string; features: string[] }[]; + delay_period: bigint; + transferChannel: { + portId: string; + channelId: IBCChannelID; + counterPartyPortId: string; + counterPartyChannelId: IBCChannelID; + ordering: Order; + state: IBCChannelState; + version: string; // e.eg. 'ics20-1' + }; +}; + /** * Info for a Cosmos-based chain. */ export type CosmosChainInfo = { chainId: string; - ibcConnectionInfo: { - id: string; // e.g. connection-0 - client_id: string; // '07-tendermint-0' - state: 'OPEN' | 'TRYOPEN' | 'INIT' | 'CLOSED'; - counterparty: { - client_id: string; - connection_id: string; - prefix: { - key_prefix: string; - }; - }; - versions: { identifier: string; features: string[] }[]; - delay_period: bigint; - }; + connections: MapStore; // chainId or wellKnownName icaEnabled: boolean; icqEnabled: boolean; pfmEnabled: boolean; @@ -197,3 +217,9 @@ export interface IcaAccount { export type LiquidStakingMethods = { liquidStake: (amount: AmountArg) => Promise; }; + +export type IBCMsgTransferOptions = { + timeoutHeight?: MsgTransfer['timeoutHeight']; + timeoutTimestamp?: MsgTransfer['timeoutTimestamp']; + memo?: string; +}; diff --git a/packages/orchestration/src/examples/stakeAtom.contract.js b/packages/orchestration/src/examples/stakeAtom.contract.js index d2e879b84b7..4c87f855f74 100644 --- a/packages/orchestration/src/examples/stakeAtom.contract.js +++ b/packages/orchestration/src/examples/stakeAtom.contract.js @@ -1,11 +1,13 @@ /** * @file Example contract that uses orchestration */ + import { makeTracer, StorageNodeShape } from '@agoric/internal'; -import { makeDurableZone } from '@agoric/zone/durable.js'; import { V as E } from '@agoric/vow/vat.js'; -import { M } from '@endo/patterns'; import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport'; +import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; +import { makeDurableZone } from '@agoric/zone/durable.js'; +import { M } from '@endo/patterns'; import { prepareStakingAccountKit } from '../exos/stakingAccountKit.js'; const trace = makeTracer('StakeAtom'); @@ -61,7 +63,7 @@ export const start = async (zcf, privateArgs, baggage) => { zcf, ); - async function makeAccount() { + async function makeAccountKit() { const account = await E(orchestration).makeAccount( hostConnectionId, controllerConnectionId, @@ -94,19 +96,20 @@ export const start = async (zcf, privateArgs, baggage) => { 'StakeAtom', M.interface('StakeAtomI', { makeAccount: M.callWhen().returns(M.remotable('ChainAccount')), - makeAcountInvitationMaker: M.call().returns(M.promise()), + makeAcountInvitationMaker: M.callWhen().returns(InvitationShape), }), { async makeAccount() { trace('makeAccount'); - return makeAccount().then(({ account }) => account); + const { account } = await makeAccountKit(); + return account; }, makeAcountInvitationMaker() { trace('makeCreateAccountInvitation'); return zcf.makeInvitation( async seat => { seat.exit(); - return makeAccount(); + return makeAccountKit(); }, 'wantStakingAccount', undefined, diff --git a/packages/orchestration/src/examples/stakeBld.contract.js b/packages/orchestration/src/examples/stakeBld.contract.js index 93dc23fd080..1214dd3fd5b 100644 --- a/packages/orchestration/src/examples/stakeBld.contract.js +++ b/packages/orchestration/src/examples/stakeBld.contract.js @@ -1,15 +1,20 @@ /** * @file Stake BLD contract - * */ - import { makeTracer } from '@agoric/internal'; +import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; +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 { M } from '@endo/patterns'; import { E } from '@endo/far'; -import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; -import { atomicTransfer } from '@agoric/zoe/src/contractSupport/atomicTransfer.js'; -import { prepareLocalchainAccountKit } from '../exos/localchainAccountKit.js'; +import { deeplyFulfilled } from '@endo/marshal'; +import { M } from '@endo/patterns'; +import { prepareLocalChainAccountKit } from '../exos/local-chain-account-kit.js'; +import { prepareMockChainInfo } from '../utils/mockChainInfo.js'; + +/** + * @import {TimerBrand, TimerService} from '@agoric/time'; + */ const trace = makeTracer('StakeBld'); @@ -20,12 +25,15 @@ const trace = makeTracer('StakeBld'); * localchain: import('@agoric/vats/src/localchain.js').LocalChain; * marshaller: Marshaller; * storageNode: StorageNode; + * timerService: TimerService; + * timerBrand: TimerBrand; * }} privateArgs * @param {import("@agoric/vat-data").Baggage} baggage */ export const start = async (zcf, privateArgs, baggage) => { - const { BLD } = zcf.getTerms().brands; + const BLD = zcf.getTerms().brands.In; + // XXX is this safe to call before prepare statements are completed? const bldAmountShape = await E(BLD).getAmountShape(); const zone = makeDurableZone(baggage); @@ -34,48 +42,81 @@ export const start = async (zcf, privateArgs, baggage) => { baggage, privateArgs.marshaller, ); - const makeLocalchainAccountKit = prepareLocalchainAccountKit( + + // Mocked until #8879 + // Would expect this to be instantiated elsewhere, and passed in as a reference + const agoricChainInfo = prepareMockChainInfo(zone); + + const makeLocalChainAccountKit = prepareLocalChainAccountKit( baggage, makeRecorderKit, zcf, + privateArgs.timerService, + privateArgs.timerBrand, + agoricChainInfo, ); - const publicFacet = zone.exo('StakeBld', undefined, { - makeStakeBldInvitation() { - return zcf.makeInvitation( - async seat => { - const { give } = seat.getProposal(); - trace('makeStakeBldInvitation', give); - // XXX type appears local but it's remote - const account = await E(privateArgs.localchain).makeAccount(); - const lcaSeatKit = zcf.makeEmptySeatKit(); - atomicTransfer(zcf, seat, lcaSeatKit.zcfSeat, give); - seat.exit(); - trace('makeStakeBldInvitation tryExit lca userSeat'); - await E(lcaSeatKit.userSeat).tryExit(); - trace('awaiting payouts'); - const payouts = await E(lcaSeatKit.userSeat).getPayouts(); - const { holder, invitationMakers } = makeLocalchainAccountKit( - account, - privateArgs.storageNode, - ); - trace('awaiting deposit'); - await E(account).deposit(await payouts.In); + async function makeLocalAccountKit() { + const account = await E(privateArgs.localchain).makeAccount(); + const address = await E(account).getAddress(); + return makeLocalChainAccountKit({ + account, + address, + storageNode: privateArgs.storageNode, + }); + } - return { + const publicFacet = zone.exo( + 'StakeBld', + M.interface('StakeBldI', { + makeAccount: M.callWhen().returns(M.remotable('LocalChainAccountHolder')), + makeAcountInvitationMaker: M.callWhen().returns(InvitationShape), + makeStakeBldInvitation: M.callWhen().returns(InvitationShape), + }), + { + makeStakeBldInvitation() { + return zcf.makeInvitation( + async seat => { + const { give } = seat.getProposal(); + trace('makeStakeBldInvitation', give); + const { holder, invitationMakers } = await makeLocalAccountKit(); + const { In } = await deeplyFulfilled( + withdrawFromSeat(zcf, seat, give), + ); + await E(holder).deposit(In); + seat.exit(); + return harden({ + publicSubscribers: holder.getPublicTopics(), + invitationMakers, + account: holder, + }); + }, + 'wantStake', + undefined, + M.splitRecord({ + give: { In: bldAmountShape }, + }), + ); + }, + async makeAccount() { + trace('makeAccount'); + const { holder } = await makeLocalAccountKit(); + return holder; + }, + makeAcountInvitationMaker() { + trace('makeCreateAccountInvitation'); + return zcf.makeInvitation(async seat => { + seat.exit(); + const { holder, invitationMakers } = await makeLocalAccountKit(); + return harden({ publicSubscribers: holder.getPublicTopics(), invitationMakers, account: holder, - }; - }, - 'wantStake', - undefined, - M.splitRecord({ - give: { In: bldAmountShape }, - }), - ); + }); + }, 'wantLocalChainAccount'); + }, }, - }); + ); return { publicFacet }; }; diff --git a/packages/orchestration/src/examples/swapExample.contract.js b/packages/orchestration/src/examples/swapExample.contract.js index 985a836852a..8fca312ecd3 100644 --- a/packages/orchestration/src/examples/swapExample.contract.js +++ b/packages/orchestration/src/examples/swapExample.contract.js @@ -84,6 +84,7 @@ export const start = async (zcf, privateArgs) => { // deposit funds from user seat to LocalChainAccount const payments = await withdrawFromSeat(zcf, seat, give); await deeplyFulfilled(objectMap(payments, localAccount.deposit)); + seat.exit(); // build swap instructions with orcUtils library const transferMsg = orcUtils.makeOsmosisSwap({ diff --git a/packages/orchestration/src/exos/chainAccountKit.js b/packages/orchestration/src/exos/chainAccountKit.js index 1781cfdc65a..605b9504612 100644 --- a/packages/orchestration/src/exos/chainAccountKit.js +++ b/packages/orchestration/src/exos/chainAccountKit.js @@ -1,5 +1,4 @@ /** @file ChainAccount exo */ - import { NonNullish } from '@agoric/assert'; import { makeTracer } from '@agoric/internal'; import { V as E } from '@agoric/vow/vat.js'; diff --git a/packages/orchestration/src/exos/local-chain-account-kit.js b/packages/orchestration/src/exos/local-chain-account-kit.js new file mode 100644 index 00000000000..e6953579e30 --- /dev/null +++ b/packages/orchestration/src/exos/local-chain-account-kit.js @@ -0,0 +1,253 @@ +/** @file Use-object for the owner of a localchain account */ +import { NonNullish } from '@agoric/assert'; +import { typedJson } from '@agoric/cosmic-proto/vatsafe'; +import { AmountShape, PaymentShape } from '@agoric/ertp'; +import { makeTracer } from '@agoric/internal'; +import { UnguardedHelperI } from '@agoric/internal/src/typeGuards.js'; +import { M, prepareExoClassKit } from '@agoric/vat-data'; +import { TopicsRecordShape } from '@agoric/zoe/src/contractSupport/index.js'; +import { E } from '@endo/far'; +import { + AmountArgShape, + ChainAddressShape, + IBCTransferOptionsShape, +} from '../typeGuards.js'; +import { makeTimestampHelper } from '../utils/time.js'; + +/** + * @import {LocalChainAccount} from '@agoric/vats/src/localchain.js'; + * @import {AmountArg, ChainAddress, DenomAmount, IBCMsgTransferOptions, CosmosChainInfo} from '@agoric/orchestration'; + * @import {RecorderKit, MakeRecorderKit} from '@agoric/zoe/src/contractSupport/recorder.js'. + * @import {Baggage} from '@agoric/vat-data'; + * @import {TimerService, TimerBrand} from '@agoric/time'; + * @import {TimestampHelper} from '../utils/time.js'; + */ + +// partial until #8879 +/** @typedef {Pick} AgoricChainInfo */ + +const trace = makeTracer('LCAH'); + +const { Fail } = assert; +/** + * @typedef {object} LocalChainAccountNotification + * @property {string} address + */ + +/** + * @typedef {{ + * topicKit: RecorderKit; + * account: LocalChainAccount | null; + * address: ChainAddress['address']; + * }} State + */ + +const HolderI = M.interface('holder', { + getPublicTopics: M.call().returns(TopicsRecordShape), + makeDelegateInvitation: M.call(M.string(), AmountShape).returns(M.promise()), + makeCloseAccountInvitation: M.call().returns(M.promise()), + makeTransferAccountInvitation: M.call().returns(M.promise()), + deposit: M.callWhen(PaymentShape).returns(AmountShape), + withdraw: M.callWhen(AmountShape).returns(PaymentShape), + transfer: M.call(AmountArgShape, ChainAddressShape) + .optional(IBCTransferOptionsShape) + .returns(M.promise()), + getAddress: M.call().returns(M.string()), +}); + +/** @type {{ [name: string]: [description: string, valueShape: Pattern] }} */ +const PUBLIC_TOPICS = { + account: ['Account holder status', M.any()], +}; + +/** + * @param {Baggage} baggage + * @param {MakeRecorderKit} makeRecorderKit + * @param {ZCF} zcf + * @param {TimerService} timerService + * @param {TimerBrand} timerBrand + * @param {AgoricChainInfo} agoricChainInfo + */ +export const prepareLocalChainAccountKit = ( + baggage, + makeRecorderKit, + zcf, + timerService, + timerBrand, + agoricChainInfo, +) => { + const timestampHelper = makeTimestampHelper(timerService, timerBrand); + const makeAccountHolderKit = prepareExoClassKit( + baggage, + 'Account Holder', + { + helper: UnguardedHelperI, + holder: HolderI, + invitationMakers: M.interface('invitationMakers', { + Delegate: HolderI.payload.methodGuards.makeDelegateInvitation, + CloseAccount: HolderI.payload.methodGuards.makeCloseAccountInvitation, + TransferAccount: + HolderI.payload.methodGuards.makeTransferAccountInvitation, + }), + }, + /** + * @param {object} initState + * @param {LocalChainAccount} initState.account + * @param {ChainAddress['address']} initState.address + * @param {StorageNode} initState.storageNode + * @returns {State} + */ + ({ account, address, storageNode }) => { + // must be the fully synchronous maker because the kit is held in durable state + // @ts-expect-error XXX Patterns + const topicKit = makeRecorderKit(storageNode, PUBLIC_TOPICS.account[1]); + + // #9162 use ChainAddress object instead of `address` string + return { account, address, topicKit }; + }, + { + helper: { + /** @throws if this holder no longer owns the account */ + owned() { + const { account } = this.state; + if (!account) { + throw Fail`Using account holder after transfer`; + } + return account; + }, + getUpdater() { + return this.state.topicKit.recorder; + }, + }, + invitationMakers: { + Delegate(validatorAddress, amount) { + return this.facets.holder.makeDelegateInvitation( + validatorAddress, + amount, + ); + }, + CloseAccount() { + return this.facets.holder.makeCloseAccountInvitation(); + }, + TransferAccount() { + return this.facets.holder.makeTransferAccountInvitation(); + }, + }, + holder: { + getPublicTopics() { + const { topicKit } = this.state; + return harden({ + account: { + description: PUBLIC_TOPICS.account[0], + subscriber: topicKit.subscriber, + storagePath: topicKit.recorder.getStoragePath(), + }, + }); + }, + /** + * + * @param {string} validatorAddress + * @param {Amount<'nat'>} ertpAmount + */ + async makeDelegateInvitation(validatorAddress, ertpAmount) { + trace('makeDelegateInvitation', validatorAddress, ertpAmount); + + // TODO #9211 lookup denom from brand + const amount = { + amount: String(ertpAmount.value), + denom: 'ubld', + }; + + return zcf.makeInvitation(async seat => { + // TODO should it allow delegating more BLD? + seat.exit(); + const lca = this.facets.helper.owned(); + trace('lca', lca); + const delegatorAddress = await E(lca).getAddress(); + trace('delegatorAddress', delegatorAddress); + const [result] = await E(lca).executeTx([ + typedJson('/cosmos.staking.v1beta1.MsgDelegate', { + amount, + validatorAddress, + delegatorAddress, + }), + ]); + trace('got result', result); + return result; + }, 'Delegate'); + }, + makeCloseAccountInvitation() { + throw Error('not yet implemented'); + }, + /** + * Starting a transfer revokes the account holder. The associated updater + * will get a special notification that the account is being transferred. + */ + makeTransferAccountInvitation() { + throw Error('not yet implemented'); + }, + /** @type {LocalChainAccount['deposit']} */ + async deposit(payment, optAmountShape) { + return E(this.facets.helper.owned()).deposit(payment, optAmountShape); + }, + /** @type {LocalChainAccount['withdraw']} */ + async withdraw(amount) { + return E(this.facets.helper.owned()).withdraw(amount); + }, + /** + * @returns {ChainAddress['address']} + */ + getAddress() { + return NonNullish(this.state.address, 'Chain address not available.'); + }, + /** + * @param {AmountArg} amount an ERTP {@link Amount} or a {@link DenomAmount} + * @param {ChainAddress} destination + * @param {IBCMsgTransferOptions} [opts] if either timeoutHeight or timeoutTimestamp are not supplied, a default timeoutTimestamp will be set for 5 minutes in the future + * @returns {Promise} + */ + async transfer(amount, destination, opts) { + trace('Transferring funds from LCA over IBC'); + // TODO #9211 lookup denom from brand + if ('brand' in amount) throw Fail`ERTP Amounts not yet supported`; + + // TODO #8879 chainInfo and #9063 well-known chains + const { transferChannel } = agoricChainInfo.connections.get( + destination.chainId, + ); + + 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()); + + const [result] = await E(this.facets.helper.owned()).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); + }, + }, + }, + ); + return makeAccountHolderKit; +}; +/** @typedef {ReturnType>} LocalChainAccountKit */ diff --git a/packages/orchestration/src/exos/localchainAccountKit.js b/packages/orchestration/src/exos/localchainAccountKit.js deleted file mode 100644 index 77e90704091..00000000000 --- a/packages/orchestration/src/exos/localchainAccountKit.js +++ /dev/null @@ -1,155 +0,0 @@ -/** @file Use-object for the owner of a localchain account */ -import { typedJson } from '@agoric/cosmic-proto/vatsafe'; -import { AmountShape } from '@agoric/ertp'; -import { makeTracer } from '@agoric/internal'; -import { UnguardedHelperI } from '@agoric/internal/src/typeGuards.js'; -import { M, prepareExoClassKit } from '@agoric/vat-data'; -import { TopicsRecordShape } from '@agoric/zoe/src/contractSupport/index.js'; -import { E } from '@endo/far'; - -const trace = makeTracer('LCAH'); - -const { Fail } = assert; -/** - * @typedef {object} LocalChainAccountNotification - * @property {string} address - */ - -/** - * @typedef {{ - * topicKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit; - * account: import('@agoric/vats/src/localchain.js').LocalChainAccount | null; - * }} State - */ - -const HolderI = M.interface('holder', { - getPublicTopics: M.call().returns(TopicsRecordShape), - makeDelegateInvitation: M.call(M.string(), AmountShape).returns(M.promise()), - makeCloseAccountInvitation: M.call().returns(M.promise()), - makeTransferAccountInvitation: M.call().returns(M.promise()), -}); - -/** @type {{ [name: string]: [description: string, valueShape: Pattern] }} */ -const PUBLIC_TOPICS = { - account: ['Account holder status', M.any()], -}; - -/** - * @param {import('@agoric/swingset-liveslots').Baggage} baggage - * @param {import('@agoric/zoe/src/contractSupport/recorder.js').MakeRecorderKit} makeRecorderKit - * @param {ZCF} zcf - */ -export const prepareLocalchainAccountKit = (baggage, makeRecorderKit, zcf) => { - const makeAccountHolderKit = prepareExoClassKit( - baggage, - 'Account Holder', - { - helper: UnguardedHelperI, - holder: HolderI, - invitationMakers: M.interface('invitationMakers', { - Delegate: HolderI.payload.methodGuards.makeDelegateInvitation, - CloseAccount: HolderI.payload.methodGuards.makeCloseAccountInvitation, - TransferAccount: - HolderI.payload.methodGuards.makeTransferAccountInvitation, - }), - }, - /** - * @param {import('@agoric/vats/src/localchain.js').LocalChainAccount} account - * @param {StorageNode} storageNode - * @returns {State} - */ - (account, storageNode) => { - // must be the fully synchronous maker because the kit is held in durable state - // @ts-expect-error XXX Patterns - const topicKit = makeRecorderKit(storageNode, PUBLIC_TOPICS.account[1]); - - return { account, topicKit }; - }, - { - helper: { - /** @throws if this holder no longer owns the account */ - owned() { - const { account } = this.state; - if (!account) { - throw Fail`Using account holder after transfer`; - } - return account; - }, - getUpdater() { - return this.state.topicKit.recorder; - }, - }, - invitationMakers: { - Delegate(validatorAddress, amount) { - return this.facets.holder.makeDelegateInvitation( - validatorAddress, - amount, - ); - }, - CloseAccount() { - return this.facets.holder.makeCloseAccountInvitation(); - }, - TransferAccount() { - return this.facets.holder.makeTransferAccountInvitation(); - }, - }, - holder: { - getPublicTopics() { - const { topicKit } = this.state; - return harden({ - account: { - description: PUBLIC_TOPICS.account[0], - subscriber: topicKit.subscriber, - storagePath: topicKit.recorder.getStoragePath(), - }, - }); - }, - /** - * - * @param {string} validatorAddress - * @param {Amount<'nat'>} ertpAmount - */ - async makeDelegateInvitation(validatorAddress, ertpAmount) { - trace('makeDelegateInvitation', validatorAddress, ertpAmount); - - // FIXME get values from proposal or args - // FIXME brand handling and amount scaling - const amount = { - amount: String(ertpAmount.value), - denom: 'ubld', - }; - - return zcf.makeInvitation(async seat => { - // TODO should it allow delegating more BLD? - seat.exit(); - const lca = this.facets.helper.owned(); - trace('lca', lca); - const delegatorAddress = await E(lca).getAddress(); - trace('delegatorAddress', delegatorAddress); - const result = await E(lca).executeTx([ - typedJson('/cosmos.staking.v1beta1.MsgDelegate', { - amount, - validatorAddress, - delegatorAddress, - }), - ]); - trace('got result', result); - return result; - }, 'Delegate'); - }, - makeCloseAccountInvitation() { - throw Error('not yet implemented'); - }, - /** - * Starting a transfer revokes the account holder. The associated updater - * will get a special notification that the account is being transferred. - */ - makeTransferAccountInvitation() { - throw Error('not yet implemented'); - }, - }, - }, - ); - return makeAccountHolderKit; -}; -/** @typedef {ReturnType>} LocalchainAccountKit */ diff --git a/packages/orchestration/src/facade.js b/packages/orchestration/src/facade.js index dec417bdfd2..7dcfd9aafe4 100644 --- a/packages/orchestration/src/facade.js +++ b/packages/orchestration/src/facade.js @@ -27,7 +27,7 @@ const makeLocalChainFacade = localchain => { allowedMessages: [], allowedQueries: [], chainId: 'agoric-3', - ibcConnectionInfo: anyVal, + connections: anyVal, ibcHooksEnabled: true, icaEnabled: true, icqEnabled: true, diff --git a/packages/orchestration/src/orchestration-api.ts b/packages/orchestration/src/orchestration-api.ts index 9275fdac7ec..3ee02806bb0 100644 --- a/packages/orchestration/src/orchestration-api.ts +++ b/packages/orchestration/src/orchestration-api.ts @@ -12,7 +12,7 @@ import type { } from '@agoric/ertp/src/types.js'; import type { LocalChainAccount } from '@agoric/vats/src/localchain.js'; import type { Timestamp } from '@agoric/time'; -import type { KnownChains } from './types.js'; +import type { IBCMsgTransferOptions, KnownChains } from './types.js'; /** * A denom that designates a path to a token type on some blockchain. @@ -45,7 +45,7 @@ export type DenomAmount = { value: bigint; // Nat }; -/** Amounts can be provided as pure data using denoms or as native Amounts */ +/** Amounts can be provided as pure data using denoms or as ERTP Amounts */ export type AmountArg = DenomAmount | Amount; /** An address on some blockchain, e.g., cosmos, eth, etc. */ @@ -145,9 +145,9 @@ export interface OrchestrationAccountI { /** * Transfer an amount to another account, typically on another chain. * The promise settles when the transfer is complete. - * @param amount - the amount to transfer. + * @param amount - the amount to transfer. Can be provided as pure data using denoms or as ERTP Amounts. * @param destination - the account to transfer the amount to. - * @param memo - an optional memo to include with the transfer, which could drive custom PFM behavior + * @param [opts] - an optional memo to include with the transfer, which could drive custom PFM behavior, and timeout parameters * @returns void * * TODO document the mapping from the address to the destination chain. @@ -155,7 +155,7 @@ export interface OrchestrationAccountI { transfer: ( amount: AmountArg, destination: ChainAddress, - memo?: string, + opts?: IBCMsgTransferOptions, ) => Promise; /** diff --git a/packages/orchestration/src/proposals/start-stakeBld.js b/packages/orchestration/src/proposals/start-stakeBld.js index 3168886a88b..036ce51624d 100644 --- a/packages/orchestration/src/proposals/start-stakeBld.js +++ b/packages/orchestration/src/proposals/start-stakeBld.js @@ -9,7 +9,13 @@ const trace = makeTracer('StartStakeBld', true); * @param {BootstrapPowers & {installation: {consume: {stakeBld: Installation}}}} powers */ export const startStakeBld = async ({ - consume: { board, chainStorage, localchain, startUpgradable }, + consume: { + board, + chainStorage, + chainTimerService: chainTimerServiceP, + localchain, + startUpgradable, + }, installation: { consume: { stakeBld }, }, @@ -28,15 +34,21 @@ export const startStakeBld = async ({ // NB: committee must only publish what it intended to be public const marshaller = await E(board).getPublishingMarshaller(); - // FIXME this isn't detecting missing privateArgs + const [timerService, timerBrand] = await Promise.all([ + chainTimerServiceP, + chainTimerServiceP.then(ts => E(ts).getTimerBrand()), + ]); + /** @type {StartUpgradableOpts} */ const startOpts = { label: 'stakeBld', installation: stakeBld, - issuerKeywordRecord: harden({ BLD: await stakeIssuer }), + issuerKeywordRecord: harden({ In: await stakeIssuer }), terms: {}, privateArgs: { localchain: await localchain, + timerService, + timerBrand, storageNode, marshaller, }, @@ -54,6 +66,7 @@ export const getManifestForStakeBld = ({ restoreRef }, { installKeys }) => { consume: { board: true, chainStorage: true, + chainTimerService: true, localchain: true, startUpgradable: true, }, diff --git a/packages/orchestration/src/typeGuards.js b/packages/orchestration/src/typeGuards.js index d84928dcf31..79d48ad3f1c 100644 --- a/packages/orchestration/src/typeGuards.js +++ b/packages/orchestration/src/typeGuards.js @@ -1,4 +1,3 @@ -// @ts-check import { AmountShape } from '@agoric/ertp'; import { M } from '@endo/patterns'; @@ -26,3 +25,15 @@ export const ChainAmountShape = harden({ denom: M.string(), value: M.nat() }); export const AmountArgShape = M.or(AmountShape, ChainAmountShape); export const DelegationShape = M.record(); // TODO: DelegationShape fields + +export const IBCTransferOptionsShape = M.splitRecord( + {}, + { + timeoutTimestamp: M.bigint(), + timeoutHeight: { + revisionHeight: M.bigint(), + revisionNumber: M.bigint(), + }, + memo: M.string(), + }, +); diff --git a/packages/orchestration/src/utils/mockChainInfo.js b/packages/orchestration/src/utils/mockChainInfo.js new file mode 100644 index 00000000000..61d153b1cf0 --- /dev/null +++ b/packages/orchestration/src/utils/mockChainInfo.js @@ -0,0 +1,86 @@ +/** + * @file Mocked Chain Info object until #8879 + */ +import { + Order, + State as IBCChannelState, +} from '@agoric/cosmic-proto/ibc/core/channel/v1/channel.js'; +import { State as IBCConnectionState } from '@agoric/cosmic-proto/ibc/core/connection/v1/connection.js'; + +/** + * @import {Zone} from '@agoric/zone'; + * @import {CosmosChainInfo, IBCConnectionInfo} from '../cosmos-api.js'; + */ + +/** + * currently keyed by ChainId, as this is what we have + * available in ChainAddress to determine the correct IBCChannelID's + * for a .transfer() msg. + * @type {Record} + */ +const connectionEntries = harden({ + cosmoslocal: { + id: 'connection-1', + client_id: '07-tendermint-3', + counterparty: { + client_id: '07-tendermint-2', + connection_id: 'connection-1', + prefix: { + key_prefix: '', + }, + }, + state: IBCConnectionState.STATE_OPEN, + transferChannel: { + portId: 'transfer', + channelId: 'channel-1', + counterPartyChannelId: 'channel-1', + counterPartyPortId: 'transfer', + ordering: Order.ORDER_UNORDERED, + state: IBCChannelState.STATE_OPEN, + version: 'ics20-1', + }, + versions: [{ identifier: '', features: ['', ''] }], + delay_period: 0n, + }, + osmosislocal: { + id: 'connection-0', + client_id: '07-tendermint-2', + counterparty: { + client_id: '07-tendermint-2', + connection_id: 'connection-1', + prefix: { + key_prefix: '', + }, + }, + state: IBCConnectionState.STATE_OPEN, + transferChannel: { + portId: 'transfer', + channelId: 'channel-0', + counterPartyChannelId: 'channel-1', + counterPartyPortId: 'transfer', + ordering: Order.ORDER_UNORDERED, + state: IBCChannelState.STATE_OPEN, + version: 'ics20-1', + }, + versions: [{ identifier: '', features: ['', ''] }], + delay_period: 0n, + }, +}); + +/** + * @param {Zone} zone + * @returns {Pick} + */ +export const prepareMockChainInfo = zone => { + const agoricConnections = + /** @type {import('@agoric/store').MapStore} */ ( + zone.mapStore('ibcConnections') + ); + + agoricConnections.addAll(Object.entries(connectionEntries)); + + return harden({ + chainId: 'agoriclocal', + connections: agoricConnections, + }); +}; diff --git a/packages/orchestration/src/utils/time.js b/packages/orchestration/src/utils/time.js new file mode 100644 index 00000000000..91f491d700e --- /dev/null +++ b/packages/orchestration/src/utils/time.js @@ -0,0 +1,38 @@ +import { E } from '@endo/far'; +import { TimeMath } from '@agoric/time'; + +/** + * @import {RelativeTimeRecord, TimerBrand, TimerService} from '@agoric/time'; + * @import {MsgTransfer} from '@agoric/cosmic-proto/ibc/applications/transfer/v1/tx.js'; + */ + +export const SECONDS_PER_MINUTE = 60n; +export const NANOSECONDS_PER_SECOND = 1_000_000_000n; + +/** + * @param {TimerService} timer + * @param {TimerBrand} timerBrand + */ +export function makeTimestampHelper(timer, timerBrand) { + return harden({ + /** + * Takes the current time from ChainTimerService and adds a relative + * time to determine a timeout timestamp in nanoseconds. + * Useful for {@link MsgTransfer.timeoutTimestamp}. + * @param {RelativeTimeRecord} [relativeTime] defaults to 5 minutes + * @returns {Promise} Timeout timestamp in absolute nanoseconds since unix epoch + */ + async getTimeoutTimestampNS(relativeTime) { + const currentTime = await E(timer).getCurrentTimestamp(); + const timeout = + relativeTime || + TimeMath.coerceRelativeTimeRecord(SECONDS_PER_MINUTE * 5n, timerBrand); + return ( + TimeMath.addAbsRel(currentTime, timeout).absValue * + NANOSECONDS_PER_SECOND + ); + }, + }); +} + +/** @typedef {Awaited>} TimestampHelper */ diff --git a/packages/orchestration/test/examples/stake-bld.contract.test.ts b/packages/orchestration/test/examples/stake-bld.contract.test.ts new file mode 100644 index 00000000000..f314e486d8c --- /dev/null +++ b/packages/orchestration/test/examples/stake-bld.contract.test.ts @@ -0,0 +1,171 @@ +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { AmountMath, makeIssuerKit } from '@agoric/ertp'; +import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js'; +import { prepareLocalChainTools } from '@agoric/vats/src/localchain.js'; +import { makeFakeBoard } from '@agoric/vats/tools/board-utils.js'; +import { buildRootObject as buildBankVatRoot } from '@agoric/vats/src/vat-bank.js'; +import { setUpZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; +import { withAmountUtils } from '@agoric/zoe/tools/test-utils.js'; +import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; +import { makeHeapZone } from '@agoric/zone'; +import { E } from '@endo/far'; +import path from 'path'; +import { makeFakeLocalchainBridge } from '../supports.js'; + +const { keys } = Object; +const dirname = path.dirname(new URL(import.meta.url).pathname); + +const contractFile = `${dirname}/../../src/examples/stakeBld.contract.js`; +type StartFn = + typeof import('@agoric/orchestration/src/examples/stakeBld.contract.js').start; + +const bootstrap = async (t, { issuerKit }) => { + t.log('bootstrap vat dependencies'); + const zone = makeHeapZone(); + const bankManager = await buildBankVatRoot( + undefined, + undefined, + zone.mapStore('bankManager'), + ).makeBankManager(); + await E(bankManager).addAsset('ubld', 'BLD', 'Staking Token', issuerKit); + + const localchainBridge = makeFakeLocalchainBridge(zone); + const localchain = prepareLocalChainTools( + zone.subZone('localchain'), + ).makeLocalChain({ + bankManager, + system: localchainBridge, + }); + const timer = buildManualTimer(t.log); + const marshaller = makeFakeBoard().getReadonlyMarshaller(); + const storage = makeFakeStorageKit('mockChainStorageRoot', { + sequence: false, + }); + return { + timer, + localchain, + marshaller, + storage, + }; +}; + +const coreEval = async ( + t, + { timer, localchain, marshaller, storage, stake }, +) => { + t.log('install stakeBld contract'); + const { zoe, bundleAndInstall } = await setUpZoeForTest(); + const installation: Installation = + await bundleAndInstall(contractFile); + + const { publicFacet } = await E(zoe).startInstance( + installation, + { In: stake.issuer }, + {}, + { + localchain, + marshaller, + storageNode: storage.rootNode, + timerService: timer, + timerBrand: timer.getTimerBrand(), + }, + ); + return { publicFacet, zoe }; +}; + +test('stakeBld contract - makeAccount, deposit, withdraw', async t => { + const issuerKit = makeIssuerKit('BLD'); + const stake = withAmountUtils(issuerKit); + + const bootstrapSpace = await bootstrap(t, { issuerKit }); + const { publicFacet } = await coreEval(t, { ...bootstrapSpace, stake }); + + t.log('make a LocalChainAccount'); + const account = await E(publicFacet).makeAccount(); + t.truthy(account, 'account is returned'); + t.regex(await E(account).getAddress(), /agoric1/); + + const oneHundredStakeAmt = stake.make(1_000_000_000n); + const oneHundredStakePmt = issuerKit.mint.mintPayment(oneHundredStakeAmt); + + t.log('deposit 100 bld to account'); + const depositResp = await E(account).deposit(oneHundredStakePmt); + t.true(AmountMath.isEqual(depositResp, oneHundredStakeAmt), 'deposit'); + + // TODO validate balance, .getBalance() + + t.log('withdraw 1 bld from account'); + const withdrawResp = await E(account).withdraw(oneHundredStakeAmt); + // @ts-expect-error Argument of type 'Payment' is not assignable to parameter of type 'ERef>'. + const withdrawAmt = await stake.issuer.getAmountOf(withdrawResp); + t.true(AmountMath.isEqual(withdrawAmt, oneHundredStakeAmt), 'withdraw'); + + t.log('cannot withdraw more than balance'); + await t.throwsAsync( + () => E(account).withdraw(oneHundredStakeAmt), + { + message: /Withdrawal of {.*} failed/, + }, + 'cannot withdraw more than balance', + ); +}); + +test('stakeBld contract - makeStakeBldInvitation', async t => { + const issuerKit = makeIssuerKit('BLD'); + const stake = withAmountUtils(issuerKit); + + const bootstrapSpace = await bootstrap(t, { issuerKit }); + const { publicFacet, zoe } = await coreEval(t, { ...bootstrapSpace, stake }); + + t.log('call makeStakeBldInvitation'); + const inv = await E(publicFacet).makeStakeBldInvitation(); + + const hundred = stake.make(1_000_000_000n); + + t.log('make an offer for an account'); + // Want empty until (at least) #9087 + const userSeat = await E(zoe).offer( + inv, + { give: { In: hundred } }, + { In: stake.mint.mintPayment(hundred) }, + ); + const { invitationMakers } = await E(userSeat).getOfferResult(); + t.truthy(invitationMakers, 'received continuing invitation'); + + t.log('make Delegate offer using invitationMakers'); + const delegateInv = await E(invitationMakers).Delegate('agoric1validator1', { + brand: stake.brand, + value: 1_000_000_000n, + }); + const delegateOffer = await E(zoe).offer( + delegateInv, + { give: { In: hundred } }, + { In: stake.mint.mintPayment(hundred) }, + ); + const res = await E(delegateOffer).getOfferResult(); + t.deepEqual(res, {}); + t.log('Successfully delegated'); + + await t.throwsAsync(() => E(invitationMakers).TransferAccount(), { + message: 'not yet implemented', + }); + await t.throwsAsync(() => E(invitationMakers).CloseAccount(), { + message: 'not yet implemented', + }); +}); + +test('stakeBld contract - makeAccountInvitationMaker', async t => { + const issuerKit = makeIssuerKit('BLD'); + const stake = withAmountUtils(issuerKit); + + const bootstrapSpace = await bootstrap(t, { issuerKit }); + const { publicFacet, zoe } = await coreEval(t, { ...bootstrapSpace, stake }); + + t.log('call makeAcountInvitationMaker'); + const inv = await E(publicFacet).makeAcountInvitationMaker(); + + const userSeat = await E(zoe).offer(inv); + const offerResult = await E(userSeat).getOfferResult(); + t.true('account' in offerResult, 'received account'); + t.truthy('invitationMakers' in offerResult, 'received continuing invitation'); +}); diff --git a/packages/orchestration/test/exos/local-chain-account-kit.test.ts b/packages/orchestration/test/exos/local-chain-account-kit.test.ts new file mode 100644 index 00000000000..0aaf9f3f717 --- /dev/null +++ b/packages/orchestration/test/exos/local-chain-account-kit.test.ts @@ -0,0 +1,159 @@ +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { AmountMath, makeIssuerKit } from '@agoric/ertp'; +import { makeMockChainStorageRoot } from '@agoric/internal/src/storage-test-utils.js'; +import { M, makeScalarBigMapStore } from '@agoric/vat-data'; +import { prepareLocalChainTools } from '@agoric/vats/src/localchain.js'; +import { makeFakeBoard } from '@agoric/vats/tools/board-utils.js'; +import { buildRootObject as buildBankVatRoot } from '@agoric/vats/src/vat-bank.js'; +import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; +import { withAmountUtils } from '@agoric/zoe/tools/test-utils.js'; +import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; +import { makeHeapZone } from '@agoric/zone'; +import { E, Far } from '@endo/far'; +import { makeFakeLocalchainBridge } from '../supports.js'; +import { prepareLocalChainAccountKit } from '../../src/exos/local-chain-account-kit.js'; +import { prepareMockChainInfo } from '../../src/utils/mockChainInfo.js'; +import { ChainAddress } from '../../src/orchestration-api.js'; +import { NANOSECONDS_PER_SECOND } from '../../src/utils/time.js'; + +test('localChainAccountKit - transfer', async t => { + const bootstrap = async () => { + const zone = makeHeapZone(); + const issuerKit = makeIssuerKit('BLD'); + const stake = withAmountUtils(issuerKit); + + const bankManager = await buildBankVatRoot( + undefined, + undefined, + zone.mapStore('bankManager'), + ).makeBankManager(); + + await E(bankManager).addAsset('ubld', 'BLD', 'Staking Token', issuerKit); + const localchainBridge = makeFakeLocalchainBridge(zone); + const localchain = prepareLocalChainTools( + zone.subZone('localchain'), + ).makeLocalChain({ + bankManager, + system: localchainBridge, + }); + const timer = buildManualTimer(t.log); + const marshaller = makeFakeBoard().getReadonlyMarshaller(); + + return { + timer, + localchain, + marshaller, + stake, + issuerKit, + rootZone: zone, + }; + }; + + const { timer, localchain, stake, marshaller, issuerKit, rootZone } = + await bootstrap(); + + t.log('chainInfo mocked via `prepareMockChainInfo` until #8879'); + const agoricChainInfo = prepareMockChainInfo(rootZone.subZone('chainInfo')); + + t.log('exo setup - prepareLocalChainAccountKit'); + const baggage = makeScalarBigMapStore('baggage', { + durable: true, + }); + const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller); + const makeLocalChainAccountKit = prepareLocalChainAccountKit( + baggage, + makeRecorderKit, + // @ts-expect-error mocked zcf. use `stake-bld.contract.test.ts` to test LCA with offer + Far('MockZCF', {}), + timer, + timer.getTimerBrand(), + agoricChainInfo, + ); + + t.log('request account from vat-localchain'); + const lca = await E(localchain).makeAccount(); + const address = await E(lca).getAddress(); + + t.log('make a LocalChainAccountKit'); + const { holder: account } = makeLocalChainAccountKit({ + account: lca, + address, + storageNode: makeMockChainStorageRoot().makeChildNode('lcaKit'), + }); + + t.truthy(account, 'account is returned'); + t.regex(await E(account).getAddress(), /agoric1/); + + const oneHundredStakeAmt = stake.make(1_000_000_000n); + const oneHundredStakePmt = issuerKit.mint.mintPayment(oneHundredStakeAmt); + const oneStakeAmt = stake.make(1_000_000n); + + t.log('deposit 100 bld to account'); + const depositResp = await E(account).deposit(oneHundredStakePmt); + t.true(AmountMath.isEqual(depositResp, oneHundredStakeAmt), 'deposit'); + + const destination: ChainAddress = { + chainId: 'cosmoslocal', + address: 'cosmos1pleab', + addressEncoding: 'bech32', + }; + + // TODO #9211, support ERTP amounts + t.log('ERTP Amounts not yet supported for AmountArg'); + await t.throwsAsync(() => E(account).transfer(oneStakeAmt, destination), { + message: 'ERTP Amounts not yet supported', + }); + + t.log('.transfer() 1 bld to cosmos using DenomAmount'); + const transferResp = await E(account).transfer( + { denom: 'ubld', value: 1_000_000n }, + destination, + ); + t.is(transferResp, undefined, 'Successful transfer returns Promise.'); + + await t.throwsAsync( + () => E(account).transfer({ denom: 'ubld', value: 504n }, destination), + { + message: 'simulated unexpected MsgTransfer packet timeout', + }, + ); + + const unknownDestination: ChainAddress = { + chainId: 'fakenet', + address: 'fakenet1pleab', + addressEncoding: 'bech32', + }; + await t.throwsAsync( + () => E(account).transfer({ denom: 'ubld', value: 1n }, unknownDestination), + { + message: /not found(.*)fakenet/, + }, + 'cannot create transfer msg with unknown chainId', + ); + + await t.notThrowsAsync( + () => + E(account).transfer({ denom: 'ubld', value: 10n }, destination, { + memo: 'hello', + }), + 'can create transfer msg with memo', + ); + // TODO, intercept/spy the bridge message to see that it has a memo + + await t.notThrowsAsync( + () => + E(account).transfer({ denom: 'ubld', value: 10n }, destination, { + // sets to current time, which shouldn't work in a real env + timeoutTimestamp: BigInt(new Date().getTime()) * NANOSECONDS_PER_SECOND, + }), + 'accepts custom timeoutTimestamp', + ); + + await t.notThrowsAsync( + () => + E(account).transfer({ denom: 'ubld', value: 10n }, destination, { + timeoutHeight: { revisionHeight: 100n, revisionNumber: 1n }, + }), + 'accepts custom timeoutHeight', + ); +}); diff --git a/packages/orchestration/test/staking-ops.test.ts b/packages/orchestration/test/staking-ops.test.ts index 52cd9564e2e..159563f308f 100644 --- a/packages/orchestration/test/staking-ops.test.ts +++ b/packages/orchestration/test/staking-ops.test.ts @@ -1,4 +1,3 @@ -// @ts-check import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { MsgWithdrawDelegatorRewardResponse } from '@agoric/cosmic-proto/cosmos/distribution/v1beta1/tx.js'; diff --git a/packages/orchestration/test/types.test-d.ts b/packages/orchestration/test/types.test-d.ts index edc7daee4a3..89505e66e55 100644 --- a/packages/orchestration/test/types.test-d.ts +++ b/packages/orchestration/test/types.test-d.ts @@ -7,7 +7,7 @@ import { typedJson } from '@agoric/cosmic-proto'; import type { MsgDelegateResponse } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; import type { QueryAllBalancesResponse } from '@agoric/cosmic-proto/cosmos/bank/v1beta1/query.js'; import type { ChainAddress, CosmosValidatorAddress } from '../src/types.js'; -import type { LocalchainAccountKit } from '../src/exos/localchainAccountKit.js'; +import type { LocalChainAccountKit } from '../src/exos/local-chain-account-kit.js'; const validatorAddr = { chainId: 'agoric3', @@ -30,7 +30,7 @@ expectNotType(chainAddr); } { - const lcak: LocalchainAccountKit = null as any; + const lcak: LocalChainAccountKit = null as any; const lca = lcak.helper.owned(); const results = await lca.executeTx([ typedJson('/cosmos.staking.v1beta1.MsgDelegate', { diff --git a/packages/orchestration/test/utils/time.test.ts b/packages/orchestration/test/utils/time.test.ts new file mode 100644 index 00000000000..5c2fb015929 --- /dev/null +++ b/packages/orchestration/test/utils/time.test.ts @@ -0,0 +1,40 @@ +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; +import { TimeMath } from '@agoric/time'; +import { + makeTimestampHelper, + NANOSECONDS_PER_SECOND, + SECONDS_PER_MINUTE, +} from '../../src/utils/time.js'; + +test('makeTimestampHelper - getCurrentTimestamp', async t => { + const timer = buildManualTimer(t.log); + const timerBrand = timer.getTimerBrand(); + t.is(timer.getCurrentTimestamp().absValue, 0n, 'current time is 0n'); + + const { getTimeoutTimestampNS } = makeTimestampHelper(timer, timerBrand); + await null; + t.is( + await getTimeoutTimestampNS(), + 5n * SECONDS_PER_MINUTE * NANOSECONDS_PER_SECOND, + 'default timestamp is 5 minutes from current time, in nanoseconds', + ); + + t.is( + await getTimeoutTimestampNS( + TimeMath.coerceRelativeTimeRecord(1n, timerBrand), + ), + 1n * NANOSECONDS_PER_SECOND, + 'timestamp is 1 second since unix epoch, in nanoseconds', + ); + + // advance timer by 3 seconds + await timer.tickN(3); + t.is( + await getTimeoutTimestampNS( + TimeMath.coerceRelativeTimeRecord(1n, timerBrand), + ), + (1n + 3n) * NANOSECONDS_PER_SECOND, + 'timestamp is 4 seconds since unix epoch, in nanoseconds', + ); +}); diff --git a/packages/vats/test/localchain.test.js b/packages/vats/test/localchain.test.js index b960aac08cd..c8542ad8da7 100644 --- a/packages/vats/test/localchain.test.js +++ b/packages/vats/test/localchain.test.js @@ -46,9 +46,7 @@ const makeTestContext = async _t => { /** @param {LocalChainPowers} powers */ const makeLocalChain = async powers => { const zone = makeDurableZone(provideBaggage('localchain')); - return prepareLocalChainTools(zone.subZone('localchain')).makeLocalChain( - powers, - ); + return prepareLocalChainTools(zone).makeLocalChain(powers); }; const localchain = await makeLocalChain({ @@ -94,7 +92,7 @@ test('localchain - deposit and withdraw', async t => { t.is(getInterfaceOf(lca), 'Alleged: LocalChainAccount'); const address = await E(lca).getAddress(); - t.is(address, 'agoric1fakeBridgeAddress'); + t.is(address, 'agoric1fakeLCAAddress'); contractsLca = lca; }, deposit: async () => { diff --git a/packages/vats/tools/fake-bridge.js b/packages/vats/tools/fake-bridge.js index be4d9afbf26..e191e72c880 100644 --- a/packages/vats/tools/fake-bridge.js +++ b/packages/vats/tools/fake-bridge.js @@ -1,7 +1,10 @@ import { Fail } from '@agoric/assert'; import assert from 'node:assert/strict'; -/** @import {ScopedBridgeManager} from '../src/types.js'; */ +/** + * @import {MsgDelegateResponse} from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; + * @import {ScopedBridgeManager} from '../src/types.js'; + */ /** * @param {import('@agoric/zone').Zone} zone @@ -50,6 +53,7 @@ export const makeFakeLocalchainBridge = ( onFromBridge = () => {}, ) => { let hndlr; + let lcaExecuteTxSequence = 0; return zone.exo('Fake Localchain Bridge Manager', undefined, { getBridgeId: () => 'vlocalchain', toBridge: async obj => { @@ -58,7 +62,33 @@ export const makeFakeLocalchainBridge = ( console.info('toBridge', type, method, params); switch (type) { case 'VLOCALCHAIN_ALLOCATE_ADDRESS': - return 'agoric1fakeBridgeAddress'; + return 'agoric1fakeLCAAddress'; + case 'VLOCALCHAIN_EXECUTE_TX': { + lcaExecuteTxSequence += 1; + return obj.messages.map(message => { + switch (message['@type']) { + // TODO #9402 reference bank to ensure caller has tokens they are transferring + case '/ibc.applications.transfer.v1.MsgTransfer': { + if (message.token.amount === '504') { + throw Error( + 'simulated unexpected MsgTransfer packet timeout', + ); + } + // like `JsonSafe`, but bigints are converted to numbers + // XXX should vlocalchain return a string instead of number for bigint? + return { + sequence: lcaExecuteTxSequence, + }; + } + case '/cosmos.staking.v1beta1.MsgDelegate': { + return /** @type {MsgDelegateResponse} */ {}; + } + // returns one empty object per message unless specified + default: + return {}; + } + }); + } default: Fail`unknown type ${type}`; }