From f63f41a4ff1f839269113d103998b81e735645a5 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 30 Jul 2024 12:03:17 -0400 Subject: [PATCH 1/9] test: refactoring --- .../local-orchestration-account-kit.test.ts | 41 ++++++++++--------- packages/orchestration/test/supports.ts | 10 ++--- packages/vow/test/vat.test.js | 8 ++-- 3 files changed, 30 insertions(+), 29 deletions(-) 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 aee995de057..53de7c1ff07 100644 --- a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts +++ b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts @@ -2,7 +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 { heapVowE as E } from '@agoric/vow/vat.js'; +import { heapVowE as VE } from '@agoric/vow/vat.js'; import { TargetApp } from '@agoric/vats/src/bridge-target.js'; import { ChainAddress } from '../../src/orchestration-api.js'; import { NANOSECONDS_PER_SECOND } from '../../src/utils/time.js'; @@ -25,13 +25,13 @@ test('deposit, withdraw', async t => { const oneHundredStakePmt = await utils.pourPayment(stake.units(100)); t.log('deposit 100 bld to account'); - await E(account).deposit(oneHundredStakePmt); + await VE(account).deposit(oneHundredStakePmt); // FIXME #9211 // t.deepEqual(await E(account).getBalance('ubld'), stake.units(100)); // XXX races in the bridge await eventLoopIteration(); - const withdrawal1 = await E(account).withdraw(stake.units(50)); + const withdrawal1 = await VE(account).withdraw(stake.units(50)); t.true( AmountMath.isEqual( await stake.issuer.getAmountOf(withdrawal1), @@ -40,16 +40,16 @@ test('deposit, withdraw', async t => { ); await t.throwsAsync( - E(account).withdraw(stake.units(51)), + VE(account).withdraw(stake.units(51)), undefined, 'fails to overwithdraw', ); await t.notThrowsAsync( - E(account).withdraw(stake.units(50)), + VE(account).withdraw(stake.units(50)), 'succeeeds at exactly empty', ); await t.throwsAsync( - E(account).withdraw(stake.make(1n)), + VE(account).withdraw(stake.make(1n)), undefined, 'fails to overwithdraw', ); @@ -66,7 +66,7 @@ test('delegate, undelegate', async t => { utils, } = common; - await E(account).deposit(await utils.pourPayment(bld.units(100))); + await VE(account).deposit(await utils.pourPayment(bld.units(100))); const validatorAddress = 'agoric1validator1'; @@ -74,8 +74,8 @@ test('delegate, undelegate', async t => { // 1. these succeed even if funds aren't available // 2. there are no return values // 3. there are no side-effects such as assets being locked - await E(account).delegate(validatorAddress, bld.units(999)); - const undelegateP = E(account).undelegate(validatorAddress, bld.units(999)); + await VE(account).delegate(validatorAddress, bld.units(999)); + const undelegateP = VE(account).undelegate(validatorAddress, bld.units(999)); const completionTime = UNBOND_PERIOD_SECONDS + maxClockSkew; const notTooSoon = Promise.race([ @@ -106,7 +106,7 @@ test('transfer', async t => { const oneHundredStakePmt = await utils.pourPayment(stake.units(100)); t.log('deposit 100 bld to account'); - await E(account).deposit(oneHundredStakePmt); + await VE(account).deposit(oneHundredStakePmt); // FIXME #9211 // t.deepEqual(await E(account).getBalance('ubld'), stake.units(100)); @@ -118,19 +118,19 @@ test('transfer', async t => { // TODO #9211, support ERTP amounts t.log('ERTP Amounts not yet supported for AmountArg'); - await t.throwsAsync(() => E(account).transfer(stake.units(1), destination), { + await t.throwsAsync(() => VE(account).transfer(stake.units(1), destination), { message: 'ERTP Amounts not yet supported', }); t.log('.transfer() 1 bld to cosmos using DenomAmount'); - const transferResp = await E(account).transfer( + const transferResp = await VE(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), + () => VE(account).transfer({ denom: 'ubld', value: 504n }, destination), { message: 'simulated unexpected MsgTransfer packet timeout', }, @@ -142,14 +142,15 @@ test('transfer', async t => { encoding: 'bech32', }; await t.throwsAsync( - () => E(account).transfer({ denom: 'ubld', value: 1n }, unknownDestination), + () => + VE(account).transfer({ denom: 'ubld', value: 1n }, unknownDestination), { message: /connection not found: agoric-3<->fakenet/ }, 'cannot create transfer msg with unknown chainId', ); await t.notThrowsAsync( () => - E(account).transfer({ denom: 'ubld', value: 10n }, destination, { + VE(account).transfer({ denom: 'ubld', value: 10n }, destination, { memo: 'hello', }), 'can create transfer msg with memo', @@ -158,7 +159,7 @@ test('transfer', async t => { await t.notThrowsAsync( () => - E(account).transfer({ denom: 'ubld', value: 10n }, destination, { + VE(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, }), @@ -167,7 +168,7 @@ test('transfer', async t => { await t.notThrowsAsync( () => - E(account).transfer({ denom: 'ubld', value: 10n }, destination, { + VE(account).transfer({ denom: 'ubld', value: 10n }, destination, { timeoutHeight: { revisionHeight: 100n, revisionNumber: 1n }, }), 'accepts custom timeoutHeight', @@ -193,12 +194,12 @@ test('monitor transfers', async t => { }, }); - const { value: target } = await E(account).getAddress(); - const appRegistration = await E(account).monitorTransfers(tap); + const { value: target } = await VE(account).getAddress(); + const appRegistration = await VE(account).monitorTransfers(tap); // simulate upcall from golang to VM const simulateIncomingTransfer = async () => - E(transferBridge).fromBridge( + VE(transferBridge).fromBridge( buildVTransferEvent({ receiver: target, }), diff --git a/packages/orchestration/test/supports.ts b/packages/orchestration/test/supports.ts index b3754320e28..3ceff581a79 100644 --- a/packages/orchestration/test/supports.ts +++ b/packages/orchestration/test/supports.ts @@ -13,7 +13,7 @@ import { makeFakeLocalchainBridge, makeFakeTransferBridge, } from '@agoric/vats/tools/fake-bridge.js'; -import { prepareVowTools } from '@agoric/vow'; +import { prepareSwingsetVowTools } from '@agoric/vow/vat.js'; import type { Installation } from '@agoric/zoe/src/zoeService/utils.js'; import { buildZoeManualTimer } from '@agoric/zoe/tools/manualTimer.js'; import { withAmountUtils } from '@agoric/zoe/tools/test-utils.js'; @@ -69,7 +69,7 @@ export const commonSetup = async (t: ExecutionContext) => { }), ); - const vowTools = prepareVowTools(rootZone.subZone('vows')); + const vowTools = prepareSwingsetVowTools(rootZone.subZone('vows')); const transferBridge = makeFakeTransferBridge(rootZone); const { makeBridgeTargetKit } = prepareBridgeTargetModule( @@ -90,9 +90,9 @@ export const commonSetup = async (t: ExecutionContext) => { finisher.useRegistry(bridgeTargetKit.targetRegistry); await E(transferBridge).initHandler(bridgeTargetKit.bridgeHandler); - const localBrigeMessages = [] as any[]; + const localBridgeMessages = [] as any[]; const localchainBridge = makeFakeLocalchainBridge(rootZone, obj => - localBrigeMessages.push(obj), + localBridgeMessages.push(obj), ); const localchain = prepareLocalChainTools( rootZone.subZone('localchain'), @@ -162,7 +162,7 @@ export const commonSetup = async (t: ExecutionContext) => { }, utils: { pourPayment, - inspectLocalBridge: () => harden([...localBrigeMessages]), + inspectLocalBridge: () => harden([...localBridgeMessages]), inspectDibcBridge: () => E(ibcBridge).inspectDibcBridge(), }, }; diff --git a/packages/vow/test/vat.test.js b/packages/vow/test/vat.test.js index 019742ec4e5..4babdb3a04b 100644 --- a/packages/vow/test/vat.test.js +++ b/packages/vow/test/vat.test.js @@ -2,7 +2,7 @@ import test from 'ava'; import { E, Far } from '@endo/far'; -import { heapVowE, heapVowTools } from '../vat.js'; +import { heapVowE as VE, heapVowTools } from '../vat.js'; const { makeVowKit } = heapVowTools; @@ -13,7 +13,7 @@ test('heap messages', async t => { /** @type {ReturnType>} */ const { vow, resolver } = makeVowKit(); - const retP = heapVowE(vow).hello('World'); + const retP = VE(vow).hello('World'); resolver.resolve(greeter); // Happy path: WE(vow)[method](...args) calls the method. @@ -29,10 +29,10 @@ test('heap messages', async t => { ); // Happy path: await WE.when unwraps the vow. - t.is(await heapVowE.when(vow), greeter); + t.is(await VE.when(vow), greeter); t.is( - await heapVowE.when(vow, res => { + await VE.when(vow, res => { t.is(res, greeter); return 'done'; }), From bc6d19d18705bd635315acec330b7528ed9c385e Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 31 Jul 2024 12:22:35 -0400 Subject: [PATCH 2/9] chore(types): sequence is bigint --- packages/vats/src/types.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vats/src/types.d.ts b/packages/vats/src/types.d.ts index 5931569378d..a2f901dd144 100644 --- a/packages/vats/src/types.d.ts +++ b/packages/vats/src/types.d.ts @@ -142,9 +142,9 @@ export type IBCPacket = { source_port: IBCPortID; destination_channel: IBCChannelID; destination_port: IBCPortID; - sequence?: number; - timeout_height?: number; - timeout_timestamp?: number; + sequence?: PacketSDKType['sequence']; + timeout_height?: PacketSDKType['timeout_height']; + timeout_timestamp?: PacketSDKType['timeout_timestamp']; }; export type IBCCounterParty = { From 6368cfdb65ee909df64cdcad0f59d017c58d9afd Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Wed, 31 Jul 2024 16:39:43 -0400 Subject: [PATCH 3/9] feat: PacketTools exo --- .../orchestration/src/exos/packet-tools.js | 538 ++++++++++++++++++ 1 file changed, 538 insertions(+) create mode 100644 packages/orchestration/src/exos/packet-tools.js diff --git a/packages/orchestration/src/exos/packet-tools.js b/packages/orchestration/src/exos/packet-tools.js new file mode 100644 index 00000000000..ea524ae938f --- /dev/null +++ b/packages/orchestration/src/exos/packet-tools.js @@ -0,0 +1,538 @@ +import { assertAllDefined } from '@agoric/internal'; +import { makeMarshal, decodeToJustin } from '@endo/marshal'; +import { + base64ToBytes, + byteSourceToBase64, + Shape as NetworkShape, +} from '@agoric/network'; +import { M, matches } from '@endo/patterns'; +import { E } from '@endo/far'; +import { pickFacet } from '@agoric/vat-data'; + +// As specified in ICS20, the success result is a base64-encoded '\0x1' byte. +export const ICS20_TRANSFER_SUCCESS_RESULT = 'AQ=='; + +const { toCapData } = makeMarshal(undefined, undefined, { + marshalName: 'JustEncoder', + serializeBodyFormat: 'capdata', +}); +const just = obj => { + const { body } = toCapData(obj); + return decodeToJustin(JSON.parse(body), true); +}; + +/** + * @import {Bytes} from '@agoric/network'; + * @import {EVow, Remote, Vow, VowResolver, VowTools} from '@agoric/vow'; + * @import {JsonSafe, TypedJson, ResponseTo} from '@agoric/cosmic-proto'; + * @import {LocalChainAccount} from '@agoric/vats/src/localchain.js'; + * @import {TargetApp, TargetRegistration} from '@agoric/vats/src/bridge-target.js'; + */ + +/** + * @callback MatchEvent + * @param {EVow} pattern + * @returns {Vow<{ resolver: VowResolver; match: Vow }>} + */ + +/** + * @typedef {object} PacketSender + * @property {(opts: PacketOptions) => Vow} sendPacket + */ + +/** + * @typedef {object} PacketOptions + * @property {string} [opName] + * @property {PacketTimeout} [timeout] + */ + +/** + * @typedef {Pick< + * import('../cosmos-api').IBCMsgTransferOptions, + * 'timeoutHeight' | 'timeoutTimestamp' + * >} PacketTimeout + */ + +const { Fail, bare } = assert; +const { Vow$ } = NetworkShape; // TODO #9611 + +const EVow$ = shape => M.or(Vow$(shape), M.promise(/* shape */)); + +const sink = () => {}; +harden(sink); + +/** + * Create a pattern for alterative representations of a sequence number. + * + * @param {any} sequence + * @returns {import('@endo/patterns').Pattern} + */ +const createSequencePattern = sequence => { + const sequencePatterns = []; + + try { + const bintSequence = BigInt(sequence); + bintSequence > 0n && sequencePatterns.push(bintSequence); + } catch (e) { + // ignore + } + + const numSequence = Number(sequence); + numSequence > 0 && + Number.isSafeInteger(numSequence) && + sequencePatterns.push(numSequence); + + const strSequence = String(sequence); + strSequence && sequencePatterns.push(strSequence); + + if (!sequencePatterns.find(seq => seq === sequence)) { + sequencePatterns.push(sequence); + } + + switch (sequencePatterns.length) { + case 0: + throw Fail`sequence ${sequence} is not valid`; + case 1: + return sequencePatterns[0]; + default: + return M.or(...sequencePatterns); + } +}; +harden(createSequencePattern); + +/** + * @param {import('@agoric/base-zone').Zone} zone + * @param {VowTools} vowTools + */ +export const prepareTransferSender = (zone, { watch }) => { + const makeTransferSenderKit = zone.exoClassKit( + 'TransferSenderKit', + { + public: M.interface('TransferSender', { + sendPacket: M.call(M.any()).returns(Vow$(M.record())), + }), + responseWatcher: M.interface('responseWatcher', { + onFulfilled: M.call([M.record()]).returns(M.any()), + }), + }, + /** + * @param {{ + * executeTx: LocalChainAccount['executeTx']; + * }} txExecutor + * @param {TypedJson<'/ibc.applications.transfer.v1.MsgTransfer'>} transferMsg + */ + (txExecutor, transferMsg) => ({ + txExecutor, + transferMsg: harden(transferMsg), + }), + { + public: { + sendPacket() { + const { txExecutor, transferMsg } = this.state; + return watch( + E(txExecutor).executeTx([transferMsg]), + this.facets.responseWatcher, + ); + }, + }, + responseWatcher: { + /** + * Wait for successfully sending the transfer packet. + * + * @param {[ + * JsonSafe< + * ResponseTo< + * TypedJson<'/ibc.applications.transfer.v1.MsgTransfer'> + * > + * >, + * ]} response + */ + onFulfilled([{ sequence }]) { + const { transferMsg } = this.state; + + // Match the port/channel and sequence number. + return M.splitRecord({ + source_port: transferMsg.sourcePort, + source_channel: transferMsg.sourceChannel, + sequence: createSequencePattern(sequence), + }); + }, + }, + }, + ); + + /** + * @param {Parameters} args + */ + return (...args) => makeTransferSenderKit(...args).public; +}; +harden(prepareTransferSender); + +/** + * @param {import('@agoric/base-zone').Zone} zone + * @param {VowTools} vowTools + */ +export const preparePacketTools = (zone, vowTools) => { + const { allVows, makeVowKit, watch, when } = vowTools; + const makeTransferSender = prepareTransferSender(zone, vowTools); + const makePacketToolsKit = zone.exoClassKit( + 'PacketToolsKit', + { + public: M.interface('PacketTools', { + waitForIBCAck: M.call(EVow$(M.remotable('PacketSender'))) + .optional(M.any()) + .returns(EVow$(M.string())), + matchFirstPacket: M.call(M.any()).returns(EVow$(M.any())), + monitorTransfers: M.call(M.remotable('TargetApp')).returns( + EVow$(M.any()), + ), + }), + tap: M.interface('tap', { + // eslint-disable-next-line no-restricted-syntax + receiveUpcall: M.callWhen(M.any()).returns(M.any()), + }), + monitorRegistration: M.interface('monitorRegistration', { + // eslint-disable-next-line no-restricted-syntax + updateTargetApp: M.callWhen( + M.await(M.remotable('TargetApp')), + ).returns(), + // eslint-disable-next-line no-restricted-syntax + revoke: M.callWhen().returns(), + }), + watchPacketMatch: M.interface('watchPacketMatch', { + onFulfilled: M.call(M.any(), M.record()).returns(M.any()), + }), + watchPacketPattern: M.interface('watchPacketPattern', { + onFulfilled: M.call(M.any(), M.record()).returns(M.any()), + onRejected: M.call(M.any(), M.record()).returns(M.any()), + }), + watchDecrPendingPatterns: M.interface('watchDecrPendingPatterns', { + onFulfilled: M.call(M.any()).returns(M.any()), + onRejected: M.call(M.any()).returns(M.any()), + }), + sendPacketWatcher: M.interface('sendPacketWatcher', { + onFulfilled: M.call( + [M.record(), M.remotable('PacketSender')], + M.record(), + ).returns(M.any()), + }), + utils: M.interface('utils', { + subscribeToTransfers: M.call().returns(M.promise()), + unsubscribeFromTransfers: M.call().returns(M.promise()), + incrPendingPatterns: M.call().returns(Vow$(M.undefined())), + decrPendingPatterns: M.call().returns(Vow$(M.undefined())), + }), + packetWasSentWatcher: M.interface('packetWasSentWatcher', { + onFulfilled: M.call(M.pattern(), M.record()).returns(M.any()), + }), + processIBCReplyWatcher: M.interface('processIBCReplyWatcher', { + onFulfilled: M.call(M.record(), M.record()).returns(Vow$(M.string())), + }), + subscribeToPatternWatcher: M.interface('subscribeToPatternWatcher', { + onFulfilled: M.call(M.pattern()).returns(Vow$(M.any())), + }), + rejectResolverAndRethrowWatcher: M.interface('rejectResolverWatcher', { + onRejected: M.call(M.any(), { + resolver: M.remotable('resolver'), + }).returns(M.any()), + }), + }, + /** + * @param {LocalChainAccount} lca + */ + lca => { + const resolverToPattern = zone.detached().mapStore('resolverToPattern'); + return { + lca, + reg: /** @type {Remote | null} */ (null), + resolverToPattern, + upcallQueue: /** @type {any[] | null} */ (null), + pending: 0, + extra: null, + monitor: /** @type {Remote | null} */ (null), + }; + }, + { + public: { + /** + * @param {ERef} monitor + */ + // eslint-disable-next-line no-restricted-syntax + async monitorTransfers(monitor) { + // We set the monitor here, but we only ever subscribe our + // this.facets.tap handler to transfers. + const mreg = this.facets.monitorRegistration; + await mreg.updateTargetApp(monitor); + return mreg; + }, + /** + * @type {MatchEvent} + */ + matchFirstPacket(patternP) { + return watch( + this.facets.utils.incrPendingPatterns(), + this.facets.watchPacketMatch, + { patternP }, + ); + }, + /** + * @param {Remote} packetSender + * @param {PacketOptions} [opts] + * @returns {Vow} + */ + waitForIBCAck(packetSender, opts) { + const { opName = 'Unknown' } = opts || {}; + + /** @type {import('@agoric/vow').VowKit} */ + const pattern = makeVowKit(); + + // Establish the packet matcher immediately, but don't fulfill + // the match until after pattern.vow has been resolved. + const matchV = watch( + allVows([ + this.facets.public.matchFirstPacket(pattern.vow), + packetSender, + ]), + this.facets.sendPacketWatcher, + { opts, patternResolver: pattern.resolver }, + ); + + // If the pattern is fulfilled, process it. + const processedV = watch(matchV, this.facets.processIBCReplyWatcher, { + opName, + }); + + // If anything fails, try to reject the packet sender. + return watch( + processedV, + this.facets.rejectResolverAndRethrowWatcher, + { + resolver: pattern.resolver, + }, + ); + }, + }, + monitorRegistration: { + /** @type {TargetRegistration['updateTargetApp']} */ + // eslint-disable-next-line no-restricted-syntax + async updateTargetApp(tap) { + this.state.monitor = await tap; + await this.facets.utils.subscribeToTransfers(); + }, + /** @type {TargetRegistration['revoke']} */ + // eslint-disable-next-line no-restricted-syntax + async revoke() { + this.state.monitor = null; + }, + }, + tap: { + // eslint-disable-next-line no-restricted-syntax + async receiveUpcall(obj) { + const { monitor, resolverToPattern, upcallQueue, pending } = + this.state; + console.debug( + `Trying ${resolverToPattern.getSize()} current patterns and ${pending} pending patterns against`, + just(obj), + ); + + if (monitor) { + // Call the monitor (if any), but in a future turn. + void E(monitor).receiveUpcall(obj); + } + // Check all our fulfilled patterns for matches. + for (const [resolver, pattern] of resolverToPattern.entries()) { + if (matches(obj, pattern)) { + console.debug('Matched pattern:', just(pattern)); + resolver.resolve(obj); + resolverToPattern.delete(resolver); + return; + } + } + if (upcallQueue) { + // We have some pending patterns (ones that have been requested but + // haven't yet settled) that may match this object. + console.debug('Stashing object in upcallQueue'); + this.state.upcallQueue = harden(upcallQueue.concat(obj)); + } + console.debug('No match yet.'); + }, + }, + sendPacketWatcher: { + onFulfilled([{ match }, sender], ctx) { + return watch( + E(sender).sendPacket(ctx.opts), + this.facets.packetWasSentWatcher, + { + ...ctx, + match, + }, + ); + }, + }, + packetWasSentWatcher: { + onFulfilled(packetPattern, ctx) { + const { patternResolver, match } = ctx; + // Match an acknowledgement or timeout packet. + const ackOrTimeoutPacket = M.or( + M.splitRecord({ + event: 'acknowledgementPacket', + packet: packetPattern, + acknowledgement: M.string(), + }), + M.splitRecord({ + event: 'timeoutPacket', + packet: packetPattern, + }), + ); + patternResolver.resolve(ackOrTimeoutPacket); + return match; + }, + }, + subscribeToPatternWatcher: { + onFulfilled(_pattern) { + // FIXME: Implement this! + return watch({ + type: 'VTRANSFER_IBC_EVENT', + event: 'acknowledgementPacket', + acknowledgement: byteSourceToBase64( + JSON.stringify({ result: 'AQ==' }), + ), + }); + }, + }, + processIBCReplyWatcher: { + onFulfilled({ event, acknowledgement }, { opName }) { + assertAllDefined({ event, acknowledgement }); + switch (event) { + case 'acknowledgementPacket': + return base64ToBytes(acknowledgement); + case 'timeoutPacket': + throw Fail`${bare(opName)} operation received timeout packet`; + default: + throw Fail`Unexpected event: ${event}`; + } + }, + }, + rejectResolverAndRethrowWatcher: { + onRejected(rej, { resolver }) { + resolver.reject(rej); + throw rej; + }, + }, + watchPacketMatch: { + onFulfilled(_, { patternP }) { + const { vow, resolver } = makeVowKit(); + const patternV = watch( + patternP, + this.facets.watchPacketPattern, + harden({ resolver }), + ); + /* void */ watch(patternV, this.facets.watchDecrPendingPatterns); + return harden({ match: vow, resolver }); + }, + }, + watchDecrPendingPatterns: { + onFulfilled() { + return this.facets.utils.decrPendingPatterns(); + }, + onRejected() { + return this.facets.utils.decrPendingPatterns(); + }, + }, + watchPacketPattern: { + onFulfilled(pattern, { resolver }) { + const { resolverToPattern, upcallQueue } = this.state; + + console.debug('watchPacketPattern onFulfilled', just(pattern)); + if (!upcallQueue) { + // Save the pattern for later. + console.debug('No upcall queue yet. Save the pattern for later.'); + resolverToPattern.init(resolver, pattern); + return; + } + + // Try matching the first in queue. + const i = upcallQueue.findIndex(obj => matches(obj, pattern)); + if (i < 0) { + // No match yet. Save the pattern for later. + console.debug('No match yet. Save the pattern for later.'); + resolverToPattern.init(resolver, pattern); + return; + } + + // Success! Remove the matched object from the queue. + console.debug( + 'Success! Remove the matched object from the queue.', + just(upcallQueue[i]), + ); + resolver.resolve(upcallQueue[i]); + this.state.upcallQueue = harden( + upcallQueue.slice(0, i).concat(upcallQueue.slice(i + 1)), + ); + }, + onRejected(reason, { resolver }) { + resolver.reject(reason); + }, + }, + + utils: { + incrPendingPatterns() { + const { pending, reg, upcallQueue } = this.state; + this.state.pending += 1; + if (!upcallQueue) { + this.state.upcallQueue = harden([]); + } + if (reg || pending > 0) { + return watch(undefined); + } + return watch(this.facets.utils.subscribeToTransfers()); + }, + decrPendingPatterns() { + this.state.pending -= 1; + if (this.state.pending > 0) { + return; + } + this.state.pending = 0; + this.state.upcallQueue = null; + return watch(this.facets.utils.unsubscribeFromTransfers()); + }, + subscribeToTransfers() { + // Subscribe to the transfers for this account. + const { lca, reg } = this.state; + if (reg) { + return when(reg); + } + const { tap } = this.facets; + return when(E(lca).monitorTransfers(tap), r => { + this.state.reg = r; + return r; + }); + }, + unsubscribeFromTransfers() { + const { reg, monitor } = this.state; + if (!reg || monitor) { + return undefined; + } + // this.state.reg = null; + // return E(reg).revoke().then(sink); + }, + }, + }, + { + finish(context) { + void context.facets.utils.subscribeToTransfers(); + }, + }, + ); + + const makePacketTools = pickFacet(makePacketToolsKit, 'public'); + + return harden({ makePacketTools, makeTransferSender }); +}; +harden(preparePacketTools); + +/** + * @typedef {Awaited< + * ReturnType['makePacketTools']> + * >} PacketTools + */ From 96c4ee7b8d8be3c90395d1f0a556e37cfe8df88f Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 31 Jul 2024 16:40:08 -0400 Subject: [PATCH 4/9] feat(tools): improve IBC mocks --- packages/orchestration/tools/ibc-mocks.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/orchestration/tools/ibc-mocks.ts b/packages/orchestration/tools/ibc-mocks.ts index cc056eccd37..778cc1513ee 100644 --- a/packages/orchestration/tools/ibc-mocks.ts +++ b/packages/orchestration/tools/ibc-mocks.ts @@ -6,9 +6,11 @@ import { } from '@agoric/cosmic-proto/tendermint/abci/types.js'; import { encodeBase64, btoa } from '@endo/base64'; import { toRequestQueryJson } from '@agoric/cosmic-proto'; -import { IBCChannelID, VTransferIBCEvent } from '@agoric/vats'; +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 { makeQueryPacket, makeTxPacket } from '../src/utils/packet.js'; import { ChainAddress } from '../src/orchestration-api.js'; @@ -136,10 +138,12 @@ type BuildVTransferEventParams = { sender?: ChainAddress['value']; /** defaults to agoric1fakeLCAAddress. set to a different value to simulate an outgoing transfer event */ receiver?: ChainAddress['value']; + target?: ChainAddress['value']; amount?: bigint; denom?: string; destinationChannel?: IBCChannelID; sourceChannel?: IBCChannelID; + sequence?: PacketSDKType['sequence']; }; /** @@ -170,11 +174,13 @@ type BuildVTransferEventParams = { export const buildVTransferEvent = ({ event = 'acknowledgementPacket' as const, sender = 'cosmos1AccAddress', - receiver = 'agoric1fakeLCAAddress', + receiver = LOCALCHAIN_DEFAULT_ADDRESS, + target = LOCALCHAIN_DEFAULT_ADDRESS, amount = 10n, denom = 'uatom', destinationChannel = 'channel-0' as IBCChannelID, sourceChannel = 'channel-405' as IBCChannelID, + sequence = 0n, }: BuildVTransferEventParams = {}): VTransferIBCEvent => ({ type: VTRANSFER_IBC_EVENT, blockHeight: 0, @@ -182,7 +188,7 @@ export const buildVTransferEvent = ({ event, acknowledgement: btoa(JSON.stringify({ result: 'AQ==' })), relayer: 'agoric123', - target: receiver, + target, packet: { data: btoa( JSON.stringify( @@ -198,5 +204,6 @@ export const buildVTransferEvent = ({ source_channel: sourceChannel, destination_port: 'transfer', source_port: 'transfer', - }, + sequence, + } as IBCPacket, }); From a7d198e57871ac4d7a8eb17872e1f7e08b98dde1 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Tue, 30 Jul 2024 12:59:37 -0400 Subject: [PATCH 5/9] feat(orch): `waitForIBCAck` Co-Author: Turadg Aleahmad --- .../orchestration/restart-contracts.test.ts | 7 +- packages/boot/tools/supports.ts | 3 +- .../src/exos/local-orchestration-account.js | 129 +++++++++++------- .../orchestration/src/exos/packet-tools.js | 4 + .../examples/auto-stake-it.contract.test.ts | 3 +- .../test/examples/sendAnywhere.test.ts | 12 +- .../snapshots/sendAnywhere.test.ts.md | 4 + .../snapshots/sendAnywhere.test.ts.snap | Bin 957 -> 1007 bytes .../snapshots/unbondExample.test.ts.md | 4 + .../snapshots/unbondExample.test.ts.snap | Bin 881 -> 933 bytes .../local-orchestration-account-kit.test.ts | 107 ++++++++++++--- packages/orchestration/test/supports.ts | 24 ++++ 12 files changed, 223 insertions(+), 74 deletions(-) diff --git a/packages/boot/test/orchestration/restart-contracts.test.ts b/packages/boot/test/orchestration/restart-contracts.test.ts index 80e942e2a00..730a5f8b565 100644 --- a/packages/boot/test/orchestration/restart-contracts.test.ts +++ b/packages/boot/test/orchestration/restart-contracts.test.ts @@ -18,8 +18,9 @@ test.before(async t => { }); test.after.always(t => t.context.shutdown?.()); -// Not interesting because it doesn't wait on other chains. Leaving here because maybe it will before it's done. -test.serial('sendAnywhere', async t => { +// FIXME the test needs to be able to send the acknowledgementPacket ack +// so that the transfer vow resolves. +test.serial.failing('sendAnywhere', async t => { const { walletFactoryDriver, buildProposal, @@ -109,7 +110,7 @@ const hasResult = (r: UpdateRecord) => { }; // Tests restart but not of an orchestration() flow -test('stakeAtom', async t => { +test.serial('stakeAtom', async t => { const { buildProposal, evalProposal, diff --git a/packages/boot/tools/supports.ts b/packages/boot/tools/supports.ts index 0af6b514588..91186e94808 100644 --- a/packages/boot/tools/supports.ts +++ b/packages/boot/tools/supports.ts @@ -474,10 +474,11 @@ export const makeSwingsetTestKit = async ( } case BridgeId.PROVISION: case BridgeId.PROVISION_SMART_WALLET: - case BridgeId.VTRANSFER: case BridgeId.WALLET: console.warn('Bridge returning undefined for', bridgeId, ':', obj); return undefined; + case BridgeId.VTRANSFER: + throw Error('FIXME bridge support for vtransfer'); case BridgeId.STORAGE: return storage.toStorage(obj); case BridgeId.VLOCALCHAIN: diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index 450fbe3cbe4..19e910cc2ea 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -6,6 +6,7 @@ import { Shape as NetworkShape } from '@agoric/network'; import { M } from '@agoric/vat-data'; import { VowShape } from '@agoric/vow'; import { E } from '@endo/far'; + import { ChainAddressShape, DenomAmountShape, @@ -16,6 +17,10 @@ import { import { maxClockSkew } from '../utils/cosmos.js'; import { orchestrationAccountMethods } from '../utils/orchestrationAccount.js'; import { makeTimestampHelper } from '../utils/time.js'; +import { + preparePacketTools, + ICS20_TRANSFER_SUCCESS_RESULT, +} from './packet-tools.js'; /** * @import {HostOf} from '@agoric/async-flow'; @@ -24,12 +29,14 @@ import { makeTimestampHelper } from '../utils/time.js'; * @import {RecorderKit, MakeRecorderKit} from '@agoric/zoe/src/contractSupport/recorder.js'. * @import {Zone} from '@agoric/zone'; * @import {Remote} from '@agoric/internal'; + * @import {Bytes} from '@agoric/network'; * @import {InvitationMakers} from '@agoric/smart-wallet/src/types.js'; * @import {TimerService, TimestampRecord} from '@agoric/time'; - * @import {Vow, VowTools} from '@agoric/vow'; - * @import {TypedJson, JsonSafe} from '@agoric/cosmic-proto'; - * @import {Matcher} from '@endo/patterns'; + * @import {PromiseVow, EVow, Vow, VowTools} from '@agoric/vow'; + * @import {TypedJson, JsonSafe, ResponseTo} from '@agoric/cosmic-proto'; + * @import {Matcher, Pattern} from '@endo/patterns'; * @import {ChainHub} from './chain-hub.js'; + * @import {PacketTools} from './packet-tools.js'; */ const trace = makeTracer('LOA'); @@ -37,6 +44,8 @@ const trace = makeTracer('LOA'); const { Fail } = assert; const { Vow$ } = NetworkShape; // TODO #9611 +const EVow$ = shape => M.or(Vow$(shape), M.promise(/* shape */)); + /** * @typedef {object} LocalChainAccountNotification * @property {string} address @@ -45,6 +54,7 @@ const { Vow$ } = NetworkShape; // TODO #9611 /** * @typedef {{ * topicKit: RecorderKit; + * packetTools: PacketTools; * account: LocalChainAccount; * address: ChainAddress; * }} State @@ -57,9 +67,11 @@ const HolderI = M.interface('holder', { deposit: M.call(PaymentShape).returns(VowShape), withdraw: M.call(AmountShape).returns(Vow$(PaymentShape)), executeTx: M.call(M.arrayOf(M.record())).returns(Vow$(M.record())), - monitorTransfers: M.call(M.remotable('TransferTap')).returns( - Vow$(M.remotable('TargetRegistration')), - ), + waitForIBCAck: M.call(EVow$(M.remotable('PacketSender'))) + .optional(M.any()) + .returns(EVow$(M.string())), + matchFirstPacket: M.call(M.any()).returns(EVow$(M.any())), + monitorTransfers: M.call(M.remotable('TargetApp')).returns(EVow$(M.any())), }); /** @type {{ [name: string]: [description: string, valueShape: Matcher] }} */ @@ -80,9 +92,14 @@ export const prepareLocalOrchestrationAccountKit = ( makeRecorderKit, zcf, timerService, - { watch, allVows, asVow, when }, + vowTools, chainHub, ) => { + const { watch, allVows, asVow, when } = vowTools; + const { makeTransferSender, makePacketTools } = preparePacketTools( + zone.subZone('packetTools'), + vowTools, + ); const timestampHelper = makeTimestampHelper(timerService); /** Make an object wrapping an LCA with Zoe interfaces. */ @@ -116,16 +133,17 @@ export const prepareLocalOrchestrationAccountKit = ( .optional(M.arrayOf(M.undefined())) .returns(M.any()), }), - returnVoidWatcher: M.interface('returnVoidWatcher', { - onFulfilled: M.call(M.any()) - .optional(M.arrayOf(M.undefined())) - .returns(M.undefined()), + returnVoidWatcher: M.interface('transferResponseWatcher', { + onFulfilled: M.call(M.any()).returns(M.undefined()), }), getBalanceWatcher: M.interface('getBalanceWatcher', { onFulfilled: M.call(AmountShape) .optional(DenomShape) .returns(DenomAmountShape), }), + verifyTransferSuccess: M.interface('verifyTransferSuccess', { + onFulfilled: M.call(M.any()).returns(), + }), invitationMakers: M.interface('invitationMakers', { Delegate: M.call(M.string(), AmountShape).returns(M.promise()), Undelegate: M.call(M.string(), AmountShape).returns(M.promise()), @@ -144,8 +162,9 @@ export const prepareLocalOrchestrationAccountKit = ( const topicKit = makeRecorderKit(storageNode, PUBLIC_TOPICS.account[1]); // TODO determine what goes in vstorage https://github.com/Agoric/agoric-sdk/issues/9066 void E(topicKit.recorder).write(''); + const packetTools = makePacketTools(account); - return { account, address, topicKit }; + return { account, address, topicKit, packetTools }; }, { invitationMakers: { @@ -228,26 +247,34 @@ export const prepareLocalOrchestrationAccountKit = ( [{ transferChannel }, timeoutTimestamp], { opts, amount, destination }, ) { - return watch( - E(this.state.account).executeTx([ - typedJson('/ibc.applications.transfer.v1.MsgTransfer', { - sourcePort: transferChannel.portId, - sourceChannel: transferChannel.channelId, - token: { - amount: String(amount.value), - denom: amount.denom, - }, - sender: this.state.address.value, - receiver: destination.value, - timeoutHeight: opts?.timeoutHeight ?? { - revisionHeight: 0n, - revisionNumber: 0n, - }, - timeoutTimestamp, - memo: opts?.memo ?? '', - }), - ]), + const transferMsg = typedJson( + '/ibc.applications.transfer.v1.MsgTransfer', + { + sourcePort: transferChannel.portId, + sourceChannel: transferChannel.channelId, + token: { + amount: String(amount.value), + denom: amount.denom, + }, + sender: this.state.address.value, + receiver: destination.value, + timeoutHeight: opts?.timeoutHeight ?? { + revisionHeight: 0n, + revisionNumber: 0n, + }, + timeoutTimestamp, + memo: opts?.memo ?? '', + }, ); + + // Begin capturing packets, send the transfer packet, then return a + // vow for the acknowledgement data. + const { holder } = this.facets; + const sender = makeTransferSender( + /** @type {any} */ (holder), + transferMsg, + ); + return holder.waitForIBCAck(sender); }, }, /** @@ -264,15 +291,8 @@ export const prepareLocalOrchestrationAccountKit = ( return results[0]; }, }, - /** - * takes an array of results (from `executeEncodedTx`) and returns void - * since we are not interested in the result - */ returnVoidWatcher: { - /** - * @param {unknown} _result - */ - onFulfilled(_result) { + onFulfilled() { return undefined; }, }, @@ -290,7 +310,27 @@ export const prepareLocalOrchestrationAccountKit = ( return harden({ denom, value: natAmount.value }); }, }, + verifyTransferSuccess: { + onFulfilled(ackData) { + const { result, error } = JSON.parse(ackData); + error === undefined || Fail`ICS20-1 transfer error ${error}`; + result === ICS20_TRANSFER_SUCCESS_RESULT || + Fail`ICS20-1 transfer unsuccessful with ack result ${result}`; + }, + }, holder: { + /** @type {HostOf} */ + waitForIBCAck(sender, opts) { + return watch(E(this.state.packetTools).waitForIBCAck(sender, opts)); + }, + /** @type {HostOf} */ + matchFirstPacket(patternV) { + return watch(E(this.state.packetTools).matchFirstPacket(patternV)); + }, + /** @type {HostOf} */ + monitorTransfers(tap) { + return watch(E(this.state.packetTools).monitorTransfers(tap)); + }, /** @type {HostOf} */ asContinuingOffer() { // @ts-expect-error XXX invitationMakers @@ -464,15 +504,13 @@ export const prepareLocalOrchestrationAccountKit = ( ? 0n : E(timestampHelper).getTimeoutTimestampNS()); + // vow for the message response const transferV = watch( allVows([connectionInfoV, timeoutTimestampVowOrValue]), this.facets.transferWatcher, { opts, amount, destination }, ); - // FIXME https://github.com/Agoric/agoric-sdk/issues/9783 - // don't resolve the vow until the transfer is confirmed on remote - // and reject vow if the transfer fails - return watch(transferV, this.facets.returnVoidWatcher); + return watch(transferV, this.facets.verifyTransferSuccess); }); }, /** @type {HostOf} */ @@ -482,14 +520,11 @@ export const prepareLocalOrchestrationAccountKit = ( throw Fail`not yet implemented`; }); }, - /** @type {HostOf} */ - monitorTransfers(tap) { - return watch(E(this.state.account).monitorTransfers(tap)); - }, }, }, ); return makeLocalOrchestrationAccountKit; }; + /** @typedef {ReturnType} MakeLocalOrchestrationAccountKit */ /** @typedef {ReturnType} LocalOrchestrationAccountKit */ diff --git a/packages/orchestration/src/exos/packet-tools.js b/packages/orchestration/src/exos/packet-tools.js index ea524ae938f..a31fcf362f1 100644 --- a/packages/orchestration/src/exos/packet-tools.js +++ b/packages/orchestration/src/exos/packet-tools.js @@ -494,6 +494,8 @@ export const preparePacketTools = (zone, vowTools) => { } this.state.pending = 0; this.state.upcallQueue = null; + // FIXME when it returns undefined this causes an error: + // In "unsubscribeFromTransfers" method of (PacketToolsKit utils): result: undefined "[undefined]" - Must be a promise return watch(this.facets.utils.unsubscribeFromTransfers()); }, subscribeToTransfers() { @@ -503,6 +505,8 @@ export const preparePacketTools = (zone, vowTools) => { return when(reg); } const { tap } = this.facets; + // XXX racy; fails if subscribeToTransfers is called while this promise is in flight + // e.g. 'Target "agoric1fakeLCAAddress" already registered' return when(E(lca).monitorTransfers(tap), r => { this.state.reg = r; return r; diff --git a/packages/orchestration/test/examples/auto-stake-it.contract.test.ts b/packages/orchestration/test/examples/auto-stake-it.contract.test.ts index 261fb9605fb..0eef254adad 100644 --- a/packages/orchestration/test/examples/auto-stake-it.contract.test.ts +++ b/packages/orchestration/test/examples/auto-stake-it.contract.test.ts @@ -25,7 +25,7 @@ test('auto-stake-it - make accounts, register tap, return invitationMakers', asy bootstrap: { storage }, commonPrivateArgs, mocks: { transferBridge }, - utils: { inspectLocalBridge, inspectDibcBridge }, + utils: { inspectLocalBridge, inspectDibcBridge, transmitTransferAck }, } = await commonSetup(t); const { zoe, bundleAndInstall } = await setUpZoeForTest(); @@ -118,6 +118,7 @@ test('auto-stake-it - make accounts, register tap, return invitationMakers', asy }, 'tokens transferred from LOA to COA', ); + await transmitTransferAck(); const { acknowledgement } = (await inspectDibcBridge()).at( -1, ) as IBCEvent<'acknowledgementPacket'>; diff --git a/packages/orchestration/test/examples/sendAnywhere.test.ts b/packages/orchestration/test/examples/sendAnywhere.test.ts index 1bcf4fd2771..0f7f11593df 100644 --- a/packages/orchestration/test/examples/sendAnywhere.test.ts +++ b/packages/orchestration/test/examples/sendAnywhere.test.ts @@ -5,11 +5,16 @@ import { E } from '@endo/far'; import path from 'path'; import { mustMatch } from '@endo/patterns'; import { makeIssuerKit } from '@agoric/ertp'; -import { inspectMapStore } from '@agoric/internal/src/testing-utils.js'; +import { + eventLoopIteration, + inspectMapStore, +} from '@agoric/internal/src/testing-utils.js'; +import { inspect } from 'util'; import { CosmosChainInfo, IBCConnectionInfo } from '../../src/cosmos-api.js'; import { commonSetup } from '../supports.js'; import { SingleAmountRecord } from '../../src/examples/sendAnywhere.contract.js'; import { registerChain } from '../../src/chain-info.js'; +import { buildVTransferEvent } from '../../tools/ibc-mocks.js'; const dirname = path.dirname(new URL(import.meta.url).pathname); @@ -58,7 +63,7 @@ test('send using arbitrary chain info', async t => { bootstrap, commonPrivateArgs, brands: { ist }, - utils: { inspectLocalBridge, pourPayment }, + utils: { inspectLocalBridge, pourPayment, transmitTransferAck }, } = await commonSetup(t); const vt = bootstrap.vowTools; @@ -124,6 +129,7 @@ test('send using arbitrary chain info', async t => { { Send }, { destAddr: 'hot1destAddr', chainName }, ); + await transmitTransferAck(); await vt.when(E(userSeat).getOfferResult()); const history = inspectLocalBridge(); @@ -157,6 +163,7 @@ test('send using arbitrary chain info', async t => { { Send }, { destAddr: 'cosmos1destAddr', chainName: 'cosmoshub' }, ); + await transmitTransferAck(); await vt.when(E(userSeat).getOfferResult()); const history = inspectLocalBridge(); const { messages, address: execAddr } = history.at(-1); @@ -203,6 +210,7 @@ test('send using arbitrary chain info', async t => { { Send }, { destAddr: 'hot1destAddr', chainName: 'hot' }, ); + await transmitTransferAck(); await vt.when(E(userSeat).getOfferResult()); const history = inspectLocalBridge(); const { messages, address: execAddr } = history.at(-1); diff --git a/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.md b/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.md index 96a80b9caa8..1ebc230c630 100644 --- a/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.md +++ b/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.md @@ -50,6 +50,10 @@ Generated by [AVA](https://avajs.dev). Orchestrator_kindHandle: 'Alleged: kind', RemoteChainFacade_kindHandle: 'Alleged: kind', chainName: {}, + packetTools: { + PacketToolsKit_kindHandle: 'Alleged: kind', + TransferSenderKit_kindHandle: 'Alleged: kind', + }, }, vows: { PromiseWatcher_kindHandle: 'Alleged: kind', diff --git a/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.snap b/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.snap index 19f55894a6e3711a0547a07b79c4b5d39b8b0a2c..533b05bc1302e9f41d0b9939bf16a14d63ad1efd 100644 GIT binary patch literal 1007 zcmVR?m;yL>PS&+liet$)=l9x?PZwxWEA+xF7@)p(WaiExYP& zTa^n;?AgS`nTcj5gu;bhIB@_3;)tmD2N0(oIieyYkoXTOA@;-@6C-InF#p{#h z@4YwQulYWIf6$9XFjV*MGac!e^?S76r+pSFPRD8>w7Pdc45_AfYCl`3VD=D~xLp9- z0KNe53xG!e{sZt70bVA+Iw`kEd8hXQ3p63~*R{1ZkUB^Oq=|J>Y7+~@X_2R|P9*Kc z%-@~#;z$krwDwif8wjo?4Rm7pZOKv>G8izWCDoDOiMd`{nC%b6R3%?uTl#8!EZ8i8=iV&= z@0Wn@O2A(w;28@DEZ|2AIJAK0Y+%O*qz!yy8wo9C6Y7%kuQu?94YVBKjstw@01q95 zRyJwwp#%Km09VVv*Ja?(GH|{E>{kF$G3YguZhu|@zN!GfSAdplz#9fUYi;IwqGr_d zQYRcmypvX@XgfS<&{f3yG1G#lfQxf>7HR0T-;8EnMuHdhV6!{n3GpVMN;)2|)R`HD z%Xx)d7#nke9N4@d+oHdxg=9tDxU{FKW<{cC5VGwqy1spMza~6*i}snkO@~Y#-HpK# zsx?P-X%tT+JA!<+3^|C!v@7J!LZToTMP>ILD0h z%78|EYtlRFh{7Fb&`;^OEYi?9zsGp!?{+h^4K@;* z6+6B`$it;Vc)^VQ4jr+?^!JjfHm1Rl>3tz$l~l+w`*&B0QKZ;ys)IrCVr<+J(`#Ha z$!V<4ko*hDxi@tbs}-on`@rc)N$(OXeQP$Gxm{tOx=9>!EtT3Hu;Cf&sB)^vIa9=X z$pc>TfT<}|&uuP}l2aGDSi%iL-l#4zQDr<7(-GrZCAH~SfsyHgcS1@(T@oTM&iKwxRp3Dt z__qqYUIRX<8CunxmTk`I^p`GKYlSC7mYtimPt3^~py@+A8&qj23;U2|pW;IX$YR=K d@kg7fWB&2azE2M{lfKea>c_e;6@d{9008w-=~n;% literal 957 zcmV;u148^kRzV`!yk(X00000000A>R!xf>MHqgnzk8CI-N|lTlWe39dNB|LFCiz#=(r&pcg?yR z0z#36Imj(ZUpzKM+Um!9)JG0%@?6RFcHT^!% z^M2G@@7q7__Yx5f)2GjwidDk;J=*WnK8sUM$LT<*^y%{`qKZCf{A0q!^dTPcx&Sr- zJOl6>fWH9z2jD6J-X*{aaoWV$>OEqiB4qKpyu1w31aU!@utJPgVuILh^2P_5q`ia% z+gUG((?M`2Ru6|Uk2WZe5;li5J4wR&EV>y?ZJNf)Jr)WXF?j}Ji;xWh%*{ScpYX7o zh)Hhxu3+*)!Ih+;$}I;4OH;^jz)~ftiUrTjzHw}}J{OZT|9YeF)%;ShX#vmg8Ng=- z@Ph&TX#iJE;Gqe;Fo8o8c*_FrSOB+xZ!E2#M!BFKab8-$fdw>eVAlq|wt-)5jppby z_s|CZwt@E?;F$yb?f@+p*mgDQIh|?^UEr|`{Ne)t>hPupFI!ucemJL{<)uzEig{;# zDs@}ob&a0Jyq_>7_#CjdV3(1a&g%Q|G|E`;x)H2*GoBN7`9#w3_)J|{Qn*r8sHAau zAy5J9mt#}(ca)H|p zGRbBTR)p*)V$v0ID^sBuvAxP^`B@(2T{@nBxwr^z!szfW9p@#;JI>bgyfvUP-^hBi zj;P&n8vT@xDDE-wl~-YUynSDwOB_K;^ fk0oEM&jSmN{)_{Ry}VVMHqf&cW*!9IA_Z?!HGfZf`)#RxmLY$pLx zpt;?-yUoqboMz^@gf2Z_T{WFD2l&+i zwq4)@7x=~nesh6c54h<8_dMWJ&kCrs7|@V-KX|}T9`L6JywtGhzD*B%4Pe*+zH0#M zK5*;42%Tp7^80TA#7nG~;8%&M0nJ zqI9{Ow52*6n?w%Tol3T%aqi^-9Tg{AGuI}J4i4xj4?$jWZmz!@KD(C}w4WPIhf>m} z39CJnw#v4Z9B$4`OSE$b%dNg~I>#Cwu2H}@7aU*4Aq<)q(nOtfi2)7tfRb?%C+{WUwsY$y$@cl~q^l*%AVrq;LUkmZmrK`SGg4465UlIx;~ z7MtZr$zi71U22kk{bXFbBPZ_&!<3+0Um*Bf`MtMI#`O}WPls}&r4)TY+~Td-Y*q%g z_rK%3Veba#%MkcFw01&Z)7)=E;QJ5=B8z&dLcJLQ2NCdzOiPcE3OF6lc4$NLA|TX_HzazBVdLSsnXxT-iRFm)8ZdzjLan&|RiZ zm`IsgP9Y@D-x2VS9oF!?urBUKy7YEtoOT$$zf=51#U0w~(H@Iw*Dw|5Kb!vnJ7eF% Hg9-ouu-VV- literal 881 zcmV-%1CIPbRzVK@$>k3XPKwXDBM_`0WM3faV}G z&r;nd$RCRc00000000A>R!wUgM;Lx)cePr@@fWt6CUL^LmzF?Fp|{>tx56ck9c(8~ zPGK}Vk|rL_h}qf5?k&gCOQDCH6A1J_lt2$9&_jPgAcxXIPyGXd5VVfu)ofF=+0!!5 z^FI52&pz(7rDjLz!zbJdD>+XsHCCjG4$_{s>BA>ULM^@9X#1#R^pJ=|Edbj9egyD4 zfWH8|25^M{Ul3r0gv%t{ZQtk25;A#RT3P~`2dRTBVug6C#0RM@ldE56hPEY-Z)R;N z(q4RB*n3ApC0kS_l8K5!)fHUpp&0N)2rLcQsPMkM$(0Ggc7gTYMQIHhp8tWXN$(nO#{c86kHclWI} zys8@)_bs)&N)!u({9Hfl^wrau$`t#CcDXsEN8Fsw!*2!F@`Tl*LS}}aQvNJbI#Lf? z+U#bQ={~<*s#YV*y}UsOZBfo3;cW+rC(-}|AOe1Ispm>}0Le@7s!)n>d;j1-&>Z!gM9k^F5*;ayds%yYwlN96)CX^e%Ml4v zYed4U*hj?sr~!P^aN>#nE1v1ggwA}OaY@@;9~ Hun7PFt);Xa 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 53de7c1ff07..be4291a6d6d 100644 --- a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts +++ b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts @@ -4,7 +4,7 @@ import { AmountMath } from '@agoric/ertp'; import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; import { heapVowE as VE } from '@agoric/vow/vat.js'; import { TargetApp } from '@agoric/vats/src/bridge-target.js'; -import { ChainAddress } from '../../src/orchestration-api.js'; +import { ChainAddress, type AmountArg } from '../../src/orchestration-api.js'; import { NANOSECONDS_PER_SECOND } from '../../src/utils/time.js'; import { commonSetup } from '../supports.js'; import { UNBOND_PERIOD_SECONDS } from '../ibc-mocks.js'; @@ -96,8 +96,11 @@ test('transfer', async t => { const makeTestLOAKit = prepareMakeTestLOAKit(t, common.bootstrap); const account = await makeTestLOAKit(); + const { value: sender } = await VE(account).getAddress(); + const { brands: { bld: stake }, + mocks: { transferBridge }, utils, } = common; @@ -115,6 +118,30 @@ test('transfer', async t => { value: 'cosmos1pleab', encoding: 'bech32', }; + const sourceChannel = 'channel-5'; // observed in toBridge VLOCALCHAIN_EXECUTE_TX sourceChannel + + // TODO rename to lastSequence + /** The running tally of transfer messages that were sent over the bridge */ + let sequence = 0n; + /** + * Helper to start the transfer without awaiting the result. It await the + * event loop so the promise starts and increments sequence for use in the + * acknowledgementPacket bridge message and wants + * @param amount + * @param dest + * @param opts + */ + const startTransfer = async ( + amount: AmountArg, + dest: ChainAddress, + opts = {}, + ) => { + const transferP = VE(account).transfer(amount, dest, opts); + sequence += 1n; + // Ensure the toBridge of the transferP happens before the fromBridge is awaited after this function returns + await eventLoopIteration(); + return { transferP }; + }; // TODO #9211, support ERTP amounts t.log('ERTP Amounts not yet supported for AmountArg'); @@ -123,14 +150,29 @@ test('transfer', async t => { }); t.log('.transfer() 1 bld to cosmos using DenomAmount'); - const transferResp = await VE(account).transfer( + const { transferP } = await startTransfer( { denom: 'ubld', value: 1_000_000n }, destination, ); - t.is(transferResp, undefined, 'Successful transfer returns Promise.'); + t.is(await Promise.race([transferP, 'not yet']), 'not yet'); + + // simulate incoming message so that the transfer promise resolves + await VE(transferBridge).fromBridge( + buildVTransferEvent({ + receiver: destination.value, + sender, + sourceChannel, + sequence, + }), + ); + + const transferRes = await transferP; + t.is(transferRes, undefined, 'Successful transfer returns Promise.'); await t.throwsAsync( - () => VE(account).transfer({ denom: 'ubld', value: 504n }, destination), + // XXX the bridge fakes the timeout response automatically for 504n + (await startTransfer({ denom: 'ubld', value: 504n }, destination)) + .transferP, { message: 'simulated unexpected MsgTransfer packet timeout', }, @@ -141,36 +183,57 @@ test('transfer', async t => { value: 'fakenet1pleab', encoding: 'bech32', }; + // XXX dev has to know not to startTransfer here await t.throwsAsync( - () => - VE(account).transfer({ denom: 'ubld', value: 1n }, unknownDestination), + VE(account).transfer({ denom: 'ubld', value: 1n }, unknownDestination), { message: /connection not found: agoric-3<->fakenet/ }, 'cannot create transfer msg with unknown chainId', ); - await t.notThrowsAsync( - () => - VE(account).transfer({ denom: 'ubld', value: 10n }, destination, { - memo: 'hello', + /** + * Helper to start the transfer AND send the ack packet so this promise can be awaited + * @param amount + * @param dest + * @param opts + */ + const doTransfer = async ( + amount: AmountArg, + dest: ChainAddress, + opts = {}, + ) => { + const { transferP: promise } = await startTransfer(amount, dest, opts); + // simulate incoming message so that promise resolves + await VE(transferBridge).fromBridge( + buildVTransferEvent({ + receiver: dest.value, + sender, + sourceChannel, + sequence, }), + ); + return promise; + }; + + await t.notThrowsAsync( + doTransfer({ 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( - () => - VE(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, - }), + doTransfer({ 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( - () => - VE(account).transfer({ denom: 'ubld', value: 10n }, destination, { - timeoutHeight: { revisionHeight: 100n, revisionNumber: 1n }, - }), + doTransfer({ denom: 'ubld', value: 10n }, destination, { + timeoutHeight: { revisionHeight: 100n, revisionNumber: 1n }, + }), 'accepts custom timeoutHeight', ); }); @@ -195,6 +258,10 @@ test('monitor transfers', async t => { }); const { value: target } = await VE(account).getAddress(); + // XXX let the PacketTools subscribeToTransfers complete before triggering it + // again with monitorTransfers + await eventLoopIteration(); + const appRegistration = await VE(account).monitorTransfers(tap); // simulate upcall from golang to VM @@ -211,6 +278,6 @@ test('monitor transfers', async t => { t.is(upcallCount, 2, 'second upcall received'); await appRegistration.revoke(); - await t.throwsAsync(simulateIncomingTransfer()); + await simulateIncomingTransfer(); t.is(upcallCount, 2, 'no more events after app is revoked'); }); diff --git a/packages/orchestration/test/supports.ts b/packages/orchestration/test/supports.ts index 3ceff581a79..cbc58d3cf21 100644 --- a/packages/orchestration/test/supports.ts +++ b/packages/orchestration/test/supports.ts @@ -21,9 +21,11 @@ import { makeHeapZone, type Zone } from '@agoric/zone'; import { makeDurableZone } from '@agoric/zone/durable.js'; import { E } from '@endo/far'; import type { ExecutionContext } from 'ava'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; import { registerKnownChains } from '../src/chain-info.js'; import { prepareCosmosInterchainService } from '../src/exos/cosmos-interchain-service.js'; import { setupFakeNetwork } from './network-fakes.js'; +import { buildVTransferEvent } from '../tools/ibc-mocks.js'; export { makeFakeLocalchainBridge, @@ -124,6 +126,27 @@ export const commonSetup = async (t: ExecutionContext) => { await registerKnownChains(agoricNamesAdmin, () => {}); + let ibcSequenceNonce = 0n; + /** simulate incoming message as if the transfer completed over IBC */ + const transmitTransferAck = async () => { + // assume this is called after each outgoing IBC transfer + ibcSequenceNonce += 1n; + // let the promise for the transfer start + await eventLoopIteration(); + const lastMsgTransfer = localBridgeMessages.at(-1).messages[0]; + await E(transferBridge).fromBridge( + buildVTransferEvent({ + receiver: lastMsgTransfer.receiver, + sender: lastMsgTransfer.sender, + target: lastMsgTransfer.sender, + sourceChannel: lastMsgTransfer.sourceChannel, + sequence: ibcSequenceNonce, + }), + ); + // let the bridge handler finish + await eventLoopIteration(); + }; + return { bootstrap: { agoricNames, @@ -164,6 +187,7 @@ export const commonSetup = async (t: ExecutionContext) => { pourPayment, inspectLocalBridge: () => harden([...localBridgeMessages]), inspectDibcBridge: () => E(ibcBridge).inspectDibcBridge(), + transmitTransferAck, }, }; }; From 07dfc45afd7f4d87779470f1a2bd2d7cd2de04e4 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Sat, 3 Aug 2024 22:27:23 -0600 Subject: [PATCH 6/9] feat(orchestration): split generic packet-tools from IBC tools --- packages/orchestration/src/exos/ibc-packet.js | 219 +++++++++++++++++ .../src/exos/local-orchestration-account.js | 73 +++--- .../orchestration/src/exos/packet-tools.js | 232 +++--------------- 3 files changed, 284 insertions(+), 240 deletions(-) create mode 100644 packages/orchestration/src/exos/ibc-packet.js diff --git a/packages/orchestration/src/exos/ibc-packet.js b/packages/orchestration/src/exos/ibc-packet.js new file mode 100644 index 00000000000..dd25ac55fa9 --- /dev/null +++ b/packages/orchestration/src/exos/ibc-packet.js @@ -0,0 +1,219 @@ +import { assertAllDefined } from '@agoric/internal'; +import { base64ToBytes, Shape as NetworkShape } from '@agoric/network'; +import { M } from '@endo/patterns'; +import { E } from '@endo/far'; + +// As specified in ICS20, the success result is a base64-encoded '\0x1' byte. +export const ICS20_TRANSFER_SUCCESS_RESULT = 'AQ=='; + +/** + * @import {JsonSafe, TypedJson, ResponseTo} from '@agoric/cosmic-proto'; + * @import {Vow, VowTools} from '@agoric/vow'; + * @import {LocalChainAccount} from '@agoric/vats/src/localchain.js'; + * @import {PacketOptions} from './packet-tools.js'; + */ + +const { Fail, bare } = assert; +const { Vow$ } = NetworkShape; // TODO #9611 + +/** + * Create a pattern for alterative representations of a sequence number. + * + * @param {any} sequence + * @returns {Pattern} + */ +export const createSequencePattern = sequence => { + const sequencePatterns = []; + + try { + const bintSequence = BigInt(sequence); + bintSequence > 0n && sequencePatterns.push(bintSequence); + } catch (e) { + // ignore + } + + const numSequence = Number(sequence); + numSequence > 0 && + Number.isSafeInteger(numSequence) && + sequencePatterns.push(numSequence); + + const strSequence = String(sequence); + strSequence && sequencePatterns.push(strSequence); + + if (!sequencePatterns.find(seq => seq === sequence)) { + sequencePatterns.push(sequence); + } + + switch (sequencePatterns.length) { + case 0: + throw Fail`sequence ${sequence} is not valid`; + case 1: + return sequencePatterns[0]; + default: + return M.or(...sequencePatterns); + } +}; +harden(createSequencePattern); + +/** + * @param {import('@agoric/base-zone').Zone} zone + * @param {VowTools & { makeIBCReplyKit: MakeIBCReplyKit }} powers + */ +export const prepareIBCTransferSender = (zone, { watch, makeIBCReplyKit }) => { + const makeIBCTransferSenderKit = zone.exoClassKit( + 'IBCTransferSenderKit', + { + public: M.interface('IBCTransferSender', { + sendPacket: M.call(Vow$(M.any()), M.any()).returns(Vow$(M.record())), + }), + responseWatcher: M.interface('responseWatcher', { + onFulfilled: M.call([M.record()], M.record()).returns(M.any()), + }), + verifyTransferSuccess: M.interface('verifyTransferSuccess', { + onFulfilled: M.call(M.any()).returns(), + }), + }, + /** + * @param {{ + * executeTx: LocalChainAccount['executeTx']; + * }} txExecutor + * @param {TypedJson<'/ibc.applications.transfer.v1.MsgTransfer'>} transferMsg + */ + (txExecutor, transferMsg) => ({ + txExecutor, + transferMsg: harden(transferMsg), + }), + { + public: { + sendPacket(match, opts) { + const { txExecutor, transferMsg } = this.state; + return watch( + E(txExecutor).executeTx([transferMsg]), + this.facets.responseWatcher, + { opts, match }, + ); + }, + }, + responseWatcher: { + /** + * Wait for successfully sending the transfer packet. + * + * @param {[ + * JsonSafe< + * ResponseTo< + * TypedJson<'/ibc.applications.transfer.v1.MsgTransfer'> + * > + * >, + * ]} response + * @param {Record} ctx + */ + onFulfilled([{ sequence }], ctx) { + const { match } = ctx; + const { transferMsg } = this.state; + + // Match the port/channel and sequence number. + const replyPacketPattern = M.splitRecord({ + source_port: transferMsg.sourcePort, + source_channel: transferMsg.sourceChannel, + sequence: createSequencePattern(sequence), + }); + + const { resultV: ackDataV, ...rest } = makeIBCReplyKit( + replyPacketPattern, + match, + ctx, + ); + const resultV = watch(ackDataV, this.facets.verifyTransferSuccess); + return harden({ resultV, ...rest }); + }, + }, + verifyTransferSuccess: { + onFulfilled(ackData) { + let obj; + try { + obj = JSON.parse(ackData); + } catch { + Fail`ICS20-1 transfer ack data is not JSON: ${ackData}`; + } + const { result, error } = obj; + error === undefined || Fail`ICS20-1 transfer error ${error}`; + result ?? Fail`Missing result in ICS20-1 transfer ack ${obj}`; + result === ICS20_TRANSFER_SUCCESS_RESULT || + Fail`ICS20-1 transfer unsuccessful with ack result ${result}`; + }, + }, + }, + ); + + /** + * @param {Parameters} args + */ + return (...args) => makeIBCTransferSenderKit(...args).public; +}; +harden(prepareIBCTransferSender); + +/** + * @param {import('@agoric/base-zone').Zone} zone + * @param {VowTools} vowTools + */ +export const prepareIBCReplyKit = (zone, vowTools) => { + const { watch } = vowTools; + const ibcWatcher = zone.exo( + 'ibcResultWatcher', + M.interface('processIBCWatcher', { + onFulfilled: M.call(M.record(), M.record()).returns(Vow$(M.string())), + }), + { + onFulfilled({ event, acknowledgement }, { opName = 'unknown' }) { + assertAllDefined({ event, acknowledgement }); + switch (event) { + case 'acknowledgementPacket': + return base64ToBytes(acknowledgement); + case 'timeoutPacket': + throw Fail`${bare(opName)} operation received timeout packet`; + default: + throw Fail`Unexpected event: ${event}`; + } + }, + }, + ); + + /** + * @param {Pattern} replyPacketPattern + * @param {Vow} matchV + * @param {PacketOptions} opts + */ + const makeIBCReplyKit = (replyPacketPattern, matchV, opts) => { + const eventPattern = M.or( + M.splitRecord({ + event: 'acknowledgementPacket', + packet: replyPacketPattern, + acknowledgement: M.string(), + }), + M.splitRecord({ + event: 'timeoutPacket', + packet: replyPacketPattern, + }), + ); + const resultV = watch(matchV, ibcWatcher, opts); + return harden({ eventPattern, resultV }); + }; + + return makeIBCReplyKit; +}; +harden(prepareIBCReplyKit); +/** @typedef {ReturnType} MakeIBCReplyKit */ + +/** + * @param {import('@agoric/base-zone').Zone} zone + * @param {VowTools} vowTools + */ +export const prepareIBCTools = (zone, vowTools) => { + const makeIBCReplyKit = prepareIBCReplyKit(zone, vowTools); + const makeIBCTransferSender = prepareIBCTransferSender(zone, { + makeIBCReplyKit, + ...vowTools, + }); + return harden({ makeIBCTransferSender, makeIBCReplyKit }); +}; +harden(prepareIBCTools); diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index 19e910cc2ea..f5b90894ef8 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -17,10 +17,8 @@ import { import { maxClockSkew } from '../utils/cosmos.js'; import { orchestrationAccountMethods } from '../utils/orchestrationAccount.js'; import { makeTimestampHelper } from '../utils/time.js'; -import { - preparePacketTools, - ICS20_TRANSFER_SUCCESS_RESULT, -} from './packet-tools.js'; +import { preparePacketTools } from './packet-tools.js'; +import { prepareIBCTools } from './ibc-packet.js'; /** * @import {HostOf} from '@agoric/async-flow'; @@ -67,7 +65,7 @@ const HolderI = M.interface('holder', { deposit: M.call(PaymentShape).returns(VowShape), withdraw: M.call(AmountShape).returns(Vow$(PaymentShape)), executeTx: M.call(M.arrayOf(M.record())).returns(Vow$(M.record())), - waitForIBCAck: M.call(EVow$(M.remotable('PacketSender'))) + sendThenWaitForAck: M.call(EVow$(M.remotable('PacketSender'))) .optional(M.any()) .returns(EVow$(M.string())), matchFirstPacket: M.call(M.any()).returns(EVow$(M.any())), @@ -96,7 +94,11 @@ export const prepareLocalOrchestrationAccountKit = ( chainHub, ) => { const { watch, allVows, asVow, when } = vowTools; - const { makeTransferSender, makePacketTools } = preparePacketTools( + const { makeIBCTransferSender } = prepareIBCTools( + zone.subZone('ibcTools'), + vowTools, + ); + const makePacketTools = preparePacketTools( zone.subZone('packetTools'), vowTools, ); @@ -133,17 +135,14 @@ export const prepareLocalOrchestrationAccountKit = ( .optional(M.arrayOf(M.undefined())) .returns(M.any()), }), - returnVoidWatcher: M.interface('transferResponseWatcher', { - onFulfilled: M.call(M.any()).returns(M.undefined()), + returnVoidWatcher: M.interface('returnVoidWatcher', { + onFulfilled: M.call(M.any()).optional(M.any()).returns(M.undefined()), }), getBalanceWatcher: M.interface('getBalanceWatcher', { onFulfilled: M.call(AmountShape) .optional(DenomShape) .returns(DenomAmountShape), }), - verifyTransferSuccess: M.interface('verifyTransferSuccess', { - onFulfilled: M.call(M.any()).returns(), - }), invitationMakers: M.interface('invitationMakers', { Delegate: M.call(M.string(), AmountShape).returns(M.promise()), Undelegate: M.call(M.string(), AmountShape).returns(M.promise()), @@ -267,14 +266,15 @@ export const prepareLocalOrchestrationAccountKit = ( }, ); - // Begin capturing packets, send the transfer packet, then return a - // vow for the acknowledgement data. const { holder } = this.facets; - const sender = makeTransferSender( + const sender = makeIBCTransferSender( /** @type {any} */ (holder), transferMsg, ); - return holder.waitForIBCAck(sender); + // Begin capturing packets, send the transfer packet, then return a + // vow that rejects unless the packet acknowledgment comes back and is + // verified. + return holder.sendThenWaitForAck(sender); }, }, /** @@ -310,27 +310,7 @@ export const prepareLocalOrchestrationAccountKit = ( return harden({ denom, value: natAmount.value }); }, }, - verifyTransferSuccess: { - onFulfilled(ackData) { - const { result, error } = JSON.parse(ackData); - error === undefined || Fail`ICS20-1 transfer error ${error}`; - result === ICS20_TRANSFER_SUCCESS_RESULT || - Fail`ICS20-1 transfer unsuccessful with ack result ${result}`; - }, - }, holder: { - /** @type {HostOf} */ - waitForIBCAck(sender, opts) { - return watch(E(this.state.packetTools).waitForIBCAck(sender, opts)); - }, - /** @type {HostOf} */ - matchFirstPacket(patternV) { - return watch(E(this.state.packetTools).matchFirstPacket(patternV)); - }, - /** @type {HostOf} */ - monitorTransfers(tap) { - return watch(E(this.state.packetTools).monitorTransfers(tap)); - }, /** @type {HostOf} */ asContinuingOffer() { // @ts-expect-error XXX invitationMakers @@ -482,7 +462,7 @@ export const prepareLocalOrchestrationAccountKit = ( * @param {IBCMsgTransferOptions} [opts] if either timeoutHeight or * timeoutTimestamp are not supplied, a default timeoutTimestamp will * be set for 5 minutes in the future - * @returns {Vow} + * @returns {Vow} */ transfer(amount, destination, opts) { return asVow(() => { @@ -504,13 +484,14 @@ export const prepareLocalOrchestrationAccountKit = ( ? 0n : E(timestampHelper).getTimeoutTimestampNS()); - // vow for the message response - const transferV = watch( + // don't resolve the vow until the transfer is confirmed on remote + // and reject vow if the transfer fails for any reason + const resultV = watch( allVows([connectionInfoV, timeoutTimestampVowOrValue]), this.facets.transferWatcher, { opts, amount, destination }, ); - return watch(transferV, this.facets.verifyTransferSuccess); + return resultV; }); }, /** @type {HostOf} */ @@ -520,6 +501,20 @@ export const prepareLocalOrchestrationAccountKit = ( throw Fail`not yet implemented`; }); }, + /** @type {HostOf} */ + sendThenWaitForAck(sender, opts) { + return watch( + E(this.state.packetTools).sendThenWaitForAck(sender, opts), + ); + }, + /** @type {HostOf} */ + matchFirstPacket(patternV) { + return watch(E(this.state.packetTools).matchFirstPacket(patternV)); + }, + /** @type {HostOf} */ + monitorTransfers(tap) { + return watch(E(this.state.packetTools).monitorTransfers(tap)); + }, }, }, ); diff --git a/packages/orchestration/src/exos/packet-tools.js b/packages/orchestration/src/exos/packet-tools.js index a31fcf362f1..fc78bdff30f 100644 --- a/packages/orchestration/src/exos/packet-tools.js +++ b/packages/orchestration/src/exos/packet-tools.js @@ -1,17 +1,9 @@ -import { assertAllDefined } from '@agoric/internal'; import { makeMarshal, decodeToJustin } from '@endo/marshal'; -import { - base64ToBytes, - byteSourceToBase64, - Shape as NetworkShape, -} from '@agoric/network'; +import { Shape as NetworkShape } from '@agoric/network'; import { M, matches } from '@endo/patterns'; import { E } from '@endo/far'; import { pickFacet } from '@agoric/vat-data'; -// As specified in ICS20, the success result is a base64-encoded '\0x1' byte. -export const ICS20_TRANSFER_SUCCESS_RESULT = 'AQ=='; - const { toCapData } = makeMarshal(undefined, undefined, { marshalName: 'JustEncoder', serializeBodyFormat: 'capdata', @@ -22,9 +14,8 @@ const just = obj => { }; /** - * @import {Bytes} from '@agoric/network'; + * @import {Pattern} from '@endo/patterns'; * @import {EVow, Remote, Vow, VowResolver, VowTools} from '@agoric/vow'; - * @import {JsonSafe, TypedJson, ResponseTo} from '@agoric/cosmic-proto'; * @import {LocalChainAccount} from '@agoric/vats/src/localchain.js'; * @import {TargetApp, TargetRegistration} from '@agoric/vats/src/bridge-target.js'; */ @@ -37,7 +28,9 @@ const just = obj => { /** * @typedef {object} PacketSender - * @property {(opts: PacketOptions) => Vow} sendPacket + * @property {( + * opts: PacketOptions, + * ) => Vow<{ eventPattern: Pattern; resultV: Vow }>} sendPacket */ /** @@ -53,7 +46,6 @@ const just = obj => { * >} PacketTimeout */ -const { Fail, bare } = assert; const { Vow$ } = NetworkShape; // TODO #9611 const EVow$ = shape => M.or(Vow$(shape), M.promise(/* shape */)); @@ -61,127 +53,20 @@ const EVow$ = shape => M.or(Vow$(shape), M.promise(/* shape */)); const sink = () => {}; harden(sink); -/** - * Create a pattern for alterative representations of a sequence number. - * - * @param {any} sequence - * @returns {import('@endo/patterns').Pattern} - */ -const createSequencePattern = sequence => { - const sequencePatterns = []; - - try { - const bintSequence = BigInt(sequence); - bintSequence > 0n && sequencePatterns.push(bintSequence); - } catch (e) { - // ignore - } - - const numSequence = Number(sequence); - numSequence > 0 && - Number.isSafeInteger(numSequence) && - sequencePatterns.push(numSequence); - - const strSequence = String(sequence); - strSequence && sequencePatterns.push(strSequence); - - if (!sequencePatterns.find(seq => seq === sequence)) { - sequencePatterns.push(sequence); - } - - switch (sequencePatterns.length) { - case 0: - throw Fail`sequence ${sequence} is not valid`; - case 1: - return sequencePatterns[0]; - default: - return M.or(...sequencePatterns); - } -}; -harden(createSequencePattern); - -/** - * @param {import('@agoric/base-zone').Zone} zone - * @param {VowTools} vowTools - */ -export const prepareTransferSender = (zone, { watch }) => { - const makeTransferSenderKit = zone.exoClassKit( - 'TransferSenderKit', - { - public: M.interface('TransferSender', { - sendPacket: M.call(M.any()).returns(Vow$(M.record())), - }), - responseWatcher: M.interface('responseWatcher', { - onFulfilled: M.call([M.record()]).returns(M.any()), - }), - }, - /** - * @param {{ - * executeTx: LocalChainAccount['executeTx']; - * }} txExecutor - * @param {TypedJson<'/ibc.applications.transfer.v1.MsgTransfer'>} transferMsg - */ - (txExecutor, transferMsg) => ({ - txExecutor, - transferMsg: harden(transferMsg), - }), - { - public: { - sendPacket() { - const { txExecutor, transferMsg } = this.state; - return watch( - E(txExecutor).executeTx([transferMsg]), - this.facets.responseWatcher, - ); - }, - }, - responseWatcher: { - /** - * Wait for successfully sending the transfer packet. - * - * @param {[ - * JsonSafe< - * ResponseTo< - * TypedJson<'/ibc.applications.transfer.v1.MsgTransfer'> - * > - * >, - * ]} response - */ - onFulfilled([{ sequence }]) { - const { transferMsg } = this.state; - - // Match the port/channel and sequence number. - return M.splitRecord({ - source_port: transferMsg.sourcePort, - source_channel: transferMsg.sourceChannel, - sequence: createSequencePattern(sequence), - }); - }, - }, - }, - ); - - /** - * @param {Parameters} args - */ - return (...args) => makeTransferSenderKit(...args).public; -}; -harden(prepareTransferSender); - /** * @param {import('@agoric/base-zone').Zone} zone * @param {VowTools} vowTools */ export const preparePacketTools = (zone, vowTools) => { const { allVows, makeVowKit, watch, when } = vowTools; - const makeTransferSender = prepareTransferSender(zone, vowTools); + const makePacketToolsKit = zone.exoClassKit( 'PacketToolsKit', { public: M.interface('PacketTools', { - waitForIBCAck: M.call(EVow$(M.remotable('PacketSender'))) + sendThenWaitForAck: M.call(EVow$(M.remotable('PacketSender'))) .optional(M.any()) - .returns(EVow$(M.string())), + .returns(EVow$(M.any())), matchFirstPacket: M.call(M.any()).returns(EVow$(M.any())), monitorTransfers: M.call(M.remotable('TargetApp')).returns( EVow$(M.any()), @@ -216,21 +101,18 @@ export const preparePacketTools = (zone, vowTools) => { M.record(), ).returns(M.any()), }), + packetWasSentWatcher: M.interface('packetWasSentWatcher', { + onFulfilled: M.call( + { eventPattern: M.pattern(), resultV: Vow$(M.any()) }, + M.record(), + ).returns(M.any()), + }), utils: M.interface('utils', { subscribeToTransfers: M.call().returns(M.promise()), unsubscribeFromTransfers: M.call().returns(M.promise()), incrPendingPatterns: M.call().returns(Vow$(M.undefined())), decrPendingPatterns: M.call().returns(Vow$(M.undefined())), }), - packetWasSentWatcher: M.interface('packetWasSentWatcher', { - onFulfilled: M.call(M.pattern(), M.record()).returns(M.any()), - }), - processIBCReplyWatcher: M.interface('processIBCReplyWatcher', { - onFulfilled: M.call(M.record(), M.record()).returns(Vow$(M.string())), - }), - subscribeToPatternWatcher: M.interface('subscribeToPatternWatcher', { - onFulfilled: M.call(M.pattern()).returns(Vow$(M.any())), - }), rejectResolverAndRethrowWatcher: M.interface('rejectResolverWatcher', { onRejected: M.call(M.any(), { resolver: M.remotable('resolver'), @@ -278,11 +160,9 @@ export const preparePacketTools = (zone, vowTools) => { /** * @param {Remote} packetSender * @param {PacketOptions} [opts] - * @returns {Vow} + * @returns {Vow} */ - waitForIBCAck(packetSender, opts) { - const { opName = 'Unknown' } = opts || {}; - + sendThenWaitForAck(packetSender, opts = {}) { /** @type {import('@agoric/vow').VowKit} */ const pattern = makeVowKit(); @@ -294,22 +174,19 @@ export const preparePacketTools = (zone, vowTools) => { packetSender, ]), this.facets.sendPacketWatcher, - { opts, patternResolver: pattern.resolver }, + { opts }, ); - // If the pattern is fulfilled, process it. - const processedV = watch(matchV, this.facets.processIBCReplyWatcher, { - opName, + // When the packet is sent, resolve the resultV for the reply. + const resultV = watch(matchV, this.facets.packetWasSentWatcher, { + opts, + patternResolver: pattern.resolver, }); // If anything fails, try to reject the packet sender. - return watch( - processedV, - this.facets.rejectResolverAndRethrowWatcher, - { - resolver: pattern.resolver, - }, - ); + return watch(resultV, this.facets.rejectResolverAndRethrowWatcher, { + resolver: pattern.resolver, + }); }, }, monitorRegistration: { @@ -359,58 +236,14 @@ export const preparePacketTools = (zone, vowTools) => { }, sendPacketWatcher: { onFulfilled([{ match }, sender], ctx) { - return watch( - E(sender).sendPacket(ctx.opts), - this.facets.packetWasSentWatcher, - { - ...ctx, - match, - }, - ); + return watch(E(sender).sendPacket(match, ctx.opts)); }, }, packetWasSentWatcher: { - onFulfilled(packetPattern, ctx) { - const { patternResolver, match } = ctx; - // Match an acknowledgement or timeout packet. - const ackOrTimeoutPacket = M.or( - M.splitRecord({ - event: 'acknowledgementPacket', - packet: packetPattern, - acknowledgement: M.string(), - }), - M.splitRecord({ - event: 'timeoutPacket', - packet: packetPattern, - }), - ); - patternResolver.resolve(ackOrTimeoutPacket); - return match; - }, - }, - subscribeToPatternWatcher: { - onFulfilled(_pattern) { - // FIXME: Implement this! - return watch({ - type: 'VTRANSFER_IBC_EVENT', - event: 'acknowledgementPacket', - acknowledgement: byteSourceToBase64( - JSON.stringify({ result: 'AQ==' }), - ), - }); - }, - }, - processIBCReplyWatcher: { - onFulfilled({ event, acknowledgement }, { opName }) { - assertAllDefined({ event, acknowledgement }); - switch (event) { - case 'acknowledgementPacket': - return base64ToBytes(acknowledgement); - case 'timeoutPacket': - throw Fail`${bare(opName)} operation received timeout packet`; - default: - throw Fail`Unexpected event: ${event}`; - } + onFulfilled({ eventPattern, resultV }, ctx) { + const { patternResolver } = ctx; + patternResolver.resolve(eventPattern); + return resultV; }, }, rejectResolverAndRethrowWatcher: { @@ -530,13 +363,10 @@ export const preparePacketTools = (zone, vowTools) => { ); const makePacketTools = pickFacet(makePacketToolsKit, 'public'); - - return harden({ makePacketTools, makeTransferSender }); + return makePacketTools; }; harden(preparePacketTools); /** - * @typedef {Awaited< - * ReturnType['makePacketTools']> - * >} PacketTools + * @typedef {Awaited>>} PacketTools */ From 40995017d38a9ee7cb3b2cdda50c77592666c104 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Sat, 3 Aug 2024 22:28:21 -0600 Subject: [PATCH 7/9] chore(orchestration): update AVA snapshots --- .../snapshots/sendAnywhere.test.ts.md | 6 +++++- .../snapshots/sendAnywhere.test.ts.snap | Bin 1007 -> 1064 bytes .../snapshots/unbondExample.test.ts.md | 6 +++++- .../snapshots/unbondExample.test.ts.snap | Bin 933 -> 985 bytes .../local-orchestration-account-kit.test.ts | 5 ++++- 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.md b/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.md index 1ebc230c630..c833c759c21 100644 --- a/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.md +++ b/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.md @@ -50,9 +50,13 @@ Generated by [AVA](https://avajs.dev). Orchestrator_kindHandle: 'Alleged: kind', RemoteChainFacade_kindHandle: 'Alleged: kind', chainName: {}, + ibcTools: { + IBCTransferSenderKit_kindHandle: 'Alleged: kind', + ibcResultWatcher_kindHandle: 'Alleged: kind', + ibcResultWatcher_singleton: 'Alleged: ibcResultWatcher', + }, packetTools: { PacketToolsKit_kindHandle: 'Alleged: kind', - TransferSenderKit_kindHandle: 'Alleged: kind', }, }, vows: { diff --git a/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.snap b/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.snap index 533b05bc1302e9f41d0b9939bf16a14d63ad1efd..1017ab57dbcf460571804c45ac4b9397c78be41f 100644 GIT binary patch literal 1064 zcmV+@1lRjPRzVSI=)7MHqc!d%a%&Hc4922I>nJBz{Op95~?yEJvgyiu;2i zm1?{@vA15&ShKS=g;OrvkPs3g+@Nsc#D#wVB&1$|Qzhu(Nwp0J1h(L<(O<*|N`_WQKYT%XgiKM=}4dgwS*(c9&(voJCJkpd~S z0qg?!48S7*e**Xqz;gt6jR0#T-yr$D-aTe3LgtV4dL5(!VuIALMl$Op3t}|L^KS)` z_B__y4|<;K51Q||>h93xPKR>GV-c*<@;ugO&Ku2XOw(99V78DBlPeIbgmefHg)Qno z;C9;+lQ8t+ znVg-EPnJ-PIjT)vFOX~o`ED6<=!r>N$h|<>V#IDHx@84nme=Swdb2T4ZNljA8Xbov z2pwnW``jK-mv@5R%n_*_r_s-FTpX$CJkw>|Y3{evuHT4wiG}NP+6g2q>`#T$FSN?^ zz{cfY-WL8y_|3g#WV2=4BH*go5#DOoZ%p&WU13x26v=bUfy<#@Slu?Ysgv637Z<6B z)VtX_U?ZVey5*~c{I%2w%bv2|r6U%GOlxTzxjp;35Z>%qyuK7Ejj26k>Lggf^mW|X z`%;WtpWUL$9;9#6rJG{1!xfX9dj2`uvJtL(L%E*60`+847%eI31CouNYPDKoisa6m zF@`>{GIuP%wzNTIo+(mJOudgS;K%}gu>iBEiB%G7c8kDm9ac?@KN5EJxVY$Q-e=s| zQUBY}D?3-WuS?4PLnfzZn#uE=7Pn^~FhB6rd3})(NL)RebH0v^cC8t8;pEkJR3O=( zi@>8IkS%FdE+nd)(Wg62@qJMxXdJM zUILCwz~d6IRR-QKYg)yGR!$#5(HCntu~T1uadx7Y7v|&`P|v}e-ocSMQ|Az8pOHfh i$UNF(-iKRJV$GBPGEI7@m~8rrO7#>4aHNLt3;+O(aSQnX literal 1007 zcmVR?m;yL>PS&+liet$)=l9x?PZwxWEA+xF7@)p(WaiExYP& zTa^n;?AgS`nTcj5gu;bhIB@_3;)tmD2N0(oIieyYkoXTOA@;-@6C-InF#p{#h z@4YwQulYWIf6$9XFjV*MGac!e^?S76r+pSFPRD8>w7Pdc45_AfYCl`3VD=D~xLp9- z0KNe53xG!e{sZt70bVA+Iw`kEd8hXQ3p63~*R{1ZkUB^Oq=|J>Y7+~@X_2R|P9*Kc z%-@~#;z$krwDwif8wjo?4Rm7pZOKv>G8izWCDoDOiMd`{nC%b6R3%?uTl#8!EZ8i8=iV&= z@0Wn@O2A(w;28@DEZ|2AIJAK0Y+%O*qz!yy8wo9C6Y7%kuQu?94YVBKjstw@01q95 zRyJwwp#%Km09VVv*Ja?(GH|{E>{kF$G3YguZhu|@zN!GfSAdplz#9fUYi;IwqGr_d zQYRcmypvX@XgfS<&{f3yG1G#lfQxf>7HR0T-;8EnMuHdhV6!{n3GpVMN;)2|)R`HD z%Xx)d7#nke9N4@d+oHdxg=9tDxU{FKW<{cC5VGwqy1spMza~6*i}snkO@~Y#-HpK# zsx?P-X%tT+JA!<+3^|C!v@7J!LZToTMP>ILD0h z%78|EYtlRFh{7Fb&`;^OEYi?9zsGp!?{+h^4K@;* z6+6B`$it;Vc)^VQ4jr+?^!JjfHm1Rl>3tz$l~l+w`*&B0QKZ;ys)IrCVr<+J(`#Ha z$!V<4ko*hDxi@tbs}-on`@rc)N$(OXeQP$Gxm{tOx=9>!EtT3Hu;Cf&sB)^vIa9=X z$pc>TfT<}|&uuP}l2aGDSi%iL-l#4zQDr<7(-GrZCAH~SfsyHgcS1@(T@oTM&iKwxRp3Dt z__qqYUIRX<8CunxmTk`I^p`GKYlSC7mYtimPt3^~py@+A8&qj23;U2|pW;IX$YR=K d@kg7fWB&2azE2M{lfKea>c_e;6@d{9008w-=~n;% diff --git a/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.md b/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.md index a30e6fc60d0..98c8b7a6295 100644 --- a/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.md +++ b/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.md @@ -43,9 +43,13 @@ Generated by [AVA](https://avajs.dev). omniflixhub: 'Alleged: RemoteChainFacade public', stride: 'Alleged: RemoteChainFacade public', }, + ibcTools: { + IBCTransferSenderKit_kindHandle: 'Alleged: kind', + ibcResultWatcher_kindHandle: 'Alleged: kind', + ibcResultWatcher_singleton: 'Alleged: ibcResultWatcher', + }, packetTools: { PacketToolsKit_kindHandle: 'Alleged: kind', - TransferSenderKit_kindHandle: 'Alleged: kind', }, }, vows: { diff --git a/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.snap b/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.snap index 4aa04fe3834b1578fa29a59249f7049e403d955b..92e7e65fe769a934f23e85bac107ae2ea2e2e76f 100644 GIT binary patch literal 985 zcmV;~119`IRzVj4ppS zjrkE2KOc(-00000000A>R!wgkMHqf&cfH=Vn=i$oq)ov+AVej&a6w#<8?aI}B~g;5 zLPDy>vtxVf@r;?B#Vxn~gH}jz;6%^#KX3uTg%e!h2%@J-`~rl$j_vhqi|p)aHP7=t z^UnJ<^KH8&6ze4qpKuc!!Fgh+HgO{9An7WTJbV&G)X-0=H5X-!J`#|i31A<@+pe}ni3t$UmqLZ-j#>+2vZAZ3s>Y!atNT#(WRdF{hg)0W`%!?Y#h zq+7oko4dVOMtf97f=_5G4Iy}kNAK51n5D6K#F^3&*9#b~60%2t2^`Yofn-ggh8gsA z4hoo(MpI@o zE%PLn9l?!~6T-!*^*qzksa=mpUdBo;>%mSlm6_pfIn;D8Sg0!J6fPGP3dguK6)2GH z=Ga%AW1}=*){X6BLk(Z%6sv?hE6*yvHtwxdvX5wo>pQf^^|&wGV8+^*vYIp&spezK z)jXvw)Uc`aL28)l^V34M!Zh>p79C7hwx+HPIqlt|gDeDD#kraO2f@qxc~1M4!DukC zCS`o-!L*gPE#$B^HO65`&n-=rc!C@drB-e`{p#S?JGvbOC--R zgfC}$yImT~CS^2Qy526%Tv1D(X6K0amEp^6ze-3uR|X*)S$|CXJcAzRpdfCs6Q#ts zHowmy)d6KaZqC4QdCj-cp;rAk;diKE-R0AC=~NAGNW-as_M(QE#mv#v^80~It^-}D9<|G z67dtGcP6jIdW&{Cw8QJPZMd#yZ|VO4Mo@Eb HVhaEO_uSYE literal 933 zcmV;W16uq+RzVRy}VVMHqf&cW*!9IA_Z?!HGfZf`)#RxmLY$pLx zpt;?-yUoqboMz^@gf2Z_T{WFD2l&+i zwq4)@7x=~nesh6c54h<8_dMWJ&kCrs7|@V-KX|}T9`L6JywtGhzD*B%4Pe*+zH0#M zK5*;42%Tp7^80TA#7nG~;8%&M0nJ zqI9{Ow52*6n?w%Tol3T%aqi^-9Tg{AGuI}J4i4xj4?$jWZmz!@KD(C}w4WPIhf>m} z39CJnw#v4Z9B$4`OSE$b%dNg~I>#Cwu2H}@7aU*4Aq<)q(nOtfi2)7tfRb?%C+{WUwsY$y$@cl~q^l*%AVrq;LUkmZmrK`SGg4465UlIx;~ z7MtZr$zi71U22kk{bXFbBPZ_&!<3+0Um*Bf`MtMI#`O}WPls}&r4)TY+~Td-Y*q%g z_rK%3Veba#%MkcFw01&Z)7)=E;QJ5=B8z&dLcJLQ2NCdzOiPcE3OF6lc4$NLA|TX_HzazBVdLSsnXxT-iRFm)8ZdzjLan&|RiZ zm`IsgP9Y@D-x2VS9oF!?urBUKy7YEtoOT$$zf=51#U0w~(H@Iw*Dw|5Kb!vnJ7eF% Hg9-ouu-VV- 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 be4291a6d6d..030250ef8c8 100644 --- a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts +++ b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts @@ -167,7 +167,10 @@ test('transfer', async t => { ); const transferRes = await transferP; - t.is(transferRes, undefined, 'Successful transfer returns Promise.'); + t.true( + transferRes === undefined, + 'Successful transfer returns Promise.', + ); await t.throwsAsync( // XXX the bridge fakes the timeout response automatically for 504n From 4e8037d41bf16ec0bf7dbcb778d7fc86b43704b0 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Sun, 4 Aug 2024 13:07:22 -0600 Subject: [PATCH 8/9] test(orch): consolidate outbound bridge support --- packages/boot/tools/supports.ts | 215 +++++++++--------- .../test/examples/swapExample.test.ts | 2 +- 2 files changed, 110 insertions(+), 107 deletions(-) diff --git a/packages/boot/tools/supports.ts b/packages/boot/tools/supports.ts index 91186e94808..377cfc16bfa 100644 --- a/packages/boot/tools/supports.ts +++ b/packages/boot/tools/supports.ts @@ -382,131 +382,134 @@ export const makeSwingsetTestKit = async ( switch (bridgeId) { case BridgeId.BANK: { trace( - 'bridgeOutbound BANK', + 'bridgeOutbound bank', obj.type, obj.recipient, obj.amount, obj.denom, ); + break; + } + case BridgeId.STORAGE: + return storage.toStorage(obj); + case BridgeId.PROVISION: + case BridgeId.PROVISION_SMART_WALLET: + case BridgeId.WALLET: + console.warn('Bridge returning undefined for', bridgeId, ':', obj); + return undefined; + default: + break; + } + + const bridgeTargetRegistered = new Set(); + const bridgeType = `${bridgeId}:${obj.type}`; + switch (bridgeType) { + case `${BridgeId.BANK}:VBANK_GET_MODULE_ACCOUNT_ADDRESS`: { // bridgeOutbound bank : { // moduleName: 'vbank/reserve', // type: 'VBANK_GET_MODULE_ACCOUNT_ADDRESS' // } - switch (obj.type) { - case 'VBANK_GET_MODULE_ACCOUNT_ADDRESS': { - const { moduleName } = obj; - const moduleDescriptor = Object.values(VBankAccount).find( - ({ module }) => module === moduleName, - ); - if (!moduleDescriptor) { - return 'undefined'; - } - return moduleDescriptor.address; - } - - // Observed message: - // address: 'agoric1megzytg65cyrgzs6fvzxgrcqvwwl7ugpt62346', - // denom: 'ibc/toyatom', - // type: 'VBANK_GET_BALANCE' - case 'VBANK_GET_BALANCE': { - // TODO consider letting config specify vbank assets - // empty balances for test. - return '0'; - } + const { moduleName } = obj; + const moduleDescriptor = Object.values(VBankAccount).find( + ({ module }) => module === moduleName, + ); + if (!moduleDescriptor) { + return 'undefined'; + } + return moduleDescriptor.address; + } - case 'VBANK_GRAB': - case 'VBANK_GIVE': { - lastNonce += 1n; - // Also empty balances. - return harden({ - type: 'VBANK_BALANCE_UPDATE', - nonce: `${lastNonce}`, - updated: [], - }); - } + // Observed message: + // address: 'agoric1megzytg65cyrgzs6fvzxgrcqvwwl7ugpt62346', + // denom: 'ibc/toyatom', + // type: 'VBANK_GET_BALANCE' + case `${BridgeId.BANK}:VBANK_GET_BALANCE`: { + // TODO consider letting config specify vbank assets + // empty balances for test. + return '0'; + } - default: { - return 'undefined'; - } - } + case `${BridgeId.BANK}:VBANK_GRAB`: + case `${BridgeId.BANK}:VBANK_GIVE`: { + lastNonce += 1n; + // Also empty balances. + return harden({ + type: 'VBANK_BALANCE_UPDATE', + nonce: `${lastNonce}`, + updated: [], + }); } - case BridgeId.CORE: - case BridgeId.DIBC: - switch (obj.type) { - case 'IBC_METHOD': - switch (obj.method) { - 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); - } - } - default: - return undefined; + + case `${BridgeId.CORE}:IBC_METHOD`: + case `${BridgeId.DIBC}:IBC_METHOD`: + case `${BridgeId.VTRANSFER}:IBC_METHOD`: { + switch (obj.method) { + 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); + } } default: return undefined; } - case BridgeId.PROVISION: - case BridgeId.PROVISION_SMART_WALLET: - case BridgeId.WALLET: - console.warn('Bridge returning undefined for', bridgeId, ':', obj); + } + case `${BridgeId.VTRANSFER}:BRIDGE_TARGET_REGISTER`: { + bridgeTargetRegistered.add(obj.target); return undefined; - case BridgeId.VTRANSFER: - throw Error('FIXME bridge support for vtransfer'); - case BridgeId.STORAGE: - return storage.toStorage(obj); - case BridgeId.VLOCALCHAIN: - switch (obj.type) { - case 'VLOCALCHAIN_ALLOCATE_ADDRESS': - return 'agoric1mockVlocalchainAddress'; - case 'VLOCALCHAIN_EXECUTE_TX': { - return obj.messages.map(message => { - switch (message['@type']) { - case '/cosmos.staking.v1beta1.MsgDelegate': { - if (message.amount.amount === '504') { - // FIXME - how can we propagate the error? - // this results in `syscall.callNow failed: device.invoke failed, see logs for details` - throw Error('simulated packet timeout'); - } - return {} as JsonSafe; - } - // returns one empty object per message unless specified - default: - return {}; + } + case `${BridgeId.VTRANSFER}:BRIDGE_TARGET_UNREGISTER`: { + bridgeTargetRegistered.delete(obj.target); + return undefined; + } + case `${BridgeId.VLOCALCHAIN}:VLOCALCHAIN_ALLOCATE_ADDRESS`: { + return 'agoric1mockVlocalchainAddress'; + } + case `${BridgeId.VLOCALCHAIN}:VLOCALCHAIN_EXECUTE_TX`: { + return obj.messages.map(message => { + switch (message['@type']) { + case '/cosmos.staking.v1beta1.MsgDelegate': { + if (message.amount.amount === '504') { + // FIXME - how can we propagate the error? + // this results in `syscall.callNow failed: device.invoke failed, see logs for details` + throw Error('simulated packet timeout'); } - }); + return {} as JsonSafe; + } + // returns one empty object per message unless specified + default: + return {}; } - default: - throw Error(`VLOCALCHAIN message of unknown type ${obj.type}`); - } - default: - throw Error(`unknown bridgeId ${bridgeId}`); + }); + } + default: { + throw Error(`FIXME missing support for ${bridgeId}: ${obj.type}`); + } } }; diff --git a/packages/orchestration/test/examples/swapExample.test.ts b/packages/orchestration/test/examples/swapExample.test.ts index f87ab716969..12a79c09c1d 100644 --- a/packages/orchestration/test/examples/swapExample.test.ts +++ b/packages/orchestration/test/examples/swapExample.test.ts @@ -20,7 +20,7 @@ Error#20: {"type":1,"data":"CmgKIy9jb3Ntb3Muc3Rha2luZy52MWJldGExLk1zZ0RlbGVnYXRl at parseTxPacket (file:///Users/markmiller/src/ongithub/agoric/agoric-sdk/packages/orchestration/src/utils/packet.js:87:14) ``` */ -test.skip('start', async t => { +test.failing('start', async t => { const { bootstrap, brands: { ist }, From 0df9f8230b1172cd2c52edc4ecc270d29649af46 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Mon, 5 Aug 2024 14:16:06 -0600 Subject: [PATCH 9/9] docs(localchain): clarify `executeTx` JSDoc --- packages/vats/src/localchain.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/vats/src/localchain.js b/packages/vats/src/localchain.js index 9ece0eb8c98..4637e0d12e1 100644 --- a/packages/vats/src/localchain.js +++ b/packages/vats/src/localchain.js @@ -151,10 +151,14 @@ export const prepareLocalChainAccountKit = (zone, { watch }) => return E(purse).withdraw(amount); }, /** - * Execute a batch of messages in order as a single atomic transaction - * and return the responses. If any of the messages fails, the entire - * batch will be rolled back. Use `typedJson()` on the arguments to get - * typed return values. + * Execute a batch of messages on the local chain. Note in particular, + * that for IBC `MsgTransfer`, execution only queues a packet for the + * local chain's IBC stack, and returns a `MsgTransferResponse` + * immediately, not waiting for the confirmation on the other chain. + * + * Messages are executed in order as a single atomic transaction and + * returns the responses. If any of the messages fails, the entire batch + * will be rolled back on the local chain. * * @template {TypedJson[]} MT messages tuple (use const with multiple * elements or it will be a mixed array) @@ -162,6 +166,8 @@ export const prepareLocalChainAccountKit = (zone, { watch }) => * @returns {PromiseVowOfTupleMappedToGenerics<{ * [K in keyof MT]: JsonSafe>; * }>} + * @see {typedJson} which can be used on arguments to get typed return + * values. */ async executeTx(messages) { const { address, system } = this.state;