@agoric/ertp 0.16.3-dev-64cb69f.0 → 0.16.3-dev-c5284e4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agoric/ertp",
3
- "version": "0.16.3-dev-64cb69f.0+64cb69f",
3
+ "version": "0.16.3-dev-c5284e4.0+c5284e4",
4
4
  "description": "Electronic Rights Transfer Protocol (ERTP). A smart contract framework for exchanging electronic rights",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -39,23 +39,24 @@
39
39
  },
40
40
  "homepage": "https://github.com/Agoric/agoric-sdk#readme",
41
41
  "dependencies": {
42
- "@agoric/assert": "0.6.1-dev-64cb69f.0+64cb69f",
43
- "@agoric/notifier": "0.6.3-dev-64cb69f.0+64cb69f",
44
- "@agoric/store": "0.9.3-dev-64cb69f.0+64cb69f",
45
- "@agoric/vat-data": "0.5.3-dev-64cb69f.0+64cb69f",
46
- "@endo/eventual-send": "^1.1.0",
47
- "@endo/far": "^1.0.2",
48
- "@endo/marshal": "^1.1.0",
49
- "@endo/nat": "^5.0.2",
50
- "@endo/patterns": "^1.1.0",
51
- "@endo/promise-kit": "^1.0.2"
42
+ "@agoric/assert": "0.6.1-dev-c5284e4.0+c5284e4",
43
+ "@agoric/notifier": "0.6.3-dev-c5284e4.0+c5284e4",
44
+ "@agoric/store": "0.9.3-dev-c5284e4.0+c5284e4",
45
+ "@agoric/vat-data": "0.5.3-dev-c5284e4.0+c5284e4",
46
+ "@agoric/zone": "0.2.3-dev-c5284e4.0+c5284e4",
47
+ "@endo/eventual-send": "^1.1.2",
48
+ "@endo/far": "^1.0.4",
49
+ "@endo/marshal": "^1.3.0",
50
+ "@endo/nat": "^5.0.4",
51
+ "@endo/patterns": "^1.2.0",
52
+ "@endo/promise-kit": "^1.0.4"
52
53
  },
53
54
  "devDependencies": {
54
- "@agoric/swingset-vat": "0.32.3-dev-64cb69f.0+64cb69f",
55
- "@endo/bundle-source": "^3.0.2",
55
+ "@agoric/swingset-vat": "0.32.3-dev-c5284e4.0+c5284e4",
56
+ "@endo/bundle-source": "^3.1.0",
56
57
  "@fast-check/ava": "^1.1.5",
57
58
  "ava": "^5.3.0",
58
- "tsd": "^0.28.1"
59
+ "tsd": "^0.30.4"
59
60
  },
60
61
  "files": [
61
62
  "src",
@@ -84,7 +85,7 @@
84
85
  "access": "public"
85
86
  },
86
87
  "typeCoverage": {
87
- "atLeast": 90.6
88
+ "atLeast": 90.56
88
89
  },
89
- "gitHead": "64cb69fccd4b6ababc0f089dd80b4955a70f1da3"
90
+ "gitHead": "c5284e4721ad2702c4551f216bb8eed7abe4d7cf"
90
91
  }
@@ -0,0 +1,32 @@
1
+ import { AmountMath } from './amountMath.js';
2
+
3
+ /**
4
+ * @template {AssetKind} [K=AssetKind]
5
+ * @typedef {object} AmountStore
6
+ * @property {() => Amount<K>} getAmount
7
+ * @property {(delta: Amount<K>) => void} increment
8
+ * @property {(delta: Amount<K>) => boolean} decrement
9
+ */
10
+
11
+ /**
12
+ * @template {AssetKind} [K=AssetKind]
13
+ * @param {object} state
14
+ * @param {string} key
15
+ * @returns {AmountStore<K>}
16
+ */
17
+ export const makeAmountStore = (state, key) => {
18
+ return harden({
19
+ getAmount: () => state[key],
20
+ increment: delta => {
21
+ state[key] = AmountMath.add(state[key], delta);
22
+ },
23
+ decrement: delta => {
24
+ if (AmountMath.isGTE(state[key], delta)) {
25
+ state[key] = AmountMath.subtract(state[key], delta);
26
+ return true;
27
+ }
28
+ return false;
29
+ },
30
+ });
31
+ };
32
+ harden(makeAmountStore);
package/src/index.js CHANGED
@@ -3,3 +3,10 @@
3
3
  export * from './amountMath.js';
4
4
  export * from './issuerKit.js';
5
5
  export * from './typeGuards.js';
6
+
7
+ /**
8
+ * Importing Baggage from `@agoric/ertp` is deprecated. Import Baggage from
9
+ * `@agoric/vat-data` instead
10
+ *
11
+ * @typedef {import('@agoric/vat-data').Baggage} Baggage
12
+ */
package/src/issuerKit.js CHANGED
@@ -1,8 +1,9 @@
1
1
  // @jessie-check
2
2
 
3
- import { assert } from '@agoric/assert';
3
+ import { assert, Fail } from '@agoric/assert';
4
4
  import { assertPattern } from '@agoric/store';
5
5
  import { makeScalarBigMapStore } from '@agoric/vat-data';
6
+ import { makeDurableZone } from '@agoric/zone/durable.js';
6
7
 
7
8
  import { AssetKind, assertAssetKind } from './amountMath.js';
8
9
  import { coerceDisplayInfo } from './displayInfo.js';
@@ -10,10 +11,6 @@ import { preparePaymentLedger } from './paymentLedger.js';
10
11
 
11
12
  import './types-ambient.js';
12
13
 
13
- // TODO Why does TypeScript lose the `MapStore` typing of `Baggage` here, even
14
- // though it knows the correct type at the exporting `@agoric/vat-data`
15
- /** @typedef {import('@agoric/vat-data').Baggage} Baggage */
16
-
17
14
  /**
18
15
  * @template {AssetKind} K
19
16
  * @typedef {object} IssuerRecord
@@ -28,7 +25,9 @@ import './types-ambient.js';
28
25
  *
29
26
  * @template {AssetKind} K
30
27
  * @param {IssuerRecord<K>} issuerRecord
31
- * @param {Baggage} issuerBaggage
28
+ * @param {import('@agoric/zone').Zone} issuerZone
29
+ * @param {RecoverySetsOption} recoverySetsState Omitted from issuerRecord
30
+ * because it was added in an upgrade.
32
31
  * @param {ShutdownWithFailure} [optShutdownWithFailure] If this issuer fails in
33
32
  * the middle of an atomic action (which btw should never happen), it
34
33
  * potentially leaves its ledger in a corrupted state. If this function was
@@ -40,7 +39,8 @@ import './types-ambient.js';
40
39
  */
41
40
  const setupIssuerKit = (
42
41
  { name, assetKind, displayInfo, elementShape },
43
- issuerBaggage,
42
+ issuerZone,
43
+ recoverySetsState,
44
44
  optShutdownWithFailure = undefined,
45
45
  ) => {
46
46
  assert.typeof(name, 'string');
@@ -60,11 +60,12 @@ const setupIssuerKit = (
60
60
  /** @type {PaymentLedger<K>} */
61
61
  // @ts-expect-error could be instantiated with different subtype of AssetKind
62
62
  const { issuer, mint, brand, mintRecoveryPurse } = preparePaymentLedger(
63
- issuerBaggage,
63
+ issuerZone,
64
64
  name,
65
65
  assetKind,
66
66
  cleanDisplayInfo,
67
67
  elementShape,
68
+ recoverySetsState,
68
69
  optShutdownWithFailure,
69
70
  );
70
71
 
@@ -80,13 +81,19 @@ harden(setupIssuerKit);
80
81
 
81
82
  /** The key at which the issuer record is stored. */
82
83
  const INSTANCE_KEY = 'issuer';
84
+ /**
85
+ * The key at which the issuerKit's `RecoverySetsOption` state is stored.
86
+ * Introduced by an upgrade, so may be absent on a predecessor incarnation. See
87
+ * `RecoverySetsOption` for defaulting behavior.
88
+ */
89
+ const RECOVERY_SETS_STATE = 'recoverySetsState';
83
90
 
84
91
  /**
85
92
  * Used _only_ to upgrade a predecessor issuerKit. Use `makeDurableIssuerKit` to
86
93
  * make a new one.
87
94
  *
88
95
  * @template {AssetKind} K
89
- * @param {Baggage} issuerBaggage
96
+ * @param {import('@agoric/vat-data').Baggage} issuerBaggage
90
97
  * @param {ShutdownWithFailure} [optShutdownWithFailure] If this issuer fails in
91
98
  * the middle of an atomic action (which btw should never happen), it
92
99
  * potentially leaves its ledger in a corrupted state. If this function was
@@ -94,21 +101,46 @@ const INSTANCE_KEY = 'issuer';
94
101
  * unit of computation, like the enclosing vat, can be shutdown before
95
102
  * anything else is corrupted by that corrupted state. See
96
103
  * https://github.com/Agoric/agoric-sdk/issues/3434
104
+ * @param {RecoverySetsOption} [recoverySetsOption] Added in upgrade, so last
105
+ * and optional. See `RecoverySetsOption` for defaulting behavior.
97
106
  * @returns {IssuerKit<K>}
98
107
  */
99
108
  export const upgradeIssuerKit = (
100
109
  issuerBaggage,
101
110
  optShutdownWithFailure = undefined,
111
+ recoverySetsOption = undefined,
102
112
  ) => {
103
113
  const issuerRecord = issuerBaggage.get(INSTANCE_KEY);
104
- return setupIssuerKit(issuerRecord, issuerBaggage, optShutdownWithFailure);
114
+ const issuerZone = makeDurableZone(issuerBaggage);
115
+ const oldRecoverySetsState = issuerBaggage.has(RECOVERY_SETS_STATE)
116
+ ? issuerBaggage.get(RECOVERY_SETS_STATE)
117
+ : 'hasRecoverySets';
118
+ if (
119
+ oldRecoverySetsState === 'noRecoverySets' &&
120
+ recoverySetsOption === 'hasRecoverySets'
121
+ ) {
122
+ Fail`Cannot (yet?) upgrade from 'noRecoverySets' to 'hasRecoverySets'`;
123
+ }
124
+ if (
125
+ oldRecoverySetsState === 'hasRecoverySets' &&
126
+ recoverySetsOption === 'noRecoverySets'
127
+ ) {
128
+ Fail`Cannot (yet?) upgrade from 'hasRecoverySets' to 'noRecoverySets'`;
129
+ }
130
+ const recoverySetsState = recoverySetsOption || oldRecoverySetsState;
131
+ return setupIssuerKit(
132
+ issuerRecord,
133
+ issuerZone,
134
+ recoverySetsState,
135
+ optShutdownWithFailure,
136
+ );
105
137
  };
106
138
  harden(upgradeIssuerKit);
107
139
 
108
140
  /**
109
141
  * Does baggage already have an issuerKit?
110
142
  *
111
- * @param {Baggage} baggage
143
+ * @param {import('@agoric/vat-data').Baggage} baggage
112
144
  */
113
145
  export const hasIssuer = baggage => baggage.has(INSTANCE_KEY);
114
146
 
@@ -121,8 +153,14 @@ export const hasIssuer = baggage => baggage.has(INSTANCE_KEY);
121
153
  * typically, the amount of an invitation payment is a singleton set. Such a
122
154
  * payment is often referred to in the singular as "an invitation".)
123
155
  *
156
+ * `recoverySetsOption` added in upgrade. Note that `IssuerOptionsRecord` is
157
+ * never stored, so we never need to worry about inheriting one from a
158
+ * predecessor predating the introduction of recovery sets. See
159
+ * `RecoverySetsOption` for defaulting behavior.
160
+ *
124
161
  * @typedef {Partial<{
125
162
  * elementShape: Pattern;
163
+ * recoverySetsOption: RecoverySetsOption;
126
164
  * }>} IssuerOptionsRecord
127
165
  */
128
166
 
@@ -142,7 +180,7 @@ export const hasIssuer = baggage => baggage.has(INSTANCE_KEY);
142
180
  * basic fungible tokens.
143
181
  *
144
182
  * `displayInfo` gives information to the UI on how to display the amount.
145
- * @param {Baggage} issuerBaggage
183
+ * @param {import('@agoric/vat-data').Baggage} issuerBaggage
146
184
  * @param {string} name
147
185
  * @param {K} [assetKind]
148
186
  * @param {AdditionalDisplayInfo} [displayInfo]
@@ -163,11 +201,24 @@ export const makeDurableIssuerKit = (
163
201
  assetKind = AssetKind.NAT,
164
202
  displayInfo = harden({}),
165
203
  optShutdownWithFailure = undefined,
166
- { elementShape = undefined } = {},
204
+ { elementShape = undefined, recoverySetsOption = undefined } = {},
167
205
  ) => {
168
- const issuerData = harden({ name, assetKind, displayInfo, elementShape });
206
+ const issuerData = harden({
207
+ name,
208
+ assetKind,
209
+ displayInfo,
210
+ elementShape,
211
+ });
169
212
  issuerBaggage.init(INSTANCE_KEY, issuerData);
170
- return setupIssuerKit(issuerData, issuerBaggage, optShutdownWithFailure);
213
+ const issuerZone = makeDurableZone(issuerBaggage);
214
+ const recoverySetsState = recoverySetsOption || 'hasRecoverySets';
215
+ issuerBaggage.init(RECOVERY_SETS_STATE, recoverySetsState);
216
+ return setupIssuerKit(
217
+ issuerData,
218
+ issuerZone,
219
+ recoverySetsState,
220
+ optShutdownWithFailure,
221
+ );
171
222
  };
172
223
  harden(makeDurableIssuerKit);
173
224
 
@@ -187,7 +238,7 @@ harden(makeDurableIssuerKit);
187
238
  * basic fungible tokens.
188
239
  *
189
240
  * `displayInfo` gives information to the UI on how to display the amount.
190
- * @param {Baggage} issuerBaggage
241
+ * @param {import('@agoric/vat-data').Baggage} issuerBaggage
191
242
  * @param {string} name
192
243
  * @param {K} [assetKind]
193
244
  * @param {AdditionalDisplayInfo} [displayInfo]
@@ -211,12 +262,19 @@ export const prepareIssuerKit = (
211
262
  options = {},
212
263
  ) => {
213
264
  if (hasIssuer(issuerBaggage)) {
214
- const { elementShape: _ = undefined } = options;
215
- const issuerKit = upgradeIssuerKit(issuerBaggage, optShutdownWithFailure);
265
+ const { elementShape: _ = undefined, recoverySetsOption = undefined } =
266
+ options;
267
+ const issuerKit = upgradeIssuerKit(
268
+ issuerBaggage,
269
+ optShutdownWithFailure,
270
+ recoverySetsOption,
271
+ );
216
272
 
217
273
  // TODO check consistency with name, assetKind, displayInfo, elementShape.
218
274
  // Consistency either means that these are the same, or that they differ
219
- // in a direction we are prepared to upgrade.
275
+ // in a direction we are prepared to upgrade. Note that it is the
276
+ // responsibility of `upgradeIssuerKit` to check consistency of
277
+ // `recoverySetsOption`, so continue to not do that here.
220
278
 
221
279
  // @ts-expect-error Type parameter confusion.
222
280
  return issuerKit;
@@ -274,7 +332,7 @@ export const makeIssuerKit = (
274
332
  assetKind = AssetKind.NAT,
275
333
  displayInfo = harden({}),
276
334
  optShutdownWithFailure = undefined,
277
- { elementShape = undefined } = {},
335
+ { elementShape = undefined, recoverySetsOption = undefined } = {},
278
336
  ) =>
279
337
  makeDurableIssuerKit(
280
338
  makeScalarBigMapStore('dropped issuer kit', { durable: true }),
@@ -282,6 +340,6 @@ export const makeIssuerKit = (
282
340
  assetKind,
283
341
  displayInfo,
284
342
  optShutdownWithFailure,
285
- { elementShape },
343
+ { elementShape, recoverySetsOption },
286
344
  );
287
345
  harden(makeIssuerKit);
@@ -12,10 +12,10 @@ import {
12
12
  } from '@agoric/store';
13
13
  import '../types-ambient.js';
14
14
 
15
- /** @type {CopyBag} */
15
+ /** @type {import('@endo/patterns').CopyBag} */
16
16
  const empty = makeCopyBag([]);
17
17
 
18
- /** @type {MathHelpers<CopyBag>} */
18
+ /** @type {MathHelpers<import('@endo/patterns').CopyBag>} */
19
19
  export const copyBagMathHelpers = harden({
20
20
  doCoerce: bag => {
21
21
  mustMatch(bag, M.bag(), 'bag of amount');
package/src/payment.js CHANGED
@@ -1,26 +1,18 @@
1
1
  // @jessie-check
2
2
 
3
3
  import { initEmpty } from '@agoric/store';
4
- import { prepareExoClass } from '@agoric/vat-data';
5
-
6
- /** @typedef {import('@endo/patterns').MethodGuard} MethodGuard */
7
- /**
8
- * @template {Record<string | symbol, MethodGuard>} [T=Record<string | symbol, MethodGuard>]
9
- * @typedef {import('@endo/patterns').InterfaceGuard<T>} InterfaceGuard
10
- */
11
- /** @typedef {import('@agoric/vat-data').Baggage} Baggage */
12
4
 
5
+ // TODO Type InterfaceGuard better than InterfaceGuard<any>
13
6
  /**
14
7
  * @template {AssetKind} K
15
- * @param {Baggage} issuerBaggage
8
+ * @param {import('@agoric/zone').Zone} issuerZone
16
9
  * @param {string} name
17
10
  * @param {Brand<K>} brand
18
- * @param {InterfaceGuard} PaymentI
11
+ * @param {import('@endo/patterns').InterfaceGuard<any>} PaymentI
19
12
  * @returns {() => Payment<K>}
20
13
  */
21
- export const preparePaymentKind = (issuerBaggage, name, brand, PaymentI) => {
22
- const makePayment = prepareExoClass(
23
- issuerBaggage,
14
+ export const preparePaymentKind = (issuerZone, name, brand, PaymentI) => {
15
+ const makePayment = issuerZone.exoClass(
24
16
  `${name} payment`,
25
17
  PaymentI,
26
18
  initEmpty,
@@ -3,11 +3,6 @@
3
3
  /* eslint-disable no-use-before-define */
4
4
  import { isPromise } from '@endo/promise-kit';
5
5
  import { mustMatch, M, keyEQ } from '@agoric/store';
6
- import {
7
- provideDurableWeakMapStore,
8
- prepareExo,
9
- provide,
10
- } from '@agoric/vat-data';
11
6
  import { AmountMath } from './amountMath.js';
12
7
  import { preparePaymentKind } from './payment.js';
13
8
  import { preparePurseKind } from './purse.js';
@@ -15,8 +10,6 @@ import { preparePurseKind } from './purse.js';
15
10
  import '@agoric/store/exported.js';
16
11
  import { BrandI, makeIssuerInterfaces } from './typeGuards.js';
17
12
 
18
- /** @typedef {import('@agoric/vat-data').Baggage} Baggage */
19
-
20
13
  const { details: X, quote: q, Fail } = assert;
21
14
 
22
15
  /**
@@ -74,25 +67,31 @@ const amountShapeFromElementShape = (brand, assetKind, elementShape) => {
74
67
  * minting and transfer authority originates here.
75
68
  *
76
69
  * @template {AssetKind} K
77
- * @param {Baggage} issuerBaggage
70
+ * @param {import('@agoric/zone').Zone} issuerZone
78
71
  * @param {string} name
79
72
  * @param {K} assetKind
80
73
  * @param {DisplayInfo<K>} displayInfo
81
74
  * @param {Pattern} elementShape
75
+ * @param {RecoverySetsOption} recoverySetsState
82
76
  * @param {ShutdownWithFailure} [optShutdownWithFailure]
83
77
  * @returns {PaymentLedger<K>}
84
78
  */
85
79
  export const preparePaymentLedger = (
86
- issuerBaggage,
80
+ issuerZone,
87
81
  name,
88
82
  assetKind,
89
83
  displayInfo,
90
84
  elementShape,
85
+ recoverySetsState,
91
86
  optShutdownWithFailure = undefined,
92
87
  ) => {
93
88
  /** @type {Brand<K>} */
94
- // @ts-expect-error XXX callWhen
95
- const brand = prepareExo(issuerBaggage, `${name} brand`, BrandI, {
89
+ // Should be
90
+ // at-ts-expect-error XXX callWhen
91
+ // but ran into the usual disagreement between local lint and CI
92
+ // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
93
+ // @ts-ignore
94
+ const brand = issuerZone.exo(`${name} brand`, BrandI, {
96
95
  isMyIssuer(allegedIssuer) {
97
96
  // BrandI delays calling this method until `allegedIssuer` is a Remotable
98
97
  return allegedIssuer === issuer;
@@ -121,7 +120,7 @@ export const preparePaymentLedger = (
121
120
  amountShape,
122
121
  );
123
122
 
124
- const makePayment = preparePaymentKind(issuerBaggage, name, brand, PaymentI);
123
+ const makePayment = preparePaymentKind(issuerZone, name, brand, PaymentI);
125
124
 
126
125
  /** @type {ShutdownWithFailure} */
127
126
  const shutdownLedgerWithFailure = reason => {
@@ -139,18 +138,16 @@ export const preparePaymentLedger = (
139
138
  };
140
139
 
141
140
  /** @type {WeakMapStore<Payment, Amount>} */
142
- const paymentLedger = provideDurableWeakMapStore(
143
- issuerBaggage,
144
- 'paymentLedger',
145
- { valueShape: amountShape },
146
- );
141
+ const paymentLedger = issuerZone.weakMapStore('paymentLedger', {
142
+ valueShape: amountShape,
143
+ });
147
144
 
148
145
  /**
149
- * A withdrawn live payment is associated with the recovery set of the purse
150
- * it was withdrawn from. Let's call these "recoverable" payments. All
151
- * recoverable payments are live, but not all live payments are recoverable.
152
- * We do the bookkeeping for payment recovery with this weakmap from
153
- * recoverable payments to the recovery set they are in. A bunch of
146
+ * A (non-empty) withdrawn live payment is associated with the recovery set of
147
+ * the purse it was withdrawn from. Let's call these "recoverable" payments.
148
+ * All recoverable payments are live, but not all live payments are
149
+ * recoverable. We do the bookkeeping for payment recovery with this weakmap
150
+ * from recoverable payments to the recovery set they are in. A bunch of
154
151
  * interesting invariants here:
155
152
  *
156
153
  * - Every payment that is a key in the outer `paymentRecoverySets` weakMap is
@@ -162,12 +159,12 @@ export const preparePaymentLedger = (
162
159
  * - A purse's recovery set only contains payments withdrawn from that purse and
163
160
  * not yet consumed.
164
161
  *
162
+ * If `recoverySetsState === 'noRecoverySets'`, then nothing should ever be
163
+ * added to this WeakStore.
164
+ *
165
165
  * @type {WeakMapStore<Payment, SetStore<Payment>>}
166
166
  */
167
- const paymentRecoverySets = provideDurableWeakMapStore(
168
- issuerBaggage,
169
- 'paymentRecoverySets',
170
- );
167
+ const paymentRecoverySets = issuerZone.weakMapStore('paymentRecoverySets');
171
168
 
172
169
  /**
173
170
  * To maintain the invariants listed in the `paymentRecoverySets` comment,
@@ -178,7 +175,11 @@ export const preparePaymentLedger = (
178
175
  * @param {SetStore<Payment>} [optRecoverySet]
179
176
  */
180
177
  const initPayment = (payment, amount, optRecoverySet = undefined) => {
181
- if (optRecoverySet !== undefined) {
178
+ if (recoverySetsState === 'noRecoverySets') {
179
+ optRecoverySet === undefined ||
180
+ Fail`when recoverSetsState === 'noRecoverySets', optRecoverySet must be empty`;
181
+ }
182
+ if (optRecoverySet !== undefined && !AmountMath.isEmpty(amount)) {
182
183
  optRecoverySet.add(payment);
183
184
  paymentRecoverySets.init(payment, optRecoverySet);
184
185
  }
@@ -200,10 +201,6 @@ export const preparePaymentLedger = (
200
201
  }
201
202
  };
202
203
 
203
- /** @type {(left: Amount, right: Amount) => Amount} */
204
- const add = (left, right) => AmountMath.add(left, right, brand);
205
- /** @type {(left: Amount, right: Amount) => Amount} */
206
- const subtract = (left, right) => AmountMath.subtract(left, right, brand);
207
204
  /** @type {(allegedAmount: Amount) => Amount} */
208
205
  const coerce = allegedAmount => AmountMath.coerce(brand, allegedAmount);
209
206
  /** @type {(left: Amount, right: Amount) => boolean} */
@@ -239,17 +236,13 @@ export const preparePaymentLedger = (
239
236
  /**
240
237
  * Used by the purse code to implement purse.deposit
241
238
  *
242
- * @param {Amount} currentBalance - the current balance of the purse before a
243
- * deposit
244
- * @param {(newPurseBalance: Amount) => void} updatePurseBalance - commit the
245
- * purse balance
239
+ * @param {import('./amountStore.js').AmountStore} balanceStore
246
240
  * @param {Payment} srcPayment
247
241
  * @param {Pattern} [optAmountShape]
248
242
  * @returns {Amount}
249
243
  */
250
244
  const depositInternal = (
251
- currentBalance,
252
- updatePurseBalance,
245
+ balanceStore,
253
246
  srcPayment,
254
247
  optAmountShape = undefined,
255
248
  ) => {
@@ -261,13 +254,12 @@ export const preparePaymentLedger = (
261
254
  assertLivePayment(srcPayment);
262
255
  const srcPaymentBalance = paymentLedger.get(srcPayment);
263
256
  assertAmountConsistent(srcPaymentBalance, optAmountShape);
264
- const newPurseBalance = add(srcPaymentBalance, currentBalance);
265
257
  try {
266
258
  // COMMIT POINT
267
259
  // Move the assets in `srcPayment` into this purse, using up the
268
260
  // source payment, such that total assets are conserved.
269
261
  deletePayment(srcPayment);
270
- updatePurseBalance(newPurseBalance);
262
+ balanceStore.increment(srcPaymentBalance);
271
263
  } catch (err) {
272
264
  shutdownLedgerWithFailure(err);
273
265
  throw err;
@@ -278,30 +270,19 @@ export const preparePaymentLedger = (
278
270
  /**
279
271
  * Used by the purse code to implement purse.withdraw
280
272
  *
281
- * @param {Amount} currentBalance - the current balance of the purse before a
282
- * withdrawal
283
- * @param {(newPurseBalance: Amount) => void} updatePurseBalance - commit the
284
- * purse balance
273
+ * @param {import('./amountStore.js').AmountStore} balanceStore
285
274
  * @param {Amount} amount - the amount to be withdrawn
286
- * @param {SetStore<Payment>} recoverySet
275
+ * @param {SetStore<Payment>} [recoverySet]
287
276
  * @returns {Payment}
288
277
  */
289
- const withdrawInternal = (
290
- currentBalance,
291
- updatePurseBalance,
292
- amount,
293
- recoverySet,
294
- ) => {
278
+ const withdrawInternal = (balanceStore, amount, recoverySet = undefined) => {
295
279
  amount = coerce(amount);
296
- AmountMath.isGTE(currentBalance, amount) ||
297
- Fail`Withdrawal of ${amount} failed because the purse only contained ${currentBalance}`;
298
- const newPurseBalance = subtract(currentBalance, amount);
299
-
300
280
  const payment = makePayment();
281
+ // COMMIT POINT Move the withdrawn assets from this purse into
282
+ // payment. Total assets must remain conserved.
283
+ balanceStore.decrement(amount) ||
284
+ Fail`Withdrawal of ${amount} failed because the purse only contained ${balanceStore.getAmount()}`;
301
285
  try {
302
- // COMMIT POINT Move the withdrawn assets from this purse into
303
- // payment. Total assets must remain conserved.
304
- updatePurseBalance(newPurseBalance);
305
286
  initPayment(payment, amount, recoverySet);
306
287
  } catch (err) {
307
288
  shutdownLedgerWithFailure(err);
@@ -313,7 +294,7 @@ export const preparePaymentLedger = (
313
294
  /** @type {() => Purse<K>} */
314
295
  // @ts-expect-error type parameter confusion
315
296
  const makeEmptyPurse = preparePurseKind(
316
- issuerBaggage,
297
+ issuerZone,
317
298
  name,
318
299
  assetKind,
319
300
  brand,
@@ -322,11 +303,17 @@ export const preparePaymentLedger = (
322
303
  depositInternal,
323
304
  withdrawInternal,
324
305
  }),
306
+ recoverySetsState,
307
+ paymentRecoverySets,
325
308
  );
326
309
 
327
310
  /** @type {Issuer<K>} */
328
- // @ts-expect-error cast due to callWhen discrepancy
329
- const issuer = prepareExo(issuerBaggage, `${name} issuer`, IssuerI, {
311
+ // Should be
312
+ // at-ts-expect-error cast due to callWhen discrepancy
313
+ // but ran into the usual disagreement between local lint and CI
314
+ // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
315
+ // @ts-ignore
316
+ const issuer = issuerZone.exo(`${name} issuer`, IssuerI, {
330
317
  getBrand() {
331
318
  return brand;
332
319
  },
@@ -379,20 +366,28 @@ export const preparePaymentLedger = (
379
366
  * Because the `mintRecoveryPurse` is placed in baggage, even if the caller of
380
367
  * `makeIssuerKit` drops it on the floor, it can still be recovered in an
381
368
  * emergency upgrade.
382
- *
383
- * @type {Purse<K>}
384
369
  */
385
- const mintRecoveryPurse = provide(issuerBaggage, 'mintRecoveryPurse', () =>
386
- makeEmptyPurse(),
370
+ // Should be
371
+ // at-ts-expect-error checked cast
372
+ // but ran into the usual disagreement between local lint and IDE lint.
373
+ // Don't know yet about lint under CI.
374
+ // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
375
+ // @ts-ignore
376
+ const mintRecoveryPurse = /** @type {Purse<K>} */ (
377
+ issuerZone.makeOnce('mintRecoveryPurse', () => makeEmptyPurse())
387
378
  );
388
379
 
389
380
  /** @type {Mint<K>} */
390
- const mint = prepareExo(issuerBaggage, `${name} mint`, MintI, {
381
+ const mint = issuerZone.exo(`${name} mint`, MintI, {
391
382
  getIssuer() {
392
383
  return issuer;
393
384
  },
394
385
  mintPayment(newAmount) {
395
- // @ts-expect-error checked cast
386
+ // Should be
387
+ // at-ts-expect-error checked cast
388
+ // but ran into the usual disagreement between local lint and CI
389
+ // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
390
+ // @ts-ignore
396
391
  newAmount = coerce(newAmount);
397
392
  mustMatch(newAmount, amountShape, 'minted amount');
398
393
  // `rawPayment` is not associated with any recovery set, and
package/src/purse.js CHANGED
@@ -1,45 +1,72 @@
1
- import { M } from '@agoric/store';
2
- import { prepareExoClassKit, makeScalarBigSetStore } from '@agoric/vat-data';
1
+ import { M, makeCopySet } from '@agoric/store';
3
2
  import { AmountMath } from './amountMath.js';
4
3
  import { makeTransientNotifierKit } from './transientNotifier.js';
5
-
6
- // TODO `InterfaceGuard` type parameter
7
- /** @typedef {import('@endo/patterns').InterfaceGuard} InterfaceGuard */
8
- /** @typedef {import('@agoric/vat-data').Baggage} Baggage */
4
+ import { makeAmountStore } from './amountStore.js';
9
5
 
10
6
  const { Fail } = assert;
11
7
 
8
+ const EMPTY_COPY_SET = makeCopySet([]);
9
+
10
+ // TODO Type InterfaceGuard better than InterfaceGuard<any>
12
11
  /**
13
- * @param {Baggage} issuerBaggage
12
+ * @param {import('@agoric/zone').Zone} issuerZone
14
13
  * @param {string} name
15
14
  * @param {AssetKind} assetKind
16
15
  * @param {Brand} brand
17
16
  * @param {{
18
- * purse: InterfaceGuard;
19
- * depositFacet: InterfaceGuard;
17
+ * purse: import('@endo/patterns').InterfaceGuard<any>;
18
+ * depositFacet: import('@endo/patterns').InterfaceGuard<any>;
20
19
  * }} PurseIKit
21
20
  * @param {{
22
21
  * depositInternal: any;
23
22
  * withdrawInternal: any;
24
23
  * }} purseMethods
24
+ * @param {RecoverySetsOption} recoverySetsState
25
+ * @param {WeakMapStore<Payment, SetStore<Payment>>} paymentRecoverySets
25
26
  */
26
27
  export const preparePurseKind = (
27
- issuerBaggage,
28
+ issuerZone,
28
29
  name,
29
30
  assetKind,
30
31
  brand,
31
32
  PurseIKit,
32
33
  purseMethods,
34
+ recoverySetsState,
35
+ paymentRecoverySets,
33
36
  ) => {
34
37
  const amountShape = brand.getAmountShape();
35
38
 
36
39
  // Note: Virtual for high cardinality, but *not* durable, and so
37
40
  // broken across an upgrade.
41
+ // TODO propagate zonifying to notifiers, maybe?
38
42
  const { provideNotifier, update: updateBalance } = makeTransientNotifierKit();
39
43
 
40
- const updatePurseBalance = (state, newPurseBalance, purse) => {
41
- state.currentBalance = newPurseBalance;
42
- updateBalance(purse, purse.getCurrentAmount());
44
+ /**
45
+ * If `recoverySetsState === 'hasRecoverySets'` (the normal state), then just
46
+ * return `state.recoverySet`.
47
+ *
48
+ * If `recoverySetsState === 'noRecoverySets'`, return `undefined`. Callers
49
+ * must be aware that the `undefined` return happens iff `recoverySetsState
50
+ * === 'noRecoverySets'`, and to avoid storing or retrieving anything from the
51
+ * actual recovery set.
52
+ *
53
+ * @param {{ recoverySet: SetStore<Payment> }} state
54
+ * @returns {SetStore<Payment> | undefined}
55
+ */
56
+ const maybeRecoverySet = state => {
57
+ const { recoverySet } = state;
58
+ if (recoverySetsState === 'hasRecoverySets') {
59
+ return recoverySet;
60
+ } else {
61
+ recoverySetsState === 'noRecoverySets' ||
62
+ Fail`recoverSetsState must be noRecoverySets if it isn't hasRecoverSets`;
63
+ paymentRecoverySets !== undefined ||
64
+ Fail`paymentRecoverySets must always be defined`;
65
+ recoverySet.getSize() === 0 ||
66
+ Fail`With noRecoverySets, recoverySet must be empty`;
67
+
68
+ return undefined;
69
+ }
43
70
  };
44
71
 
45
72
  // - This kind is a pair of purse and depositFacet that have a 1:1
@@ -49,17 +76,14 @@ export const preparePurseKind = (
49
76
  // that created depositFacet as needed. But this approach ensures a constant
50
77
  // identity for the facet and exercises the multi-faceted object style.
51
78
  const { depositInternal, withdrawInternal } = purseMethods;
52
- const makePurseKit = prepareExoClassKit(
53
- issuerBaggage,
79
+ const makePurseKit = issuerZone.exoClassKit(
54
80
  `${name} Purse`,
55
81
  PurseIKit,
56
82
  () => {
57
83
  const currentBalance = AmountMath.makeEmpty(brand, assetKind);
58
84
 
59
85
  /** @type {SetStore<Payment>} */
60
- const recoverySet = makeScalarBigSetStore('recovery set', {
61
- durable: true,
62
- });
86
+ const recoverySet = issuerZone.detached().setStore('recovery set');
63
87
 
64
88
  return {
65
89
  currentBalance,
@@ -72,28 +96,36 @@ export const preparePurseKind = (
72
96
  // PurseI does *not* delay `deposit` until `srcPayment` is fulfulled.
73
97
  // See the comments on PurseI.deposit in typeGuards.js
74
98
  const { state } = this;
99
+ const { purse } = this.facets;
100
+ const balanceStore = makeAmountStore(state, 'currentBalance');
75
101
  // Note COMMIT POINT within deposit.
76
- return depositInternal(
77
- state.currentBalance,
78
- newPurseBalance =>
79
- updatePurseBalance(state, newPurseBalance, this.facets.purse),
102
+ const srcPaymentBalance = depositInternal(
103
+ balanceStore,
80
104
  srcPayment,
81
105
  optAmountShape,
82
106
  );
107
+ updateBalance(purse, balanceStore.getAmount());
108
+ return srcPaymentBalance;
83
109
  },
84
110
  withdraw(amount) {
85
111
  const { state } = this;
112
+ const { purse } = this.facets;
113
+
114
+ const optRecoverySet = maybeRecoverySet(state);
115
+ const balanceStore = makeAmountStore(state, 'currentBalance');
86
116
  // Note COMMIT POINT within withdraw.
87
- return withdrawInternal(
88
- state.currentBalance,
89
- newPurseBalance =>
90
- updatePurseBalance(state, newPurseBalance, this.facets.purse),
117
+ const payment = withdrawInternal(
118
+ balanceStore,
91
119
  amount,
92
- state.recoverySet,
120
+ optRecoverySet,
93
121
  );
122
+ updateBalance(purse, balanceStore.getAmount());
123
+ return payment;
94
124
  },
95
125
  getCurrentAmount() {
96
- return this.state.currentBalance;
126
+ const { state } = this;
127
+ const balanceStore = makeAmountStore(state, 'currentBalance');
128
+ return balanceStore.getAmount();
97
129
  },
98
130
  getCurrentAmountNotifier() {
99
131
  return provideNotifier(this.facets.purse);
@@ -107,18 +139,27 @@ export const preparePurseKind = (
107
139
  },
108
140
 
109
141
  getRecoverySet() {
110
- return this.state.recoverySet.snapshot();
142
+ const { state } = this;
143
+ const optRecoverySet = maybeRecoverySet(state);
144
+ if (optRecoverySet === undefined) {
145
+ return EMPTY_COPY_SET;
146
+ }
147
+ return optRecoverySet.snapshot();
111
148
  },
112
149
  recoverAll() {
113
150
  const { state, facets } = this;
114
151
  let amount = AmountMath.makeEmpty(brand, assetKind);
115
- for (const payment of state.recoverySet.keys()) {
152
+ const optRecoverySet = maybeRecoverySet(state);
153
+ if (optRecoverySet === undefined) {
154
+ return amount; // empty at this time
155
+ }
156
+ for (const payment of optRecoverySet.keys()) {
116
157
  // This does cause deletions from the set while iterating,
117
158
  // but this special case is allowed.
118
159
  const delta = facets.purse.deposit(payment);
119
160
  amount = AmountMath.add(amount, delta, brand);
120
161
  }
121
- state.recoverySet.getSize() === 0 ||
162
+ optRecoverySet.getSize() === 0 ||
122
163
  Fail`internal: Remaining unrecovered payments: ${facets.purse.getRecoverySet()}`;
123
164
  return amount;
124
165
  },
@@ -2,11 +2,6 @@
2
2
 
3
3
  /// <reference types="ses"/>
4
4
 
5
- /**
6
- * @template {Key} [K=Key]
7
- * @typedef {import('@endo/patterns').CopyBag<K>} CopyBag
8
- */
9
-
10
5
  /**
11
6
  * @template {AssetKind} [K=AssetKind]
12
7
  * @typedef {object} Amount Amounts are descriptions of digital assets,
@@ -22,8 +17,8 @@
22
17
  */
23
18
 
24
19
  /**
25
- * @typedef {NatValue | SetValue | CopySet | CopyBag} AmountValue An
26
- * `AmountValue` describes a set or quantity of assets that can be owned or
20
+ * @typedef {NatValue | SetValue | CopySet | import('@endo/patterns').CopyBag} AmountValue
21
+ * An `AmountValue` describes a set or quantity of assets that can be owned or
27
22
  * shared.
28
23
  *
29
24
  * A fungible `AmountValue` uses a non-negative bigint to represent a quantity
@@ -58,7 +53,7 @@
58
53
  * : K extends 'copySet'
59
54
  * ? CopySet
60
55
  * : K extends 'copyBag'
61
- * ? CopyBag
56
+ * ? import('@endo/patterns').CopyBag
62
57
  * : never} AssetValueForKind
63
58
  */
64
59
 
@@ -70,7 +65,7 @@
70
65
  * ? 'set'
71
66
  * : V extends CopySet
72
67
  * ? 'copySet'
73
- * : V extends CopyBag
68
+ * : V extends import('@endo/patterns').CopyBag
74
69
  * ? 'copyBag'
75
70
  * : never} AssetKindForValue
76
71
  */
@@ -178,8 +173,9 @@
178
173
  * @template {AssetKind} [K=AssetKind]
179
174
  * @typedef {object} PaymentLedger
180
175
  * @property {Mint<K>} mint
181
- * @property {Purse<K>} mintRecoveryPurse Useful only to get the recovery set
182
- * associated with minted payments that are still live.
176
+ * @property {Purse<K>} mintRecoveryPurse Externally useful only if this issuer
177
+ * uses recovery sets. Can be used to get the recovery set associated with
178
+ * minted payments that are still live.
183
179
  * @property {Issuer<K>} issuer
184
180
  * @property {Brand<K>} brand
185
181
  */
@@ -188,8 +184,9 @@
188
184
  * @template {AssetKind} [K=AssetKind]
189
185
  * @typedef {object} IssuerKit
190
186
  * @property {Mint<K>} mint
191
- * @property {Purse<K>} mintRecoveryPurse Useful only to get the recovery set
192
- * associated with minted payments that are still live.
187
+ * @property {Purse<K>} mintRecoveryPurse Externally useful only if this issuer
188
+ * uses recovery sets. Can be used to get the recovery set associated with
189
+ * minted payments that are still live.
193
190
  * @property {Issuer<K>} issuer
194
191
  * @property {Brand<K>} brand
195
192
  * @property {DisplayInfo} displayInfo
@@ -222,6 +219,24 @@
222
219
 
223
220
  // /////////////////////////// Purse / Payment /////////////////////////////////
224
221
 
222
+ /**
223
+ * Issuers first became durable with mandatory recovery sets. Later they were
224
+ * made optional, but there is no support for converting from one state to the
225
+ * other. Thus, absence of a `RecoverySetsOption` state is equivalent to
226
+ * `'hasRecoverySets'`. In the absence of a `recoverySetsOption` parameter,
227
+ * upgradeIssuerKit defaults to the predecessor's `RecoverySetsOption` state, or
228
+ * `'hasRecoverySets'` if none.
229
+ *
230
+ * At this time, issuers started in one of the states (`'noRecoverySets'`, or
231
+ * `'hasRecoverySets'`) cannot be converted to the other on upgrade. If this
232
+ * transition is needed, it can likely be supported in a future upgrade. File an
233
+ * issue on github and explain what you need and why.
234
+ *
235
+ * @typedef {'hasRecoverySets' | 'noRecoverySets'} RecoverySetsOption
236
+ */
237
+
238
+ // /////////////////////////// Purse / Payment /////////////////////////////////
239
+
225
240
  /**
226
241
  * @callback DepositFacetReceive
227
242
  * @param {Payment} payment
@@ -281,10 +296,14 @@
281
296
  * can spend the assets at stake on other things. Afterwards, if the recipient
282
297
  * of the original check finally gets around to depositing it, their deposit
283
298
  * fails.
299
+ *
300
+ * Returns an empty set if this issuer does not support recovery sets.
284
301
  * @property {() => Amount<K>} recoverAll For use in emergencies, such as coming
285
302
  * back from a traumatic crash and upgrade. This deposits all the payments in
286
303
  * this purse's recovery set into the purse itself, returning the total amount
287
304
  * of assets recovered.
305
+ *
306
+ * Returns an empty amount if this issuer does not support recovery sets.
288
307
  */
289
308
 
290
309
  /**
package/src/types.js CHANGED
@@ -3,11 +3,6 @@ export {};
3
3
 
4
4
  /// <reference types="ses"/>
5
5
 
6
- /**
7
- * @template {Key} [K=Key]
8
- * @typedef {import('@endo/patterns').CopyBag<K>} CopyBag
9
- */
10
-
11
6
  /**
12
7
  * @template {AssetKind} [K=AssetKind]
13
8
  * @typedef {object} Amount Amounts are descriptions of digital assets,
@@ -23,8 +18,8 @@ export {};
23
18
  */
24
19
 
25
20
  /**
26
- * @typedef {NatValue | SetValue | CopySet | CopyBag} AmountValue An
27
- * `AmountValue` describes a set or quantity of assets that can be owned or
21
+ * @typedef {NatValue | SetValue | CopySet | import('@endo/patterns').CopyBag} AmountValue
22
+ * An `AmountValue` describes a set or quantity of assets that can be owned or
28
23
  * shared.
29
24
  *
30
25
  * A fungible `AmountValue` uses a non-negative bigint to represent a quantity
@@ -59,7 +54,7 @@ export {};
59
54
  * : K extends 'copySet'
60
55
  * ? CopySet
61
56
  * : K extends 'copyBag'
62
- * ? CopyBag
57
+ * ? import('@endo/patterns').CopyBag
63
58
  * : never} AssetValueForKind
64
59
  */
65
60
 
@@ -71,7 +66,7 @@ export {};
71
66
  * ? 'set'
72
67
  * : V extends CopySet
73
68
  * ? 'copySet'
74
- * : V extends CopyBag
69
+ * : V extends import('@endo/patterns').CopyBag
75
70
  * ? 'copyBag'
76
71
  * : never} AssetKindForValue
77
72
  */
@@ -179,8 +174,9 @@ export {};
179
174
  * @template {AssetKind} [K=AssetKind]
180
175
  * @typedef {object} PaymentLedger
181
176
  * @property {Mint<K>} mint
182
- * @property {Purse<K>} mintRecoveryPurse Useful only to get the recovery set
183
- * associated with minted payments that are still live.
177
+ * @property {Purse<K>} mintRecoveryPurse Externally useful only if this issuer
178
+ * uses recovery sets. Can be used to get the recovery set associated with
179
+ * minted payments that are still live.
184
180
  * @property {Issuer<K>} issuer
185
181
  * @property {Brand<K>} brand
186
182
  */
@@ -189,8 +185,9 @@ export {};
189
185
  * @template {AssetKind} [K=AssetKind]
190
186
  * @typedef {object} IssuerKit
191
187
  * @property {Mint<K>} mint
192
- * @property {Purse<K>} mintRecoveryPurse Useful only to get the recovery set
193
- * associated with minted payments that are still live.
188
+ * @property {Purse<K>} mintRecoveryPurse Externally useful only if this issuer
189
+ * uses recovery sets. Can be used to get the recovery set associated with
190
+ * minted payments that are still live.
194
191
  * @property {Issuer<K>} issuer
195
192
  * @property {Brand<K>} brand
196
193
  * @property {DisplayInfo} displayInfo
@@ -223,6 +220,24 @@ export {};
223
220
 
224
221
  // /////////////////////////// Purse / Payment /////////////////////////////////
225
222
 
223
+ /**
224
+ * Issuers first became durable with mandatory recovery sets. Later they were
225
+ * made optional, but there is no support for converting from one state to the
226
+ * other. Thus, absence of a `RecoverySetsOption` state is equivalent to
227
+ * `'hasRecoverySets'`. In the absence of a `recoverySetsOption` parameter,
228
+ * upgradeIssuerKit defaults to the predecessor's `RecoverySetsOption` state, or
229
+ * `'hasRecoverySets'` if none.
230
+ *
231
+ * At this time, issuers started in one of the states (`'noRecoverySets'`, or
232
+ * `'hasRecoverySets'`) cannot be converted to the other on upgrade. If this
233
+ * transition is needed, it can likely be supported in a future upgrade. File an
234
+ * issue on github and explain what you need and why.
235
+ *
236
+ * @typedef {'hasRecoverySets' | 'noRecoverySets'} RecoverySetsOption
237
+ */
238
+
239
+ // /////////////////////////// Purse / Payment /////////////////////////////////
240
+
226
241
  /**
227
242
  * @callback DepositFacetReceive
228
243
  * @param {Payment} payment
@@ -282,10 +297,14 @@ export {};
282
297
  * can spend the assets at stake on other things. Afterwards, if the recipient
283
298
  * of the original check finally gets around to depositing it, their deposit
284
299
  * fails.
300
+ *
301
+ * Returns an empty set if this issuer does not support recovery sets.
285
302
  * @property {() => Amount<K>} recoverAll For use in emergencies, such as coming
286
303
  * back from a traumatic crash and upgrade. This deposits all the payments in
287
304
  * this purse's recovery set into the purse itself, returning the total amount
288
305
  * of assets recovered.
306
+ *
307
+ * Returns an empty amount if this issuer does not support recovery sets.
289
308
  */
290
309
 
291
310
  /**