Skip to content

Commit

Permalink
feat(sendAnywhere): handle failed IBC transfer
Browse files Browse the repository at this point in the history
  • Loading branch information
0xpatrickdev committed Sep 12, 2024
1 parent 5ec37ef commit ff9c7d0
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const contract = async (
const orchFns = orchestrateAll(flows, {
zcf,
contractState,
localTransfer: zoeTools.localTransfer,
zoeTools,
});

const publicFacet = zone.exo(
Expand Down
29 changes: 18 additions & 11 deletions packages/orchestration/src/examples/send-anywhere.flows.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { NonNullish } from '@agoric/internal';
import { Fail, 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';
*/
Expand All @@ -17,13 +18,13 @@ const { entries } = Object;
* @param {Orchestrator} orch
* @param {object} ctx
* @param {{ localAccount?: OrchestrationAccountI & LocalAccountMethods }} ctx.contractState
* @param {GuestOf<ZoeTools['localTransfer']>} ctx.localTransfer
* @param {GuestInterface<ZoeTools>} ctx.zoeTools
* @param {ZCFSeat} seat
* @param {{ chainName: string; destAddr: string }} offerArgs
*/
export const sendIt = async (
orch,
{ contractState, localTransfer },
{ contractState, zoeTools: { localTransfer, withdrawToSeat } },
seat,
offerArgs,
) => {
Expand Down Expand Up @@ -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(Fail`IBC Transfer failed ${q(e)}`);
}

seat.exit();
};
harden(sendIt);
194 changes: 189 additions & 5 deletions packages/orchestration/test/examples/send-anywhere.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -229,10 +229,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;
Expand All @@ -253,3 +251,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<StartFn> =
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 vt.when(E(userSeat).tryExit());
const payouts = await vt.when(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.true(AmountMath.isEqual(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<StartFn> =
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 vt.when(E(userSeat).tryExit());
const payouts = await E(userSeat).getPayouts();
const amountReturned = await moolah.issuer.getAmountOf(payouts.Moo);
t.true(AmountMath.isEqual(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<StartFn> =
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]"}}',
},
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
},
},
},
Expand Down
Binary file not shown.

0 comments on commit ff9c7d0

Please sign in to comment.