From d8a78d48b0c5984cab214502ab971021c9c57b3c Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 15 Aug 2024 17:02:43 -0400 Subject: [PATCH 1/6] feat: inspect fake bank bridge messages --- packages/orchestration/test/supports.ts | 6 +++++- packages/vats/tools/bank-utils.js | 3 ++- packages/vats/tools/fake-bridge.js | 13 ++++++++----- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/orchestration/test/supports.ts b/packages/orchestration/test/supports.ts index e5832ebe2a5..a1eab4d3bee 100644 --- a/packages/orchestration/test/supports.ts +++ b/packages/orchestration/test/supports.ts @@ -47,7 +47,10 @@ export const commonSetup = async (t: ExecutionContext) => { const bld = withAmountUtils(makeIssuerKit('BLD')); const ist = withAmountUtils(makeIssuerKit('IST')); - const { bankManager, pourPayment } = await makeFakeBankManagerKit(); + const bankBridgeMessages = [] as any[]; + const { bankManager, pourPayment } = await makeFakeBankManagerKit({ + onToBridge: obj => bankBridgeMessages.push(obj), + }); await E(bankManager).addAsset('ubld', 'BLD', 'Staking Token', bld.issuerKit); await E(bankManager).addAsset( 'uist', @@ -211,6 +214,7 @@ export const commonSetup = async (t: ExecutionContext) => { pourPayment, inspectLocalBridge: () => harden([...localBridgeMessages]), inspectDibcBridge: () => E(ibcBridge).inspectDibcBridge(), + inspectBankBridge: () => harden([...bankBridgeMessages]), registerAgoricBld, transmitTransferAck, }, diff --git a/packages/vats/tools/bank-utils.js b/packages/vats/tools/bank-utils.js index cf077e6807d..eb0ec53dc11 100644 --- a/packages/vats/tools/bank-utils.js +++ b/packages/vats/tools/bank-utils.js @@ -64,7 +64,8 @@ export const makeFakeBankKit = issuerKits => { /** * @param {object} [opts] - * @param {import('./fake-bridge.js').Balances} opts.balances initial balances + * @param {import('./fake-bridge.js').Balances} [opts.balances] initial balances + * @param {(obj) => unknown} [opts.onToBridge] handler for toBridge messages */ export const makeFakeBankManagerKit = async opts => { const baggage = makeScalarBigMapStore('baggage'); diff --git a/packages/vats/tools/fake-bridge.js b/packages/vats/tools/fake-bridge.js index 36c39af8d37..d8e1585d6a7 100644 --- a/packages/vats/tools/fake-bridge.js +++ b/packages/vats/tools/fake-bridge.js @@ -27,14 +27,16 @@ const INFINITE_AMOUNT = 99999999999n; * transaction outside the Agoric VM. (Similarly for deposits.) * * @param {import('@agoric/zone').Zone} zone - * @param {object} opts - * @param {Balances} opts.balances initial balances + * @param {object} [opts] + * @param {Balances} [opts.balances] initial balances + * @param {(obj) => void} [opts.onToBridge] * @returns {ScopedBridgeManager<'bank'>} * @see {makeFakeBankManagerKit} and its `pourPayment` for a helper */ -export const makeFakeBankBridge = (zone, opts = { balances: {} }) => { - const { balances } = opts; - +export const makeFakeBankBridge = ( + zone, + { balances = {}, onToBridge = () => {} } = {}, +) => { const currentBalance = ({ address, denom }) => address === FAUCET_ADDRESS ? INFINITE_AMOUNT @@ -46,6 +48,7 @@ export const makeFakeBankBridge = (zone, opts = { balances: {} }) => { return zone.exo('Fake Bank Bridge Manager', undefined, { getBridgeId: () => 'bank', toBridge: async obj => { + onToBridge(obj); const { method, type, ...params } = obj; trace('toBridge', type, method, params); switch (obj.type) { From 03a2b8511e7dd2b3cb41abb9e284ed9b09cf57ba Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 15 Aug 2024 17:16:44 -0400 Subject: [PATCH 2/6] feat: zoeTools.withdrawToSeat - retriable that withdraws payments (described by an IssuerKeywordRecord) from a LCA to a user seat --- packages/orchestration/src/utils/zoe-tools.js | 139 +++++- .../test/fixtures/zoe-tools.contract.js | 112 +++++ .../test/fixtures/zoe-tools.flows.js | 128 ++++++ packages/orchestration/test/supports.ts | 12 + .../test/utils/zoe-tools.test.ts | 410 ++++++++++++++++++ 5 files changed, 779 insertions(+), 22 deletions(-) create mode 100644 packages/orchestration/test/fixtures/zoe-tools.contract.js create mode 100644 packages/orchestration/test/fixtures/zoe-tools.flows.js create mode 100644 packages/orchestration/test/utils/zoe-tools.test.ts diff --git a/packages/orchestration/src/utils/zoe-tools.js b/packages/orchestration/src/utils/zoe-tools.js index abf7f5bb683..59306435247 100644 --- a/packages/orchestration/src/utils/zoe-tools.js +++ b/packages/orchestration/src/utils/zoe-tools.js @@ -1,13 +1,14 @@ -import { Fail } from '@endo/errors'; -import { atomicTransfer } from '@agoric/zoe/src/contractSupport/index.js'; +import { makeError, q, Fail } from '@endo/errors'; +import { depositToSeat } from '@agoric/zoe/src/contractSupport/index.js'; import { E } from '@endo/far'; +const { assign, keys, values } = Object; + /** * @import {InvitationMakers} from '@agoric/smart-wallet/src/types.js'; * @import {ResolvedPublicTopic} from '@agoric/zoe/src/contractSupport/topics.js'; - * @import {Vow, VowTools} from '@agoric/vow'; + * @import {VowTools} from '@agoric/vow'; * @import {Zone} from '@agoric/zone'; - * @import {OrchestrationAccount} from '../orchestration-api.js' * @import {LocalAccountMethods} from '../types.js'; */ @@ -24,31 +25,44 @@ import { E } from '@endo/far'; * @typedef {( * srcSeat: ZCFSeat, * localAccount: LocalAccountMethods, - * give: AmountKeywordRecord, + * amounts: AmountKeywordRecord, * ) => Promise} LocalTransfer */ +/** + * @typedef {( + * localAccount: LocalAccountMethods, + * destSeat: ZCFSeat, + * amounts: AmountKeywordRecord, + * ) => Promise} WithdrawToSeat + */ + /** * @param {Zone} zone * @param {{ zcf: ZCF; vowTools: VowTools }} io */ -export const makeZoeTools = (zone, { zcf, vowTools }) => { +export const makeZoeTools = ( + zone, + { zcf, vowTools: { retriable, when, allVows, allSettled } }, +) => { /** - * Transfer the `give` a seat to a local account. + * Transfer the `amounts` from `srcSeat` to `localAccount`. If any of the + * deposits fail, everything will be rolled back to the `srcSeat`. Supports + * multiple items in the `amounts` {@link AmountKeywordRecord}. */ - const localTransfer = vowTools.retriable( + const localTransfer = retriable( zone, 'localTransfer', /** * @type {LocalTransfer} */ - async (srcSeat, localAccount, give) => { + async (srcSeat, localAccount, amounts) => { !srcSeat.hasExited() || Fail`The seat cannot have exited.`; const { zcfSeat: tempSeat, userSeat: userSeatP } = zcf.makeEmptySeatKit(); const userSeat = await userSeatP; - atomicTransfer(zcf, srcSeat, tempSeat, give); + zcf.atomicRearrange(harden([[srcSeat, tempSeat, amounts]])); tempSeat.exit(); - // TODO get the userSeat into baggage so it's at least recoverable + // TODO (#9541) get the userSeat into baggage so it's at least recoverable // const userSeat = await subzone.makeOnce( // 'localTransferHelper', // async () => { @@ -56,28 +70,109 @@ export const makeZoeTools = (zone, { zcf, vowTools }) => { // zcf.makeEmptySeatKit(); // const uSeat = await userSeatP; // // TODO how do I store in the place for this retriable? - // atomicTransfer(zcf, srcSeat, tempSeat, give); + // atomicTransfer(zcf, srcSeat, tempSeat, amounts); // tempSeat.exit(); // return uSeat; // }, // ); - // Now all the `give` are accessible, so we can move them to the localAccount` + // Now all the `amounts` are accessible, so we can move them to the localAccount + const payments = await Promise.all( + keys(amounts).map(kw => E(userSeat).getPayout(kw)), + ); + const settleDeposits = await when( + allSettled(payments.map(pmt => E(localAccount).deposit(pmt))), + ); + // if any of the deposits to localAccount failed, unwind all of the allocations + if (settleDeposits.find(x => x.status === 'rejected')) { + const amts = values(amounts); + const errors = []; + // withdraw the successfully deposited payments + const paymentsOrWithdrawVs = settleDeposits.map((x, i) => { + if (x.status === 'rejected') { + errors.push(x.reason); + return payments[i]; + } + return E(localAccount).withdraw(amts[i]); + }); + + // return all payments to the srcSeat + const paymentsToReturn = await when(allVows(paymentsOrWithdrawVs)); + const paymentKwr = harden( + keys(amounts).reduce( + (kwr, kw, i) => assign(kwr, { [kw]: paymentsToReturn[i] }), + {}, + ), + ); + const depositResponse = await depositToSeat( + zcf, + srcSeat, + amounts, + paymentKwr, + ); + console.debug(depositResponse); + throw makeError(`One or more deposits failed ${q(errors)}`); + } + // TODO #9541 remove userSeat from baggage + }, + ); + + /** + * Transfer the `amounts` from a `localAccount` to the `recipientSeat`. If any + * of the withdrawals fail, everything will be rolled back to the + * `srcLocalAccount`. Supports multiple items in the `amounts` + * {@link PaymentKeywordRecord}. + */ + const withdrawToSeat = retriable( + zone, + 'withdrawToSeat', + /** @type {WithdrawToSeat} */ + async (localAccount, destSeat, amounts) => { + await null; + !destSeat.hasExited() || Fail`The seat cannot have exited.`; + + const settledWithdrawals = await when( + allSettled(values(amounts).map(amt => E(localAccount).withdraw(amt))), + ); - const depositVs = Object.entries(give).map(async ([kw, _amount]) => { - const pmt = await E(userSeat).getPayout(kw); - // TODO arrange recovery on upgrade of pmt? - return localAccount.deposit(pmt); - }); - await vowTools.when(vowTools.allVows(depositVs)); - // TODO remove userSeat from baggage - // TODO reject non-vbank issuers - // TODO recover failed deposits + // if any of the withdrawals were rejected, unwind the successful ones + if (settledWithdrawals.find(x => x.status === 'rejected')) { + const returnPaymentVs = []; + const errors = []; + for (const result of settledWithdrawals) { + if (result.status === 'fulfilled') { + returnPaymentVs.push(E(localAccount).deposit(result.value)); + } else { + errors.push(result.reason); + } + } + await when(allVows(returnPaymentVs)); + throw makeError(`One or more withdrawals failed ${q(errors)}`); + } + // successfully withdrew payments from srcLocalAccount, deposit to recipientSeat + const paymentKwr = harden( + keys(amounts).reduce( + (acc, kw, i) => + assign(acc, { + [kw]: /** @type {{ value: Amount }[]} */ (settledWithdrawals)[i] + .value, + }), + {}, + ), + ); + const depositResponse = await depositToSeat( + zcf, + destSeat, + amounts, + paymentKwr, + ); + console.debug(depositResponse); }, ); return harden({ localTransfer, + withdrawToSeat, }); }; /** @typedef {ReturnType} ZoeTools */ diff --git a/packages/orchestration/test/fixtures/zoe-tools.contract.js b/packages/orchestration/test/fixtures/zoe-tools.contract.js new file mode 100644 index 00000000000..67e3ae5268c --- /dev/null +++ b/packages/orchestration/test/fixtures/zoe-tools.contract.js @@ -0,0 +1,112 @@ +/** + * @file Testing fixture that takes shortcuts to ensure we hit error paths + * around `zoeTools.localTransfer` and `zoeTools.withdrawToSeat` + */ + +import { makeSharedStateRecord } from '@agoric/async-flow'; +import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; +import { E } from '@endo/far'; +import { M } from '@endo/patterns'; +import { withOrchestration } from '../../src/utils/start-helper.js'; +import { prepareChainHubAdmin } from '../../src/exos/chain-hub-admin.js'; +import * as flows from './zoe-tools.flows.js'; +import fetchedChainInfo from '../../src/fetched-chain-info.js'; + +const { values } = Object; + +/** + * @import {TimerService} from '@agoric/time'; + * @import {LocalChain} from '@agoric/vats/src/localchain.js'; + * @import {NameHub} from '@agoric/vats'; + * @import {Remote} from '@agoric/vow'; + * @import {Zone} from '@agoric/zone'; + * @import {AssetInfo} from '@agoric/vats/src/vat-bank.js'; + * @import {CosmosInterchainService} from '@agoric/orchestration'; + * @import {OrchestrationTools} from '../../src/utils/start-helper.js'; + */ + +/** + * @typedef {{ + * localchain: Remote; + * orchestrationService: Remote; + * storageNode: Remote; + * timerService: Remote; + * agoricNames: Remote; + * }} OrchestrationPowers + */ + +/** + * @param {ZCF} zcf + * @param {OrchestrationPowers & { + * marshaller: Marshaller; + * }} privateArgs + * @param {Zone} zone + * @param {OrchestrationTools} tools + */ +const contract = async ( + zcf, + privateArgs, + zone, + { chainHub, orchestrateAll, zoeTools }, +) => { + const contractState = makeSharedStateRecord( + /** @type {{ account: OrchestrationAccount | undefined }} */ { + localAccount: undefined, + }, + ); + + const creatorFacet = prepareChainHubAdmin(zone, chainHub); + + const orchFns = orchestrateAll(flows, { + zcf, + contractState, + zoeTools, + }); + + // register assets in ChainHub ourselves, + // UNTIL https://github.com/Agoric/agoric-sdk/issues/9752 + const assets = /** @type {AssetInfo[]} */ ( + await E(E(privateArgs.agoricNames).lookup('vbankAsset')).values() + ); + for (const chainName of ['agoric', 'cosmoshub']) { + chainHub.registerChain(chainName, fetchedChainInfo[chainName]); + } + for (const brand of values(zcf.getTerms().brands)) { + const info = assets.find(a => a.brand === brand); + if (info) { + chainHub.registerAsset(info.denom, { + // we are only registering agoric assets, so safe to use denom and + // hardcode chainName + baseDenom: info.denom, + baseName: 'agoric', + chainName: 'agoric', + brand, + }); + } + } + + const publicFacet = zone.exo( + 'Zoe Tools Test PF', + M.interface('Zoe Tools Test PF', { + makeDepositSendInvitation: M.callWhen().returns(InvitationShape), + makeDepositInvitation: M.callWhen().returns(InvitationShape), + makeWithdrawInvitation: M.callWhen().returns(InvitationShape), + }), + { + makeDepositSendInvitation() { + return zcf.makeInvitation(orchFns.depositSend, 'depositSend'); + }, + makeDepositInvitation() { + return zcf.makeInvitation(orchFns.deposit, 'deposit'); + }, + makeWithdrawInvitation() { + return zcf.makeInvitation(orchFns.withdraw, 'withdraw'); + }, + }, + ); + + return { publicFacet, creatorFacet }; +}; + +export const start = withOrchestration(contract); +harden(start); diff --git a/packages/orchestration/test/fixtures/zoe-tools.flows.js b/packages/orchestration/test/fixtures/zoe-tools.flows.js new file mode 100644 index 00000000000..15338b63d2a --- /dev/null +++ b/packages/orchestration/test/fixtures/zoe-tools.flows.js @@ -0,0 +1,128 @@ +/** + * @file Testing fixture that takes shortcuts to ensure we hit error paths + * around `zoeTools.localTransfer` and `zoeTools.withdrawToSeat`. + */ + +import { makeError, q } from '@endo/errors'; +import { mustMatch } from '@endo/patterns'; +import { ChainAddressShape } from '../../src/typeGuards.js'; + +const { values } = Object; + +/** + * @import {GuestInterface} from '@agoric/async-flow'; + * @import {Orchestrator, LocalAccountMethods, OrchestrationAccountI, OrchestrationFlow, ChainAddress} from '@agoric/orchestration'; + * @import {ZoeTools} from '../../src/utils/zoe-tools.js'; + */ + +/** + * Accept one or more deposits and send them to an account on the local chain + * using MsgSend. Intentionally skips a check to ensure an asset is in vbank to + * facilitate failure path testing of ZoeTools. + * + * @satisfies {OrchestrationFlow} + * @param {Orchestrator} orch + * @param {object} ctx + * @param {{ localAccount?: OrchestrationAccountI & LocalAccountMethods }} ctx.contractState + * @param {GuestInterface} ctx.zoeTools + * @param {ZCFSeat} seat + * @param {{ destAddr: ChainAddress }} offerArgs + */ +export const depositSend = async ( + orch, + { contractState, zoeTools: { localTransfer, withdrawToSeat } }, + seat, + offerArgs, +) => { + mustMatch(offerArgs, harden({ destAddr: ChainAddressShape })); + const { destAddr } = offerArgs; + assert(destAddr.value.startsWith('agoric1'), 'must send to a local address'); + + const { give } = seat.getProposal(); + + await null; + if (!contractState.localAccount) { + const agoricChain = await orch.getChain('agoric'); + contractState.localAccount = await agoricChain.makeAccount(); + } + + await localTransfer(seat, contractState.localAccount, give); + + try { + await contractState.localAccount.sendAll(destAddr, values(give)); + } catch (error) { + await withdrawToSeat(contractState.localAccount, seat, give); + const errMsg = makeError(`SendAll failed ${q(error)}`); + seat.exit(errMsg); + throw errMsg; + } + seat.exit(); +}; +harden(depositSend); + +/** + * Accept one or more deposits and transfer them to the contract's local + * account. + * + * @satisfies {OrchestrationFlow} + * @param {Orchestrator} orch + * @param {object} ctx + * @param {{ localAccount?: OrchestrationAccountI & LocalAccountMethods }} ctx.contractState + * @param {GuestInterface} ctx.zoeTools + * @param {ZCFSeat} seat + */ +export const deposit = async ( + orch, + { contractState, zoeTools: { localTransfer } }, + seat, +) => { + const { give } = seat.getProposal(); + + await null; + if (!contractState.localAccount) { + const agoricChain = await orch.getChain('agoric'); + contractState.localAccount = await agoricChain.makeAccount(); + } + + try { + await localTransfer(seat, contractState.localAccount, give); + } catch (e) { + seat.exit(e); + throw e; + } + seat.exit(); +}; +harden(deposit); + +/** + * Withdraw funds from the contract's local account to the offer's seat. + * + * @satisfies {OrchestrationFlow} + * @param {Orchestrator} orch + * @param {object} ctx + * @param {{ localAccount?: OrchestrationAccountI & LocalAccountMethods }} ctx.contractState + * @param {GuestInterface} ctx.zoeTools + * @param {ZCFSeat} seat + */ +export const withdraw = async ( + orch, + { contractState, zoeTools: { withdrawToSeat } }, + seat, +) => { + const { want } = seat.getProposal(); + + await null; + if (!contractState.localAccount) { + const agoricChain = await orch.getChain('agoric'); + contractState.localAccount = await agoricChain.makeAccount(); + } + + try { + await withdrawToSeat(contractState.localAccount, seat, want); + } catch (e) { + seat.exit(e); + throw e; + } + seat.exit(); +}; +harden(withdraw); diff --git a/packages/orchestration/test/supports.ts b/packages/orchestration/test/supports.ts index a1eab4d3bee..21603240711 100644 --- a/packages/orchestration/test/supports.ts +++ b/packages/orchestration/test/supports.ts @@ -63,6 +63,7 @@ export const commonSetup = async (t: ExecutionContext) => { const { mint: _b, ...bldSansMint } = bld; const { mint: _i, ...istSansMint } = ist; // XXX real bankManager does this. fake should too? + // TODO https://github.com/Agoric/agoric-sdk/issues/9966 await makeWellKnownSpaces(agoricNamesAdmin, t.log, ['vbankAsset']); await E(E(agoricNamesAdmin).lookupAdmin('vbankAsset')).update( 'uist', @@ -75,6 +76,17 @@ export const commonSetup = async (t: ExecutionContext) => { displayInfo: { IOU: true }, }), ); + await E(E(agoricNamesAdmin).lookupAdmin('vbankAsset')).update( + 'ubld', + /** @type {AssetInfo} */ harden({ + brand: bld.brand, + issuer: bld.issuer, + issuerName: 'BLD', + denom: 'ubld', + proposedName: 'BLD', + displayInfo: { IOU: true }, + }), + ); const vowTools = prepareSwingsetVowTools(rootZone.subZone('vows')); diff --git a/packages/orchestration/test/utils/zoe-tools.test.ts b/packages/orchestration/test/utils/zoe-tools.test.ts new file mode 100644 index 00000000000..e9535ea919f --- /dev/null +++ b/packages/orchestration/test/utils/zoe-tools.test.ts @@ -0,0 +1,410 @@ +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import type { TestFn } from 'ava'; + +import path from 'path'; +import { setUpZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; +import { makeIssuerKit } from '@agoric/ertp'; +import { AmountUtils, withAmountUtils } from '@agoric/zoe/tools/test-utils.js'; +import { Issuer } from '@agoric/ertp/src/types.js'; +import { E } from '@endo/far'; +import { + LOCALCHAIN_DEFAULT_ADDRESS, + SIMULATED_ERRORS, +} from '@agoric/vats/tools/fake-bridge.js'; +import { commonSetup } from '../supports.js'; + +const dirname = path.dirname(new URL(import.meta.url).pathname); + +const contractName = 'zoeTools'; +const contractFile = `${dirname}/../../test/fixtures/zoe-tools.contract.js`; +type StartFn = typeof import('../../test/fixtures/zoe-tools.contract.js').start; + +type TestContext = Awaited> & { + brands: Awaited>['brands'] & { + moolah: AmountUtils; + }; + zoe: ZoeService; + contractKit: StartedInstanceKit; + getIssuer: (keyword: string) => Issuer<'nat'>; +}; + +const test = anyTest as TestFn; + +test.beforeEach(async t => { + t.log('bootstrap, orchestration core-eval'); + const common = await commonSetup(t); + const { + bootstrap, + commonPrivateArgs, + brands: { ist, bld }, + } = common; + + const moolah = withAmountUtils(makeIssuerKit('MOO')); + t.log('Making Moolah issuer kit', moolah); + + t.log('contract coreEval', contractName); + const { zoe, bundleAndInstall } = await setUpZoeForTest(); + const installation: Installation = + await bundleAndInstall(contractFile); + + const issuerKeywordRecord = harden({ + IST: ist.issuer, + BLD: bld.issuer, + MOO: moolah.issuer, + }); + t.log('issuerKeywordRecord', issuerKeywordRecord); + + const storageNode = await E(bootstrap.storage.rootNode).makeChildNode( + contractName, + ); + const contractKit = await E(zoe).startInstance( + installation, + issuerKeywordRecord, + {}, + { ...commonPrivateArgs, storageNode }, + ); + + const getIssuer = (key: string) => issuerKeywordRecord[key] as Issuer<'nat'>; + + t.context = { + ...common, + brands: { + ...common.brands, + moolah, + }, + contractKit, + getIssuer, + zoe, + }; +}); + +/** + * Tests to ensure `localTransfer` recovers when presented non-vbank asset(s) + * Also tests withdrawSeat, as it's used in the recovery path. + */ +test('zoeTool.localTransfer error paths', async t => { + const { + bootstrap, + brands: { ist, bld, moolah }, + utils: { pourPayment }, + contractKit, + zoe, + getIssuer, + } = t.context; + const publicFacet = await E(zoe).getPublicFacet(contractKit.instance); + const vt = bootstrap.vowTools; + + const destAddr = { + chainId: 'agoriclocal', + value: 'agoric1testrecipient', + encoding: 'bech32', + }; + + t.log('localTransfer recovers when presented non-vbank asset'); + { + const tenMoolah = moolah.make(10n); + const MOO = await E(moolah.mint).mintPayment(tenMoolah); + const userSeat = await E(zoe).offer( + E(publicFacet).makeDepositSendInvitation(), + { give: { MOO: tenMoolah } }, + { MOO }, + { + destAddr, + }, + ); + await t.throwsAsync(vt.when(E(userSeat).getOfferResult()), { + message: + 'One or more deposits failed ["[Error: key \\"[Alleged: MOO brand]\\" not found in collection \\"brandToAssetRecord\\"]"]', + }); + await E(userSeat).tryExit(); + const payouts = await E(userSeat).getPayouts(); + const payoutEntries = Object.entries(payouts); + t.is(payoutEntries.length, 1, 'expecting 1 MOO payout'); + for (const [kw, pmt] of payoutEntries) { + const payment = await getIssuer(kw).getAmountOf(pmt); + t.is(payment.value, 10n, `${kw} payment returned`); + } + } + + t.log('localTransfer recovers from: give: { IST, MOO }'); + { + const tenMoolah = moolah.make(10n); + const MOO = await E(moolah.mint).mintPayment(tenMoolah); + const tenStable = ist.make(10n); + const IST = await pourPayment(tenStable); + const userSeat = await E(zoe).offer( + E(publicFacet).makeDepositSendInvitation(), + { give: { IST: tenStable, MOO: tenMoolah } }, + { IST, MOO }, + { + destAddr, + }, + ); + await t.throwsAsync(vt.when(E(userSeat).getOfferResult()), { + message: + 'One or more deposits failed ["[Error: key \\"[Alleged: MOO brand]\\" not found in collection \\"brandToAssetRecord\\"]"]', + }); + await E(userSeat).tryExit(); + const payouts = await E(userSeat).getPayouts(); + const payoutEntries = Object.entries(payouts); + t.is(payoutEntries.length, 2, 'expecting IST, MOO payouts'); + for (const [kw, pmt] of payoutEntries) { + const payment = await getIssuer(kw).getAmountOf(pmt); + t.is(payment.value, 10n, `${kw} payment returned`); + } + } + + t.log('localTransfer recovers from: give: { BLD, MOO, IST } '); + { + const tenMoolah = moolah.make(10n); + const MOO = await E(moolah.mint).mintPayment(tenMoolah); + const tenStable = ist.make(10n); + const IST = await pourPayment(tenStable); + const tenStake = bld.make(10n); + const BLD = await pourPayment(tenStake); + const userSeat = await E(zoe).offer( + E(publicFacet).makeDepositSendInvitation(), + { give: { BLD: tenStake, MOO: tenMoolah, IST: tenStable } }, + { BLD, MOO, IST }, + { + destAddr, + }, + ); + await t.throwsAsync(vt.when(E(userSeat).getOfferResult()), { + message: + 'One or more deposits failed ["[Error: key \\"[Alleged: MOO brand]\\" not found in collection \\"brandToAssetRecord\\"]"]', + }); + await E(userSeat).tryExit(); + const payouts = await E(userSeat).getPayouts(); + const payoutEntries = Object.entries(payouts); + t.is(payoutEntries.length, 3, 'expecting BLD, IST, MOO payouts'); + for (const [kw, pmt] of payoutEntries) { + const payment = await getIssuer(kw).getAmountOf(pmt); + t.is(payment.value, 10n, `${kw} payment returned`); + } + } + t.log('withdrawToSeat recovers from: simulated sendAll failure '); + { + const tenStable = ist.make(SIMULATED_ERRORS.BAD_REQUEST); + const IST = await pourPayment(tenStable); + const tenStake = bld.make(SIMULATED_ERRORS.BAD_REQUEST); + const BLD = await pourPayment(tenStake); + const userSeat = await E(zoe).offer( + E(publicFacet).makeDepositSendInvitation(), + { give: { BLD: tenStake, IST: tenStable } }, + { BLD, IST }, + { + destAddr, + }, + ); + await t.throwsAsync(vt.when(E(userSeat).getOfferResult()), { + message: 'SendAll failed "[Error: simulated error]"', + }); + await E(userSeat).hasExited(); + const payouts = await E(userSeat).getPayouts(); + const payoutEntries = Object.entries(payouts); + t.is(payoutEntries.length, 2, 'expecting BLD, IST payouts'); + for (const [kw, pmt] of payoutEntries) { + const payment = await getIssuer(kw).getAmountOf(pmt); + t.is( + payment.value, + SIMULATED_ERRORS.BAD_REQUEST, + `${kw} payment returned`, + ); + } + } +}); + +test('localTransfer happy path', async t => { + const { + bootstrap, + brands: { ist, bld }, + utils: { pourPayment, inspectBankBridge, inspectLocalBridge }, + contractKit, + zoe, + getIssuer, + } = t.context; + const publicFacet = await E(zoe).getPublicFacet(contractKit.instance); + const vt = bootstrap.vowTools; + + const destAddr = { + chainId: 'agoriclocal', + value: 'agoric1testrecipient', + encoding: 'bech32', + }; + + const tenStable = ist.make(10n); + const tenStake = bld.make(10n); + + const expectedAmounts = [ + { + denom: 'ubld', + amount: '10', + }, + { + denom: 'uist', + amount: '10', + }, + ]; + + { + t.log('localTransfer happy path via depositAndSend'); + const IST = await pourPayment(tenStable); + const BLD = await pourPayment(tenStake); + + const userSeat = await E(zoe).offer( + E(publicFacet).makeDepositSendInvitation(), + { give: { BLD: tenStake, IST: tenStable } }, + { BLD, IST }, + { + destAddr, + }, + ); + + await vt.when(E(userSeat).getOfferResult()); + const payouts = await E(userSeat).getPayouts(); + const payoutEntries = Object.entries(payouts); + t.is(payoutEntries.length, 2, 'expecting BLD, IST payouts'); + for (const [kw, pmt] of payoutEntries) { + const payment = await getIssuer(kw).getAmountOf(pmt); + t.is(payment.value, 0n, `no payout for ${kw}`); + } + + t.like( + inspectBankBridge().filter(m => m.type === 'VBANK_GIVE'), + expectedAmounts.map(x => ({ + ...x, + recipient: LOCALCHAIN_DEFAULT_ADDRESS, + })), + 'funds deposited to contract LCA', + ); + t.like( + inspectLocalBridge().find(x => x.type === 'VLOCALCHAIN_EXECUTE_TX') + ?.messages?.[0], + { + '@type': '/cosmos.bank.v1beta1.MsgSend', + fromAddress: LOCALCHAIN_DEFAULT_ADDRESS, + toAddress: destAddr.value, + amount: expectedAmounts, + }, + 'sendAll sent', + ); + } + { + t.log('localTransfer happy path via deposit'); + const IST = await pourPayment(tenStable); + const BLD = await pourPayment(tenStake); + + const userSeat = await E(zoe).offer( + E(publicFacet).makeDepositInvitation(), + { give: { BLD: tenStake, IST: tenStable } }, + { BLD, IST }, + ); + + await vt.when(E(userSeat).getOfferResult()); + + const payouts = await E(userSeat).getPayouts(); + const payoutEntries = Object.entries(payouts); + for (const [kw, pmt] of payoutEntries) { + const payment = await getIssuer(kw).getAmountOf(pmt); + t.is(payment.value, 0n, `no payout for ${kw}`); + } + + t.like( + inspectBankBridge().filter(m => m.type === 'VBANK_GIVE'), + expectedAmounts.map(x => ({ + ...x, + recipient: LOCALCHAIN_DEFAULT_ADDRESS, + })), + 'funds deposited to contract LCA', + ); + } +}); + +test('withdraw (withdrawToSeat) from LCA with insufficient balance', async t => { + const { + bootstrap, + brands: { ist, bld }, + contractKit, + zoe, + } = t.context; + const publicFacet = await E(zoe).getPublicFacet(contractKit.instance); + const vt = bootstrap.vowTools; + + const tenStable = ist.make(10n); + const tenStake = bld.make(10n); + + const userSeat = await E(zoe).offer(E(publicFacet).makeWithdrawInvitation(), { + want: { BLD: tenStake, IST: tenStable }, + }); + + await t.throwsAsync(vt.when(E(userSeat).getOfferResult()), { + message: + 'One or more withdrawals failed ["[RangeError: -10 is negative]","[RangeError: -10 is negative]"]', + }); +}); + +test('withdraw (withdrawToSeat) happy path', async t => { + const { + bootstrap, + brands: { ist, bld }, + contractKit, + zoe, + getIssuer, + utils: { pourPayment }, + } = t.context; + const publicFacet = await E(zoe).getPublicFacet(contractKit.instance); + const vt = bootstrap.vowTools; + + const tenStable = ist.make(10n); + const tenStake = bld.make(10n); + + t.log('ensure contract LCA has funds to withdraw'); + const IST = await pourPayment(tenStable); + const BLD = await pourPayment(tenStake); + + const depositSeat = await E(zoe).offer( + E(publicFacet).makeDepositInvitation(), + { give: { BLD: tenStake, IST: tenStable } }, + { BLD, IST }, + ); + await vt.when(E(depositSeat).getOfferResult()); + + const withdrawSeat = await E(zoe).offer( + E(publicFacet).makeWithdrawInvitation(), + { + want: { BLD: tenStake, IST: tenStable }, + }, + ); + await vt.when(E(withdrawSeat).getOfferResult()); + + const payouts = await E(withdrawSeat).getPayouts(); + const payoutEntries = Object.entries(payouts); + t.is(payoutEntries.length, 2, 'expecting BLD, IST payouts'); + for (const [kw, pmt] of payoutEntries) { + const payment = await getIssuer(kw).getAmountOf(pmt); + t.is(payment.value, 10n, `${kw} payment given`); + } +}); + +test('withdrawToSeat, unknown brand', async t => { + const { + bootstrap, + brands: { moolah }, + contractKit, + zoe, + } = t.context; + const publicFacet = await E(zoe).getPublicFacet(contractKit.instance); + const vt = bootstrap.vowTools; + + const tenMoolah = moolah.make(10n); + + const userSeat = await E(zoe).offer(E(publicFacet).makeWithdrawInvitation(), { + want: { MOO: tenMoolah }, + }); + + await t.throwsAsync(vt.when(E(userSeat).getOfferResult()), { + message: + 'One or more withdrawals failed ["[Error: key \\"[Alleged: MOO brand]\\" not found in collection \\"brandToAssetRecord\\"]"]', + }); +}); From a90626ade0c3cd36ed949962fdf1fdb60660b77a Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 15 Aug 2024 17:25:58 -0400 Subject: [PATCH 3/6] feat(sendAnywhere): handle failed IBC transfer --- .../src/examples/send-anywhere.contract.js | 2 +- .../src/examples/send-anywhere.flows.js | 29 ++- .../test/examples/send-anywhere.test.ts | 194 +++++++++++++++++- .../snapshots/send-anywhere.test.ts.md | 8 +- .../snapshots/send-anywhere.test.ts.snap | Bin 1064 -> 1118 bytes 5 files changed, 214 insertions(+), 19 deletions(-) diff --git a/packages/orchestration/src/examples/send-anywhere.contract.js b/packages/orchestration/src/examples/send-anywhere.contract.js index ebe77f1af7d..fc7bfc1891b 100644 --- a/packages/orchestration/src/examples/send-anywhere.contract.js +++ b/packages/orchestration/src/examples/send-anywhere.contract.js @@ -47,7 +47,7 @@ const contract = async ( const orchFns = orchestrateAll(flows, { zcf, contractState, - localTransfer: zoeTools.localTransfer, + zoeTools, }); const publicFacet = zone.exo( diff --git a/packages/orchestration/src/examples/send-anywhere.flows.js b/packages/orchestration/src/examples/send-anywhere.flows.js index 8573dea7b13..71844d4ab3c 100644 --- a/packages/orchestration/src/examples/send-anywhere.flows.js +++ b/packages/orchestration/src/examples/send-anywhere.flows.js @@ -1,8 +1,9 @@ import { NonNullish } from '@agoric/internal'; +import { makeError, q } from '@endo/errors'; import { M, mustMatch } from '@endo/patterns'; /** - * @import {GuestOf} from '@agoric/async-flow'; + * @import {GuestInterface} from '@agoric/async-flow'; * @import {ZoeTools} from '../utils/zoe-tools.js'; * @import {Orchestrator, LocalAccountMethods, OrchestrationAccountI, OrchestrationFlow} from '../types.js'; */ @@ -17,13 +18,13 @@ const { entries } = Object; * @param {Orchestrator} orch * @param {object} ctx * @param {{ localAccount?: OrchestrationAccountI & LocalAccountMethods }} ctx.contractState - * @param {GuestOf} ctx.localTransfer + * @param {GuestInterface} ctx.zoeTools * @param {ZCFSeat} seat * @param {{ chainName: string; destAddr: string }} offerArgs */ export const sendIt = async ( orch, - { contractState, localTransfer }, + { contractState, zoeTools: { localTransfer, withdrawToSeat } }, seat, offerArgs, ) => { @@ -51,14 +52,20 @@ export const sendIt = async ( await localTransfer(seat, contractState.localAccount, give); - await contractState.localAccount.transfer( - { denom, value: amt.value }, - { - value: destAddr, - encoding: 'bech32', - chainId, - }, - ); + try { + await contractState.localAccount.transfer( + { denom, value: amt.value }, + { + value: destAddr, + encoding: 'bech32', + chainId, + }, + ); + } catch (e) { + await withdrawToSeat(contractState.localAccount, seat, give); + throw seat.fail(makeError(`IBC Transfer failed ${q(e)}`)); + } + seat.exit(); }; harden(sendIt); diff --git a/packages/orchestration/test/examples/send-anywhere.test.ts b/packages/orchestration/test/examples/send-anywhere.test.ts index b350c53678e..3d2177a6aa8 100644 --- a/packages/orchestration/test/examples/send-anywhere.test.ts +++ b/packages/orchestration/test/examples/send-anywhere.test.ts @@ -4,17 +4,17 @@ import { setUpZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; import { E } from '@endo/far'; import path from 'path'; import { mustMatch } from '@endo/patterns'; -import { makeIssuerKit } from '@agoric/ertp'; +import { AmountMath, makeIssuerKit } from '@agoric/ertp'; import { eventLoopIteration, inspectMapStore, } from '@agoric/internal/src/testing-utils.js'; -import { inspect } from 'util'; +import { SIMULATED_ERRORS } from '@agoric/vats/tools/fake-bridge.js'; +import { withAmountUtils } from '@agoric/zoe/tools/test-utils.js'; import { CosmosChainInfo, IBCConnectionInfo } from '../../src/cosmos-api.js'; import { commonSetup } from '../supports.js'; import { SingleAmountRecord } from '../../src/examples/send-anywhere.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); @@ -226,10 +226,8 @@ test('send using arbitrary chain info', async t => { test('baggage', async t => { const { - bootstrap, commonPrivateArgs, brands: { ist }, - utils: { inspectLocalBridge, pourPayment }, } = await commonSetup(t); let contractBaggage; @@ -250,3 +248,189 @@ test('baggage', async t => { const tree = inspectMapStore(contractBaggage); t.snapshot(tree, 'contract baggage after start'); }); + +test('failed ibc transfer returns give', async t => { + t.log('bootstrap, orchestration core-eval'); + const { + bootstrap, + commonPrivateArgs, + brands: { ist }, + utils: { inspectLocalBridge, pourPayment, inspectBankBridge }, + } = await commonSetup(t); + const vt = bootstrap.vowTools; + + const { zoe, bundleAndInstall } = await setUpZoeForTest(); + + t.log('contract coreEval', contractName); + + const installation: Installation = + await bundleAndInstall(contractFile); + + const storageNode = await E(bootstrap.storage.rootNode).makeChildNode( + contractName, + ); + const sendKit = await E(zoe).startInstance( + installation, + { Stable: ist.issuer }, + {}, + { ...commonPrivateArgs, storageNode }, + ); + + t.log('client sends an ibc transfer we expect will timeout'); + + const publicFacet = await E(zoe).getPublicFacet(sendKit.instance); + const inv = E(publicFacet).makeSendInvitation(); + const amt = await E(zoe).getInvitationDetails(inv); + t.is(amt.description, 'send'); + + const anAmt = ist.make(SIMULATED_ERRORS.TIMEOUT); + const Send = await pourPayment(anAmt); + const userSeat = await E(zoe).offer( + inv, + { give: { Send: anAmt } }, + { Send }, + { destAddr: 'cosmos1destAddr', chainName: 'cosmoshub' }, + ); + + await eventLoopIteration(); + await E(userSeat).hasExited(); + const payouts = await E(userSeat).getPayouts(); + t.log('Failed offer payouts', payouts); + const amountReturned = await ist.issuer.getAmountOf(payouts.Send); + t.log('Failed offer Send amount', amountReturned); + t.deepEqual(anAmt, amountReturned, 'give is returned'); + + await t.throwsAsync(vt.when(E(userSeat).getOfferResult()), { + message: + 'IBC Transfer failed "[Error: simulated unexpected MsgTransfer packet timeout]"', + }); + + t.log('ibc MsgTransfer was attempted from a local chain account'); + const history = inspectLocalBridge(); + t.like(history, [ + { type: 'VLOCALCHAIN_ALLOCATE_ADDRESS' }, + { type: 'VLOCALCHAIN_EXECUTE_TX' }, + ]); + const [_alloc, { messages, address: execAddr }] = history; + t.is(messages.length, 1); + const [txfr] = messages; + t.log('local bridge', txfr); + t.like(txfr, { + '@type': '/ibc.applications.transfer.v1.MsgTransfer', + sender: execAddr, + sourcePort: 'transfer', + token: { amount: '504', denom: 'uist' }, + }); + + t.log('deposit to and withdrawal from LCA is observed in bank bridge'); + const bankHistory = inspectBankBridge(); + t.log('bank bridge', bankHistory); + t.deepEqual( + bankHistory[bankHistory.length - 2], + { + type: 'VBANK_GIVE', + recipient: 'agoric1fakeLCAAddress', + denom: 'uist', + amount: '504', + }, + 'funds sent to LCA', + ); + t.deepEqual( + bankHistory[bankHistory.length - 1], + { + type: 'VBANK_GRAB', + sender: 'agoric1fakeLCAAddress', + denom: 'uist', + amount: '504', + }, + 'funds withdrawn from LCA in catch block', + ); +}); + +test('non-vbank asset presented is returned', async t => { + t.log('bootstrap, orchestration core-eval'); + const { bootstrap, commonPrivateArgs } = await commonSetup(t); + const vt = bootstrap.vowTools; + + const { zoe, bundleAndInstall } = await setUpZoeForTest(); + + const moolah = withAmountUtils(makeIssuerKit('MOO')); + + const installation: Installation = + await bundleAndInstall(contractFile); + const storageNode = await E(bootstrap.storage.rootNode).makeChildNode( + contractName, + ); + const sendKit = await E(zoe).startInstance( + installation, + { MOO: moolah.issuer }, + {}, + { ...commonPrivateArgs, storageNode }, + ); + + const publicFacet = await E(zoe).getPublicFacet(sendKit.instance); + const inv = E(publicFacet).makeSendInvitation(); + + const anAmt = moolah.make(10n); + const Moo = moolah.mint.mintPayment(anAmt); + const userSeat = await E(zoe).offer( + inv, + { give: { Moo: anAmt } }, + { Moo }, + { destAddr: 'cosmos1destAddr', chainName: 'cosmoshub' }, + ); + + await t.throwsAsync(vt.when(E(userSeat).getOfferResult()), { + message: + '[object Alleged: MOO brand guest wrapper] not registered in vbank', + }); + + await E(userSeat).tryExit(); + const payouts = await E(userSeat).getPayouts(); + const amountReturned = await moolah.issuer.getAmountOf(payouts.Moo); + t.deepEqual(anAmt, amountReturned, 'give is returned'); +}); + +test('rejects multi-asset send', async t => { + t.log('bootstrap, orchestration core-eval'); + const { + bootstrap, + commonPrivateArgs, + brands: { ist, bld }, + utils: { pourPayment }, + } = await commonSetup(t); + const vt = bootstrap.vowTools; + + const { zoe, bundleAndInstall } = await setUpZoeForTest(); + + const installation: Installation = + await bundleAndInstall(contractFile); + const storageNode = await E(bootstrap.storage.rootNode).makeChildNode( + contractName, + ); + const sendKit = await E(zoe).startInstance( + installation, + { BLD: bld.issuer, IST: ist.issuer }, + {}, + { ...commonPrivateArgs, storageNode }, + ); + + const publicFacet = await E(zoe).getPublicFacet(sendKit.instance); + const inv = E(publicFacet).makeSendInvitation(); + + const tenBLD = bld.make(10n); + const tenIST = ist.make(10n); + + await t.throwsAsync( + E(zoe).offer( + inv, + { give: { BLD: tenBLD, IST: tenIST } }, + { BLD: await pourPayment(tenBLD), IST: await pourPayment(tenIST) }, + { destAddr: 'cosmos1destAddr', chainName: 'cosmoshub' }, + ), + { + message: + '"send" proposal: give: Must not have more than 1 properties: {"BLD":{"brand":"[Alleged: BLD brand]","value":"[10n]"},"IST":{"brand":"[Alleged: IST brand]","value":"[10n]"}}', + }, + ); +}); diff --git a/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.md b/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.md index 506844dfddd..a835e96a709 100644 --- a/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.md +++ b/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.md @@ -36,8 +36,12 @@ Generated by [AVA](https://avajs.dev). 0: { contractState_kindHandle: 'Alleged: kind', contractState_singleton: 'Alleged: contractState', - localTransfer_kindHandle: 'Alleged: kind', - localTransfer_singleton: 'Alleged: localTransfer', + zoeTools: { + localTransfer_kindHandle: 'Alleged: kind', + localTransfer_singleton: 'Alleged: localTransfer', + withdrawToSeat_kindHandle: 'Alleged: kind', + withdrawToSeat_singleton: 'Alleged: withdrawToSeat', + }, }, }, }, diff --git a/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.snap b/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.snap index 1017ab57dbcf460571804c45ac4b9397c78be41f..fc84d7f6361077398b9303cada551421deac665c 100644 GIT binary patch literal 1118 zcmV-k1flyuRzVSI=)7MHqc!d%a%&h+~qpNgJpyT##B2NE|qk2CPJ+B#P6P z9!fRdo!DEiXRO(AOv@=3ZXgaI%8|kaPF(m0Ktk#T5C-PT=ZD?%ne7Z(>nDj+6E6-y+uOtK(GjlA&I zK+=}S>U)Eh=lY%cJFdFZb-A-ex#O`AR@v}8)@IHd^-)aISlVZ{kPee`5Els9B0w0n zsDF>!4NnY%&=-+V!4_OeYO5gfOC(Ye((W){Nvd4IgK#Y~4d;%;&<|GMjI3tw3pOfX z`TY#wWPq4pnv6U3y++#oYauVO@#2Iwh6IKEv2gl> zRvBH`xcrq(;rE1J-;E;c8@4S5T-CRPH{bIc(|l=1*wi~k@(gp}VxSjPw?S>{q_+C` z=}|=LY}WT#Pbijd`vpS&jx@ruN9=cLj|Cyqii{(-WgiIPom`98Bau>{+Fho`!3svp zacA#I(Q|!vlPbHDzD*Zyh~YL@Omgb^XK2exu}q`oPNEwgB7G29-Heq>`9=A6me%1^jFQW>FJcNbK1x0=INnH8K83(A5XUX;3KFj_x#*XSpqI%id@Hz*A@SMM5C)^sG(zIyTH&GwRgI^JP>Z z*`JEQqau(kX;m&Hs+`fMJ5BL@u|UYB(zHKC#vL*2F|Pce(Tx(YqaVRKrO|gHLgbae zSD*a9^jQh`y#%b5HJxHYN4Lak__Z2L>(mcloSo?7xd}N2403t39C@Vf3pRd-3dCS# zpj!sMEdx)=z?F)YRwQ>PVY&qyl<)Ll7rRmYKZcp#dLroLyZ8hyrx kK^)0E+G5@Z>tV_3SI=)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 From f09ce4a8669d6a0b0c1572d8d14fa86b9bcb567c Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 12 Sep 2024 12:27:21 -0400 Subject: [PATCH 4/6] feat: return failed transfer from contract localOrchAccount to seat --- .../src/examples/send-anywhere.flows.js | 4 ++- .../examples/staking-combinations.flows.js | 5 ++- .../snapshots/staking-combinations.test.ts.md | 6 ++++ .../staking-combinations.test.ts.snap | Bin 1624 -> 1672 bytes .../examples/staking-combinations.test.ts | 34 ++++++++++++++++++ 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/orchestration/src/examples/send-anywhere.flows.js b/packages/orchestration/src/examples/send-anywhere.flows.js index 71844d4ab3c..c8231d915b0 100644 --- a/packages/orchestration/src/examples/send-anywhere.flows.js +++ b/packages/orchestration/src/examples/send-anywhere.flows.js @@ -63,7 +63,9 @@ export const sendIt = async ( ); } catch (e) { await withdrawToSeat(contractState.localAccount, seat, give); - throw seat.fail(makeError(`IBC Transfer failed ${q(e)}`)); + const errorMsg = `IBC Transfer failed ${q(e)}`; + seat.exit(errorMsg); + throw makeError(errorMsg); } seat.exit(); diff --git a/packages/orchestration/src/examples/staking-combinations.flows.js b/packages/orchestration/src/examples/staking-combinations.flows.js index 2961ee96dac..c7ab71afc32 100644 --- a/packages/orchestration/src/examples/staking-combinations.flows.js +++ b/packages/orchestration/src/examples/staking-combinations.flows.js @@ -76,9 +76,8 @@ export const depositAndDelegate = async ( try { await contractState.localAccount.transfer(give.Stake, address); } catch (cause) { - // TODO, put funds back on user seat and exit - // https://github.com/Agoric/agoric-sdk/issues/9925 - throw makeError('ibc transfer failed', undefined, { cause }); + await zoeTools.withdrawToSeat(contractState.localAccount, seat, give); + throw seat.fail(makeError('ibc transfer failed', undefined, { cause })); } seat.exit(); await account.delegate(validator, give.Stake); diff --git a/packages/orchestration/test/examples/snapshots/staking-combinations.test.ts.md b/packages/orchestration/test/examples/snapshots/staking-combinations.test.ts.md index 8fa76401b61..5b0b7e27271 100644 --- a/packages/orchestration/test/examples/snapshots/staking-combinations.test.ts.md +++ b/packages/orchestration/test/examples/snapshots/staking-combinations.test.ts.md @@ -51,6 +51,8 @@ Generated by [AVA](https://avajs.dev). zoeTools: { localTransfer_kindHandle: 'Alleged: kind', localTransfer_singleton: 'Alleged: localTransfer', + withdrawToSeat_kindHandle: 'Alleged: kind', + withdrawToSeat_singleton: 'Alleged: withdrawToSeat', }, }, }, @@ -76,6 +78,8 @@ Generated by [AVA](https://avajs.dev). zoeTools: { localTransfer_kindHandle: 'Alleged: kind', localTransfer_singleton: 'Alleged: localTransfer', + withdrawToSeat_kindHandle: 'Alleged: kind', + withdrawToSeat_singleton: 'Alleged: withdrawToSeat', }, }, }, @@ -101,6 +105,8 @@ Generated by [AVA](https://avajs.dev). zoeTools: { localTransfer_kindHandle: 'Alleged: kind', localTransfer_singleton: 'Alleged: localTransfer', + withdrawToSeat_kindHandle: 'Alleged: kind', + withdrawToSeat_singleton: 'Alleged: withdrawToSeat', }, }, }, diff --git a/packages/orchestration/test/examples/snapshots/staking-combinations.test.ts.snap b/packages/orchestration/test/examples/snapshots/staking-combinations.test.ts.snap index 0419690848faed7bf50e70dc8e24ce45df0f447f..baa5d7049abe11fd6ccbe8934889dc42429bdf0e 100644 GIT binary patch literal 1672 zcmV;326y>ERzV$x2o=y96{o_1OQl`_K}CfS z@+Nk6XObuzp!ATOoPFQ>=Iy-iy*FcDm@gY#U9`_$WRC6_jM)yg9Nji)&0gS+efFZJ zQHLHH$%WB@<{wdrG67&OfY$(A1@IGqzW@vo;4uPZNi0KR)8*q#bqHyH?C$OcNrH5M zq>&||90`L&Gh}#I-J)fKmG;%khHfvEp4XjYi@K>zQd2Wn3zi%+3|3*CU&;K=T0 zDz`LdZNM=_$Rq(;<~X%and*eW8?NcR$E2v-bS$bmuI0MNlH``Uz--5&j?PWjoDZ#- zBPV#nURufXG<=$~<_31nhX5l4d>8_L4FL~_0Uib}hk@V2z~d3%NCY?&0WL&@4GsA> zR3Xau5#UAy7>WWc3Va*|LNQ=l3^*DS++(79{M{Jvehl~{2JGnoUhV+C>;U2lutNc! zRs@QEks@+L0jL7JrvN{S@PYvMTZ@uD5<+hCV_H==$68qmDvbfb-PX;D!5nV30R3%m zKT_by?bMr5>f8((L2;sPx{e3ThDB?&4XM<}P>>m<*BERQNMNy8_VUV%!z~sxjQos4 z9Twy*QiK%ZD;1DisWwZm}iE0WW0U;Ef4xP1hZj zSJ|ADt$5vya*EbkPkP&-H5gr-qBVC5R_#dn@dc`zllAhFA_6N;a9>Apex$&&eVkXz zy2-RX<_X<#JI$24V?hIYWX7S3x>;G`bBXLtaHa^c<7@Xo-;#Z`W;0hOsLBF2^Ios7 zw0XI06IU(aY*A=f+a~{^_wKYE)fh z&ayFy=3{>e?z4DRx7k7Js0+ckh-HG+DQI0uOcqQ=P!^PGPcki16_1oxu4{;JOGMkeKrcU?c%(3E=&N z0MCo?(9a3rwg?-PhIiOa^%GqysqWEOjoZ32W@@{b!79{Q5(@QT5>>j$maE7$p7R)c z>!!BsIA&_I7B%hp=1fb1goL!kXoXw4+WNQD&XZVW%e_M!8mikc7V72JlWlDVsZ!b$ ztEhKm&(85ypfk+Wn6;VKpqJJC%&r^GW{W@~keX?}?Jr{8_aahNu9WS@-bf+g&%1!j zUBK-wK|P8j-)s`dCxMxy08L6zo&@Si;1dzrBWY4pNsb0MOQs04u#^O|Qou6Gq0dEbv(txRV7kIpAmxxRL{uUO?*=D00%Ch=-Pe)}7NmYX?3J z_`Rii?`u2zfbK4z>0R07X!Cq+3*`4VyY%vz^q(F{N7v($!rsxQTE|U1JvX7dPdXMA zfI&mOZ%ra8&TYOpSMzaq`U}raE2lYIIR@Q3YPhwfhP$MO2m0=PH5~5SQp4R*!<(zC zK_MMq^#RxVfJ{F?`+;}*fgk#T(E(w|B*ZxteQ^LdI{;i60HS$dS6*=G5?wmZ<$*Wy zz;}6IbP%9}z@sV1E*;~a9`(ZLO*g#!&@5O((smsw>13!YM2?c%#4%8)^WL1rj-hQx? literal 1624 zcmV-e2B-N!RzVZt$XPfnowYzTG9xC-vkbnc6%8j;v!?fNucY2JjbvQ35t#c?7m6?G&WT0c)TXGZ8LSCQlnjGanD%_JVG}qq z2bjt&jaln(OcF9hfTlS{?K7r2Y49c2bkSo{RBk#JRUOxI(_=|+OI={LV^K%vrfbfJ z+UCe9zGN@2*~b zKcNayeux0KBEV=Dz`B4ByMRy>7>@$Sqk?->bdS9o1+GSc`%z%J8+g4NxX}&76kwYI zyr2jaLn1}wm;z7*xTF9-iSU8|_ghCKeZ+;@=1W>tH%rYb1(illaJO}{VlaoBO~6pg z+m94@vb*#~lsY$qMldp2H(kd==8{EgwRNe~$54rV5 zLmd|6Es}&3V(kvdF85m9RQJ#dvyRY3W-a&0a~@Q>g_@+gQMcGKtesamnR8|4hGHJ=Q$LR(^Vafa616m;B?@)HYGH>c|5Wkm#5oZ!BO;`~T~XX^y7 zmUWY9)8;ANad(;-H)BBqdV0>Gi@I4^=5vYc4R9t2v16<6fxaaNYmLoZoun!YyqOPp zeWm1OTdp3MTSi5E?FnvIxm}#~##t<>DzBSPaf%!3n-@OL?fbY&ja4W&Ft`dXpDTk& zs!}a*PY$(1;ep>~x^eyQImwwFh{z2bk#rPWA|swOfRT-|hh}_5e3UXij3z$APgppv8f! zaRHte;h|sSz&#N*EDi6lTk4&??Naw^tj2BKDVf@CX0Qr%mW4t+n0S>gvXw4!jc`9l5Zvfyk0yZ=A}lGvtR!$+gk_{c5I3whJYkU?9>e#m z8PX}gK_Rh&Dd3e9@J&ZG0i{0Ry*}VhA25{`qUn+DJ>@j8kOr=$fjem++z-t5 z1E2H*5Bh<_8G#}tQPeWPsSNOG26&JG(plhm7Wgs?C3Yv~rfS_A%(uQN!&`H9RCWj14~eYS=rtsfLH6h6|n5 zppfp52Z7H9f%}8N^bl}i2)Ho>Byz%#iA#qiH3#T9;C&I=D?+<}$pLqBz|K7IY99D1 z4@8E6y~BcQpR~%QVc^;@a9f0?rB${UfE@+EE&yK_fWHesX#{v}L~tFDR{3!R_;~~v z9~Gd3(rk?i)f$zrB7twr9Vnzy7g`LR?mqtSNjx{7z?W37?;78e_%2sIhGNI7RnR!b zIMaMi-#Gs%#70`%@bI$Oe+;y WVMRLcFstbPJo^td|1&qW9RL6d>i?nu diff --git a/packages/orchestration/test/examples/staking-combinations.test.ts b/packages/orchestration/test/examples/staking-combinations.test.ts index a7540dd8a0c..7a9c61ac010 100644 --- a/packages/orchestration/test/examples/staking-combinations.test.ts +++ b/packages/orchestration/test/examples/staking-combinations.test.ts @@ -12,6 +12,7 @@ import { MsgTransferResponse, } from '@agoric/cosmic-proto/ibc/applications/transfer/v1/tx.js'; import { IBCMethod } from '@agoric/vats'; +import { SIMULATED_ERRORS } from '@agoric/vats/tools/fake-bridge.js'; import { protoMsgMocks, UNBOND_PERIOD_SECONDS } from '../ibc-mocks.js'; import { commonSetup } from '../supports.js'; import { @@ -205,4 +206,37 @@ test('start', async t => { // snapshot the resulting contract baggage const tree = inspectMapStore(contractBaggage); t.snapshot(tree, 'contract baggage after start'); + + { + t.log('payments from failed transfers are returned'); + const bldAmt = bld.make(SIMULATED_ERRORS.TIMEOUT); + + const seat = await E(zoe).offer( + await E(result.invitationMakers).DepositAndDelegate(), + { + give: { Stake: bldAmt }, + exit: { waived: null }, + }, + { + Stake: await pourPayment(bldAmt), + }, + { + validator: { + chainId: 'cosmoshub', + value: 'cosmosvaloper1test', + encoding: 'bech32', + }, + }, + ); + await t.throwsAsync(vt.when(E(seat).getOfferResult()), { + message: 'ibc transfer failed', + }); + await vt.when(E(seat).hasExited()); + const payouts = await E(seat).getPayouts(); + t.deepEqual( + await bld.issuer.getAmountOf(payouts.Stake), + bldAmt, + 'Stake is returned', + ); + } }); From 41fbac04262f2b035dfee31e6b323a84c215c541 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 19 Sep 2024 14:59:11 -0400 Subject: [PATCH 5/6] feat: `zoeTools` uses `asVow` - `retriable` does not seem to be a good fit for these functions as they are not idempotent --- .../orchestration/src/utils/start-helper.js | 4 +- packages/orchestration/src/utils/zoe-tools.js | 58 ++++++++++-------- .../snapshots/send-anywhere.test.ts.md | 1 - .../snapshots/send-anywhere.test.ts.snap | Bin 1118 -> 1102 bytes .../snapshots/staking-combinations.test.ts.md | 1 - .../staking-combinations.test.ts.snap | Bin 1672 -> 1664 bytes .../snapshots/unbond.contract.test.ts.md | 1 - .../snapshots/unbond.contract.test.ts.snap | Bin 987 -> 981 bytes 8 files changed, 34 insertions(+), 31 deletions(-) diff --git a/packages/orchestration/src/utils/start-helper.js b/packages/orchestration/src/utils/start-helper.js index f11da8db32c..e86cb6e6b0a 100644 --- a/packages/orchestration/src/utils/start-helper.js +++ b/packages/orchestration/src/utils/start-helper.js @@ -63,8 +63,6 @@ export const provideOrchestration = ( orchestration: zone.subZone('orchestration'), /** system names for vows */ vows: zone.subZone('vows'), - /** system names for zoe */ - zoe: zone.subZone('zoe'), /** contract-provided names, and subzones */ contract: zone.subZone('contract'), }; @@ -76,7 +74,7 @@ export const provideOrchestration = ( const chainHub = makeChainHub(agoricNames, vowTools); - const zoeTools = makeZoeTools(zones.zoe, { zcf, vowTools }); + const zoeTools = makeZoeTools(zcf, vowTools); const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller); const makeLocalOrchestrationAccountKit = prepareLocalOrchestrationAccountKit( diff --git a/packages/orchestration/src/utils/zoe-tools.js b/packages/orchestration/src/utils/zoe-tools.js index 59306435247..e81a1136e3c 100644 --- a/packages/orchestration/src/utils/zoe-tools.js +++ b/packages/orchestration/src/utils/zoe-tools.js @@ -1,3 +1,20 @@ +/** + * @file Helper functions for transferring payments between a LocalChainAccount + * and a ZCFSeat. + * + * Maintainers: This exists as an endowment for orchestrated async-flows so we + * can make use of E and promises. The logic for recovering partial failures + * is also an added convenience for developers. + * + * Functions are written using `asVow` and non-resumable promises as we expect + * each invocation to resolve promptly - there are no timers or interchain + * network calls. + * + * A promise resolved promptly is currently safe from being severed by an + * upgrade because we only trigger vat upgrades as the result of network + * input. + */ + import { makeError, q, Fail } from '@endo/errors'; import { depositToSeat } from '@agoric/zoe/src/contractSupport/index.js'; import { E } from '@endo/far'; @@ -5,10 +22,10 @@ import { E } from '@endo/far'; const { assign, keys, values } = Object; /** + * @import {HostOf} from '@agoric/async-flow'; * @import {InvitationMakers} from '@agoric/smart-wallet/src/types.js'; * @import {ResolvedPublicTopic} from '@agoric/zoe/src/contractSupport/topics.js'; * @import {VowTools} from '@agoric/vow'; - * @import {Zone} from '@agoric/zone'; * @import {LocalAccountMethods} from '../types.js'; */ @@ -38,25 +55,19 @@ const { assign, keys, values } = Object; */ /** - * @param {Zone} zone - * @param {{ zcf: ZCF; vowTools: VowTools }} io + * @param {ZCF} zcf + * @param {VowTools} vowTools */ -export const makeZoeTools = ( - zone, - { zcf, vowTools: { retriable, when, allVows, allSettled } }, -) => { +export const makeZoeTools = (zcf, { when, allVows, allSettled, asVow }) => { /** * Transfer the `amounts` from `srcSeat` to `localAccount`. If any of the * deposits fail, everything will be rolled back to the `srcSeat`. Supports * multiple items in the `amounts` {@link AmountKeywordRecord}. + * + * @type {HostOf} */ - const localTransfer = retriable( - zone, - 'localTransfer', - /** - * @type {LocalTransfer} - */ - async (srcSeat, localAccount, amounts) => { + const localTransfer = (srcSeat, localAccount, amounts) => + asVow(async () => { !srcSeat.hasExited() || Fail`The seat cannot have exited.`; const { zcfSeat: tempSeat, userSeat: userSeatP } = zcf.makeEmptySeatKit(); const userSeat = await userSeatP; @@ -114,21 +125,18 @@ export const makeZoeTools = ( throw makeError(`One or more deposits failed ${q(errors)}`); } // TODO #9541 remove userSeat from baggage - }, - ); + }); /** * Transfer the `amounts` from a `localAccount` to the `recipientSeat`. If any * of the withdrawals fail, everything will be rolled back to the * `srcLocalAccount`. Supports multiple items in the `amounts` - * {@link PaymentKeywordRecord}. + * {@link PaymentKeywordRecord} + * + * @type {HostOf} */ - const withdrawToSeat = retriable( - zone, - 'withdrawToSeat', - /** @type {WithdrawToSeat} */ - async (localAccount, destSeat, amounts) => { - await null; + const withdrawToSeat = (localAccount, destSeat, amounts) => + asVow(async () => { !destSeat.hasExited() || Fail`The seat cannot have exited.`; const settledWithdrawals = await when( @@ -167,12 +175,12 @@ export const makeZoeTools = ( paymentKwr, ); console.debug(depositResponse); - }, - ); + }); return harden({ localTransfer, withdrawToSeat, }); }; + /** @typedef {ReturnType} ZoeTools */ diff --git a/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.md b/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.md index a835e96a709..11710ddf60d 100644 --- a/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.md +++ b/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.md @@ -68,5 +68,4 @@ Generated by [AVA](https://avajs.dev). VowInternalsKit_kindHandle: 'Alleged: kind', WatchUtils_kindHandle: 'Alleged: kind', }, - zoe: {}, } diff --git a/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.snap b/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.snap index fc84d7f6361077398b9303cada551421deac665c..bbf9e53542d64bc4c6152de2241123e5fe71373e 100644 GIT binary patch literal 1102 zcmV-U1hM-;RzVSI=)7MHqc!ukH1(*d_^0S_=$^f)JESa6(8`l42#$k|;?N zaR4;lo!E=VGuG@F6AqPnrVu(P_TsnRK-VxKD=B(SH-7f7i&lhy)_oVjk9l0*m^u6khEDD(Xh)oKs z0M-HA2k;Ys-vB%TuuOn?l5ddwX6Ga3XhLR>^?Dto3X%t@VV-0bNERg5AkW+!Dca$z zwKeK+&+oO~@$`p%Pq^z;xSWNs>I&zq%iK3wlbE3~zs($}T&B)JoFn8_0)*ir^$&&9 z=5ic_?nXkkBZXGf(Lv;gNTe#2(__9?RC`hc;m47%^|2iLlR8yi$N;Zo0GTo8-T;fNw0|7Yn$W13t(BU*-&2-lP>D&P>owmPA z2Hp2WmoqIz2)Hz3$B~B4!m2mfr6)z&9W>h`5fJZ)v7*D_xvUmbxRO*zq;YvBkN}&G zza_gnS}K-y8_PSIYL+I8IYM5uPt3hAJuoBTyhXcA?bAL}et_DHpc*q&n|get*c38| zLJqhbx24(~X-5v&UZPvYQLxL~bQr!_oNaB)X#X}H1|rqdvk2+IFq;{M^ zKf`fxq@nZF4ij!`tDScJM#xJnT%Xa7Awgk3vroUBsvx!@e{D_r1L?OmqsZ2ZgA&DrVe#eTm90>R%GgIn%it3HA}br93kID8c}p6 z?04ya1))cgknMGxT`BqWSo|A_l!w&mGkqMaV)8of>@6h+p3nBFc6#aCba_vXH-u(N zQ0`x#EsMdrceKa-b5QN+q{yu(MGr|fe5%!IvsgGqEMw@zqd9XVWv&*1my5vZRoQbt82T83jctq z$(d&ABB#acINQu0aeYx=Bm@#y&+?3KV#BNrqnPp!d%pyHQ38H8t86E#oH3_6 zP4Rs(N5~(M{*;(-<#@n^4nEv6wKA|+HnvbarO_{zPlU+zz*qZa;Qcc2bs6~G)F~x& zOiL_b%7 literal 1118 zcmV-k1flyuRzVSI=)7MHqc!d%a%&h+~qpNgJpyT##B2NE|qk2CPJ+B#P6P z9!fRdo!DEiXRO(AOv@=3ZXgaI%8|kaPF(m0Ktk#T5C-PT=ZD?%ne7Z(>nDj+6E6-y+uOtK(GjlA&I zK+=}S>U)Eh=lY%cJFdFZb-A-ex#O`AR@v}8)@IHd^-)aISlVZ{kPee`5Els9B0w0n zsDF>!4NnY%&=-+V!4_OeYO5gfOC(Ye((W){Nvd4IgK#Y~4d;%;&<|GMjI3tw3pOfX z`TY#wWPq4pnv6U3y++#oYauVO@#2Iwh6IKEv2gl> zRvBH`xcrq(;rE1J-;E;c8@4S5T-CRPH{bIc(|l=1*wi~k@(gp}VxSjPw?S>{q_+C` z=}|=LY}WT#Pbijd`vpS&jx@ruN9=cLj|Cyqii{(-WgiIPom`98Bau>{+Fho`!3svp zacA#I(Q|!vlPbHDzD*Zyh~YL@Omgb^XK2exu}q`oPNEwgB7G29-Heq>`9=A6me%1^jFQW>FJcNbK1x0=INnH8K83(A5XUX;3KFj_x#*XSpqI%id@Hz*A@SMM5C)^sG(zIyTH&GwRgI^JP>Z z*`JEQqau(kX;m&Hs+`fMJ5BL@u|UYB(zHKC#vL*2F|Pce(Tx(YqaVRKrO|gHLgbae zSD*a9^jQh`y#%b5HJxHYN4Lak__Z2L>(mcloSo?7xd}N2403t39C@Vf3pRd-3dCS# zpj!sMEdx)=z?F)YRwQ>PVY&qyl<)Ll7rRmYKZcp#dLroLyZ8hyrx kK^)0E+G5@Z>tV_3n3y9(8K|97i9xGM4)oYwDxJ2JVS zk*11G<{yg)00000000B+SxamiRTMqX-?0?H1o3I+9p5K{RFl%`EnXld0X ztpEvl9=|x#jORr&&vjb~L|If_umA*f2WmHkZKZDbR#aV4A+^g2Bvc`U5LAc-3l@Mp zi9hou37G%|q-5pzo_prLd(XY^z47^(lFrpR>+}UCG@&zQ32F+>GU&WD%Y}9NLb*%@ zeR}9CKf-AI5ru?z0hj>rDu8PMz6S6sfE)o*B$OnfgQa6k6@;`t_Vo0C#6UtI38aWG zP5dCiBpKRWGiga@#Y44{u359iCpGc>oMx0Ks8QBg6BZlQbyi{Jhl>rH#F0A8RBo1; zxeCWPA=?SiG)Jg)(on~BUbjszb(j>D8^WZjuq_{VSYq5%XPG5TDl~4`X879Xz;Rx; z8hPs8?gJk50m27-30XzM`89(r;ANb7=Yz_c>1HiF>lslg@cM4I?2Y`zK;M)MO zEeOGgD2&$( z+i}{cn{)r^T+X+aU56(_l`pg1Q|;@LFDt0m1~<$cC+P1s}OfSoa~ z0d1QUbWSrW3w$<_wFb^OA&*9uA83w8X674{SskY;^E{dRI(=QqOSfD*V0IZ5dD&y! zs&cD%urto$sH*asA&L`RU)}I`aE|QfD%F>vT*EGuxA|-tj8m1CJ@;gPI~4ADHHE{h z$_4ZG_&6bR9Wsch4ezIDmD#4ZJ4}jJQl~iA7y9>Fhox(ts&h;%8e?dD?QFqACa-E1 zJ4%H*>%B|^M|gdoA(&}U-C9Fg`t0XU3r$~5V<-z-T5!}f=}F>meo7<~En9QD3rM@8 z(;9T`oj*i?pCdpf3Oo>%_Rg>j_dgQ_UWfwk%FwKf`F0feE(&bz0*o#Ro|ECe54wOW zGHk%LdHd~B|K8Oub#Ix?b4wGWMtL{WS%r!Pp^S8Q$e~K-*kTvi#_0}YU(F~lI*uCU zDU%x3Ok+=rf%v4dg=mGFn%exg*s7Dz-ZWHuV9eG^&2Kie7+jswqF6$`!~1rP zH3OYwMwyvw84X%VJdJQ8u5LvN@xR;+oaqLxbW7?{ zT=M<08@SyK6k-xI=7R2w0guIi6Ed{drAb*OY3kuD87Jh;SX+!yW|Voo$_!!sE!-de zgWR{5<5q|-5(g4-DRmK7*Kdpi_sX!i3-(AH7?oiuS0RWU)>OP57TM8ZIJcbPmSj*! zWH142N&qwgTucC;B!FlSp!Wcmdw^t8iYDs1+1!@|?oR?d30zJB*ONfD7dX`mT6New^!57e+Jv#y4_qlV`)OR7O3;ny?3 zxeRbC1B~gr{@BvpL|M9B?NG zj0^z!0B~tQa_wuI?HFkApi1>jNvxKjXj3ham@YayzlKIou$%F1Iiszg35)>{8bXo=$$f>_gihqThSxQ`uHkhJuWR_<)iBd(iRmW_&Es;hL@O0qVMRJ4m|3)c6#oMZ K9V8hm9{>P+O%Bum literal 1672 zcmV;326y>ERzV$x2o=y96{o_1OQl`_K}CfS z@+Nk6XObuzp!ATOoPFQ>=Iy-iy*FcDm@gY#U9`_$WRC6_jM)yg9Nji)&0gS+efFZJ zQHLHH$%WB@<{wdrG67&OfY$(A1@IGqzW@vo;4uPZNi0KR)8*q#bqHyH?C$OcNrH5M zq>&||90`L&Gh}#I-J)fKmG;%khHfvEp4XjYi@K>zQd2Wn3zi%+3|3*CU&;K=T0 zDz`LdZNM=_$Rq(;<~X%and*eW8?NcR$E2v-bS$bmuI0MNlH``Uz--5&j?PWjoDZ#- zBPV#nURufXG<=$~<_31nhX5l4d>8_L4FL~_0Uib}hk@V2z~d3%NCY?&0WL&@4GsA> zR3Xau5#UAy7>WWc3Va*|LNQ=l3^*DS++(79{M{Jvehl~{2JGnoUhV+C>;U2lutNc! zRs@QEks@+L0jL7JrvN{S@PYvMTZ@uD5<+hCV_H==$68qmDvbfb-PX;D!5nV30R3%m zKT_by?bMr5>f8((L2;sPx{e3ThDB?&4XM<}P>>m<*BERQNMNy8_VUV%!z~sxjQos4 z9Twy*QiK%ZD;1DisWwZm}iE0WW0U;Ef4xP1hZj zSJ|ADt$5vya*EbkPkP&-H5gr-qBVC5R_#dn@dc`zllAhFA_6N;a9>Apex$&&eVkXz zy2-RX<_X<#JI$24V?hIYWX7S3x>;G`bBXLtaHa^c<7@Xo-;#Z`W;0hOsLBF2^Ios7 zw0XI06IU(aY*A=f+a~{^_wKYE)fh z&ayFy=3{>e?z4DRx7k7Js0+ckh-HG+DQI0uOcqQ=P!^PGPcki16_1oxu4{;JOGMkeKrcU?c%(3E=&N z0MCo?(9a3rwg?-PhIiOa^%GqysqWEOjoZ32W@@{b!79{Q5(@QT5>>j$maE7$p7R)c z>!!BsIA&_I7B%hp=1fb1goL!kXoXw4+WNQD&XZVW%e_M!8mikc7V72JlWlDVsZ!b$ ztEhKm&(85ypfk+Wn6;VKpqJJC%&r^GW{W@~keX?}?Jr{8_aahNu9WS@-bf+g&%1!j zUBK-wK|P8j-)s`dCxMxy08L6zo&@Si;1dzrBWY4pNsb0MOQs04u#^O|Qou6Gq0dEbv(txRV7kIpAmxxRL{uUO?*=D00%Ch=-Pe)}7NmYX?3J z_`Rii?`u2zfbK4z>0R07X!Cq+3*`4VyY%vz^q(F{N7v($!rsxQTE|U1JvX7dPdXMA zfI&mOZ%ra8&TYOpSMzaq`U}raE2lYIIR@Q3YPhwfhP$MO2m0=PH5~5SQp4R*!<(zC zK_MMq^#RxVfJ{F?`+;}*fgk#T(E(w|B*ZxteQ^LdI{;i60HS$dS6*=G5?wmZ<$*Wy zz;}6IbP%9}z@sV1E*;~a9`(ZLO*g#!&@5O((smsw>13!YM2?c%#4%8)^WL1rj-hQx? diff --git a/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.md b/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.md index 82a7b2f488e..1a7163c5994 100644 --- a/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.md +++ b/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.md @@ -57,5 +57,4 @@ Generated by [AVA](https://avajs.dev). VowInternalsKit_kindHandle: 'Alleged: kind', WatchUtils_kindHandle: 'Alleged: kind', }, - zoe: {}, } diff --git a/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.snap b/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.snap index 90478398c1d52da9fe00bc1335147e5e5807a195..fbf713b176456c4563af10a276dffac5d86a43b3 100644 GIT binary patch literal 981 zcmV;`11kJMRzVRy~g!MHqf&yH*Qub_tMLwr)( z1+Wj`V*oz@_zl2+0JaFQLA-V19dzI0%n&kvU0YiNX@YnltJok;i&Q|W>*Sf&5=FZ) z53VNNIMRdQjmW$+j6}FkMHusp)@;Wy@AL3tFvTp5jYH0)3b|Us@H8PW5Fmr=RNoP- z6U#{oeVBuMCWTRynH2dphcu;P1FnstCXynBALd~9ww&l`oKEdHz;g}|I>0v$@P`AO zuK>3zz;_kk?+VazftOw2mTSe%S&W@e+)rKLGZ%Q^0{_@_&!*R3tO6ISz$aDUmnyL1 z0dIT2=N|CA2mIt&67|B04?W;t4|u5tBsGiPEF^cuO3YR}97Up?g}7{9pSH|(B>FKo zQe=cPbL&N>rPJDtrd~!;l=Wb{lZe#tx|k?B9xqj53kv6o3WaT)oeLDmcBNmI{Uak4 zFYCtEk)ei{*~Mu>?$?fsx;1N*M6lOspR1d6$dyi6Kjv8Lb5@5&aiaK)@@JmXi{+#v z)j?vI9Pt~4Y}J$0%PVx8{n?zmHsN%5g^tq@q!m|5^?m=z&AFic)Ob2&ScfuRddh6( zZ3{VU%}sN()J5OYd45mok<`IK-p`=Tm`sET_GP@>AY0J9a9J`MKSA;Y6ZKrGw>O}X z=uk$(()D)c*cDlNH`|ANBn>aO{B*GA%An4s)^E}gPoY~m$d9`0nw0UZHb2TCD`U!r z+<j<+a#GSCt$^n%|^`4a%qK> zLL@$iydR4Q00000000A>R!wgkMHqf&cWv)F&6kq2q)EXQLI@apFVtREb}Jkk?szJ=-F0d|J=* zywAMze$9N{X$!^r@q@?QL`HBP8>&qdOFE2u%ES*Ihaole?$VBn8pc236Tb!E0Kn$} zeg^OtfPVp8A;4P%*dWz)Qax;c#F-&v`nk5Y2C@uN16joeaW;tyQduW2zL#j)7QAsQ zX^SZCHEu-aUO$rIK9!;18Ev^K1n=_joyHimG&YVnQ##~&0mBtS_6d-|0gdlV))H!z zLXUEg&y+NpGLs@d=8$EjS&zp?Qxhqf!hth`y-(CAPFLT~t-7BnJ`Ui869@Rv0lsvA zKONwT3*2*oZ(ZPT7kI@3Zg_z7fQOzHP-i}%KJk9?fL}b|sRz7PvFKHs9`q}~#}(j* z3b0iLPO88cRp8eu@J|(}*DQ&ku;Z&W;Efs}YQQ%(ec7VVqbr4aSh8}t-3$kjY-U+0 zTM92&=5Zvuf*U0>!lkM8Jk!$IycUhUjFc?v!EP&&so@e`6Y{!KbeLy%USo9e&oKfj;nv@Z_F zgNd~$NYmI?VzJpbr}_GUVpKdw z@*G2WC)L~Q(MYx^qhaZKyEJn}l|Iex5g#bS%Wc0xNGDeY0UKL?KnFa99_AoFYO~u) ziAimKpF@_0l=Zne11IG*-$u8z8bmQarH1v&r|I&E8eNx$Ye_|Xj|4Wm;sZ~8>$a%bGL8r~A9#zsdWFJu3VdVA^mwY|*GV=hCk&(q9Nn;r2u5$3#QTM#IWzwN2L&CT*) z_0g=+7CVtooZsre?{?1pXXSkU_NNQKKe(W6E Date: Thu, 19 Sep 2024 17:38:33 -0400 Subject: [PATCH 6/6] refactor: prefer seat.exit() to seat.fail() - discussion: https://github.com/Agoric/agoric-sdk/pull/9902#discussion_r1767377401 --- .../src/examples/staking-combinations.flows.js | 6 ++++-- .../test/examples/staking-combinations.test.ts | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/orchestration/src/examples/staking-combinations.flows.js b/packages/orchestration/src/examples/staking-combinations.flows.js index c7ab71afc32..37712097096 100644 --- a/packages/orchestration/src/examples/staking-combinations.flows.js +++ b/packages/orchestration/src/examples/staking-combinations.flows.js @@ -8,7 +8,7 @@ */ import { mustMatch } from '@endo/patterns'; -import { makeError } from '@endo/errors'; +import { makeError, q } from '@endo/errors'; import { makeTracer } from '@agoric/internal'; import { ChainAddressShape } from '../typeGuards.js'; @@ -77,7 +77,9 @@ export const depositAndDelegate = async ( await contractState.localAccount.transfer(give.Stake, address); } catch (cause) { await zoeTools.withdrawToSeat(contractState.localAccount, seat, give); - throw seat.fail(makeError('ibc transfer failed', undefined, { cause })); + const errMsg = makeError(`ibc transfer failed ${q(cause)}`); + seat.exit(errMsg); + throw errMsg; } seat.exit(); await account.delegate(validator, give.Stake); diff --git a/packages/orchestration/test/examples/staking-combinations.test.ts b/packages/orchestration/test/examples/staking-combinations.test.ts index 7a9c61ac010..830f11a4042 100644 --- a/packages/orchestration/test/examples/staking-combinations.test.ts +++ b/packages/orchestration/test/examples/staking-combinations.test.ts @@ -229,7 +229,8 @@ test('start', async t => { }, ); await t.throwsAsync(vt.when(E(seat).getOfferResult()), { - message: 'ibc transfer failed', + message: + 'ibc transfer failed "[Error: simulated unexpected MsgTransfer packet timeout]"', }); await vt.when(E(seat).hasExited()); const payouts = await E(seat).getPayouts();