From 00f86fd68967f8344982dafdeff90b25aa28addd Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 15 Aug 2024 17:25:58 -0400 Subject: [PATCH] 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