Skip to content

Commit

Permalink
fix(pegasus): more POLA and less global state
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelfig committed Feb 4, 2022
1 parent c703d07 commit e7ea320
Show file tree
Hide file tree
Showing 5 changed files with 470 additions and 399 deletions.
124 changes: 124 additions & 0 deletions packages/pegasus/src/courier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { details as X } from '@agoric/assert';

import { AmountMath } from '@agoric/ertp';
import { E } from '@agoric/eventual-send';
import { Far } from '@agoric/marshal';
import { makeOncePromiseKit } from './once-promise-kit.js';

/**
* Create or return an existing courier promise kit.
*
* @template K
* @param {K} key
* @param {Store<K, PromiseRecord<Courier>>} keyToCourierPK
*/
export const getCourierPK = (key, keyToCourierPK) => {
if (keyToCourierPK.has(key)) {
return keyToCourierPK.get(key);
}

// This is the first packet for this denomination.
// Create a new Courier promise kit for it.
const courierPK = makeOncePromiseKit(() => X`${key} already pegged`);

keyToCourierPK.init(key, courierPK);
return courierPK;
};

/**
* Create the [send, receive] pair.
*
* @typedef {Object} CourierArgs
* @property {ContractFacet} zcf
* @property {ERef<BoardDepositFacet>} board
* @property {ERef<NameHub>} namesByAddress
* @property {Denom} remoteDenom
* @property {Brand} localBrand
* @property {(zcfSeat: ZCFSeat, amounts: AmountKeywordRecord) => void} retain
* @property {(zcfSeat: ZCFSeat, amounts: AmountKeywordRecord) => void} redeem
* @property {ERef<TransferProtocol>} transferProtocol
* @param {ERef<Connection>} connection
* @returns {(args: CourierArgs) => Courier}
*/
export const makeCourierMaker = connection => ({
zcf,
board,
namesByAddress,
remoteDenom,
localBrand,
retain,
redeem,
transferProtocol,
}) => {
/** @type {Sender} */
const send = async (zcfSeat, depositAddress) => {
const tryToSend = async () => {
const amount = zcfSeat.getAmountAllocated('Transfer', localBrand);
const transferPacket = await E(transferProtocol).makeTransferPacket({
value: amount.value,
remoteDenom,
depositAddress,
});

// Retain the payment. We must not proceed on failure.
retain(zcfSeat, { Transfer: amount });

// The payment is already escrowed, and proposed to retain, so try sending.
return E(connection)
.send(transferPacket)
.then(ack => E(transferProtocol).assertTransferPacketAck(ack))
.then(
_ => zcfSeat.exit(),
reason => {
// Return the payment to the seat, if possible.
redeem(zcfSeat, { Transfer: amount });
throw reason;
},
);
};

// Reflect any error back to the seat.
return tryToSend().catch(reason => {
zcfSeat.fail(reason);
});
};

/** @type {Receiver} */
const receive = async ({ value, depositAddress }) => {
const localAmount = AmountMath.make(localBrand, value);

// Look up the deposit facet for this board address, if there is one.
/** @type {DepositFacet} */
const depositFacet = await E(board)
.getValue(depositAddress)
.catch(_ => E(namesByAddress).lookup(depositAddress, 'depositFacet'));

const { userSeat, zcfSeat } = zcf.makeEmptySeatKit();

// Redeem the backing payment.
try {
redeem(zcfSeat, { Transfer: localAmount });
zcfSeat.exit();
} catch (e) {
zcfSeat.fail(e);
throw e;
}

// Once we've gotten to this point, their payment is committed and
// won't be refunded on a failed receive.
const payout = await E(userSeat).getPayout('Transfer');

// Send the payout promise to the deposit facet.
//
// We don't want to wait for the depositFacet to return, so that
// it can't hang up (i.e. DoS) an ordered channel, which relies on
// us returning promptly.
E(depositFacet)
.receive(payout)
.catch(_ => {});

return E(transferProtocol).makeTransferPacketAck(true);
};

return Far('courier', { send, receive });
};
33 changes: 33 additions & 0 deletions packages/pegasus/src/once-promise-kit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { assert } from '@agoric/assert';
import { makePromiseKit } from '@agoric/promise-kit';

/**
* Create a promise kit that will throw an exception if it is resolved or
* rejected more than once.
*
* @param {() => Details} makeReinitDetails
*/
export const makeOncePromiseKit = makeReinitDetails => {
const { promise, resolve, reject } = makePromiseKit();

let initialized = false;
/**
* @template {any[]} A
* @template R
* @param {(...args: A) => R} fn
* @returns {(...args: A) => R}
*/
const onceOnly = fn => (...args) => {
assert(!initialized, makeReinitDetails());
initialized = true;
return fn(...args);
};

/** @type {PromiseRecord<any>} */
const oncePK = harden({
promise,
resolve: onceOnly(resolve),
reject: onceOnly(reject),
});
return oncePK;
};
Loading

0 comments on commit e7ea320

Please sign in to comment.