Skip to content

Commit e7ea320

Browse files
committed
fix(pegasus): more POLA and less global state
1 parent c703d07 commit e7ea320

File tree

5 files changed

+470
-399
lines changed

5 files changed

+470
-399
lines changed

packages/pegasus/src/courier.js

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { details as X } from '@agoric/assert';
2+
3+
import { AmountMath } from '@agoric/ertp';
4+
import { E } from '@agoric/eventual-send';
5+
import { Far } from '@agoric/marshal';
6+
import { makeOncePromiseKit } from './once-promise-kit.js';
7+
8+
/**
9+
* Create or return an existing courier promise kit.
10+
*
11+
* @template K
12+
* @param {K} key
13+
* @param {Store<K, PromiseRecord<Courier>>} keyToCourierPK
14+
*/
15+
export const getCourierPK = (key, keyToCourierPK) => {
16+
if (keyToCourierPK.has(key)) {
17+
return keyToCourierPK.get(key);
18+
}
19+
20+
// This is the first packet for this denomination.
21+
// Create a new Courier promise kit for it.
22+
const courierPK = makeOncePromiseKit(() => X`${key} already pegged`);
23+
24+
keyToCourierPK.init(key, courierPK);
25+
return courierPK;
26+
};
27+
28+
/**
29+
* Create the [send, receive] pair.
30+
*
31+
* @typedef {Object} CourierArgs
32+
* @property {ContractFacet} zcf
33+
* @property {ERef<BoardDepositFacet>} board
34+
* @property {ERef<NameHub>} namesByAddress
35+
* @property {Denom} remoteDenom
36+
* @property {Brand} localBrand
37+
* @property {(zcfSeat: ZCFSeat, amounts: AmountKeywordRecord) => void} retain
38+
* @property {(zcfSeat: ZCFSeat, amounts: AmountKeywordRecord) => void} redeem
39+
* @property {ERef<TransferProtocol>} transferProtocol
40+
* @param {ERef<Connection>} connection
41+
* @returns {(args: CourierArgs) => Courier}
42+
*/
43+
export const makeCourierMaker = connection => ({
44+
zcf,
45+
board,
46+
namesByAddress,
47+
remoteDenom,
48+
localBrand,
49+
retain,
50+
redeem,
51+
transferProtocol,
52+
}) => {
53+
/** @type {Sender} */
54+
const send = async (zcfSeat, depositAddress) => {
55+
const tryToSend = async () => {
56+
const amount = zcfSeat.getAmountAllocated('Transfer', localBrand);
57+
const transferPacket = await E(transferProtocol).makeTransferPacket({
58+
value: amount.value,
59+
remoteDenom,
60+
depositAddress,
61+
});
62+
63+
// Retain the payment. We must not proceed on failure.
64+
retain(zcfSeat, { Transfer: amount });
65+
66+
// The payment is already escrowed, and proposed to retain, so try sending.
67+
return E(connection)
68+
.send(transferPacket)
69+
.then(ack => E(transferProtocol).assertTransferPacketAck(ack))
70+
.then(
71+
_ => zcfSeat.exit(),
72+
reason => {
73+
// Return the payment to the seat, if possible.
74+
redeem(zcfSeat, { Transfer: amount });
75+
throw reason;
76+
},
77+
);
78+
};
79+
80+
// Reflect any error back to the seat.
81+
return tryToSend().catch(reason => {
82+
zcfSeat.fail(reason);
83+
});
84+
};
85+
86+
/** @type {Receiver} */
87+
const receive = async ({ value, depositAddress }) => {
88+
const localAmount = AmountMath.make(localBrand, value);
89+
90+
// Look up the deposit facet for this board address, if there is one.
91+
/** @type {DepositFacet} */
92+
const depositFacet = await E(board)
93+
.getValue(depositAddress)
94+
.catch(_ => E(namesByAddress).lookup(depositAddress, 'depositFacet'));
95+
96+
const { userSeat, zcfSeat } = zcf.makeEmptySeatKit();
97+
98+
// Redeem the backing payment.
99+
try {
100+
redeem(zcfSeat, { Transfer: localAmount });
101+
zcfSeat.exit();
102+
} catch (e) {
103+
zcfSeat.fail(e);
104+
throw e;
105+
}
106+
107+
// Once we've gotten to this point, their payment is committed and
108+
// won't be refunded on a failed receive.
109+
const payout = await E(userSeat).getPayout('Transfer');
110+
111+
// Send the payout promise to the deposit facet.
112+
//
113+
// We don't want to wait for the depositFacet to return, so that
114+
// it can't hang up (i.e. DoS) an ordered channel, which relies on
115+
// us returning promptly.
116+
E(depositFacet)
117+
.receive(payout)
118+
.catch(_ => {});
119+
120+
return E(transferProtocol).makeTransferPacketAck(true);
121+
};
122+
123+
return Far('courier', { send, receive });
124+
};
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { assert } from '@agoric/assert';
2+
import { makePromiseKit } from '@agoric/promise-kit';
3+
4+
/**
5+
* Create a promise kit that will throw an exception if it is resolved or
6+
* rejected more than once.
7+
*
8+
* @param {() => Details} makeReinitDetails
9+
*/
10+
export const makeOncePromiseKit = makeReinitDetails => {
11+
const { promise, resolve, reject } = makePromiseKit();
12+
13+
let initialized = false;
14+
/**
15+
* @template {any[]} A
16+
* @template R
17+
* @param {(...args: A) => R} fn
18+
* @returns {(...args: A) => R}
19+
*/
20+
const onceOnly = fn => (...args) => {
21+
assert(!initialized, makeReinitDetails());
22+
initialized = true;
23+
return fn(...args);
24+
};
25+
26+
/** @type {PromiseRecord<any>} */
27+
const oncePK = harden({
28+
promise,
29+
resolve: onceOnly(resolve),
30+
reject: onceOnly(reject),
31+
});
32+
return oncePK;
33+
};

0 commit comments

Comments
 (0)