From 71cf0645442d461a7c99b3eb1833f6f04773e4b4 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Thu, 2 Nov 2023 19:13:55 -0700 Subject: [PATCH] refactor(ertp): reallyPrepareIssuerKit --- packages/ERTP/src/issuerKit.js | 114 +++++++++++++++++- packages/ERTP/src/paymentLedger.js | 2 + packages/ERTP/src/purse.js | 18 +++ packages/ERTP/src/types-ambient.js | 29 +++-- .../ertpService/vat-ertp-service.js | 4 +- .../src/price/fluxAggregatorContract.js | 16 +-- packages/vats/src/mintHolder.js | 14 +-- .../priceAuthorityQuoteMint.js | 42 +++---- packages/zoe/src/zoeService/feeMint.js | 34 +++--- packages/zoe/src/zoeService/makeInvitation.js | 41 ++++--- .../zoe/src/zoeService/zoeStorageManager.js | 4 +- 11 files changed, 221 insertions(+), 97 deletions(-) diff --git a/packages/ERTP/src/issuerKit.js b/packages/ERTP/src/issuerKit.js index 7b13135a14e..85581030ccf 100644 --- a/packages/ERTP/src/issuerKit.js +++ b/packages/ERTP/src/issuerKit.js @@ -10,6 +10,8 @@ import { preparePaymentLedger } from './paymentLedger.js'; import './types-ambient.js'; +// TODO Why does TypeScript lose the `MapStore` typing of `Baggage` here, even +// though it knows the correct type at the exporting `@agoric/vat-data` /** @typedef {import('@agoric/vat-data').Baggage} Baggage */ /** @@ -22,6 +24,8 @@ import './types-ambient.js'; */ /** + * Used _only_ internally, to make a new issuerKit or to revive an old one. + * * @template {AssetKind} K * @param {IssuerRecord} issuerRecord * @param {Baggage} issuerBaggage @@ -78,6 +82,9 @@ harden(setupIssuerKit); const INSTANCE_KEY = 'issuer'; /** + * Used _only_ to upgrade a predecessor issuerKit. Use `makeDurableIssuerKit` to + * make a new one. + * * @template {AssetKind} K * @param {Baggage} issuerBaggage * @param {ShutdownWithFailure} [optShutdownWithFailure] If this issuer fails in @@ -89,26 +96,51 @@ const INSTANCE_KEY = 'issuer'; * https://github.com/Agoric/agoric-sdk/issues/3434 * @returns {IssuerKit} */ -export const prepareIssuerKit = ( +export const upgradeIssuerKit = ( issuerBaggage, optShutdownWithFailure = undefined, ) => { const issuerRecord = issuerBaggage.get(INSTANCE_KEY); return setupIssuerKit(issuerRecord, issuerBaggage, optShutdownWithFailure); }; -harden(prepareIssuerKit); +harden(upgradeIssuerKit); /** - * Does baggage already have an issuer from prepareIssuerKit()? That is: does it - * have the relevant keys defined? + * Confusingly, `prepareIssuerKit` was the original name for `upgradeIssuerKit`, + * even though it is used only to upgrade a predecessor issuerKit. Use + * `makeDurableIssuerKit` to make a new one. + * + * @deprecated Use `upgradeIssuerKit` instead if that's what you want. Or + * `reallyPrepareIssuerKit` if you want the behavior that should have been + * bound to this name. + */ +export const prepareIssuerKit = upgradeIssuerKit; + +/** + * Does baggage already have an issuerKit? * * @param {Baggage} baggage */ export const hasIssuer = baggage => baggage.has(INSTANCE_KEY); -/** @typedef {Partial<{ elementShape: Pattern }>} IssuerOptionsRecord */ +/** + * `elementShape`, may only be present for collection-style amounts. If present, + * it is a `Pattern` that every element of this issuerKits's amounts must + * satisfy. For example, the Zoe Invitation issuerKit uses an elementShape + * describing the invitation details for an individual invitation. An invitation + * purse or payment has an amount that can only be a set of these. (Though + * typically, the amount of an invitation payment is a singleton set. Such a + * payment is often referred to in the singular as "an invitation".) + * + * @typedef {Partial<{ + * elementShape: Pattern; + * }>} IssuerOptionsRecord + */ /** + * Used _only_ to make a _new_ durable issuer, i.e., the initial incarnation of + * that issuer. + * * @template {AssetKind} K The name becomes part of the brand in asset * descriptions. The name is useful for debugging and double-checking * assumptions, but should not be trusted wrt any external namespace. For @@ -151,6 +183,78 @@ export const makeDurableIssuerKit = ( harden(makeDurableIssuerKit); /** + * What _should_ have been named `prepareIssuerKit`. Used to either revive a + * predecessor issuerKit, or to make a new durable one if it is absent, and to + * place it in baggage for the next successor. + * + * @template {AssetKind} K The name becomes part of the brand in asset + * descriptions. The name is useful for debugging and double-checking + * assumptions, but should not be trusted wrt any external namespace. For + * example, anyone could create a new issuer kit with name 'BTC', but it is + * not bitcoin or even related. It is only the name according to that issuer + * and brand. + * + * The assetKind will be used to import a specific mathHelpers from the + * mathHelpers library. For example, natMathHelpers, the default, is used for + * basic fungible tokens. + * + * `displayInfo` gives information to the UI on how to display the amount. + * @param {Baggage} issuerBaggage + * @param {string} name + * @param {K} [assetKind] + * @param {AdditionalDisplayInfo} [displayInfo] + * @param {ShutdownWithFailure} [optShutdownWithFailure] If this issuer fails in + * the middle of an atomic action (which btw should never happen), it + * potentially leaves its ledger in a corrupted state. If this function was + * provided, then the failed atomic action will call it, so that some larger + * unit of computation, like the enclosing vat, can be shutdown before + * anything else is corrupted by that corrupted state. See + * https://github.com/Agoric/agoric-sdk/issues/3434 + * @param {IssuerOptionsRecord} [options] + * @returns {IssuerKit} + */ +export const reallyPrepareIssuerKit = ( + issuerBaggage, + name, + // @ts-expect-error K could be instantiated with a different subtype of AssetKind + assetKind = AssetKind.NAT, + displayInfo = harden({}), + optShutdownWithFailure = undefined, + options = {}, +) => { + if (hasIssuer(issuerBaggage)) { + const { elementShape: _ = undefined } = options; + const issuerKit = upgradeIssuerKit(issuerBaggage, optShutdownWithFailure); + + // TODO check consistency with name, assetKind, displayInfo, elementShape. + // Consistency either means that these are the same, or that they differ + // in a direction we are prepared to upgrade. + + // @ts-expect-error Type parameter confusion. + return issuerKit; + } else { + const issuerKit = makeDurableIssuerKit( + issuerBaggage, + name, + assetKind, + displayInfo, + optShutdownWithFailure, + options, + ); + return issuerKit; + } +}; +harden(reallyPrepareIssuerKit); + +/** + * Used _only_ to make a new issuerKit that is effectively non-durable. This is + * currently done by making a durable one in a baggage not reachable from + * anywhere. TODO Once rebuilt on zones, this should instead just build on the + * virtual zone. See https://github.com/Agoric/agoric-sdk/pull/7116 + * + * Currently used for testing only. Should probably continue to be used for + * testing only. + * * @template {AssetKind} [K='nat'] The name becomes part of the brand in asset * descriptions. The name is useful for debugging and double-checking * assumptions, but should not be trusted wrt any external namespace. For diff --git a/packages/ERTP/src/paymentLedger.js b/packages/ERTP/src/paymentLedger.js index b4575854d14..b11ecc925a7 100644 --- a/packages/ERTP/src/paymentLedger.js +++ b/packages/ERTP/src/paymentLedger.js @@ -310,6 +310,8 @@ export const preparePaymentLedger = ( return payment; }; + /** @type {() => Purse} */ + // @ts-expect-error type parameter confusion const makeEmptyPurse = preparePurseKind( issuerBaggage, name, diff --git a/packages/ERTP/src/purse.js b/packages/ERTP/src/purse.js index 35d766eda79..74451170fe0 100644 --- a/packages/ERTP/src/purse.js +++ b/packages/ERTP/src/purse.js @@ -3,8 +3,26 @@ import { prepareExoClassKit, makeScalarBigSetStore } from '@agoric/vat-data'; import { AmountMath } from './amountMath.js'; import { makeTransientNotifierKit } from './transientNotifier.js'; +// TODO `InterfaceGuard` type parameter +/** @typedef {import('@endo/patterns').InterfaceGuard} InterfaceGuard */ +/** @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 + */ export const preparePurseKind = ( issuerBaggage, name, diff --git a/packages/ERTP/src/types-ambient.js b/packages/ERTP/src/types-ambient.js index 5fbd0e805b4..0bdc50acee0 100644 --- a/packages/ERTP/src/types-ambient.js +++ b/packages/ERTP/src/types-ambient.js @@ -115,7 +115,7 @@ /** * @callback IssuerIsLive Return true if the payment continues to exist. * - * If the payment is a promise, the operation will proceed upon resolution. + * If the payment is a promise, the operation will proceed upon fulfillment. * @param {ERef} payment * @returns {Promise} */ @@ -125,18 +125,25 @@ * Because the payment is not trusted, we cannot call a method on it directly, * and must use the issuer instead. * - * If the payment is a promise, the operation will proceed upon resolution. + * If the payment is a promise, the operation will proceed upon fulfillment. * @param {ERef} payment * @returns {Promise>} */ /** * @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. + * If the payment is a promise, the operation will proceed upon fulfillment. + * + * As always with optional `Pattern` arguments, keep in mind that technically + * the value `undefined` itself is a valid `Key` and therefore a valid + * `Pattern`. But in optional pattern position, a top level `undefined` will + * be interpreted as absence. If you want to express a `Pattern` that will + * match only `undefined`, use `M.undefined()` instead. * @param {ERef} payment * @param {Pattern} [optAmountShape] * @returns {Promise} @@ -171,7 +178,8 @@ * @template {AssetKind} [K=AssetKind] * @typedef {object} PaymentLedger * @property {Mint} mint - * @property {Purse} mintRecoveryPurse + * @property {Purse} mintRecoveryPurse Useful only to get the recovery set + * associated with minted payments that are still live. * @property {Issuer} issuer * @property {Brand} brand */ @@ -180,7 +188,8 @@ * @template {AssetKind} [K=AssetKind] * @typedef {object} IssuerKit * @property {Mint} mint - * @property {Purse} mintRecoveryPurse + * @property {Purse} mintRecoveryPurse Useful only to get the recovery set + * associated with minted payments that are still live. * @property {Issuer} issuer * @property {Brand} brand * @property {DisplayInfo} displayInfo @@ -211,6 +220,8 @@ * Payment containing newly minted amount. */ +// /////////////////////////// Purse / Payment ///////////////////////////////// + /** * @callback DepositFacetReceive * @param {Payment} payment @@ -298,6 +309,8 @@ * be treated with suspicion and verified elsewhere. */ +// /////////////////////////// MathHelpers ///////////////////////////////////// + /** * @template {AmountValue} V * @typedef {object} MathHelpers All of the difference in how digital asset diff --git a/packages/ERTP/test/swingsetTests/ertpService/vat-ertp-service.js b/packages/ERTP/test/swingsetTests/ertpService/vat-ertp-service.js index f2ee6f0463b..f675ea609d3 100644 --- a/packages/ERTP/test/swingsetTests/ertpService/vat-ertp-service.js +++ b/packages/ERTP/test/swingsetTests/ertpService/vat-ertp-service.js @@ -8,7 +8,7 @@ import { import { AssetKind, makeDurableIssuerKit, - prepareIssuerKit, + upgradeIssuerKit, } from '../../../src/index.js'; export const prepareErtpService = (baggage, exitVatWithFailure) => { @@ -36,7 +36,7 @@ export const prepareErtpService = (baggage, exitVatWithFailure) => { }); for (const issuerBaggage of issuerBaggageSet.values()) { - prepareIssuerKit(issuerBaggage, exitVatWithFailure); + upgradeIssuerKit(issuerBaggage, exitVatWithFailure); } return ertpService; diff --git a/packages/inter-protocol/src/price/fluxAggregatorContract.js b/packages/inter-protocol/src/price/fluxAggregatorContract.js index 2ac33c6482d..883139b9ea3 100644 --- a/packages/inter-protocol/src/price/fluxAggregatorContract.js +++ b/packages/inter-protocol/src/price/fluxAggregatorContract.js @@ -1,10 +1,6 @@ // @jessie-check -import { - hasIssuer, - makeDurableIssuerKit, - prepareIssuerKit, -} from '@agoric/ertp'; +import { reallyPrepareIssuerKit } from '@agoric/ertp'; import { handleParamGovernance } from '@agoric/governance'; import { makeTracer, StorageNodeShape } from '@agoric/internal'; import { prepareDurablePublishKit } from '@agoric/notifier'; @@ -72,9 +68,13 @@ export const start = async (zcf, privateArgs, baggage) => { // xxx uses contract baggage as issuerBagage, assumes one issuer in this contract /** @type {import('./roundsManager.js').QuoteKit} */ - const quoteIssuerKit = hasIssuer(baggage) - ? prepareIssuerKit(baggage) - : makeDurableIssuerKit(baggage, 'quote', 'set'); + const quoteIssuerKit = reallyPrepareIssuerKit( + baggage, + 'quote', + 'set', + undefined, + undefined, + ); const { highPrioritySendersManager, diff --git a/packages/vats/src/mintHolder.js b/packages/vats/src/mintHolder.js index 27880481009..36a3d45f17d 100644 --- a/packages/vats/src/mintHolder.js +++ b/packages/vats/src/mintHolder.js @@ -1,11 +1,7 @@ // @ts-check // @jessie-check -import { - hasIssuer, - makeDurableIssuerKit, - prepareIssuerKit, -} from '@agoric/ertp'; +import { reallyPrepareIssuerKit } from '@agoric/ertp'; /** @typedef {import('@agoric/vat-data').Baggage} Baggage */ @@ -26,12 +22,8 @@ import { * @param {Baggage} baggage */ function provideIssuerKit(zcf, baggage) { - if (!hasIssuer(baggage)) { - const { keyword, assetKind, displayInfo } = zcf.getTerms(); - return makeDurableIssuerKit(baggage, keyword, assetKind, displayInfo); - } else { - return prepareIssuerKit(baggage); - } + const { keyword, assetKind, displayInfo } = zcf.getTerms(); + return reallyPrepareIssuerKit(baggage, keyword, assetKind, displayInfo); } /** @type {ContractMeta} */ diff --git a/packages/zoe/src/contractSupport/priceAuthorityQuoteMint.js b/packages/zoe/src/contractSupport/priceAuthorityQuoteMint.js index 813d4ffbbe9..866ac4afd91 100644 --- a/packages/zoe/src/contractSupport/priceAuthorityQuoteMint.js +++ b/packages/zoe/src/contractSupport/priceAuthorityQuoteMint.js @@ -1,34 +1,22 @@ -import { - AssetKind, - makeDurableIssuerKit, - prepareIssuerKit, -} from '@agoric/ertp'; -import { makeScalarBigMapStore } from '@agoric/vat-data'; +import { AssetKind, reallyPrepareIssuerKit } from '@agoric/ertp'; +import { provideDurableMapStore } from '@agoric/vat-data'; /** * * @param {import('@agoric/vat-data').Baggage} baggage + * @returns {ERef>} */ export const provideQuoteMint = baggage => { - /** @type {ERef>} */ - let baggageQuoteMint; - if (baggage.has(`quoteMintIssuerBaggage`)) { - const issuerBaggage = baggage.get(`quoteMintIssuerBaggage`); - baggageQuoteMint = /** @type {Mint<'set'>} */ ( - prepareIssuerKit(issuerBaggage).mint - ); - } else { - const issuerBaggage = makeScalarBigMapStore( - `scaledPriceAuthority quoteMintIssuerBaggage`, - { durable: true }, - ); - baggage.init(`quoteMintIssuerBaggage`, issuerBaggage); - baggageQuoteMint = makeDurableIssuerKit( - issuerBaggage, - 'quote', - AssetKind.SET, - ).mint; - } - - return baggageQuoteMint; + const issuerBaggage = provideDurableMapStore( + baggage, + 'quoteMintIssuerBaggage', + ); + const issuerKit = reallyPrepareIssuerKit( + issuerBaggage, + 'quote', + AssetKind.SET, + undefined, + undefined, + ); + return issuerKit.mint; }; diff --git a/packages/zoe/src/zoeService/feeMint.js b/packages/zoe/src/zoeService/feeMint.js index 86b4a54c7e3..fe3e405327a 100644 --- a/packages/zoe/src/zoeService/feeMint.js +++ b/packages/zoe/src/zoeService/feeMint.js @@ -1,9 +1,9 @@ import { - makeDurableIssuerKit, AssetKind, - prepareIssuerKit, IssuerShape, BrandShape, + reallyPrepareIssuerKit, + hasIssuer, } from '@agoric/ertp'; import { initEmpty, M } from '@agoric/store'; import { @@ -13,8 +13,9 @@ import { } from '@agoric/vat-data'; import { FeeMintAccessShape } from '../typeGuards.js'; -const { Fail } = assert; +const { Fail, quote: q } = assert; +/** @deprecated Redundant. Just omit it. */ const FEE_MINT_KIT = 'FeeMintKit'; export const defaultFeeIssuerConfig = harden( @@ -32,19 +33,24 @@ export const defaultFeeIssuerConfig = harden( */ const prepareFeeMint = (zoeBaggage, feeIssuerConfig, shutdownZoeVat) => { const mintBaggage = provideDurableMapStore(zoeBaggage, 'mintBaggage'); - if (!mintBaggage.has(FEE_MINT_KIT)) { - /** @type {IssuerKit} */ - const feeIssuerKit = makeDurableIssuerKit( + if (mintBaggage.has(FEE_MINT_KIT)) { + hasIssuer(mintBaggage) || + Fail`Legacy ${q( + FEE_MINT_KIT, + )} must be redundant with normal storing of issuerKit in issuerBaggage`; + // Upgrade this legacy state by simply deleting it. + mintBaggage.delete(FEE_MINT_KIT); + } + + const feeIssuerKit = /** @type {IssuerKit<'nat'>} */ ( + reallyPrepareIssuerKit( mintBaggage, feeIssuerConfig.name, feeIssuerConfig.assetKind, feeIssuerConfig.displayInfo, shutdownZoeVat, - ); - mintBaggage.init(FEE_MINT_KIT, feeIssuerKit); - } else { - prepareIssuerKit(mintBaggage, shutdownZoeVat); - } + ) + ); const FeeMintIKit = harden({ feeMint: M.interface('FeeMint', { @@ -66,13 +72,13 @@ const prepareFeeMint = (zoeBaggage, feeIssuerConfig, shutdownZoeVat) => { const { facets } = this; facets.feeMintAccess === allegedFeeMintAccess || Fail`The object representing access to the fee brand mint was not provided`; - return mintBaggage.get(FEE_MINT_KIT); + return feeIssuerKit; }, getFeeIssuer() { - return mintBaggage.get(FEE_MINT_KIT).issuer; + return feeIssuerKit.issuer; }, getFeeBrand() { - return mintBaggage.get(FEE_MINT_KIT).brand; + return feeIssuerKit.brand; }, }, // feeMintAccess is an opaque durable object representing the right to get diff --git a/packages/zoe/src/zoeService/makeInvitation.js b/packages/zoe/src/zoeService/makeInvitation.js index cb59e7c549f..2c3e1be1191 100644 --- a/packages/zoe/src/zoeService/makeInvitation.js +++ b/packages/zoe/src/zoeService/makeInvitation.js @@ -1,13 +1,13 @@ // @jessie-check +import { Fail, q } from '@agoric/assert'; import { provideDurableMapStore } from '@agoric/vat-data'; -import { - AssetKind, - makeDurableIssuerKit, - prepareIssuerKit, -} from '@agoric/ertp'; +import { AssetKind, hasIssuer, reallyPrepareIssuerKit } from '@agoric/ertp'; import { InvitationElementShape } from '../typeGuards.js'; +/** + * Not deprecated because the first use below is still correct. + */ const ZOE_INVITATION_KIT = 'ZoeInvitationKit'; /** @@ -15,26 +15,27 @@ const ZOE_INVITATION_KIT = 'ZoeInvitationKit'; * @param {ShutdownWithFailure | undefined} shutdownZoeVat */ export const prepareInvitationKit = (baggage, shutdownZoeVat = undefined) => { - /** @type {IssuerKit<'set'> | undefined} */ - let invitationKit; - const invitationKitBaggage = provideDurableMapStore( baggage, ZOE_INVITATION_KIT, ); - if (!invitationKitBaggage.has(ZOE_INVITATION_KIT)) { - invitationKit = makeDurableIssuerKit( - invitationKitBaggage, - 'Zoe Invitation', - AssetKind.SET, - undefined, - shutdownZoeVat, - { elementShape: InvitationElementShape }, - ); - invitationKitBaggage.init(ZOE_INVITATION_KIT, invitationKit); - } else { - invitationKit = prepareIssuerKit(invitationKitBaggage); + if (invitationKitBaggage.has(ZOE_INVITATION_KIT)) { + // This legacy second use of ZOE_INVITATION_KIT is unneeded. + hasIssuer(invitationKitBaggage) || + Fail`Legacy use of ${q( + ZOE_INVITATION_KIT, + )} must be redundant with normal storing of issuerKit in issuerBaggage`; + // Upgrade this legacy state by simply deleting it. + invitationKitBaggage.delete(ZOE_INVITATION_KIT); } + const invitationKit = reallyPrepareIssuerKit( + invitationKitBaggage, + 'Zoe Invitation', + AssetKind.SET, + undefined, + shutdownZoeVat, + { elementShape: InvitationElementShape }, + ); return harden({ invitationIssuer: invitationKit.issuer, diff --git a/packages/zoe/src/zoeService/zoeStorageManager.js b/packages/zoe/src/zoeService/zoeStorageManager.js index 76d92b2dc22..1fde6e0afca 100644 --- a/packages/zoe/src/zoeService/zoeStorageManager.js +++ b/packages/zoe/src/zoeService/zoeStorageManager.js @@ -3,7 +3,7 @@ import { AssetKind, makeDurableIssuerKit, AmountMath, - prepareIssuerKit, + upgradeIssuerKit, } from '@agoric/ertp'; import { makeScalarBigMapStore, @@ -122,7 +122,7 @@ export const makeZoeStorageManager = ( 'zoeMintBaggageSet', ); for (const issuerBaggage of zoeMintBaggageSet.values()) { - prepareIssuerKit(issuerBaggage); + upgradeIssuerKit(issuerBaggage); } const makeZoeMint = prepareExoClass(