Skip to content

Commit

Permalink
feat(ERTP): Make recoverySets optional
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Oct 1, 2023
1 parent eccb862 commit cafb065
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 40 deletions.
25 changes: 19 additions & 6 deletions packages/ERTP/src/issuerKit.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import './types-ambient.js';
* @property {K} assetKind
* @property {AdditionalDisplayInfo} displayInfo
* @property {Pattern} elementShape
* @property {'noRecoverySets'} [recoverySetOption]
*/

/**
Expand All @@ -35,7 +36,7 @@ import './types-ambient.js';
* @returns {IssuerKit<K>}
*/
const setupIssuerKit = (
{ name, assetKind, displayInfo, elementShape },
{ name, assetKind, displayInfo, elementShape, recoverySetOption = undefined },
issuerBaggage,
optShutdownWithFailure = undefined,
) => {
Expand All @@ -61,6 +62,7 @@ const setupIssuerKit = (
assetKind,
cleanDisplayInfo,
elementShape,
recoverySetOption,
optShutdownWithFailure,
);

Expand Down Expand Up @@ -106,7 +108,12 @@ harden(prepareIssuerKit);
*/
export const hasIssuer = baggage => baggage.has(INSTANCE_KEY);

/** @typedef {Partial<{ elementShape: Pattern }>} IssuerOptionsRecord */
/**
* @typedef {Partial<{
* elementShape: Pattern;
* recoverySetOption: 'noRecoverySets';
* }>} IssuerOptionsRecord
*/

/**
* @template {AssetKind} K The name becomes part of the brand in asset
Expand Down Expand Up @@ -142,9 +149,15 @@ export const makeDurableIssuerKit = (
assetKind = AssetKind.NAT,
displayInfo = harden({}),
optShutdownWithFailure = undefined,
{ elementShape = undefined } = {},
{ elementShape = undefined, recoverySetOption = undefined } = {},
) => {
const issuerData = harden({ name, assetKind, displayInfo, elementShape });
const issuerData = harden({
name,
assetKind,
displayInfo,
elementShape,
recoverySetOption,
});
issuerBaggage.init(INSTANCE_KEY, issuerData);
return setupIssuerKit(issuerData, issuerBaggage, optShutdownWithFailure);
};
Expand Down Expand Up @@ -182,14 +195,14 @@ export const makeIssuerKit = (
assetKind = AssetKind.NAT,
displayInfo = harden({}),
optShutdownWithFailure = undefined,
{ elementShape = undefined } = {},
{ elementShape = undefined, recoverySetOption = undefined } = {},
) =>
makeDurableIssuerKit(
makeScalarBigMapStore('dropped issuer kit', { durable: true }),
name,
assetKind,
displayInfo,
optShutdownWithFailure,
{ elementShape },
{ elementShape, recoverySetOption },
);
harden(makeIssuerKit);
32 changes: 22 additions & 10 deletions packages/ERTP/src/paymentLedger.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const amountShapeFromElementShape = (brand, assetKind, elementShape) => {
* @param {K} assetKind
* @param {DisplayInfo<K>} displayInfo
* @param {Pattern} elementShape
* @param {'noRecoverySets'} [recoverySetOption]
* @param {ShutdownWithFailure} [optShutdownWithFailure]
* @returns {PaymentLedger<K>}
*/
Expand All @@ -88,6 +89,7 @@ export const preparePaymentLedger = (
assetKind,
displayInfo,
elementShape,
recoverySetOption = undefined,
optShutdownWithFailure = undefined,
) => {
/** @type {Brand<K>} */
Expand Down Expand Up @@ -156,18 +158,18 @@ export const preparePaymentLedger = (
* - Every payment that is a key in the outer `paymentRecoverySets` weakMap is
* also in the recovery set indexed by that payment.
* - Implied by the above but worth stating: the payment is only in at most one
* recovery set.
* one recovery set.
* - A recovery set only contains such payments.
* - Every purse is associated with exactly one recovery set unique to it.
* - A purse's recovery set only contains payments withdrawn from that purse and
* not yet consumed.
*
* @type {WeakMapStore<Payment, SetStore<Payment>>}
* @type {WeakMapStore<Payment, SetStore<Payment>> | undefined}
*/
const paymentRecoverySets = provideDurableWeakMapStore(
issuerBaggage,
'paymentRecoverySets',
);
const paymentRecoverySets =
recoverySetOption === 'noRecoverySets'
? undefined
: provideDurableWeakMapStore(issuerBaggage, 'paymentRecoverySets');

/**
* To maintain the invariants listed in the `paymentRecoverySets` comment,
Expand All @@ -179,6 +181,7 @@ export const preparePaymentLedger = (
*/
const initPayment = (payment, amount, optRecoverySet = undefined) => {
if (optRecoverySet !== undefined) {
assert(paymentRecoverySets !== undefined);
optRecoverySet.add(payment);
paymentRecoverySets.init(payment, optRecoverySet);
}
Expand All @@ -193,7 +196,7 @@ export const preparePaymentLedger = (
*/
const deletePayment = payment => {
paymentLedger.delete(payment);
if (paymentRecoverySets.has(payment)) {
if (paymentRecoverySets !== undefined && paymentRecoverySets.has(payment)) {
const recoverySet = paymentRecoverySets.get(payment);
paymentRecoverySets.delete(payment);
recoverySet.delete(payment);
Expand Down Expand Up @@ -283,14 +286,14 @@ export const preparePaymentLedger = (
* @param {(newPurseBalance: Amount) => void} updatePurseBalance - commit the
* purse balance
* @param {Amount} amount - the amount to be withdrawn
* @param {SetStore<Payment>} recoverySet
* @param {SetStore<Payment>} [recoverySet]
* @returns {Payment}
*/
const withdrawInternal = (
currentBalance,
updatePurseBalance,
amount,
recoverySet,
recoverySet = undefined,
) => {
amount = coerce(amount);
AmountMath.isGTE(currentBalance, amount) ||
Expand All @@ -310,6 +313,8 @@ export const preparePaymentLedger = (
return payment;
};

/** @type {() => Purse<K>} */
// @ts-expect-error type parameter confusion
const makeEmptyPurse = preparePurseKind(
issuerBaggage,
name,
Expand All @@ -320,6 +325,7 @@ export const preparePaymentLedger = (
depositInternal,
withdrawInternal,
}),
recoverySetOption,
);

/** @type {Issuer<K>} */
Expand Down Expand Up @@ -407,7 +413,13 @@ export const preparePaymentLedger = (
},
});

const issuerKit = harden({ issuer, mint, brand, mintRecoveryPurse });
const issuerKit = harden({
issuer,
mint,
brand,
mintRecoveryPurse:
recoverySetOption === 'noRecoverySets' ? undefined : mintRecoveryPurse,
});
return issuerKit;
};
harden(preparePaymentLedger);
40 changes: 35 additions & 5 deletions packages/ERTP/src/purse.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,33 @@ import { prepareExoClassKit, makeScalarBigSetStore } from '@agoric/vat-data';
import { AmountMath } from './amountMath.js';
import { makeTransientNotifierKit } from './transientNotifier.js';

/** @typedef {import('@agoric/vat-data').Baggage} Baggage */

const { Fail } = assert;

/**
* @param {Baggage} issuerBaggage
* @param {string} name
* @param {AssetKind} assetKind
* @param {Brand} brand
* @param {{
* purse: InterfaceGuard;
* depositFacet: InterfaceGuard;
* }} PurseIKit
* @param {{
* depositInternal: any;
* withdrawInternal: any;
* }} purseMethods
* @param {'noRecoverySets'} [recoverySetOption]
*/
export const preparePurseKind = (
issuerBaggage,
name,
assetKind,
brand,
PurseIKit,
purseMethods,
recoverySetOption = undefined,
) => {
const amountShape = brand.getAmountShape();

Expand All @@ -38,10 +56,13 @@ export const preparePurseKind = (
() => {
const currentBalance = AmountMath.makeEmpty(brand, assetKind);

/** @type {SetStore<Payment>} */
const recoverySet = makeScalarBigSetStore('recovery set', {
durable: true,
});
/** @type {SetStore<Payment> | undefined} */
const recoverySet =
recoverySetOption === 'noRecoverySets'
? undefined
: makeScalarBigSetStore('recovery set', {
durable: true,
});

return {
currentBalance,
Expand Down Expand Up @@ -89,10 +110,18 @@ export const preparePurseKind = (
},

getRecoverySet() {
if (recoverySetOption === 'noRecoverySets') {
return undefined;
}
assert(this.state.recoverySet !== undefined);
return this.state.recoverySet.snapshot();
},
recoverAll() {
if (recoverySetOption === 'noRecoverySets') {
return undefined;
}
const { state, facets } = this;
assert(state.recoverySet !== undefined);
let amount = AmountMath.makeEmpty(brand, assetKind);
for (const payment of state.recoverySet.keys()) {
// This does cause deletions from the set while iterating,
Expand All @@ -114,7 +143,8 @@ export const preparePurseKind = (
{
stateShape: {
currentBalance: amountShape,
recoverySet: M.remotable('recoverySet'),
// NOTE: Schema change!
recoverySet: M.opt(M.remotable('recoverySet')),
},
},
);
Expand Down
4 changes: 2 additions & 2 deletions packages/ERTP/src/typeGuards.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,8 @@ export const makeIssuerInterfaces = (
deposit: M.call(PaymentShape).optional(M.pattern()).returns(amountShape),
getDepositFacet: M.call().returns(DepositFacetShape),
withdraw: M.call(amountShape).returns(PaymentShape),
getRecoverySet: M.call().returns(M.setOf(PaymentShape)),
recoverAll: M.call().returns(amountShape),
getRecoverySet: M.call().returns(M.opt(M.setOf(PaymentShape))),
recoverAll: M.call().returns(M.opt(amountShape)),
});

const DepositFacetI = M.interface('DepositFacet', {
Expand Down
41 changes: 24 additions & 17 deletions packages/ERTP/src/types-ambient.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,10 @@

/**
* @callback IssuerBurn Burn all of the digital assets in the payment.
* `optAmount` is optional. If `optAmount` is present, the code will insist
* that the amount of the digital assets in the payment is equal to
* `optAmount`, to prevent sending the wrong payment and other confusion.
* `optAmountShape` is optional. If the `optAmountShape` pattern is present,
* the amount of the digital assets in the payment must match
* `optAmountShape`, to prevent sending the wrong payment and other
* confusion.
*
* If the payment is a promise, the operation will proceed upon resolution.
* @param {ERef<Payment>} payment
Expand Down Expand Up @@ -171,7 +172,8 @@
* @template {AssetKind} [K=AssetKind]
* @typedef {object} PaymentLedger
* @property {Mint<K>} mint
* @property {Purse<K>} mintRecoveryPurse
* @property {Purse<K>} [mintRecoveryPurse] Omitted if this issuer has opted out
* of payment recovery support.
* @property {Issuer<K>} issuer
* @property {Brand<K>} brand
*/
Expand All @@ -180,7 +182,8 @@
* @template {AssetKind} [K=AssetKind]
* @typedef {object} IssuerKit
* @property {Mint<K>} mint
* @property {Purse<K>} mintRecoveryPurse
* @property {Purse<K>} [mintRecoveryPurse] Omitted if this issuer has opted out
* of payment recovery support.
* @property {Issuer<K>} issuer
* @property {Brand<K>} brand
* @property {DisplayInfo} displayInfo
Expand Down Expand Up @@ -262,18 +265,22 @@
* `receive` method deposits to the current Purse.
* @property {(amount: Amount<K>) => Payment<K>} withdraw Withdraw amount from
* this purse into a new Payment.
* @property {() => CopySet<Payment<K>>} getRecoverySet The set of payments
* withdrawn from this purse that are still live. These are the payments that
* can still be recovered in emergencies by, for example, depositing into this
* purse. Such a deposit action is like canceling an outstanding check because
* you're tired of waiting for it. Once your cancellation is acknowledged, you
* can spend the assets at stake on other things. Afterwards, if the recipient
* of the original check finally gets around to depositing it, their deposit
* fails.
* @property {() => Amount<K>} recoverAll For use in emergencies, such as coming
* back from a traumatic crash and upgrade. This deposits all the payments in
* this purse's recovery set into the purse itself, returning the total amount
* of assets recovered.
* @property {() => CopySet<Payment<K>> | undefined} getRecoverySet The set of
* payments withdrawn from this purse that are still live. These are the
* payments that can still be recovered in emergencies by, for example,
* depositing into this purse. Such a deposit action is like canceling an
* outstanding check because you're tired of waiting for it. Once your
* cancellation is acknowledged, you can spend the assets at stake on other
* things. Afterwards, if the recipient of the original check finally gets
* around to depositing it, their deposit fails.
*
* Returns undefined if this issuer has opted out of payment recovery support.
* @property {() => Amount<K> | undefined} recoverAll For use in emergencies,
* such as coming back from a traumatic crash and upgrade. This deposits all
* the payments in this purse's recovery set into the purse itself, returning
* the total amount of assets recovered.
*
* Returns undefined if this issuer has opted out of payment recovery support.
*/

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/ERTP/test/unitTests/test-recovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ test('payment recovery from purse recovery set', async t => {
t.assert(keyEQ(alicePurse.getRecoverySet(), emptySet));

const aliceRecovered = alicePurse.recoverAll();
assert(aliceRecovered !== undefined);
t.assert(isEmpty(aliceRecovered));
t.assert(isEqual(alicePurse.getCurrentAmount(), precious(32n)));

t.assert(isEqual(bobPurse.getCurrentAmount(), precious(46n)));
const bobRecovered = bobPurse.recoverAll();
assert(bobRecovered !== undefined);
t.assert(isEqual(bobRecovered, precious(0n)));
t.assert(isEqual(bobPurse.getCurrentAmount(), precious(46n)));
t.assert(keyEQ(bobPurse.getRecoverySet(), emptySet));
Expand All @@ -46,6 +48,7 @@ test('payment recovery from mint recovery set', async t => {
const mindyPurse = issuer.makeEmptyPurse();
const bobPurse = issuer.makeEmptyPurse();

assert(mintRecoveryPurse !== undefined);
t.assert(keyEQ(mintRecoveryPurse.getRecoverySet(), emptySet));
const payment1 = mint.mintPayment(precious(37n));
const payment2 = mint.mintPayment(precious(41n));
Expand Down

0 comments on commit cafb065

Please sign in to comment.