Skip to content

Commit

Permalink
feat: zoeTools.withdrawToSeat
Browse files Browse the repository at this point in the history
- retriable that withdraws payments (described by an IssuerKeywordRecord) from a LCA to a user seat
  • Loading branch information
0xpatrickdev committed Sep 19, 2024
1 parent d8a78d4 commit 55abd1d
Show file tree
Hide file tree
Showing 5 changed files with 778 additions and 22 deletions.
142 changes: 120 additions & 22 deletions packages/orchestration/src/utils/zoe-tools.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { Fail } from '@endo/errors';
import { atomicTransfer } from '@agoric/zoe/src/contractSupport/index.js';
import { makeError, q, Fail } from '@endo/errors';
import {
atomicTransfer,
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';
*/

Expand All @@ -24,60 +28,154 @@ import { E } from '@endo/far';
* @typedef {(
* srcSeat: ZCFSeat,
* localAccount: LocalAccountMethods,
* give: AmountKeywordRecord,
* amounts: AmountKeywordRecord,
* ) => Promise<void>} LocalTransfer
*/

/**
* @typedef {(
* srcLocalAccount: LocalAccountMethods,
* recipientSeat: ZCFSeat,
* amounts: AmountKeywordRecord,
* ) => Promise<void>} 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);
atomicTransfer(zcf, 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 () => {
// const { zcfSeat: tempSeat, userSeat: userSeatP } =
// 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 => 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 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 (srcLocalAccount, recipientSeat, amounts) => {
await null;
!recipientSeat.hasExited() || Fail`The seat cannot have exited.`;

const settledWithdrawals = await when(
allSettled(values(amounts).map(amt => srcLocalAccount.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(srcLocalAccount.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,
recipientSeat,
amounts,
paymentKwr,
);
console.debug(depositResponse);
},
);

return harden({
localTransfer,
withdrawToSeat,
});
};
/** @typedef {ReturnType<typeof makeZoeTools>} ZoeTools */
112 changes: 112 additions & 0 deletions packages/orchestration/test/fixtures/zoe-tools.contract.js
Original file line number Diff line number Diff line change
@@ -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<LocalChain>;
* orchestrationService: Remote<CosmosInterchainService>;
* storageNode: Remote<StorageNode>;
* timerService: Remote<TimerService>;
* agoricNames: Remote<NameHub>;
* }} 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<any> | 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);
Loading

0 comments on commit 55abd1d

Please sign in to comment.