From ab95b23f1388ad89ae555674f8d67038366318e1 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 12 Aug 2024 12:05:24 -0400 Subject: [PATCH 01/14] feat: get timerBrand from currentTime TimestampRecord - remove an unnecessary vat roundtrip --- packages/orchestration/src/utils/time.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/orchestration/src/utils/time.js b/packages/orchestration/src/utils/time.js index a45848ad1ec..06f995ab06d 100644 --- a/packages/orchestration/src/utils/time.js +++ b/packages/orchestration/src/utils/time.js @@ -17,14 +17,6 @@ export const NANOSECONDS_PER_SECOND = 1_000_000_000n; * @param {Remote} timer */ export function makeTimestampHelper(timer) { - /** @type {TimerBrand | undefined} */ - let brandCache; - const getBrand = async () => { - if (brandCache) return brandCache; - brandCache = await E(timer).getTimerBrand(); - return brandCache; - }; - return harden({ /** * XXX do this need to be resumable / use Vows? @@ -43,7 +35,7 @@ export function makeTimestampHelper(timer) { relativeTime || TimeMath.coerceRelativeTimeRecord( SECONDS_PER_MINUTE * 5n, - await getBrand(), + currentTime.timerBrand, ); return ( TimeMath.addAbsRel(currentTime, timeout).absValue * From ee8b806d627214998ef96408c3885e14c9e6c1a9 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 12 Aug 2024 13:03:37 -0400 Subject: [PATCH 02/14] feat: simplify transferChannel lookup --- .../src/examples/stakeBld.contract.js | 2 +- .../src/exos/local-orchestration-account.js | 24 ++++--------------- .../local-orchestration-account-kit.test.ts | 4 ++-- .../test/exos/make-test-loa-kit.ts | 2 +- 4 files changed, 8 insertions(+), 24 deletions(-) diff --git a/packages/orchestration/src/examples/stakeBld.contract.js b/packages/orchestration/src/examples/stakeBld.contract.js index fa2473c2795..7e75cbf62f9 100644 --- a/packages/orchestration/src/examples/stakeBld.contract.js +++ b/packages/orchestration/src/examples/stakeBld.contract.js @@ -65,7 +65,7 @@ export const start = async (zcf, privateArgs, baggage) => { address: harden({ value: address, encoding: 'bech32', - chainId: 'local', + chainId: 'agoriclocal', }), storageNode: privateArgs.storageNode, }); diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index bcd64c537f4..f5eb7a8979c 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -121,11 +121,6 @@ export const prepareLocalOrchestrationAccountKit = ( .optional(M.arrayOf(M.undefined())) // empty context .returns(VowShape), }), - getChainInfoWatcher: M.interface('getChainInfoWatcher', { - onFulfilled: M.call(M.record()) // agoric chain info - .optional(ChainAddressShape) - .returns(Vow$(M.record())), // connection info - }), transferWatcher: M.interface('transferWatcher', { onFulfilled: M.call([M.record(), M.nat()]) .optional({ @@ -268,18 +263,6 @@ export const prepareLocalOrchestrationAccountKit = ( ); }, }, - getChainInfoWatcher: { - /** - * @param {ChainInfo} agoricChainInfo - * @param {ChainAddress} destination - */ - onFulfilled(agoricChainInfo, destination) { - return chainHub.getConnectionInfo( - agoricChainInfo.chainId, - destination.chainId, - ); - }, - }, transferWatcher: { /** * @param {[ @@ -556,9 +539,10 @@ export const prepareLocalOrchestrationAccountKit = ( if ('brand' in amount) throw Fail`ERTP Amounts not yet supported`; const connectionInfoV = watch( - chainHub.getChainInfo('agoric'), - this.facets.getChainInfoWatcher, - destination, + chainHub.getConnectionInfo( + this.state.address.chainId, + destination.chainId, + ), ); // set a `timeoutTimestamp` if caller does not supply either `timeoutHeight` or `timeoutTimestamp` diff --git a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts index 1d376aec26e..5930fe84ebf 100644 --- a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts +++ b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts @@ -119,7 +119,7 @@ test('transfer', async t => { value: 'cosmos1pleab', encoding: 'bech32', }; - const sourceChannel = 'channel-5'; // observed in toBridge VLOCALCHAIN_EXECUTE_TX sourceChannel + const sourceChannel = 'channel-1'; // observed in toBridge VLOCALCHAIN_EXECUTE_TX sourceChannel // TODO rename to lastSequence /** The running tally of transfer messages that were sent over the bridge */ @@ -193,7 +193,7 @@ test('transfer', async t => { // XXX dev has to know not to startTransfer here await t.throwsAsync( VE(account).transfer({ denom: 'ubld', value: 1n }, unknownDestination), - { message: /connection not found: agoric-3<->fakenet/ }, + { message: /connection not found: agoriclocal<->fakenet/ }, 'cannot create transfer msg with unknown chainId', ); diff --git a/packages/orchestration/test/exos/make-test-loa-kit.ts b/packages/orchestration/test/exos/make-test-loa-kit.ts index 765e7d48f58..018aa638fd5 100644 --- a/packages/orchestration/test/exos/make-test-loa-kit.ts +++ b/packages/orchestration/test/exos/make-test-loa-kit.ts @@ -56,7 +56,7 @@ export const prepareMakeTestLOAKit = ( account: lca, address: harden({ value: address, - chainId: 'agoric-n', + chainId: 'agoriclocal', encoding: 'bech32', }), storageNode: storageNode.makeChildNode(address), From 4d57f8155bc5a41f3dd22ac8560c10a7b1a240e6 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 12 Aug 2024 13:05:16 -0400 Subject: [PATCH 03/14] chore: ibc/applications/transfer/v1/tx.js in package exports --- packages/cosmic-proto/package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/cosmic-proto/package.json b/packages/cosmic-proto/package.json index d8a851a0f8a..8e32120e954 100644 --- a/packages/cosmic-proto/package.json +++ b/packages/cosmic-proto/package.json @@ -64,6 +64,10 @@ "types": "./dist/codegen/ibc/applications/interchain_accounts/v1/packet.d.ts", "default": "./dist/codegen/ibc/applications/interchain_accounts/v1/packet.js" }, + "./ibc/applications/transfer/v1/tx.js": { + "types": "./dist/codegen/ibc/applications/transfer/v1/tx.d.ts", + "default": "./dist/codegen/ibc/applications/transfer/v1/tx.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" From cbe876b190ab6befb057b87339d512d6ec6246a6 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 12 Aug 2024 14:20:58 -0400 Subject: [PATCH 04/14] feat: parseOutgoingTxPacket test util --- packages/orchestration/tools/ibc-mocks.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/orchestration/tools/ibc-mocks.ts b/packages/orchestration/tools/ibc-mocks.ts index 778cc1513ee..94260e2a156 100644 --- a/packages/orchestration/tools/ibc-mocks.ts +++ b/packages/orchestration/tools/ibc-mocks.ts @@ -4,13 +4,14 @@ import { RequestQuery, ResponseQuery, } from '@agoric/cosmic-proto/tendermint/abci/types.js'; -import { encodeBase64, btoa } from '@endo/base64'; +import { encodeBase64, btoa, atob, decodeBase64 } from '@endo/base64'; import { toRequestQueryJson } from '@agoric/cosmic-proto'; import { IBCChannelID, VTransferIBCEvent, type IBCPacket } from '@agoric/vats'; import { VTRANSFER_IBC_EVENT } from '@agoric/internal/src/action-types.js'; import { FungibleTokenPacketData } from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js'; import type { PacketSDKType } from '@agoric/cosmic-proto/ibc/core/channel/v1/channel.js'; import { LOCALCHAIN_DEFAULT_ADDRESS } from '@agoric/vats/tools/fake-bridge.js'; +import { TxBody } from '@agoric/cosmic-proto/cosmos/tx/v1beta1/tx.js'; import { makeQueryPacket, makeTxPacket } from '../src/utils/packet.js'; import { ChainAddress } from '../src/orchestration-api.js'; @@ -119,6 +120,16 @@ export function buildTxPacketString( return btoa(makeTxPacket(msgs.map(Any.toJSON))); } +/** + * Parse an outgoing ica tx packet. Useful for testing when inspecting + * outgoing dibc bridge messages. + * + * @param b64 base64 encoded string + */ +export const parseOutgoingTxPacket = (b64: string) => { + return TxBody.decode(decodeBase64(JSON.parse(atob(b64)).data)); +}; + /** * Build a query packet string for the mocked dibc bridge handler * @param msgs From 02cb70420cf2b002a2e1e79d7d2a9036c746a472 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 12 Aug 2024 15:20:01 -0400 Subject: [PATCH 05/14] feat: guard orchAccount.transfer opts --- packages/orchestration/src/utils/orchestrationAccount.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/orchestration/src/utils/orchestrationAccount.js b/packages/orchestration/src/utils/orchestrationAccount.js index f8b20eef642..a227d7ea343 100644 --- a/packages/orchestration/src/utils/orchestrationAccount.js +++ b/packages/orchestration/src/utils/orchestrationAccount.js @@ -6,6 +6,7 @@ import { AmountArgShape, ChainAddressShape, DenomAmountShape, + IBCTransferOptionsShape, } from '../typeGuards.js'; /** @import {OrchestrationAccountI} from '../orchestration-api.js'; */ @@ -22,7 +23,7 @@ export const orchestrationAccountMethods = { VowShape, ), transfer: M.call(AmountArgShape, ChainAddressShape) - .optional(M.record()) + .optional(IBCTransferOptionsShape) .returns(VowShape), transferSteps: M.call(AmountArgShape, M.any()).returns(VowShape), asContinuingOffer: M.call().returns( From ed5998f85b142282ff17e4a64f7333e2c3f15191 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 12 Aug 2024 16:11:26 -0400 Subject: [PATCH 06/14] feat: export createMockAckMap helper --- packages/orchestration/test/ibc-mocks.ts | 9 +-------- packages/orchestration/tools/ibc-mocks.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/orchestration/test/ibc-mocks.ts b/packages/orchestration/test/ibc-mocks.ts index af092111a84..86ef8862cf2 100644 --- a/packages/orchestration/test/ibc-mocks.ts +++ b/packages/orchestration/test/ibc-mocks.ts @@ -25,6 +25,7 @@ import { buildMsgErrorString, buildTxPacketString, buildQueryPacketString, + createMockAckMap, } from '../tools/ibc-mocks.js'; /** @@ -122,13 +123,5 @@ export const protoMsgMocks = { }, }; -export function createMockAckMap(mockMap: typeof protoMsgMocks) { - const res = Object.values(mockMap).reduce((acc, { msg, ack }) => { - acc[msg] = ack; - return acc; - }, {}); - return res; -} - export const defaultMockAckMap: Record = createMockAckMap(protoMsgMocks); diff --git a/packages/orchestration/tools/ibc-mocks.ts b/packages/orchestration/tools/ibc-mocks.ts index 94260e2a156..c030499721e 100644 --- a/packages/orchestration/tools/ibc-mocks.ts +++ b/packages/orchestration/tools/ibc-mocks.ts @@ -218,3 +218,13 @@ export const buildVTransferEvent = ({ sequence, } as IBCPacket, }); + +export function createMockAckMap( + mockMap: Record, +) { + const res = Object.values(mockMap).reduce((acc, { msg, ack }) => { + acc[msg] = ack; + return acc; + }, {}); + return res; +} From 6a90c4cff9c527ad4ab7aa5c77fd2e6c8c3ae201 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 12 Aug 2024 16:12:51 -0400 Subject: [PATCH 07/14] refactor: simplify sendPacket switch statement --- packages/boot/tools/ibc/mocks.js | 4 ++++ packages/boot/tools/supports.ts | 37 ++++++-------------------------- 2 files changed, 11 insertions(+), 30 deletions(-) diff --git a/packages/boot/tools/ibc/mocks.js b/packages/boot/tools/ibc/mocks.js index 351f5cab139..0b41cbc36de 100644 --- a/packages/boot/tools/ibc/mocks.js +++ b/packages/boot/tools/ibc/mocks.js @@ -1,4 +1,5 @@ // @ts-check +import { createMockAckMap } from '@agoric/orchestration/tools/ibc-mocks.js'; /** @import { IBCChannelID, IBCMethod, IBCEvent } from '@agoric/vats'; */ @@ -57,10 +58,13 @@ export const protoMsgMocks = { ack: responses.delegate, }, error: { + msg: '', ack: responses.error5, }, }; +export const protoMsgMockMap = createMockAckMap(protoMsgMocks); + /** * Adds parameters to IBC version string if it's JSON * @param {string} version version or JSON version string diff --git a/packages/boot/tools/supports.ts b/packages/boot/tools/supports.ts index 3b9ec5974a5..3384190d0d3 100644 --- a/packages/boot/tools/supports.ts +++ b/packages/boot/tools/supports.ts @@ -37,15 +37,13 @@ import { import type { ExecutionContext as AvaT } from 'ava'; -import type { JsonSafe } from '@agoric/cosmic-proto'; -import type { MsgDelegateResponse } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; import type { CoreEvalSDKType } from '@agoric/cosmic-proto/swingset/swingset.js'; import type { EconomyBootstrapPowers } from '@agoric/inter-protocol/src/proposals/econ-behaviors.js'; import type { SwingsetController } from '@agoric/swingset-vat/src/controller/controller.js'; import type { BridgeHandler, IBCMethod, IBCPacket } from '@agoric/vats'; import type { BootstrapRootObject } from '@agoric/vats/src/core/lib-boot.js'; import type { EProxy } from '@endo/eventual-send'; -import { icaMocks, protoMsgMocks } from './ibc/mocks.js'; +import { icaMocks, protoMsgMockMap, protoMsgMocks } from './ibc/mocks.js'; const trace = makeTracer('BSTSupport', false); @@ -449,34 +447,13 @@ export const makeSwingsetTestKit = async ( case 'startChannelOpenInit': pushInbound(BridgeId.DIBC, icaMocks.channelOpenAck(obj)); return undefined; - case 'sendPacket': - switch (obj.packet.data) { - case protoMsgMocks.delegate.msg: { - return ackLater(obj, protoMsgMocks.delegate.ack); - } - case protoMsgMocks.delegateWithOpts.msg: { - return ackLater(obj, protoMsgMocks.delegateWithOpts.ack); - } - case protoMsgMocks.queryBalance.msg: { - return ackLater(obj, protoMsgMocks.queryBalance.ack); - } - case protoMsgMocks.queryUnknownPath.msg: { - return ackLater(obj, protoMsgMocks.queryUnknownPath.ack); - } - case protoMsgMocks.queryBalanceMulti.msg: { - return ackLater(obj, protoMsgMocks.queryBalanceMulti.ack); - } - case protoMsgMocks.queryBalanceUnknownDenom.msg: { - return ackLater( - obj, - protoMsgMocks.queryBalanceUnknownDenom.ack, - ); - } - default: { - // An error that would be triggered before reception on another chain - return ackImmediately(obj, protoMsgMocks.error.ack); - } + case 'sendPacket': { + if (protoMsgMockMap[obj.packet.data]) { + return ackLater(obj, protoMsgMockMap[obj.packet.data]); } + // An error that would be triggered before reception on another chain + return ackImmediately(obj, protoMsgMocks.error.ack); + } default: return undefined; } From 8f4fe4841f3ded174c117841ecb9320ea45a4318 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 12 Aug 2024 16:16:13 -0400 Subject: [PATCH 08/14] refactor: use SIMULATED_ERRORS const --- .../orchestration/test/examples/stake-bld.contract.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/orchestration/test/examples/stake-bld.contract.test.ts b/packages/orchestration/test/examples/stake-bld.contract.test.ts index 09300cb8775..ef485c053d4 100644 --- a/packages/orchestration/test/examples/stake-bld.contract.test.ts +++ b/packages/orchestration/test/examples/stake-bld.contract.test.ts @@ -2,6 +2,7 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { AmountMath } from '@agoric/ertp'; import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; +import { SIMULATED_ERRORS } from '@agoric/vats/tools/fake-bridge.js'; import { heapVowE as E } from '@agoric/vow/vat.js'; import { setUpZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; import path from 'path'; @@ -117,7 +118,7 @@ test('makeStakeBldInvitation', async t => { 'agoric1validator1', { brand: bld.brand, - value: 504n, + value: SIMULATED_ERRORS.TIMEOUT, }, ); const delegateOffer = await E(zoe).offer(delegateInv); From b1fdde18b33237d1a2ea6f02938d998f55ce4d01 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 12 Aug 2024 17:04:00 -0400 Subject: [PATCH 09/14] feat: add transfer method to CosmosOrchestrationAccount --- .../test/bootstrapTests/orchestration.test.ts | 49 ++++ packages/boot/tools/ibc/mocks.js | 8 + .../scripts/testing/restart-stakeAtom.js | 5 +- .../src/examples/stakeIca.contract.js | 21 +- packages/orchestration/src/exos/README.md | 1 + .../src/exos/cosmos-orchestration-account.js | 147 ++++++++++-- .../src/proposals/start-stakeAtom.js | 1 + .../src/proposals/start-stakeOsmo.js | 1 + .../orchestration/src/utils/start-helper.js | 10 +- .../test/examples/stake-ica.contract.test.ts | 10 +- .../exos/cosmos-orchestration-account.test.ts | 223 +++++++++++++++++- .../test/exos/make-test-coa-kit.ts | 25 +- .../orchestration/test/staking-ops.test.ts | 60 +++-- packages/orchestration/test/types.test-d.ts | 2 - 14 files changed, 494 insertions(+), 69 deletions(-) diff --git a/packages/boot/test/bootstrapTests/orchestration.test.ts b/packages/boot/test/bootstrapTests/orchestration.test.ts index d971650f229..f0967dc4ba5 100644 --- a/packages/boot/test/bootstrapTests/orchestration.test.ts +++ b/packages/boot/test/bootstrapTests/orchestration.test.ts @@ -347,6 +347,55 @@ test('basic-flows', async t => { status: { id: 'request-loa', numWantsSatisfied: 1 }, }); t.is(readLatest('published.basicFlows.agoric1mockVlocalchainAddress'), ''); + + await wd.sendOffer({ + id: 'transfer-to-noble-from-cosmos', + invitationSpec: { + source: 'continuing', + previousOffer: 'request-coa', + invitationMakerName: 'Transfer', + }, + proposal: {}, + offerArgs: { + amount: { denom: 'ibc/uusdchash', value: 10n }, + destination: { + chainId: 'noble-1', + value: 'noble1test', + encoding: 'bech32,', + }, + }, + }); + t.like(wd.getLatestUpdateRecord(), { + status: { + id: 'transfer-to-noble-from-cosmos', + error: undefined, + }, + }); + + await wd.sendOffer({ + id: 'transfer-to-noble-from-cosmos-timeout', + invitationSpec: { + source: 'continuing', + previousOffer: 'request-coa', + invitationMakerName: 'Transfer', + }, + proposal: {}, + offerArgs: { + amount: { denom: 'ibc/uusdchash', value: SIMULATED_ERRORS.TIMEOUT }, + destination: { + chainId: 'noble-1', + value: 'noble1test', + encoding: 'bech32,', + }, + }, + }); + t.like(wd.getLatestUpdateRecord(), { + status: { + id: 'transfer-to-noble-from-cosmos-timeout', + error: + 'Error: ABCI code: 5: error handling packet: see events for details', + }, + }); }); test.serial('auto-stake-it - proposal', async t => { diff --git a/packages/boot/tools/ibc/mocks.js b/packages/boot/tools/ibc/mocks.js index 0b41cbc36de..824768df9d6 100644 --- a/packages/boot/tools/ibc/mocks.js +++ b/packages/boot/tools/ibc/mocks.js @@ -17,6 +17,9 @@ const responses = { // eyJkYXRhIjoiQ2hzeUdRb1hDaEp6YjIxbExXbHVkbUZzYVdRdFpHVnViMjBTQVRBPSJ9 queryBalanceUnknownDenom: 'eyJyZXN1bHQiOiJleUprWVhSaElqb2lRMmh6ZVVkUmIxaERhRXA2WWpJeGJFeFhiSFZrYlVaellWZFJkRnBIVm5WaU1qQlRRVlJCUFNKOSJ9', + // /ibc.applications.transfer.v1.MsgTransferResponse - sequence 0n + ibcTransfer: + 'eyJyZXN1bHQiOiJFak1LTVM5cFltTXVZWEJ3YkdsallYUnBiMjV6TG5SeVlXNXpabVZ5TG5ZeExrMXpaMVJ5WVc1elptVnlVbVZ6Y0c5dWMyVT0ifQ==', // {"error":"ABCI code: 4: error handling packet: see events for details"} error4: 'eyJlcnJvciI6IkFCQ0kgY29kZTogNDogZXJyb3IgaGFuZGxpbmcgcGFja2V0OiBzZWUgZXZlbnRzIGZvciBkZXRhaWxzIn0=', @@ -57,6 +60,11 @@ export const protoMsgMocks = { msg: 'eyJ0eXBlIjoxLCJkYXRhIjoiQ2xVS0l5OWpiM050YjNNdWMzUmhhMmx1Wnk1Mk1XSmxkR0V4TGsxelowUmxiR1ZuWVhSbEVpNEtDMk52YzIxdmN6RjBaWE4wRWhKamIzTnRiM04yWVd4dmNHVnlNWFJsYzNRYUN3b0ZkV0YwYjIwU0FqRXdFZ2RVUlZOVVNVNUhHSUNVNjl3RCIsIm1lbW8iOiIifQ==', ack: responses.delegate, }, + // MsgTransfer 10 ibc/uusdchash from cosmos1test to noble1test through channel-536 + ibcTransfer: { + msg: 'eyJ0eXBlIjoxLCJkYXRhIjoiQ25zS0tTOXBZbU11WVhCd2JHbGpZWFJwYjI1ekxuUnlZVzV6Wm1WeUxuWXhMazF6WjFSeVlXNXpabVZ5RWs0S0NIUnlZVzV6Wm1WeUVndGphR0Z1Ym1Wc0xUVXpOaG9UQ2cxcFltTXZkWFZ6WkdOb1lYTm9FZ0l4TUNJTFkyOXpiVzl6TVhSbGMzUXFDbTV2WW14bE1YUmxjM1F5QURpQThKTEwzUWc9IiwibWVtbyI6IiJ9', + ack: responses.ibcTransfer, + }, error: { msg: '', ack: responses.error5, diff --git a/packages/builders/scripts/testing/restart-stakeAtom.js b/packages/builders/scripts/testing/restart-stakeAtom.js index 85fa18429c1..8379434eb9e 100644 --- a/packages/builders/scripts/testing/restart-stakeAtom.js +++ b/packages/builders/scripts/testing/restart-stakeAtom.js @@ -19,11 +19,11 @@ const trace = makeTracer('RestartSA', true); */ export const restartStakeAtom = async ({ consume: { + agoricNames, board, chainStorage, chainTimerService, cosmosInterchainService, - contractKits, }, instance: instances, @@ -40,6 +40,7 @@ export const restartStakeAtom = async ({ const privateArgs = await deeplyFulfilledObject( harden({ + agoricNames, cosmosInterchainService, storageNode: makeStorageNodeChild(chainStorage, 'stakeAtom'), marshaller, @@ -57,11 +58,11 @@ export const getManifest = () => { manifest: { [restartStakeAtom.name]: { consume: { + agoricNames: true, board: true, chainStorage: true, chainTimerService: true, cosmosInterchainService: true, - contractKits: true, }, instance: { diff --git a/packages/orchestration/src/examples/stakeIca.contract.js b/packages/orchestration/src/examples/stakeIca.contract.js index 1905ad7f1e0..91f14ccabe0 100644 --- a/packages/orchestration/src/examples/stakeIca.contract.js +++ b/packages/orchestration/src/examples/stakeIca.contract.js @@ -11,14 +11,16 @@ import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; import { makeDurableZone } from '@agoric/zone/durable.js'; import { M } from '@endo/patterns'; import { prepareCosmosOrchestrationAccount } from '../exos/cosmos-orchestration-account.js'; +import { makeChainHub } from '../exos/chain-hub.js'; const trace = makeTracer('StakeIca'); /** * @import {Baggage} from '@agoric/vat-data'; - * @import {IBCConnectionID} from '@agoric/vats'; + * @import {Remote} from '@agoric/internal'; + * @import {IBCConnectionID, NameHub} from '@agoric/vats'; * @import {TimerService} from '@agoric/time'; * @import {ResolvedContinuingOfferResult} from '../utils/zoe-tools.js'; - * @import {ICQConnection, CosmosInterchainService} from '../types.js'; + * @import {ICQConnection, CosmosInterchainService, ChainHub} from '../types.js'; */ /** @type {ContractMeta} */ @@ -31,6 +33,7 @@ export const meta = harden({ icqEnabled: M.boolean(), }, privateArgsShape: { + agoricNames: M.remotable('agoricNames NameHub'), cosmosInterchainService: M.remotable('cosmosInterchainService'), storageNode: StorageNodeShape, marshaller: M.remotable('marshaller'), @@ -54,6 +57,7 @@ harden(privateArgsShape); /** * @param {ZCF} zcf * @param {{ + * agoricNames: Remote; * cosmosInterchainService: CosmosInterchainService; * storageNode: StorageNode; * marshaller: Marshaller; @@ -70,6 +74,7 @@ export const start = async (zcf, privateArgs, baggage) => { icqEnabled, } = zcf.getTerms(); const { + agoricNames, cosmosInterchainService: orchestration, marshaller, storageNode, @@ -86,11 +91,17 @@ export const start = async (zcf, privateArgs, baggage) => { const vowTools = prepareVowTools(zone.subZone('vows')); + const chainHub = makeChainHub(agoricNames, vowTools); + const makeCosmosOrchestrationAccount = prepareCosmosOrchestrationAccount( zone, - makeRecorderKit, - vowTools, - zcf, + { + chainHub, + makeRecorderKit, + timerService: timer, + vowTools, + zcf, + }, ); async function makeAccountKit() { diff --git a/packages/orchestration/src/exos/README.md b/packages/orchestration/src/exos/README.md index 7144d978308..37dcca5036e 100644 --- a/packages/orchestration/src/exos/README.md +++ b/packages/orchestration/src/exos/README.md @@ -120,6 +120,7 @@ classDiagram redelegate() send() sendAll() + transfer() undelegate() withdrawReward() } diff --git a/packages/orchestration/src/exos/cosmos-orchestration-account.js b/packages/orchestration/src/exos/cosmos-orchestration-account.js index b6724e70656..98d742a59d8 100644 --- a/packages/orchestration/src/exos/cosmos-orchestration-account.js +++ b/packages/orchestration/src/exos/cosmos-orchestration-account.js @@ -16,6 +16,7 @@ import { } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; import { Any } from '@agoric/cosmic-proto/google/protobuf/any.js'; import { MsgSend } from '@agoric/cosmic-proto/cosmos/bank/v1beta1/tx.js'; +import { MsgTransfer } from '@agoric/cosmic-proto/ibc/applications/transfer/v1/tx.js'; import { makeTracer } from '@agoric/internal'; import { Shape as NetworkShape } from '@agoric/network'; import { M } from '@agoric/vat-data'; @@ -28,13 +29,15 @@ import { ChainAddressShape, DelegationShape, DenomAmountShape, + IBCTransferOptionsShape, } from '../typeGuards.js'; import { maxClockSkew, tryDecodeResponse } from '../utils/cosmos.js'; import { orchestrationAccountMethods } from '../utils/orchestrationAccount.js'; +import { makeTimestampHelper } from '../utils/time.js'; /** * @import {HostOf} from '@agoric/async-flow'; - * @import {AmountArg, IcaAccount, ChainAddress, CosmosValidatorAddress, ICQConnection, StakingAccountActions, DenomAmount, OrchestrationAccountI, DenomArg} from '../types.js'; + * @import {AmountArg, IcaAccount, ChainAddress, CosmosValidatorAddress, ICQConnection, StakingAccountActions, DenomAmount, OrchestrationAccountI, IBCConnectionInfo, IBCMsgTransferOptions, ChainHub} from '../types.js'; * @import {RecorderKit, MakeRecorderKit} from '@agoric/zoe/src/contractSupport/recorder.js'; * @import {Coin} from '@agoric/cosmic-proto/cosmos/base/v1beta1/coin.js'; * @import {Delegation} from '@agoric/cosmic-proto/cosmos/staking/v1beta1/staking.js'; @@ -94,16 +97,24 @@ const toDenomAmount = c => ({ denom: c.denom, value: BigInt(c.amount) }); /** * @param {Zone} zone - * @param {MakeRecorderKit} makeRecorderKit - * @param {VowTools} vowTools - * @param {ZCF} zcf + * @param {object} powers + * @param {ChainHub} powers.chainHub + * @param {MakeRecorderKit} powers.makeRecorderKit + * @param {Remote} powers.timerService + * @param {VowTools} powers.vowTools + * @param {ZCF} powers.zcf */ export const prepareCosmosOrchestrationAccountKit = ( zone, - makeRecorderKit, - { watch, asVow, when }, - zcf, + { + chainHub, + makeRecorderKit, + timerService, + vowTools: { watch, asVow, when, allVows }, + zcf, + }, ) => { + const timestampHelper = makeTimestampHelper(timerService); const makeCosmosOrchestrationAccountKit = zone.exoClassKit( 'Cosmos Orchestration Account Holder', { @@ -132,6 +143,18 @@ export const prepareCosmosOrchestrationAccountKit = ( .optional(M.arrayOf(M.undefined())) // empty context .returns(M.arrayOf(DenomAmountShape)), }), + transferWatcher: M.interface('transferWatcher', { + onFulfilled: M.call([M.record(), M.nat()]) + .optional({ + destination: ChainAddressShape, + opts: M.or(M.undefined(), IBCTransferOptionsShape), + token: { + denom: M.string(), + amount: M.string(), + }, + }) + .returns(Vow$(M.record())), + }), holder: IcaAccountHolderI, invitationMakers: M.interface('invitationMakers', { Delegate: M.call(ChainAddressShape, AmountArgShape).returns( @@ -148,6 +171,7 @@ export const prepareCosmosOrchestrationAccountKit = ( TransferAccount: M.call().returns(M.promise()), Send: M.call().returns(M.promise()), SendAll: M.call().returns(M.promise()), + Transfer: M.call().returns(M.promise()), }), }, /** @@ -252,6 +276,42 @@ export const prepareCosmosOrchestrationAccountKit = ( return harden(coins.map(toDenomAmount)); }, }, + transferWatcher: { + /** + * @param {[ + * { transferChannel: IBCConnectionInfo['transferChannel'] }, + * bigint, + * ]} results + * @param {{ + * destination: ChainAddress; + * opts?: IBCMsgTransferOptions; + * token: Coin; + * }} ctx + */ + onFulfilled( + [{ transferChannel }, timeoutTimestamp], + { opts, token, destination }, + ) { + const results = E(this.facets.helper.owned()).executeEncodedTx([ + Any.toJSON( + MsgTransfer.toProtoMsg({ + sourcePort: transferChannel.portId, + sourceChannel: transferChannel.channelId, + token, + sender: this.state.chainAddress.value, + receiver: destination.value, + timeoutHeight: opts?.timeoutHeight ?? { + revisionHeight: 0n, + revisionNumber: 0n, + }, + timeoutTimestamp, + memo: opts?.memo ?? '', + }), + ), + ]); + return watch(results, this.facets.returnVoidWatcher); + }, + }, invitationMakers: { /** * @param {CosmosValidatorAddress} validator @@ -335,6 +395,25 @@ export const prepareCosmosOrchestrationAccountKit = ( TransferAccount() { throw Error('not yet implemented'); }, + Transfer() { + /** + * @type {OfferHandler< + * Vow, + * { + * amount: AmountArg; + * destination: ChainAddress; + * opts: IBCMsgTransferOptions; + * } + * >} + */ + const offerHandler = (seat, { amount, destination, opts }) => { + seat.exit(); + return watch( + this.facets.holder.transfer(amount, destination, opts), + ); + }; + return zcf.makeInvitation(offerHandler, 'Transfer'); + }, }, holder: { /** @type {HostOf} */ @@ -513,9 +592,37 @@ export const prepareCosmosOrchestrationAccountKit = ( }, /** @type {HostOf} */ - transfer(amount, msg) { - console.log('transferSteps got', amount, msg); - return asVow(() => Fail`not yet implemented`); + transfer(amount, destination, opts) { + trace('transfer', amount, destination, opts); + return asVow(() => { + const { helper } = this.facets; + const token = helper.amountToCoin(amount); + + const connectionInfoV = watch( + chainHub.getConnectionInfo( + this.state.chainAddress.chainId, + destination.chainId, + ), + ); + + // set a `timeoutTimestamp` if caller does not supply either `timeoutHeight` or `timeoutTimestamp` + // TODO #9324 what's a reasonable default? currently 5 minutes + const timeoutTimestampVowOrValue = + opts?.timeoutTimestamp ?? + (opts?.timeoutHeight + ? 0n + : E(timestampHelper).getTimeoutTimestampNS()); + + // Resolves when host chain successfully submits, but not when + // the receiving chain acknowledges. + // See https://github.com/Agoric/agoric-sdk/issues/9784 for a + // solution that tracks the acknowledgement on the receiving chain. + return watch( + allVows([connectionInfoV, timeoutTimestampVowOrValue]), + this.facets.transferWatcher, + { opts, token, destination }, + ); + }); }, /** @type {HostOf} */ @@ -568,9 +675,12 @@ export const prepareCosmosOrchestrationAccountKit = ( /** * @param {Zone} zone - * @param {MakeRecorderKit} makeRecorderKit - * @param {VowTools} vowTools - * @param {ZCF} zcf + * @param {object} powers + * @param {ChainHub} powers.chainHub + * @param {MakeRecorderKit} powers.makeRecorderKit + * @param {Remote} powers.timerService + * @param {VowTools} powers.vowTools + * @param {ZCF} powers.zcf * @returns {( * ...args: Parameters< * ReturnType @@ -579,16 +689,15 @@ export const prepareCosmosOrchestrationAccountKit = ( */ export const prepareCosmosOrchestrationAccount = ( zone, - makeRecorderKit, - vowTools, - zcf, + { chainHub, makeRecorderKit, timerService, vowTools, zcf }, ) => { - const makeKit = prepareCosmosOrchestrationAccountKit( - zone, + const makeKit = prepareCosmosOrchestrationAccountKit(zone, { + chainHub, makeRecorderKit, + timerService, vowTools, zcf, - ); + }); return (...args) => makeKit(...args).holder; }; /** @typedef {CosmosOrchestrationAccountKit['holder']} CosmosOrchestrationAccount */ diff --git a/packages/orchestration/src/proposals/start-stakeAtom.js b/packages/orchestration/src/proposals/start-stakeAtom.js index 8035f4fda1c..d4e4ccbd945 100644 --- a/packages/orchestration/src/proposals/start-stakeAtom.js +++ b/packages/orchestration/src/proposals/start-stakeAtom.js @@ -65,6 +65,7 @@ export const startStakeAtom = async ({ icqEnabled: cosmoshub.icqEnabled, }, privateArgs: { + agoricNames: await agoricNames, cosmosInterchainService: await cosmosInterchainService, storageNode, marshaller, diff --git a/packages/orchestration/src/proposals/start-stakeOsmo.js b/packages/orchestration/src/proposals/start-stakeOsmo.js index 6fda8e812de..57e7f25b1d0 100644 --- a/packages/orchestration/src/proposals/start-stakeOsmo.js +++ b/packages/orchestration/src/proposals/start-stakeOsmo.js @@ -70,6 +70,7 @@ export const startStakeOsmo = async ({ icqEnabled: osmosis.icqEnabled, }, privateArgs: { + agoricNames: await agoricNames, cosmosInterchainService: await cosmosInterchainService, storageNode, marshaller, diff --git a/packages/orchestration/src/utils/start-helper.js b/packages/orchestration/src/utils/start-helper.js index 1bb57f1f87f..e7a87c1b650 100644 --- a/packages/orchestration/src/utils/start-helper.js +++ b/packages/orchestration/src/utils/start-helper.js @@ -94,9 +94,13 @@ export const provideOrchestration = ( const makeCosmosOrchestrationAccount = prepareCosmosOrchestrationAccount( zones.orchestration, - makeRecorderKit, - vowTools, - zcf, + { + chainHub, + makeRecorderKit, + timerService, + vowTools, + zcf, + }, ); const makeRemoteChainFacade = prepareRemoteChainFacade(zones.orchestration, { diff --git a/packages/orchestration/test/examples/stake-ica.contract.test.ts b/packages/orchestration/test/examples/stake-ica.contract.test.ts index e50f2dd026e..dbc5ba6a4b3 100644 --- a/packages/orchestration/test/examples/stake-ica.contract.test.ts +++ b/packages/orchestration/test/examples/stake-ica.contract.test.ts @@ -17,13 +17,10 @@ import { buildQueryResponseString, } from '../../tools/ibc-mocks.js'; import type { CosmosChainInfo } from '../../src/cosmos-api.js'; -import { - AmountArg, - ChainAddress, - DenomAmount, -} from '../../src/orchestration-api.js'; +import { DenomAmount } from '../../src/orchestration-api.js'; import { maxClockSkew } from '../../src/utils/cosmos.js'; import { UNBOND_PERIOD_SECONDS } from '../ibc-mocks.js'; +import { makeChainHub } from '../../src/exos/chain-hub.js'; const dirname = path.dirname(new URL(import.meta.url).pathname); @@ -50,10 +47,12 @@ const getChainTerms = ( }; const startContract = async ({ + agoricNames, cosmosInterchainService, timer, marshaller, storage, + vowTools, issuerKeywordRecord = undefined, terms = getChainTerms('cosmoshub'), storagePath = 'stakeAtom', @@ -67,6 +66,7 @@ const startContract = async ({ issuerKeywordRecord, terms, { + agoricNames, marshaller, cosmosInterchainService, storageNode: storage.rootNode.makeChildNode(storagePath), diff --git a/packages/orchestration/test/exos/cosmos-orchestration-account.test.ts b/packages/orchestration/test/exos/cosmos-orchestration-account.test.ts index abe25b08fbd..497976a6f33 100644 --- a/packages/orchestration/test/exos/cosmos-orchestration-account.test.ts +++ b/packages/orchestration/test/exos/cosmos-orchestration-account.test.ts @@ -1,9 +1,21 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import type { TestFn } from 'ava'; import { heapVowE as E } from '@agoric/vow/vat.js'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; +import { IBCMethod } from '@agoric/vats'; +import { + MsgTransfer, + MsgTransferResponse, +} from '@agoric/cosmic-proto/ibc/applications/transfer/v1/tx.js'; +import { SIMULATED_ERRORS } from '@agoric/vats/tools/fake-bridge.js'; import { commonSetup } from '../supports.js'; import type { AmountArg, ChainAddress } from '../../src/orchestration-api.js'; import { prepareMakeTestCOAKit } from './make-test-coa-kit.js'; +import { + buildMsgResponseString, + buildTxPacketString, + parseOutgoingTxPacket, +} from '../../tools/ibc-mocks.js'; type TestContext = Awaited>; @@ -70,24 +82,217 @@ test('CosmosOrchestrationAccount - send (to addr on same chain)', async t => { ); }); -test('CosmosOrchestrationAccount - not yet implemented', async t => { - const { bootstrap } = await commonSetup(t); +test('CosmosOrchestrationAccount - transfer', async t => { + const { + brands: { ist }, + bootstrap, + utils: { inspectDibcBridge }, + mocks: { ibcBridge }, + } = t.context; + + const mockIbcTransfer = { + sourcePort: 'transfer', + sourceChannel: 'channel-536', + token: { + denom: 'ibc/uusdchash', + amount: '10', + }, + sender: 'cosmos1test', + receiver: 'noble1test', + timeoutHeight: { + revisionHeight: 0n, + revisionNumber: 0n, + }, + timeoutTimestamp: 300000000000n, // 5 mins in ns + memo: '', + }; + const buildMocks = () => { + const toTransferTxPacket = (msg: MsgTransfer) => + buildTxPacketString([MsgTransfer.toProtoMsg(msg)]); + + const defaultTransfer = toTransferTxPacket(mockIbcTransfer); + const customTimeoutHeight = toTransferTxPacket({ + ...mockIbcTransfer, + timeoutHeight: { + revisionHeight: 1000n, + revisionNumber: 1n, + }, + timeoutTimestamp: 0n, + }); + const customTimeoutTimestamp = toTransferTxPacket({ + ...mockIbcTransfer, + timeoutTimestamp: 999n, + }); + const customTimeout = toTransferTxPacket({ + ...mockIbcTransfer, + timeoutHeight: { + revisionHeight: 5000n, + revisionNumber: 5n, + }, + timeoutTimestamp: 5000n, + }); + const customMemo = toTransferTxPacket({ + ...mockIbcTransfer, + memo: JSON.stringify({ custom: 'pfm memo' }), + }); + + const transferResp = buildMsgResponseString(MsgTransferResponse, { + sequence: 0n, + }); + return { + [defaultTransfer]: transferResp, + [customTimeoutHeight]: transferResp, + [customTimeoutTimestamp]: transferResp, + [customTimeout]: transferResp, + [customMemo]: transferResp, + }; + }; + ibcBridge.setMockAck(buildMocks()); + + const getAndDecodeLatestPacket = async () => { + await eventLoopIteration(); + const { bridgeDowncalls } = await inspectDibcBridge(); + const latest = bridgeDowncalls[ + bridgeDowncalls.length - 1 + ] as IBCMethod<'sendPacket'>; + const { messages } = parseOutgoingTxPacket(latest.packet.data); + return MsgTransfer.decode(messages[0].value); + }; + + t.log('Make account on cosmoshub'); const makeTestCOAKit = prepareMakeTestCOAKit(t, bootstrap); const account = await makeTestCOAKit(); - const mockChainAddress: ChainAddress = { - value: 'cosmos1test', - chainId: 'cosmoshub-4', + + t.log('Send tokens from cosmoshub to noble'); + const mockDestination: ChainAddress = { + value: 'noble1test', + chainId: 'noble-1', encoding: 'bech32', }; + const mockAmountArg: AmountArg = { value: 10n, denom: 'ibc/uusdchash' }; + const res = E(account).transfer(mockAmountArg, mockDestination); + await eventLoopIteration(); + + t.deepEqual( + await getAndDecodeLatestPacket(), + mockIbcTransfer, + 'outgoing transfer msg matches expected default mock', + ); + t.is(await res, undefined, 'transfer returns undefined'); + + t.log('transfer accepts custom memo'); + await E(account).transfer(mockAmountArg, mockDestination, { + memo: JSON.stringify({ custom: 'pfm memo' }), + }); + t.like( + await getAndDecodeLatestPacket(), + { + memo: '{"custom":"pfm memo"}', + }, + 'accepts custom memo', + ); + + t.log('transfer accepts custom timeoutHeight'); + await E(account).transfer(mockAmountArg, mockDestination, { + timeoutHeight: { + revisionHeight: 1000n, + revisionNumber: 1n, + }, + }); + t.like( + await getAndDecodeLatestPacket(), + { + timeoutHeight: { + revisionHeight: 1000n, + revisionNumber: 1n, + }, + timeoutTimestamp: 0n, + }, + "accepts custom timeoutHeight and doesn't set timeoutTimestamp", + ); + + t.log('transfer accepts custom timeoutTimestamp'); + await E(account).transfer(mockAmountArg, mockDestination, { + timeoutTimestamp: 999n, + }); + t.like( + await getAndDecodeLatestPacket(), + { + timeoutTimestamp: 999n, + timeoutHeight: { + revisionHeight: 0n, + revisionNumber: 0n, + }, + }, + "accepts custom timeoutTimestamp and doesn't set timeoutHeight", + ); + + t.log('transfer accepts custom timeoutHeight and timeoutTimestamp'); + await E(account).transfer(mockAmountArg, mockDestination, { + timeoutHeight: { + revisionHeight: 5000n, + revisionNumber: 5n, + }, + timeoutTimestamp: 5000n, + }); + t.like( + await getAndDecodeLatestPacket(), + { + timeoutHeight: { + revisionHeight: 5000n, + revisionNumber: 5n, + }, + timeoutTimestamp: 5000n, + }, + 'accepts custom timeoutHeight and timeoutTimestamp', + ); + + t.log('transfer throws if connection is not in its chainHub'); + await t.throwsAsync( + E(account).transfer(mockAmountArg, { + ...mockDestination, + chainId: 'unknown-1', + }), + { + message: 'connection not found: cosmoshub-4<->unknown-1', + }, + ); + + t.log("transfer doesn't support ERTP brands yet. see #9211"); + await t.throwsAsync(E(account).transfer(ist.make(10n), mockDestination), { + message: 'Brands not currently supported.', + }); + + t.log('transfer timeout error recieved and handled from the bridge'); + await t.throwsAsync( + E(account).transfer( + { ...mockAmountArg, value: SIMULATED_ERRORS.TIMEOUT }, + mockDestination, + ), + { + message: 'ABCI code: 5: error handling packet: see events for details', + }, + ); + t.like( + await getAndDecodeLatestPacket(), + { + token: { + amount: String(SIMULATED_ERRORS.TIMEOUT), + }, + }, + 'timeout error received from the bridge', + ); +}); + +test('CosmosOrchestrationAccount - not yet implemented', async t => { + const { bootstrap } = await commonSetup(t); + const makeTestCOAKit = prepareMakeTestCOAKit(t, bootstrap); + const account = await makeTestCOAKit(); const mockAmountArg: AmountArg = { value: 10n, denom: 'uatom' }; await t.throwsAsync(E(account).getBalances(), { message: 'not yet implemented', }); - // XXX consider, positioning amount + address args the same for .send and .transfer - await t.throwsAsync(E(account).transfer(mockAmountArg, mockChainAddress), { - message: 'not yet implemented', - }); await t.throwsAsync(E(account).transferSteps(mockAmountArg, null as any), { message: 'not yet implemented', }); diff --git a/packages/orchestration/test/exos/make-test-coa-kit.ts b/packages/orchestration/test/exos/make-test-coa-kit.ts index 99da8b9880d..4cfb283a580 100644 --- a/packages/orchestration/test/exos/make-test-coa-kit.ts +++ b/packages/orchestration/test/exos/make-test-coa-kit.ts @@ -4,6 +4,7 @@ import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/record import type { ExecutionContext } from 'ava'; import { commonSetup } from '../supports.js'; import { prepareCosmosOrchestrationAccount } from '../../src/exos/cosmos-orchestration-account.js'; +import { makeChainHub } from '../../src/exos/chain-hub.js'; /** * A testing utility that creates a (Cosmos)ChainAccount and makes a @@ -23,8 +24,14 @@ export const prepareMakeTestCOAKit = ( bootstrap: Awaited>['bootstrap'], { zcf = Far('MockZCF', {}) } = {}, ) => { - const { cosmosInterchainService, marshaller, rootZone, timer, vowTools } = - bootstrap; + const { + cosmosInterchainService, + marshaller, + rootZone, + timer, + vowTools, + agoricNames, + } = bootstrap; const { makeRecorderKit } = prepareRecorderKitMakers( rootZone.mapStore('CosmosOrchAccountRecorder'), @@ -33,15 +40,19 @@ export const prepareMakeTestCOAKit = ( const makeCosmosOrchestrationAccount = prepareCosmosOrchestrationAccount( rootZone.subZone('CosmosOrchAccount'), - makeRecorderKit, - vowTools, - // @ts-expect-error mocked zcf - zcf, + { + chainHub: makeChainHub(agoricNames, vowTools), + makeRecorderKit, + timerService: timer, + vowTools, + // @ts-expect-error mocked zcf + zcf, + }, ); return async ({ storageNode = bootstrap.storage.rootNode.makeChildNode('accounts'), - chainId = 'cosmoshub-99', + chainId = 'cosmoshub-4', hostConnectionId = 'connection-0' as const, controllerConnectionId = 'connection-1' as const, bondDenom = 'uatom', diff --git a/packages/orchestration/test/staking-ops.test.ts b/packages/orchestration/test/staking-ops.test.ts index 7dcc4bf67ca..dc9011f8802 100644 --- a/packages/orchestration/test/staking-ops.test.ts +++ b/packages/orchestration/test/staking-ops.test.ts @@ -23,10 +23,12 @@ import { makeDurableZone } from '@agoric/zone/durable.js'; import { decodeBase64 } from '@endo/base64'; import { Far } from '@endo/far'; import { Timestamp } from '@agoric/cosmic-proto/google/protobuf/timestamp.js'; +import { makeNameHubKit } from '@agoric/vats'; import { prepareCosmosOrchestrationAccountKit } from '../src/exos/cosmos-orchestration-account.js'; import type { ChainAddress, IcaAccount, ICQConnection } from '../src/types.js'; import { encodeTxResponse } from '../src/utils/cosmos.js'; import { MILLISECONDS_PER_SECOND } from '../src/utils/time.js'; +import { makeChainHub } from '../src/exos/chain-hub.js'; const trivialDelegateResponse = encodeTxResponse( {}, @@ -206,6 +208,7 @@ const makeScenario = () => { timeStep: TICK, eventLoopIteration, }); + const { nameHub: agoricNames } = makeNameHubKit(); return { baggage, zone, @@ -216,20 +219,29 @@ const makeScenario = () => { icqConnection, vowTools, ...mockZCF(), + agoricNames, }; }; test('makeAccount() writes to storage', async t => { const s = makeScenario(); const { account, timer } = s; - const { makeRecorderKit, storageNode, zcf, icqConnection, vowTools, zone } = - s; - const make = prepareCosmosOrchestrationAccountKit( + const { + agoricNames, + makeRecorderKit, + storageNode, + zcf, + icqConnection, + vowTools, zone, + } = s; + const make = prepareCosmosOrchestrationAccountKit(zone, { + chainHub: makeChainHub(agoricNames, vowTools), makeRecorderKit, + timerService: timer, vowTools, zcf, - ); + }); const { holder } = make(account.getAddress(), 'uatom', { account, @@ -252,14 +264,22 @@ test('makeAccount() writes to storage', async t => { test('withdrawRewards() on StakingAccountHolder formats message correctly', async t => { const s = makeScenario(); const { account, calls, timer } = s; - const { makeRecorderKit, storageNode, zcf, icqConnection, vowTools, zone } = - s; - const make = prepareCosmosOrchestrationAccountKit( + const { + agoricNames, + makeRecorderKit, + storageNode, + zcf, + icqConnection, + vowTools, zone, + } = s; + const make = prepareCosmosOrchestrationAccountKit(zone, { + chainHub: makeChainHub(agoricNames, vowTools), makeRecorderKit, + timerService: timer, vowTools, zcf, - ); + }); // Higher fidelity tests below use invitationMakers. const { holder } = make(account.getAddress(), 'uatom', { @@ -282,6 +302,7 @@ test(`delegate; redelegate using invitationMakers`, async t => { const s = makeScenario(); const { account, calls, timer } = s; const { + agoricNames, makeRecorderKit, storageNode, zcf, @@ -291,12 +312,13 @@ test(`delegate; redelegate using invitationMakers`, async t => { zone, } = s; const aBrand = Far('Token') as Brand<'nat'>; - const makeAccountKit = prepareCosmosOrchestrationAccountKit( - zone, + const makeAccountKit = prepareCosmosOrchestrationAccountKit(zone, { + chainHub: makeChainHub(agoricNames, vowTools), makeRecorderKit, + timerService: timer, vowTools, zcf, - ); + }); const { invitationMakers } = makeAccountKit(account.getAddress(), 'uatom', { account, @@ -364,6 +386,7 @@ test(`withdraw rewards using invitationMakers`, async t => { const s = makeScenario(); const { account, calls, timer } = s; const { + agoricNames, makeRecorderKit, storageNode, zcf, @@ -372,12 +395,13 @@ test(`withdraw rewards using invitationMakers`, async t => { vowTools, zone, } = s; - const makeAccountKit = prepareCosmosOrchestrationAccountKit( - zone, + const makeAccountKit = prepareCosmosOrchestrationAccountKit(zone, { + chainHub: makeChainHub(agoricNames, vowTools), makeRecorderKit, + timerService: timer, vowTools, zcf, - ); + }); const { invitationMakers } = makeAccountKit(account.getAddress(), 'uatom', { account, @@ -403,6 +427,7 @@ test(`undelegate waits for unbonding period`, async t => { const s = makeScenario(); const { account, calls, timer } = s; const { + agoricNames, makeRecorderKit, storageNode, zcf, @@ -411,12 +436,13 @@ test(`undelegate waits for unbonding period`, async t => { vowTools, zone, } = s; - const makeAccountKit = prepareCosmosOrchestrationAccountKit( - zone, + const makeAccountKit = prepareCosmosOrchestrationAccountKit(zone, { + chainHub: makeChainHub(agoricNames, vowTools), makeRecorderKit, + timerService: timer, vowTools, zcf, - ); + }); const { invitationMakers } = makeAccountKit(account.getAddress(), 'uatom', { account, diff --git a/packages/orchestration/test/types.test-d.ts b/packages/orchestration/test/types.test-d.ts index 0c7512fd337..894a91938a6 100644 --- a/packages/orchestration/test/types.test-d.ts +++ b/packages/orchestration/test/types.test-d.ts @@ -71,8 +71,6 @@ expectNotType(chainAddr); const makeCosmosOrchestrationAccount = prepareCosmosOrchestrationAccount( anyVal, anyVal, - anyVal, - anyVal, ); makeCosmosOrchestrationAccount( anyVal, From 1e5f500a7a74ad1e508ad67e6daf35a789dc852e Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 12 Aug 2024 18:58:40 -0400 Subject: [PATCH 10/14] feat: ensure mock lca address is unique - to address: Error: Target 'agoric1mockVlocalchainAddress' already registered --- .../boot/test/bootstrapTests/orchestration.test.ts | 8 ++++---- packages/boot/tools/supports.ts | 10 ++++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/boot/test/bootstrapTests/orchestration.test.ts b/packages/boot/test/bootstrapTests/orchestration.test.ts index f0967dc4ba5..c46658d77ee 100644 --- a/packages/boot/test/bootstrapTests/orchestration.test.ts +++ b/packages/boot/test/bootstrapTests/orchestration.test.ts @@ -341,12 +341,12 @@ test('basic-flows', async t => { wd.getCurrentWalletRecord().offerToPublicSubscriberPaths, ); t.deepEqual(publicSubscriberPaths['request-loa'], { - account: 'published.basicFlows.agoric1mockVlocalchainAddress', + account: 'published.basicFlows.agoric1fakeLCAAddress1', }); t.like(wd.getLatestUpdateRecord(), { status: { id: 'request-loa', numWantsSatisfied: 1 }, }); - t.is(readLatest('published.basicFlows.agoric1mockVlocalchainAddress'), ''); + t.is(readLatest('published.basicFlows.agoric1fakeLCAAddress'), ''); await wd.sendOffer({ id: 'transfer-to-noble-from-cosmos', @@ -450,7 +450,7 @@ test.serial('basic-flows - portfolio holder', async t => { [ 'request-portfolio-acct', { - agoric: 'published.basicFlows.agoric1mockVlocalchainAddress', + agoric: 'published.basicFlows.agoric1fakeLCAAddress', cosmoshub: 'published.basicFlows.cosmos1test', // XXX support multiple chain addresses in ibc mocks osmosis: 'published.basicFlows.cosmos1test', @@ -464,7 +464,7 @@ test.serial('basic-flows - portfolio holder', async t => { // XXX this overrides a previous account, since mocks only provide one address t.is(readLatest('published.basicFlows.cosmos1test'), ''); // XXX this overrides a previous account, since mocks only provide one address - t.is(readLatest('published.basicFlows.agoric1mockVlocalchainAddress'), ''); + t.is(readLatest('published.basicFlows.agoric1fakeLCAAddress'), ''); const { BLD } = agoricNamesRemotes.brand; BLD || Fail`BLD missing from agoricNames`; diff --git a/packages/boot/tools/supports.ts b/packages/boot/tools/supports.ts index 3384190d0d3..36af57d01cc 100644 --- a/packages/boot/tools/supports.ts +++ b/packages/boot/tools/supports.ts @@ -24,7 +24,10 @@ import { loadSwingsetConfigFile } from '@agoric/swingset-vat'; import { makeSlogSender } from '@agoric/telemetry'; import { TimeMath, Timestamp } from '@agoric/time'; import { Fail } from '@endo/errors'; -import { fakeLocalChainBridgeTxMsgHandler } from '@agoric/vats/tools/fake-bridge.js'; +import { + fakeLocalChainBridgeTxMsgHandler, + LOCALCHAIN_DEFAULT_ADDRESS, +} from '@agoric/vats/tools/fake-bridge.js'; import { makeRunUtils, @@ -323,6 +326,7 @@ export const makeSwingsetTestKit = async ( let lastBankNonce = 0n; let ibcSequenceNonce = 0; let lcaSequenceNonce = 0; + let lcaAccountsCreated = 0; const outboundMessages = new Map(); @@ -467,7 +471,9 @@ export const makeSwingsetTestKit = async ( return undefined; } case `${BridgeId.VLOCALCHAIN}:VLOCALCHAIN_ALLOCATE_ADDRESS`: { - return 'agoric1mockVlocalchainAddress'; + const address = `${LOCALCHAIN_DEFAULT_ADDRESS}${lcaAccountsCreated || ''}`; + lcaAccountsCreated += 1; + return address; } case `${BridgeId.VLOCALCHAIN}:VLOCALCHAIN_EXECUTE_TX`: { lcaSequenceNonce += 1; From b88db1eea791a4ea95abf727838fc2bd58c6a0c9 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 12 Aug 2024 18:59:55 -0400 Subject: [PATCH 11/14] feat: LocalOrchestrationAccount Transfer invitation --- .../test/bootstrapTests/orchestration.test.ts | 44 +++++++++++++++++++ .../src/exos/local-orchestration-account.js | 20 +++++++++ 2 files changed, 64 insertions(+) diff --git a/packages/boot/test/bootstrapTests/orchestration.test.ts b/packages/boot/test/bootstrapTests/orchestration.test.ts index c46658d77ee..5be8257f3ef 100644 --- a/packages/boot/test/bootstrapTests/orchestration.test.ts +++ b/packages/boot/test/bootstrapTests/orchestration.test.ts @@ -396,6 +396,50 @@ test('basic-flows', async t => { 'Error: ABCI code: 5: error handling packet: see events for details', }, }); + + await wd.sendOffer({ + id: 'transfer-to-noble-from-agoric', + invitationSpec: { + source: 'continuing', + previousOffer: 'request-loa', + invitationMakerName: 'Transfer', + }, + proposal: {}, + offerArgs: { + amount: { denom: 'ibc/uusdchash', value: 10n }, + destination: { + chainId: 'noble-1', + value: 'noble1test', + encoding: 'bech32,', + }, + }, + }); + t.like(wd.getLatestUpdateRecord(), { + status: { + id: 'transfer-to-noble-from-agoric', + error: undefined, + }, + }); + + await t.throwsAsync( + wd.executeOffer({ + id: 'transfer-to-noble-from-agoric-timeout', + invitationSpec: { + source: 'continuing', + previousOffer: 'request-loa', + invitationMakerName: 'Transfer', + }, + proposal: {}, + offerArgs: { + amount: { denom: 'ibc/uusdchash', value: SIMULATED_ERRORS.TIMEOUT }, + destination: { + chainId: 'noble-1', + value: 'noble1test', + encoding: 'bech32,', + }, + }, + }), + ); }); test.serial('auto-stake-it - proposal', async t => { diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index f5eb7a8979c..e885384ac0c 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -149,6 +149,7 @@ export const prepareLocalOrchestrationAccountKit = ( CloseAccount: M.call().returns(M.promise()), Send: M.call().returns(M.promise()), SendAll: M.call().returns(M.promise()), + Transfer: M.call().returns(M.promise()), }), }, /** @@ -243,6 +244,25 @@ export const prepareLocalOrchestrationAccountKit = ( }; return zcf.makeInvitation(offerHandler, 'SendAll'); }, + Transfer() { + /** + * @type {OfferHandler< + * Vow, + * { + * amount: AmountArg; + * destination: ChainAddress; + * opts: IBCMsgTransferOptions; + * } + * >} + */ + const offerHandler = (seat, { amount, destination, opts }) => { + seat.exit(); + return watch( + this.facets.holder.transfer(amount, destination, opts), + ); + }; + return zcf.makeInvitation(offerHandler, 'Transfer'); + }, }, undelegateWatcher: { /** From 3481ec5c81d62908b399052fdbe4b10c681f7961 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 15 Aug 2024 18:18:41 -0400 Subject: [PATCH 12/14] types: opts: IBCMsgTransferOptions are optional --- packages/orchestration/src/exos/local-orchestration-account.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index e885384ac0c..5bfe93eb92c 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -291,7 +291,7 @@ export const prepareLocalOrchestrationAccountKit = ( * ]} results * @param {{ * destination: ChainAddress; - * opts: IBCMsgTransferOptions; + * opts?: IBCMsgTransferOptions; * amount: DenomAmount; * }} ctx */ From 9567d1ca394701ea2bffbdd142bf60486f17a65f Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 15 Aug 2024 18:34:22 -0400 Subject: [PATCH 13/14] refactor: simplify stakeIca proposals --- .../src/proposals/start-stakeAtom.js | 20 ++++++++++--------- .../src/proposals/start-stakeOsmo.js | 20 ++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/orchestration/src/proposals/start-stakeAtom.js b/packages/orchestration/src/proposals/start-stakeAtom.js index d4e4ccbd945..aac824c946a 100644 --- a/packages/orchestration/src/proposals/start-stakeAtom.js +++ b/packages/orchestration/src/proposals/start-stakeAtom.js @@ -1,4 +1,4 @@ -import { makeTracer } from '@agoric/internal'; +import { deeplyFulfilledObject, makeTracer } from '@agoric/internal'; import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js'; import { prepareVowTools } from '@agoric/vow'; import { makeHeapZone } from '@agoric/zone'; @@ -28,7 +28,7 @@ export const startStakeAtom = async ({ agoricNames, board, chainStorage, - chainTimerService, + chainTimerService: timer, cosmosInterchainService, startUpgradable, }, @@ -64,13 +64,15 @@ export const startStakeAtom = async ({ bondDenom: cosmoshub.stakingTokens[0].denom, icqEnabled: cosmoshub.icqEnabled, }, - privateArgs: { - agoricNames: await agoricNames, - cosmosInterchainService: await cosmosInterchainService, - storageNode, - marshaller, - timer: await chainTimerService, - }, + privateArgs: await deeplyFulfilledObject( + harden({ + agoricNames, + cosmosInterchainService, + storageNode, + marshaller, + timer, + }), + ), }; const { instance } = await E(startUpgradable)(startOpts); diff --git a/packages/orchestration/src/proposals/start-stakeOsmo.js b/packages/orchestration/src/proposals/start-stakeOsmo.js index 57e7f25b1d0..c6956b130b4 100644 --- a/packages/orchestration/src/proposals/start-stakeOsmo.js +++ b/packages/orchestration/src/proposals/start-stakeOsmo.js @@ -1,4 +1,4 @@ -import { makeTracer } from '@agoric/internal'; +import { deeplyFulfilledObject, makeTracer } from '@agoric/internal'; import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js'; import { prepareVowTools } from '@agoric/vow'; import { makeHeapZone } from '@agoric/zone'; @@ -33,7 +33,7 @@ export const startStakeOsmo = async ({ agoricNames, board, chainStorage, - chainTimerService, + chainTimerService: timer, cosmosInterchainService, startUpgradable, }, @@ -69,13 +69,15 @@ export const startStakeOsmo = async ({ bondDenom: osmosis.stakingTokens[0].denom, icqEnabled: osmosis.icqEnabled, }, - privateArgs: { - agoricNames: await agoricNames, - cosmosInterchainService: await cosmosInterchainService, - storageNode, - marshaller, - timer: await chainTimerService, - }, + privateArgs: await deeplyFulfilledObject( + harden({ + agoricNames, + cosmosInterchainService, + storageNode, + marshaller, + timer, + }), + ), }; const { instance } = await E(startUpgradable)(startOpts); From e3ce1600558f19df8e879760c3dc5fe4e42d3bd2 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 16 Aug 2024 07:58:28 -0400 Subject: [PATCH 14/14] refactor: rename ibc/mocks.ts must be .ts for .ts modules to find it by its .js module name --- packages/boot/test/bootstrapTests/vat-orchestration.test.ts | 2 +- packages/boot/test/tools/ibc/{mocks.test.js => mocks.test.ts} | 0 packages/boot/tools/ibc/{mocks.js => mocks.ts} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename packages/boot/test/tools/ibc/{mocks.test.js => mocks.test.ts} (100%) rename packages/boot/tools/ibc/{mocks.js => mocks.ts} (100%) diff --git a/packages/boot/test/bootstrapTests/vat-orchestration.test.ts b/packages/boot/test/bootstrapTests/vat-orchestration.test.ts index bfcbf6dbeee..7757344b6bc 100644 --- a/packages/boot/test/bootstrapTests/vat-orchestration.test.ts +++ b/packages/boot/test/bootstrapTests/vat-orchestration.test.ts @@ -24,7 +24,7 @@ const test: TestFn = anyTest; /** * To update, pass the message into `makeTxPacket` or `makeQueryPacket` from * `@agoric/orchestration`, and paste the resulting `data` key into `protoMsgMocks` - * in [mocks.js](../../tools/ibc/mocks.js). + * in [mocks.ts](../../tools/ibc/mocks.ts). * If adding a new msg, reference the mock in the `sendPacket` switch statement * in [supports.ts](../../tools/supports.ts). */ diff --git a/packages/boot/test/tools/ibc/mocks.test.js b/packages/boot/test/tools/ibc/mocks.test.ts similarity index 100% rename from packages/boot/test/tools/ibc/mocks.test.js rename to packages/boot/test/tools/ibc/mocks.test.ts diff --git a/packages/boot/tools/ibc/mocks.js b/packages/boot/tools/ibc/mocks.ts similarity index 100% rename from packages/boot/tools/ibc/mocks.js rename to packages/boot/tools/ibc/mocks.ts