diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 6dbcacdbc99..a5a24ebb559 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -4,21 +4,31 @@ const process = require('process'); const lintTypes = !!process.env.AGORIC_ESLINT_TYPES; -const deprecatedTerminology = [ +const notLoanDeprecated = [ ['currency', 'brand, asset or another descriptor'], - ['loan', 'debt'], ['blacklist', 'denylist'], ['whitelist', 'allowlist'], ['RUN', 'IST', '/RUN/'], -].flatMap(([bad, good, badRgx = `/${bad}/i`]) => - [ - ['Literal', 'value'], - ['TemplateElement', 'value.raw'], - ['Identifier', 'name'], - ].map(([selectorType, field]) => ({ - selector: `${selectorType}[${field}=${badRgx}]`, - message: `Use '${good}' instead of deprecated '${bad}'`, - })), +]; +const allDeprecated = [...notLoanDeprecated, ['loan', 'debt']]; + +const deprecatedTerminology = Object.fromEntries( + Object.entries({ + all: allDeprecated, + notLoan: notLoanDeprecated, + }).map(([category, deprecated]) => [ + category, + deprecated.flatMap(([bad, good, badRgx = `/${bad}/i`]) => + [ + ['Literal', 'value'], + ['TemplateElement', 'value.raw'], + ['Identifier', 'name'], + ].map(([selectorType, field]) => ({ + selector: `${selectorType}[${field}=${badRgx}]`, + message: `Use '${good}' instead of deprecated '${bad}'`, + })), + ), + ]), ); module.exports = { @@ -85,7 +95,14 @@ module.exports = { // instead, and upgrade to 'error' when possible // '@jessie.js/safe-await-separator': 'warn', // TODO upgrade this (or a subset) to 'error' - 'no-restricted-syntax': ['warn', ...deprecatedTerminology], + 'no-restricted-syntax': ['warn', ...deprecatedTerminology.all], + }, + }, + { + // Allow "loan" contracts to mention the word "loan". + files: ['packages/zoe/src/contracts/loan/*.js'], + rules: { + 'no-restricted-syntax': ['warn', ...deprecatedTerminology.notLoan], }, }, { diff --git a/packages/ERTP/src/types-ambient.js b/packages/ERTP/src/types-ambient.js index b9734f14a57..d5f9152ca82 100644 --- a/packages/ERTP/src/types-ambient.js +++ b/packages/ERTP/src/types-ambient.js @@ -335,7 +335,7 @@ * @property {() => Amount} getCurrentAmount * Get the amount contained in this purse. * - * @property {() => Notifier>} getCurrentAmountNotifier + * @property {() => LatestTopic>} getCurrentAmountNotifier * Get a lossy notifier for changes to this purse's balance. * * @property {PurseDeposit} deposit diff --git a/packages/agoric-cli/src/commands/reserve.js b/packages/agoric-cli/src/commands/reserve.js index 11075d291b9..21c12156206 100644 --- a/packages/agoric-cli/src/commands/reserve.js +++ b/packages/agoric-cli/src/commands/reserve.js @@ -9,7 +9,7 @@ import { outputActionAndHint } from '../lib/wallet.js'; /** * @param {import('anylogger').Logger} _logger - * @param io + * @param {*} io */ export const makeReserveCommand = (_logger, io = {}) => { const { stdout = process.stdout, stderr = process.stderr, now } = io; diff --git a/packages/agoric-cli/src/lib/format.js b/packages/agoric-cli/src/lib/format.js index 7795bd84ff3..548cf9fc9be 100644 --- a/packages/agoric-cli/src/lib/format.js +++ b/packages/agoric-cli/src/lib/format.js @@ -93,7 +93,7 @@ export const asBoardRemote = x => { /** * Summarize the balances array as user-facing informative tuples - + * * @param {import('@agoric/smart-wallet/src/smartWallet').CurrentWalletRecord['purses']} purses * @param {AssetDescriptor[]} assets */ diff --git a/packages/inter-protocol/src/price/fluxAggregatorKit.js b/packages/inter-protocol/src/price/fluxAggregatorKit.js index 713fef3ca76..854b1c1548f 100644 --- a/packages/inter-protocol/src/price/fluxAggregatorKit.js +++ b/packages/inter-protocol/src/price/fluxAggregatorKit.js @@ -82,7 +82,6 @@ const priceDescriptionFromQuote = quote => quote.quoteAmount.value[0]; * @param {StorageNode} storageNode * @param {() => PublishKit} makeDurablePublishKit * @param {import('@agoric/zoe/src/contractSupport/recorder.js').MakeRecorder} makeRecorder - * @returns a method to call once to create the prepared kit */ export const prepareFluxAggregatorKit = async ( baggage, diff --git a/packages/internal/src/callback.js b/packages/internal/src/callback.js index adbaa834da7..79e8872fc15 100644 --- a/packages/internal/src/callback.js +++ b/packages/internal/src/callback.js @@ -14,7 +14,7 @@ const ownKeys = /** * @template T - * @typedef {(...args: Parameters>) => Farable} AttenuatorMaker + * @typedef {(...args: Parameters>) => Farable} MakeAttenuator */ /** @@ -192,9 +192,9 @@ harden(isCallback); /** * Prepare an attenuator class whose methods can be redirected via callbacks. * - * @template {{ [K in PropertyKey]: (this: any, ...args: unknown[]) => any}} Methods + * @template {PropertyKey} M * @param {import('@agoric/zone').Zone} zone The zone in which to allocate attenuators. - * @param {(keyof Methods)[]} methodNames Methods to forward. + * @param {M[]} methodNames Methods to forward. * @param {object} opts * @param {InterfaceGuard} [opts.interfaceGuard] An interface guard for the * new attenuator. @@ -207,7 +207,8 @@ export const prepareAttenuator = ( ) => { /** * @typedef {(this: any, ...args: unknown[]) => any} Method - * @typedef {{ [K in keyof Methods]?: Callback | null}} Overrides + * @typedef {{ [K in M]?: Callback | null}} Overrides + * @typedef {{ [K in M]: (this: any, ...args: unknown[]) => any }} Methods */ const methods = /** @type {Methods} */ ( fromEntries( @@ -307,7 +308,6 @@ harden(prepareAttenuator); /** * Prepare an attenuator whose methodNames are derived from the interfaceGuard. * - * @template {{ [K in PropertyKey]: (this: any, ...args: unknown[]) => any}} Methods * @param {import('@agoric/zone').Zone} zone * @param {InterfaceGuard} interfaceGuard * @param {object} [opts] @@ -316,8 +316,10 @@ harden(prepareAttenuator); export const prepareGuardedAttenuator = (zone, interfaceGuard, opts = {}) => { const { methodGuards } = interfaceGuard; const methodNames = ownKeys(methodGuards); - return /** @type {AttenuatorMaker} */ ( - prepareAttenuator(zone, methodNames, { ...opts, interfaceGuard }) - ); + const makeAttenuator = prepareAttenuator(zone, methodNames, { + ...opts, + interfaceGuard, + }); + return /** @type {MakeAttenuator} */ (makeAttenuator); }; harden(prepareGuardedAttenuator); diff --git a/packages/internal/test/test-callback.js b/packages/internal/test/test-callback.js index 791a564cd82..6b020632608 100644 --- a/packages/internal/test/test-callback.js +++ b/packages/internal/test/test-callback.js @@ -286,6 +286,7 @@ test('makeAttenuator', async t => { throw Error('unexpected original.m3'); }, }); + // @ts-expect-error deliberate: omitted method t.throws(() => makeAttenuator({ target, overrides: { m3: null } }), { message: `"Attenuator" overrides["m3"] not allowed by methodNames`, }); @@ -299,6 +300,7 @@ test('makeAttenuator', async t => { message: `unimplemented "Attenuator" method "m1"`, }); await t.throwsAsync(() => atE.m2(), { message: `unexpected original.m2` }); + // @ts-expect-error deliberate: omitted method t.throws(() => atE.m3(), { message: /not a function/ }); await t.throwsAsync(() => atE.m4(), { message: /target has no method "m4"/ }); @@ -325,6 +327,7 @@ test('makeAttenuator', async t => { const p2 = atSync.m2(); t.assert(p2 instanceof Promise); t.is(await p2, 'return abc'); + // @ts-expect-error deliberate: omitted method t.throws(() => atSync.m3(), { message: /not a function/ }); t.throws(() => atSync.m4(), { message: /not a function/ }); }); diff --git a/packages/notifier/src/asyncIterableAdaptor.js b/packages/notifier/src/asyncIterableAdaptor.js index aa2e79b8528..b8ab685496d 100644 --- a/packages/notifier/src/asyncIterableAdaptor.js +++ b/packages/notifier/src/asyncIterableAdaptor.js @@ -97,7 +97,7 @@ export const observeIteration = (asyncIterableP, iterationObserver) => { * states are assumed irrelevant and dropped. * * @template T - * @param {ERef>} notifierP + * @param {ERef>} notifierP * @param {Partial>} iterationObserver * @returns {Promise} */ diff --git a/packages/notifier/src/subscribe.js b/packages/notifier/src/subscribe.js index 13f1bb92c4f..0ad83721937 100644 --- a/packages/notifier/src/subscribe.js +++ b/packages/notifier/src/subscribe.js @@ -63,15 +63,17 @@ const reconnectAsNeeded = async (getter, seed = []) => { * Create a near iterable that corresponds to a potentially far one. * * @template T - * @param {ERef>} itP + * @param {ERef>} itP */ export const subscribe = itP => Far('AsyncIterable', { [Symbol.asyncIterator]: () => { const it = E(itP)[Symbol.asyncIterator](); - return Far('AsyncIterator', { + const self = Far('AsyncIterableIterator', { + [Symbol.asyncIterator]: () => self, next: async () => E(it).next(), }); + return self; }, }); @@ -84,12 +86,13 @@ export const subscribe = itP => * @param {ERef>} topic * @param {ERef>} nextCellP * PublicationRecord corresponding with the first iteration result - * @returns {ForkableAsyncIterator} + * @returns {ForkableAsyncIterableIterator} */ const makeEachIterator = (topic, nextCellP) => { // To understand the implementation, start with // https://web.archive.org/web/20160404122250/http://wiki.ecmascript.org/doku.php?id=strawman:concurrency#infinite_queue - return Far('EachIterator', { + const self = Far('EachIterator', { + [Symbol.asyncIterator]: () => self, next: () => { const { head: resultP, @@ -124,6 +127,7 @@ const makeEachIterator = (topic, nextCellP) => { }, fork: () => makeEachIterator(topic, nextCellP), }); + return self; }; /** @@ -158,7 +162,7 @@ harden(subscribeEach); * @param {ERef>} topic * @param {bigint} [localUpdateCount] * @param {IteratorReturnResult} [terminalResult] - * @returns {ForkableAsyncIterator} + * @returns {ForkableAsyncIterableIterator} */ const cloneLatestIterator = (topic, localUpdateCount, terminalResult) => { let mutex = Promise.resolve(); @@ -192,8 +196,9 @@ const cloneLatestIterator = (topic, localUpdateCount, terminalResult) => { return harden({ done: false, value }); }; - return Far('LatestIterator', { + const self = Far('LatestIterator', { fork: () => cloneLatestIterator(topic, localUpdateCount, terminalResult), + [Symbol.asyncIterator]: () => self, next: async () => { // In this adaptor, once `next()` is called and returns an unresolved // promise, further `next()` calls will also return unresolved promises @@ -228,12 +233,13 @@ const cloneLatestIterator = (topic, localUpdateCount, terminalResult) => { return nextResult; }, }); + return self; }; /** * @template T * @param {ERef>} topic - * @returns {ForkableAsyncIterator} + * @returns {ForkableAsyncIterableIterator} */ const makeLatestIterator = topic => cloneLatestIterator(topic); diff --git a/packages/notifier/src/types-ambient.js b/packages/notifier/src/types-ambient.js index c979c8e3fc2..64536ef8b9d 100644 --- a/packages/notifier/src/types-ambient.js +++ b/packages/notifier/src/types-ambient.js @@ -27,6 +27,13 @@ * into multiple independent ForkableAsyncIterators starting from that position. */ +/** + * @template T + * @template [TReturn=any] + * @template [TNext=undefined] + * @typedef {ForkableAsyncIterator & { [Symbol.asyncIterator](): ForkableAsyncIterableIterator }} ForkableAsyncIterableIterator + */ + /** * @template T * @template [TReturn=any] diff --git a/packages/smart-wallet/src/walletFactory.js b/packages/smart-wallet/src/walletFactory.js index 3d67584d8e7..91932d486ca 100644 --- a/packages/smart-wallet/src/walletFactory.js +++ b/packages/smart-wallet/src/walletFactory.js @@ -115,7 +115,7 @@ export const makeAssetRegistry = assetPublisher => { * @typedef {import('@agoric/vats').NameHub} NameHub * * @typedef {{ - * getAssetSubscription: () => ERef> + * getAssetSubscription: () => ERef> * }} AssetPublisher */ diff --git a/packages/smart-wallet/test/test-addAsset.js b/packages/smart-wallet/test/test-addAsset.js index 907955c19be..2ba5aa0f58e 100644 --- a/packages/smart-wallet/test/test-addAsset.js +++ b/packages/smart-wallet/test/test-addAsset.js @@ -4,6 +4,7 @@ import { E } from '@endo/far'; import { buildRootObject as buildBankVatRoot } from '@agoric/vats/src/vat-bank.js'; import { makeIssuerKit } from '@agoric/ertp'; import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; +import { makeScalarMapStore } from '@agoric/store'; import { makeDefaultTestContext } from './contexts.js'; import { makeMockTestSpace } from './supports.js'; @@ -13,7 +14,10 @@ const test = anyTest; test.before(async t => { const withBankManager = async () => { const noBridge = undefined; - const bankManager = E(buildBankVatRoot()).makeBankManager(noBridge); + const baggage = makeScalarMapStore('baggage'); + const bankManager = E( + buildBankVatRoot(undefined, undefined, baggage), + ).makeBankManager(noBridge); const noop = () => {}; const space0 = await makeMockTestSpace(noop); space0.produce.bankManager.reset(); diff --git a/packages/swingset-liveslots/src/virtualObjectManager.js b/packages/swingset-liveslots/src/virtualObjectManager.js index 468a9b6860f..408777452ee 100644 --- a/packages/swingset-liveslots/src/virtualObjectManager.js +++ b/packages/swingset-liveslots/src/virtualObjectManager.js @@ -306,7 +306,13 @@ const insistSameCapData = (oldCD, newCD) => { * @param {import('@endo/marshal').FromCapData} unserialize Unserializer for this vat * @param {*} assertAcceptableSyscallCapdataSize Function to check for oversized * syscall params - * @param {import('./types').LiveSlotsOptions} liveSlotsOptions + * @param {import('./types').LiveSlotsOptions} [liveSlotsOptions] + * @param {{ WeakMap: typeof WeakMap, WeakSet: typeof WeakSet }} [powers] + * Specifying the underlying WeakMap/WeakSet objects to wrap with + * VirtualObjectAwareWeakMap/Set. By default, capture the ones currently + * defined on `globalThis` when the maker is invoked, to avoid infinite + * recursion if our returned WeakMap/WeakSet wrappers are subsequently installed + * on globalThis. * * @returns {object} a new virtual object manager. * @@ -348,6 +354,7 @@ export const makeVirtualObjectManager = ( unserialize, assertAcceptableSyscallCapdataSize, liveSlotsOptions = {}, + { WeakMap, WeakSet } = globalThis, ) => { const { allowStateShapeChanges = false } = liveSlotsOptions; diff --git a/packages/vats/src/core/basic-behaviors.js b/packages/vats/src/core/basic-behaviors.js index 8f2044f4a7b..4dc564d9d63 100644 --- a/packages/vats/src/core/basic-behaviors.js +++ b/packages/vats/src/core/basic-behaviors.js @@ -104,7 +104,7 @@ export const makeVatsFromBundles = async ({ }; harden(makeVatsFromBundles); -/** @param {BootstrapPowers} powers */ +/** @param {BootstrapSpace} powers */ export const produceStartUpgradable = async ({ consume: { zoe }, produce, // startUpgradable diff --git a/packages/vats/src/nameHub.js b/packages/vats/src/nameHub.js index 26569c78915..ffdd7626dec 100644 --- a/packages/vats/src/nameHub.js +++ b/packages/vats/src/nameHub.js @@ -50,7 +50,7 @@ export const prepareMixinMyAddress = zone => { ...NameHubIKit.nameAdmin.methodGuards, getMyAddress: M.call().returns(M.string()), }); - /** @type {import('@agoric/internal/src/callback.js').AttenuatorMaker} */ + /** @type {import('@agoric/internal/src/callback.js').MakeAttenuator} */ const mixin = prepareGuardedAttenuator(zone, MixinI, { tag: 'MyAddressNameAdmin', }); diff --git a/packages/vats/src/vat-bank.js b/packages/vats/src/vat-bank.js index 8dc92a86699..dd412113b0c 100644 --- a/packages/vats/src/vat-bank.js +++ b/packages/vats/src/vat-bank.js @@ -1,76 +1,468 @@ // @ts-check -import { assert, Fail } from '@agoric/assert'; -import { AmountMath, AssetKind } from '@agoric/ertp'; +import { AmountMath, AssetKind, BrandShape } from '@agoric/ertp'; import { E, Far } from '@endo/far'; -import { makeNotifierKit, makeSubscriptionKit } from '@agoric/notifier'; -import { makeScalarMapStore, makeScalarWeakMapStore } from '@agoric/store'; -import { whileTrue } from '@agoric/internal'; -import { makeVirtualPurse } from './virtual-purse.js'; +import { + makeNotifierKit, + makePublishKit as makeHeapPublishKit, + prepareDurablePublishKit, + subscribeEach, +} from '@agoric/notifier'; +import { M, provideLazy } from '@agoric/store'; +import { makeDurableZone } from '@agoric/zone/durable.js'; +import { prepareGuardedAttenuator } from '@agoric/internal/src/callback.js'; +import { deeplyFulfilledObject } from '@agoric/internal'; +import { BridgeHandlerI, BridgeScopedManagerI } from './bridge.js'; +import { + makeVirtualPurseKitIKit, + prepareVirtualPurse, +} from './virtual-purse.js'; import '@agoric/notifier/exported.js'; +const { Fail } = assert; + +const { VirtualPurseControllerI } = makeVirtualPurseKitIKit(); + +const BridgeChannelI = M.interface('BridgeChannel', { + fromBridge: BridgeScopedManagerI.methodGuards.fromBridge, + toBridge: BridgeScopedManagerI.methodGuards.toBridge, +}); + /** * @typedef {import('./virtual-purse').VirtualPurseController} VirtualPurseController - * @typedef {ReturnType} VirtualPurse + * @typedef {Awaited>>} VirtualPurse */ /** - * @callback BalanceUpdater - * @param {string} value - * @param {string} [nonce] + * @typedef {object} BalanceUpdater + * @property {(value: string, nonce?: string) => void} update */ +const BalanceUpdaterI = M.interface('BalanceUpdater', { + update: M.call(M.string()).optional(M.string()).returns(), +}); + +/** @typedef {Pick} BridgeChannel */ + /** - * @param {(obj: any) => Promise} bankCall - * @param {string} denom - * @param {Brand} brand - * @param {string} address - * @param {Notifier} balanceNotifier - * @param {(obj: any) => boolean} updateBalances - * @returns {VirtualPurseController} + * + * @param {import('@agoric/zone').Zone} zone + * @returns {(brand: Brand, publisher: Publisher) => BalanceUpdater} */ -const makePurseController = ( - bankCall, - denom, - brand, - address, - balanceNotifier, - updateBalances, +const prepareBalanceUpdater = zone => + zone.exoClass( + 'BalanceUpdater', + BalanceUpdaterI, + (brand, publisher) => ({ + brand, + publisher, + lastBalanceUpdate: -1n, + }), + { + update(value, nonce = undefined) { + if (nonce !== undefined) { + const thisBalanceUpdate = BigInt(nonce); + if (thisBalanceUpdate <= this.state.lastBalanceUpdate) { + return; + } + this.state.lastBalanceUpdate = thisBalanceUpdate; + } + // Convert the string value to a bigint. + const amt = AmountMath.make(this.state.brand, BigInt(value)); + this.state.publisher.publish(amt); + }, + }, + ); + +/** + * @param {import('@agoric/zone').Zone} zone + */ +const prepareBankPurseController = zone => { + /** + * @param {BridgeChannel} bankBridge + * @param {string} denom + * @param {Brand} brand + * @param {string} address + * @param {PublishKit} balanceKit + * @returns {VirtualPurseController} + */ + const makeBankPurseController = zone.exoClass( + 'BankPurseController', + VirtualPurseControllerI, + /** + * @param {BridgeChannel} bankBridge + * @param {string} denom + * @param {Brand} brand + * @param {string} address + * @param {LatestTopic} balanceTopic + */ + (bankBridge, denom, brand, address, balanceTopic) => ({ + bankBridge, + denom, + brand, + address, + balanceTopic, + }), + { + getBalances(b) { + const { brand, balanceTopic } = this.state; + assert.equal(b, brand); + return balanceTopic; + }, + async pushAmount(amt) { + const { bankBridge, denom, address, brand } = this.state; + const value = AmountMath.getValue(brand, amt); + const update = await bankBridge.toBridge({ + type: 'VBANK_GIVE', + recipient: address, + denom, + amount: `${value}`, + }); + await bankBridge.fromBridge(update); + }, + async pullAmount(amt) { + const { bankBridge, denom, address, brand } = this.state; + const value = AmountMath.getValue(brand, amt); + const update = await bankBridge.toBridge({ + type: 'VBANK_GRAB', + sender: address, + denom, + amount: `${value}`, + }); + await bankBridge.fromBridge(update); + }, + }, + ); + return makeBankPurseController; +}; + +/** @param {import('@agoric/zone').Zone} zone */ +const prepareRewardPurseController = zone => + zone.exoClass( + 'RewardPurseController', + VirtualPurseControllerI, + /** + * @param {BridgeChannel} bankChannel + * @param {string} denom + * @param {Brand} brand + */ + (bankChannel, denom, brand) => ({ bankChannel, denom, brand }), + { + getBalances(b) { + // Never resolve! + assert.equal(b, this.state.brand); + return makeNotifierKit().notifier; + }, + async pullAmount(_amount) { + throw Error(`Cannot pull from reward distributor`); + }, + async pushAmount(amount) { + const { brand, bankChannel, denom } = this.state; + const value = AmountMath.getValue(brand, amount); + await bankChannel.toBridge({ + type: 'VBANK_GIVE_TO_REWARD_DISTRIBUTOR', + denom, + amount: `${value}`, + }); + }, + }, + ); + +/** + * @param {import('@agoric/zone').Zone} zone + */ +const prepareBankChannelHandler = zone => + zone.exoClass( + 'BankChannelHandler', + BridgeHandlerI, + denomToAddressUpdater => ({ denomToAddressUpdater }), + { + async fromBridge(obj) { + switch (obj && obj.type) { + case 'VBANK_BALANCE_UPDATE': { + const { denomToAddressUpdater } = this.state; + for (const update of obj.updated) { + try { + const { address, denom, amount: value } = update; + const addressToUpdater = denomToAddressUpdater.get(denom); + const updater = addressToUpdater.get(address); + + updater.update(value, obj.nonce); + // console.info('bank balance update', update); + } catch (e) { + // console.error('Unregistered update', update); + } + } + break; + } + default: { + Fail`Unrecognized request ${obj}`; + } + } + }, + }, + ); + +/** + * Concatenate multiple iterables to form a new one. + * + * @template T + * @param {Array | AsyncIterable>} iterables + */ +async function* concatAsyncIterables(iterables) { + for (const asyncIterable of iterables) { + yield* asyncIterable; + } +} + +/** + * TODO: This should be absorbed and zone-ified into the existing publish kit. + * + * @template T + * @param {AsyncIterable} asyncIterable + * @param {(value: T, prior?: T) => unknown} skipValue + */ +const makeSubscriberFromAsyncIterable = ( + asyncIterable, + skipValue = (_value, _prior) => false, ) => { - return harden({ - async *getBalances(b) { - assert.equal(b, brand); - let updateRecord = await balanceNotifier.getUpdateSince(); - for await (const _ of whileTrue(() => updateRecord.updateCount)) { - yield updateRecord.value; - // eslint-disable-next-line no-await-in-loop - updateRecord = await balanceNotifier.getUpdateSince( - updateRecord.updateCount, + const { subscriber, publisher } = makeHeapPublishKit(); + void (async () => { + /** @type {T | undefined} */ + let prior; + // TODO: Opportunity to make this more efficient by not consuming the whole + // iterable hotly. + for await (const value of asyncIterable) { + if (skipValue(value, prior)) { + continue; + } + publisher.publish(value); + prior = value; + } + })(); + return Far('HeapSubscriber', { + ...subscriber, + ...subscribeEach(subscriber), + }); +}; + +/** + * @template T + * @param {Iterable} historyValues + * @param {EachTopic} futureSubscriber + * @param {(value: T, prior?: T) => unknown} skipValue + */ +const makeHistoricalTopic = (historyValues, futureSubscriber, skipValue) => { + // Take a synchronous snapshot of the future from now. + const futureIterable = + subscribeEach(futureSubscriber)[Symbol.asyncIterator](); + + const allHistory = concatAsyncIterables([historyValues, futureIterable]); + return makeSubscriberFromAsyncIterable(allHistory, skipValue); +}; + +/** + * @template T + @typedef {Omit, 'getSharableSubscriptionInternals'>} BaseSubscription */ +/** @type {WeakMap, Promise>>} */ +const fullAssetLists = new WeakMap(); + +/** + * Build a heap asset subscription from its durable components. + * + * @param {MapStore} brandToAssetDescriptor + * @param {EachTopic} assetSubscriber + */ +const provideAssetSubscription = (brandToAssetDescriptor, assetSubscriber) => { + /** @type {EachTopic} */ + const topic = Far('AssetTopic', { + subscribeAfter(publishCount = -1n) { + if (publishCount !== -1n) { + return assetSubscriber.subscribeAfter(publishCount); + } + + let pubList = fullAssetLists.get(brandToAssetDescriptor); + if (!pubList) { + const already = new Set(); + const fullTopic = makeHistoricalTopic( + [...brandToAssetDescriptor.values()], + assetSubscriber, + ({ denom }) => { + const found = already.has(denom); + already.add(denom); + return found; + }, ); + // Synchronously capture the first pubList entry before the + // assetSubscriber has a chance to publish more. + pubList = fullTopic.subscribeAfter(); + fullAssetLists.set(brandToAssetDescriptor, pubList); } - return updateRecord.value; + + return pubList; }, - async pushAmount(amt) { - const value = AmountMath.getValue(brand, amt); - const update = await bankCall({ - type: 'VBANK_GIVE', - recipient: address, - denom, - amount: `${value}`, - }); - updateBalances(update); + }); + return Far('AssetSubscription', { + ...topic, + ...subscribeEach(topic), + }); +}; + +export const BankI = M.interface('Bank', { + getAssetSubscription: M.call().returns(M.remotable('AssetSubscription')), + getPurse: M.callWhen(BrandShape).returns(M.remotable('VirtualPurse')), +}); + +/** + * @param {import('@agoric/zone').Zone} zone + * @param {object} makers + * @param {ReturnType} makers.makePublishKit + * @param {ReturnType} makers.makeVirtualPurse + */ +const prepareBank = (zone, { makePublishKit, makeVirtualPurse }) => { + const makeBalanceUpdater = prepareBalanceUpdater(zone); + const makeBankPurseController = prepareBankPurseController(zone); + + const makeBank = zone.exoClass( + 'Bank', + BankI, + /** + * @param {object} param0 + * @param {string} param0.address + * @param {EachTopic} param0.assetSubscriber + * @param {MapStore} param0.brandToAssetDescriptor + * @param {BridgeChannel} [param0.bankChannel] + * @param {MapStore} param0.brandToAssetRecord + * @param {MapStore} param0.brandToVPurse + * @param {MapStore>} param0.denomToAddressUpdater + */ + ({ + address, + assetSubscriber, + brandToAssetDescriptor, + bankChannel, + brandToAssetRecord, + brandToVPurse, + denomToAddressUpdater, + }) => { + return { + address, + assetSubscriber, + brandToAssetDescriptor, + bankChannel, + brandToAssetRecord, + brandToVPurse, + denomToAddressUpdater, + }; }, - async pullAmount(amt) { - const value = AmountMath.getValue(brand, amt); - const update = await bankCall({ - type: 'VBANK_GRAB', - sender: address, - denom, - amount: `${value}`, - }); - updateBalances(update); + { + getAssetSubscription() { + return provideAssetSubscription( + this.state.brandToAssetDescriptor, + this.state.assetSubscriber, + ); + }, + async getPurse(brand) { + const { + bankChannel, + address, + brandToVPurse, + brandToAssetRecord, + denomToAddressUpdater, + } = this.state; + + if (brandToVPurse.has(brand)) { + return brandToVPurse.get(brand); + } + + /** @param {ERef} purseP */ + const keepPurse = async purseP => { + const purse = await purseP; + // Need to recheck as we may have raced with another call. + if (brandToVPurse.has(brand)) { + return brandToVPurse.get(brand); + } + brandToVPurse.init(brand, purse); + return purse; + }; + + const assetRecord = brandToAssetRecord.get(brand); + if (!bankChannel) { + // Just emulate with a real purse. + return keepPurse(E(assetRecord.issuer).makeEmptyPurse()); + } + + const addressToUpdater = denomToAddressUpdater.get(assetRecord.denom); + + /** @type {PublishKit} */ + const { publisher, subscriber } = makePublishKit(); + const balanceUpdater = makeBalanceUpdater(brand, publisher); + + // Abort if we lost the race to another call. + addressToUpdater.init(address, balanceUpdater); + + // Get the initial balance. + const balanceString = await bankChannel.toBridge({ + type: 'VBANK_GET_BALANCE', + address, + denom: assetRecord.denom, + }); + balanceUpdater.update(balanceString); + + // Create and return the virtual purse. + const vpc = makeBankPurseController( + bankChannel, + assetRecord.denom, + brand, + address, + subscriber, + ); + return keepPurse(makeVirtualPurse(vpc, assetRecord)); + }, }, - }); + ); + return makeBank; +}; + +/** + * @param {MapStore} baggage + */ +const prepareFromBaggage = baggage => { + const rootZone = makeDurableZone(baggage); + + const makePublishKit = prepareDurablePublishKit( + rootZone.mapStore('publisher'), + 'PublishKit', + ); + + const detachedZone = rootZone.detached(); + const makeVirtualPurse = prepareVirtualPurse(rootZone); + const bridgeManagerToData = rootZone.mapStore('bridgeManagerToData'); + const makeBank = prepareBank(rootZone, { makePublishKit, makeVirtualPurse }); + const makeRewardPurseController = prepareRewardPurseController(rootZone); + const makeBankChannelHandler = prepareBankChannelHandler(rootZone); + + /** + * @type {import('@agoric/internal/src/callback.js').MakeAttenuator} + */ + const makeBridgeChannelAttenuator = prepareGuardedAttenuator( + rootZone.subZone('attenuators'), + BridgeChannelI, + { + tag: 'BridgeChannelAttenuator', + }, + ); + + return { + bridgeManagerToData, + detachedZone, + makeBank, + makeBankChannelHandler, + makeBridgeChannelAttenuator, + makeRewardPurseController, + makePublishKit, + makeVirtualPurse, + }; }; /** @@ -103,13 +495,24 @@ const makePurseController = ( /** * @typedef {object} Bank - * @property {() => Subscription} getAssetSubscription Returns + * @property {() => BaseSubscription} getAssetSubscription Returns * assets as they are added to the bank - * @property {(brand: Brand) => VirtualPurse} getPurse Find any existing vpurse + * @property {(brand: Brand) => Promise} getPurse Find any existing vpurse * (keyed by address and brand) or create a new one. */ -export function buildRootObject() { +export function buildRootObject(_vatPowers, _args, baggage) { + const { + bridgeManagerToData, + detachedZone, + makeBank, + makeBankChannelHandler, + makeBridgeChannelAttenuator, + makePublishKit, + makeRewardPurseController, + makeVirtualPurse, + } = prepareFromBaggage(baggage); + return Far('bankMaker', { /** * @param {ERef} [bankBridgeManagerP] a bridge @@ -123,150 +526,88 @@ export function buildRootObject() { nameAdmin = undefined, ) { const bankBridgeManager = await bankBridgeManagerP; - /** @type {WeakMapStore} */ - const brandToAssetRecord = makeScalarWeakMapStore('brand'); - /** @type {MapStore>} */ - const denomToAddressUpdater = makeScalarMapStore('denom'); + /** @type {MapStore} */ + const brandToAssetRecord = provideLazy( + bridgeManagerToData, + 'brandToAssetRecord', + () => detachedZone.mapStore('brandToAssetRecord'), + ); - const updateBalances = obj => { - switch (obj && obj.type) { - case 'VBANK_BALANCE_UPDATE': { - for (const update of obj.updated) { - try { - const { address, denom, amount: value } = update; - const addressToUpdater = denomToAddressUpdater.get(denom); - const updater = addressToUpdater.get(address); + /** @type {MapStore} */ + const brandToAssetDescriptor = provideLazy( + bridgeManagerToData, + 'brandToAssetPublications', + () => detachedZone.mapStore('brandToAssetPublications'), + ); - updater(value, obj.nonce); - console.info('bank balance update', update); - } catch (e) { - // console.error('Unregistered update', update); - } - } - return true; - } - default: - return false; - } - }; + /** @type {MapStore>} */ + const denomToAddressUpdater = provideLazy( + bridgeManagerToData, + 'denomToAddressUpdater', + () => detachedZone.mapStore('denomToAddressUpdater'), + ); + + /** @type {MapStore }>} */ + const addressToBank = provideLazy( + bridgeManagerToData, + 'addressToBank', + () => detachedZone.mapStore('addressToBank'), + ); /** * @param {ERef} [bankBridgeMgr] */ - async function makeBankCaller(bankBridgeMgr) { + async function getBankChannel(bankBridgeMgr) { // We do the logic here if the bridge manager is available. Otherwise, // the bank is not "remote" (such as on sim-chain), so we just use // immediate purses instead of virtual ones. if (!bankBridgeMgr) { return undefined; } - // We need to synchronise with the remote bank. - const handler = Far('bankHandler', { - async fromBridge(obj) { - if (!updateBalances(obj)) { - Fail`Unrecognized request ${obj && obj.type}`; - } - }, - }); + // We need to synchronise with the remote bank. + const handler = makeBankChannelHandler(denomToAddressUpdater); await E(bankBridgeMgr).initHandler(handler); // We can only downcall to the bank if there exists a bridge manager. - return obj => E(bankBridgeMgr).toBridge(obj); + return makeBridgeChannelAttenuator({ target: bankBridgeMgr }); } - const bankCall = await makeBankCaller(bankBridgeManager); + const bankChannel = await getBankChannel(bankBridgeManager); - /** @type {SubscriptionRecord} */ - const { subscription: assetSubscription, publication: assetPublication } = - makeSubscriptionKit(); - - /** @type {MapStore} */ - const addressToBank = makeScalarMapStore('address'); + /** + * CAVEAT: This history is kept on the heap, so we need to prime its pump. + * + * @type {PublishKit} + */ + const { subscriber: assetSubscriber, publisher: assetPublisher } = + makePublishKit(); /** * Create a new personal bank interface for a given address. * * @param {string} address lower-level bank account address - * @returns {Bank} + * @returns {Promise} */ - const getBankForAddress = address => { + const getBankForAddress = async address => { assert.typeof(address, 'string'); if (addressToBank.has(address)) { - return addressToBank.get(address); + return addressToBank.get(address).bank; } - /** @type {WeakMapStore} */ - const brandToVPurse = makeScalarWeakMapStore('brand'); - - /** @type {Bank} */ - const bank = Far('bank', { - getAssetSubscription() { - return assetSubscription; - }, - async getPurse(brand) { - if (brandToVPurse.has(brand)) { - return brandToVPurse.get(brand); - } - - const assetRecord = brandToAssetRecord.get(brand); - if (!bankCall) { - // Just emulate with a real purse. - const purse = E(assetRecord.issuer).makeEmptyPurse(); - brandToVPurse.init(brand, purse); - return purse; - } - - const addressToUpdater = denomToAddressUpdater.get( - assetRecord.denom, - ); - - /** @type {NotifierRecord} */ - const { updater, notifier } = makeNotifierKit(); - /** @type {bigint} */ - let lastBalanceUpdate = -1n; - /** @type {BalanceUpdater} */ - const balanceUpdater = Far( - 'balanceUpdater', - (value, nonce = undefined) => { - if (nonce !== undefined) { - const thisBalanceUpdate = BigInt(nonce); - if (thisBalanceUpdate <= lastBalanceUpdate) { - return; - } - lastBalanceUpdate = thisBalanceUpdate; - } - // Convert the string value to a bigint. - const amt = AmountMath.make(brand, BigInt(value)); - updater.updateState(amt); - }, - ); - - // Get the initial balance. - addressToUpdater.init(address, balanceUpdater); - const balanceString = await bankCall({ - type: 'VBANK_GET_BALANCE', - address, - denom: assetRecord.denom, - }); - balanceUpdater(balanceString); - - // Create and return the virtual purse. - const vpc = makePurseController( - bankCall, - assetRecord.denom, - brand, - address, - notifier, - updateBalances, - ); - const vpurse = makeVirtualPurse(vpc, assetRecord); - brandToVPurse.init(brand, vpurse); - return vpurse; - }, + /** @type {MapStore} */ + const brandToVPurse = detachedZone.mapStore('brandToVPurse'); + const bank = makeBank({ + address, + assetSubscriber, + brandToAssetDescriptor, + brandToAssetRecord, + bankChannel, + brandToVPurse, + denomToAddressUpdater, }); - addressToBank.init(address, bank); + addressToBank.init(address, harden({ bank, brandToVPurse })); return bank; }; @@ -274,10 +615,13 @@ export function buildRootObject() { /** * Returns assets as they are added to the bank. * - * @returns {Subscription} + * @returns {BaseSubscription} */ getAssetSubscription() { - return harden(assetSubscription); + return provideAssetSubscription( + brandToAssetDescriptor, + assetSubscriber, + ); }, /** * @param {string} denom @@ -285,28 +629,16 @@ export function buildRootObject() { * @returns {import('@endo/far').EOnly} */ getRewardDistributorDepositFacet(denom, feeKit) { - if (!bankCall) { + if (!bankChannel) { throw Error(`Bank doesn't implement reward collectors`); } - /** @type {VirtualPurseController} */ - const feeVpc = harden({ - async *getBalances(_brand) { - // Never resolve! - yield new Promise(_ => {}); - }, - async pullAmount(_amount) { - throw Error(`Cannot pull from reward distributor`); - }, - async pushAmount(amount) { - const value = AmountMath.getValue(feeKit.brand, amount); - await bankCall({ - type: 'VBANK_GIVE_TO_REWARD_DISTRIBUTOR', - denom, - amount: `${value}`, - }); - }, - }); + const feeVpc = makeRewardPurseController( + bankChannel, + denom, + feeKit.brand, + ); + const vp = makeVirtualPurse(feeVpc, feeKit); return E(vp).getDepositFacet(); }, @@ -319,11 +651,11 @@ export function buildRootObject() { * null if unimplemented (no bankCall) */ getModuleAccountAddress: async moduleName => { - if (!bankCall) { + if (!bankChannel) { return null; } - return bankCall({ + return bankChannel.toBridge({ type: 'VBANK_GET_MODULE_ACCOUNT_ADDRESS', moduleName, }); @@ -359,24 +691,31 @@ export function buildRootObject() { const payment = await kit.payment; await (payment && E(escrowPurse).deposit(payment)); - const assetRecord = harden({ - escrowPurse, - issuer: kit.issuer, - mint: kit.mint, + const [privateAssetRecord, toPublish] = await deeplyFulfilledObject( + harden([ + { + escrowPurse, + issuer: kit.issuer, + mint: kit.mint, + denom, + brand, + }, + { + brand, + denom, + issuerName, + issuer: kit.issuer, + proposedName, + }, + ]), + ); + brandToAssetRecord.init(brand, privateAssetRecord); + denomToAddressUpdater.init( denom, - brand, - }); - brandToAssetRecord.init(brand, assetRecord); - denomToAddressUpdater.init(denom, makeScalarMapStore('address')); - assetPublication.updateState( - harden({ - brand, - denom, - issuerName, - issuer: kit.issuer, - proposedName, - }), + detachedZone.mapStore('addressToUpdater'), ); + brandToAssetDescriptor.init(brand, toPublish); + assetPublisher.publish(toPublish); if (nameAdmin) { // publish settled issuer identity diff --git a/packages/vats/src/vat-provisioning.js b/packages/vats/src/vat-provisioning.js index d99cbc2c843..392a40f5565 100644 --- a/packages/vats/src/vat-provisioning.js +++ b/packages/vats/src/vat-provisioning.js @@ -20,7 +20,7 @@ import { const prepareSpecializedNameAdmin = zone => { const mixinMyAddress = prepareMixinMyAddress(zone); - /** @type {import('@agoric/internal/src/callback.js').AttenuatorMaker} */ + /** @type {import('@agoric/internal/src/callback.js').MakeAttenuator} */ const specialize = prepareGuardedAttenuator(zone, NameHubIKit.nameAdmin, { tag: 'NamesByAddressAdmin', }); diff --git a/packages/vats/src/virtual-purse.js b/packages/vats/src/virtual-purse.js index c4d9a7fef7c..d44a56e346e 100644 --- a/packages/vats/src/virtual-purse.js +++ b/packages/vats/src/virtual-purse.js @@ -1,16 +1,94 @@ // @ts-check -import { E, Far } from '@endo/far'; -import { makeNotifierKit, observeIteration } from '@agoric/notifier'; +import { M } from '@agoric/store'; +import { E } from '@endo/far'; import { isPromise } from '@endo/promise-kit'; +import { + AmountShape, + BrandShape, + DepositFacetShape, + NotifierShape, + PaymentShape, +} from '@agoric/ertp/src/typeGuards.js'; + import '@agoric/ertp/exported.js'; import '@agoric/notifier/exported.js'; +const { Fail } = assert; + +/** + * @param {Pattern} [brandShape] + * @param {Pattern} [amountShape] + */ +export const makeVirtualPurseKitIKit = ( + brandShape = BrandShape, + amountShape = AmountShape, +) => { + const VirtualPurseI = M.interface('VirtualPurse', { + getAllegedBrand: M.callWhen().returns(brandShape), + getCurrentAmount: M.callWhen().returns(amountShape), + getCurrentAmountNotifier: M.callWhen().returns(NotifierShape), + // PurseI does *not* delay `deposit` until `srcPayment` is fulfulled. + // Rather, the semantics of `deposit` require it to provide its + // callers with a strong guarantee that `deposit` messages are + // processed without further delay in the order they arrive. + // PurseI therefore requires that the `srcPayment` argument already + // be a remotable, not a promise. + // PurseI only calls this raw method after validating that + // `srcPayment` is a remotable, leaving it + // to this raw method to validate that this remotable is actually + // a live payment of the correct brand with sufficient funds. + deposit: M.callWhen(PaymentShape) + .optional(M.pattern()) + .returns(amountShape), + getDepositFacet: M.callWhen().returns(DepositFacetShape), + withdraw: M.callWhen(amountShape).returns(PaymentShape), + getRecoverySet: M.callWhen().returns(M.setOf(PaymentShape)), + recoverAll: M.callWhen().returns(amountShape), + }); + + const DepositFacetI = M.interface('DepositFacet', { + receive: VirtualPurseI.methodGuards.deposit, + }); + + const RetainRedeemI = M.interface('RetainRedeem', { + retain: M.callWhen(PaymentShape).optional(amountShape).returns(amountShape), + redeem: M.callWhen(amountShape).returns(PaymentShape), + }); + + const UtilsI = M.interface('Utils', { + retain: RetainRedeemI.methodGuards.retain, + redeem: RetainRedeemI.methodGuards.redeem, + recoverableClaim: M.callWhen(M.await(PaymentShape)) + .optional(amountShape) + .returns(PaymentShape), + }); + + const VirtualPurseIKit = harden({ + depositFacet: DepositFacetI, + purse: VirtualPurseI, + escrower: RetainRedeemI, + minter: RetainRedeemI, + utils: UtilsI, + }); + + const VirtualPurseControllerI = M.interface('VirtualPurseController', { + pushAmount: M.callWhen(AmountShape).returns(), + pullAmount: M.callWhen(AmountShape).returns(), + getBalances: M.call(BrandShape).returns(NotifierShape), + }); + + return { VirtualPurseIKit, VirtualPurseControllerI }; +}; + /** * @template T * @typedef {import('@endo/far').EOnly} EOnly */ +/** @typedef {(pmt: Payment, optAmountShape?: Pattern) => Promise} Retain */ +/** @typedef {(amt: Amount) => Promise} Redeem */ + /** * @typedef {object} VirtualPurseController The object that determines the * remote behaviour of a virtual purse. @@ -22,158 +100,215 @@ import '@agoric/notifier/exported.js'; * to send an amount from the "other side" to "us". This should resolve on * success and reject on failure. We can still recover assets from failure to * pull. - * @property {(brand: Brand) => AsyncIterable} getBalances Return the + * @property {(brand: Brand) => LatestTopic} getBalances Return the * current balance iterable for a given brand. */ /** - * @param {ERef} vpc the controller that represents the - * "other side" of this purse. - * @param {{ issuer: ERef, brand: Brand, mint?: ERef, - * escrowPurse?: ERef }} kit - * the contents of the issuer kit for "us". - * - * If the mint is not specified, then the virtual purse will escrow local assets - * instead of minting/burning them. That is a better option in general, but - * escrow doesn't support the case where the "other side" is also minting - * assets... our escrow purse may not have enough assets in it to redeem the - * ones that are sent from the "other side". - * @returns {EOnly} This is not just a Purse because it plays - * fast-and-loose with the synchronous Purse interface. So, the consumer of - * this result must only interact with the virtual purse via eventual-send (to - * conceal the methods that are returning promises instead of synchronously). + * @param {import('@agoric/zone').Zone} zone */ -function makeVirtualPurse(vpc, kit) { - const { brand, issuer, mint, escrowPurse } = kit; +const prepareVirtualPurseKit = zone => + zone.exoClassKit( + 'VirtualPurseKit', + makeVirtualPurseKitIKit().VirtualPurseIKit, + /** + * @param {ERef} vpc + * @param {{ issuer: ERef, brand: Brand, mint?: ERef }} issuerKit + * @param {{ recoveryPurse: ERef, escrowPurse?: ERef }} purses + */ + (vpc, issuerKit, purses) => ({ + vpc, + ...issuerKit, + ...purses, + retainerFacet: issuerKit.mint + ? /** @type {const} */ ('minter') + : /** @type {const} */ ('escrower'), + }), + { + utils: { + /** + * Claim a payment for recovery via our `recoveryPurse`. No need for this on + * the `retain` operations (since we are just burning the payment or + * depositing it directly in the `escrowPurse`). + * + * @param {ERef} payment + * @param {Amount} [optAmountShape] + */ + async recoverableClaim(payment, optAmountShape) { + const { + state: { recoveryPurse }, + } = this; + const pmt = await payment; + const amt = await E(recoveryPurse).deposit(pmt, optAmountShape); + return E(recoveryPurse).withdraw(optAmountShape || amt); + }, + /** @type {Retain} */ + async retain(payment, optAmountShape) { + const { + state: { retainerFacet }, + facets: { [retainerFacet]: retainer }, + } = this; + return retainer.retain(payment, optAmountShape); + }, + /** @type {Redeem} */ + async redeem(amount) { + const { + state: { retainerFacet }, + facets: { [retainerFacet]: retainer }, + } = this; + return retainer.redeem(amount); + }, + }, + minter: { + /** @type {Retain} */ + async retain(payment, optAmountShape) { + this.state.mint || Fail`minter cannot retain without a mint.`; + return E(this.state.issuer).burn(payment, optAmountShape); + }, + /** @type {Redeem} */ + async redeem(amount) { + const { + state: { mint }, + } = this; + if (!mint) { + throw Fail`minter cannot redeem without a mint.`; + } + return this.facets.utils.recoverableClaim( + E(mint).mintPayment(amount), + ); + }, + }, + escrower: { + /** @type {Retain} */ + async retain(payment, optAmountShape) { + const { + state: { escrowPurse }, + } = this; + if (!escrowPurse) { + throw Fail`escrower cannot retain without an escrow purse.`; + } + return E(escrowPurse).deposit(payment, optAmountShape); + }, + /** @type {Redeem} */ + async redeem(amount) { + const { + state: { escrowPurse }, + } = this; + if (!escrowPurse) { + throw Fail`escrower cannot redeem without an escrow purse.`; + } + return this.facets.utils.recoverableClaim( + E(escrowPurse).withdraw(amount), + ); + }, + }, + depositFacet: { + async receive(payment, optAmountShape = undefined) { + if (isPromise(payment)) { + throw TypeError( + `deposit does not accept promises as first argument. Instead of passing the promise (deposit(paymentPromise)), consider unwrapping the promise first: E.when(paymentPromise, actualPayment => deposit(actualPayment))`, + ); + } - const recoveryPurse = E(issuer).makeEmptyPurse(); + const amt = await this.facets.utils.retain(payment, optAmountShape); - /** - * Claim a payment for recovery via our `recoveryPurse`. No need for this on - * the `retain` operations (since we are just burning the payment or - * depositing it directly in the `escrowPurse`). - * - * @param {ERef} payment - * @param {Amount} [optAmountShape] - */ - const recoverableClaim = async (payment, optAmountShape) => { - const pmt = await payment; - const amt = await E(recoveryPurse).deposit(pmt, optAmountShape); - return E(recoveryPurse).withdraw(optAmountShape || amt); - }; + // The push must always succeed. + // + // NOTE: There is no potential recovery protocol for failed `.pushAmount`, + // there's no path to send a new payment back to the virtual purse holder. + // If we don't first retain the payment, we can't be guaranteed that it is + // the correct value, and that would be a race where somebody else might + // claim the payment before us. + return E(this.state.vpc) + .pushAmount(amt) + .then(_ => amt); + }, + }, + purse: { + async deposit(payment, optAmountShape) { + return this.facets.depositFacet.receive(payment, optAmountShape); + }, + getAllegedBrand() { + return this.state.brand; + }, + async getCurrentAmount() { + const topic = E(this.state.vpc).getBalances(this.state.brand); + return E.get(E(topic).getUpdateSince()).value; + }, + getCurrentAmountNotifier() { + const topic = E(this.state.vpc).getBalances(this.state.brand); + return topic; + }, + getDepositFacet() { + return this.facets.depositFacet; + }, + async withdraw(amount) { + // Both ensure that the amount exists, and have the other side "send" it + // to us. If this fails, the balance is not affected and the withdraw + // (properly) fails, too. + await E(this.state.vpc).pullAmount(amount); + // Amount has been successfully received from the other side. + // Try to redeem the amount. + const pmt = await this.facets.utils.redeem(amount).catch(async e => { + // We can recover from failed redemptions... just send back what we + // received. + await E(this.state.vpc).pushAmount(amount); + throw e; + }); + return pmt; + }, + getRecoverySet() { + return E(this.state.recoveryPurse).getRecoverySet(); + }, + recoverAll() { + return E(this.state.recoveryPurse).recoverAll(); + }, + }, + }, + ); + +/** + * @param {import('@agoric/zone').Zone} zone + */ +export const prepareVirtualPurse = zone => { + const makeVirtualPurseKit = prepareVirtualPurseKit(zone); /** - * @returns {{ - * retain: (pmt: Payment, optAmountShape?: Pattern) => Promise, - * redeem: (amt: Amount) => Promise, - * }} + * @param {ERef} vpc the controller that represents the + * "other side" of this purse. + * @param {{ issuer: ERef, brand: Brand, mint?: ERef, + * escrowPurse?: ERef }} params + * the contents of the issuer kit for "us". + * + * If the mint is not specified, then the virtual purse will escrow local assets + * instead of minting/burning them. That is a better option in general, but + * escrow doesn't support the case where the "other side" is also minting + * assets... our escrow purse may not have enough assets in it to redeem the + * ones that are sent from the "other side". + * @returns {Promise>>} This is not just a Purse because it plays + * fast-and-loose with the synchronous Purse interface. So, the consumer of + * this result must only interact with the virtual purse via eventual-send (to + * conceal the methods that are returning promises instead of synchronously). */ - const makeRetainRedeem = () => { - if (mint) { - const retain = (payment, optAmountShape = undefined) => - E(issuer).burn(payment, optAmountShape); - const redeem = amount => recoverableClaim(E(mint).mintPayment(amount)); - return { retain, redeem }; - } - - // If we can't mint, then we need to escrow. - const myEscrowPurse = escrowPurse || E(issuer).makeEmptyPurse(); - const retain = async (payment, optAmountShape = undefined) => - E(myEscrowPurse).deposit(payment, optAmountShape); - const redeem = amount => - recoverableClaim(E(myEscrowPurse).withdraw(amount)); - - return { retain, redeem }; + const makeVirtualPurse = async ( + vpc, + { escrowPurse: defaultEscrowPurse, ...issuerKit }, + ) => { + const [recoveryPurse, escrowPurse] = await Promise.all([ + E(issuerKit.issuer).makeEmptyPurse(), + // If we can't mint, then we need to escrow. + issuerKit.mint + ? defaultEscrowPurse + : defaultEscrowPurse || E(issuerKit.issuer).makeEmptyPurse(), + ]); + const vpurse = makeVirtualPurseKit(vpc, issuerKit, { + recoveryPurse, + escrowPurse, + }).purse; + return vpurse; }; - const { retain, redeem } = makeRetainRedeem(); - - /** @type {NotifierRecord} */ - const { notifier: balanceNotifier, updater: balanceUpdater } = - makeNotifierKit(); - - /** @type {ERef} */ - let lastBalance = E.get(balanceNotifier.getUpdateSince()).value; - - // Robustly observe the balance. - const fail = reason => { - balanceUpdater.fail(reason); - const rej = Promise.reject(reason); - rej.catch(_ => {}); - lastBalance = rej; - }; - observeIteration(E(vpc).getBalances(brand), { - fail, - updateState(nonFinalValue) { - balanceUpdater.updateState(nonFinalValue); - lastBalance = nonFinalValue; - }, - finish(completion) { - balanceUpdater.finish(completion); - lastBalance = completion; - }, - // Propagate a failed balance properly if the iteration observer fails. - }).catch(fail); - - /** @type {EOnly} */ - const depositFacet = Far('Virtual Deposit Facet', { - async receive(payment, optAmountShape = undefined) { - if (isPromise(payment)) { - throw TypeError( - `deposit does not accept promises as first argument. Instead of passing the promise (deposit(paymentPromise)), consider unwrapping the promise first: E.when(paymentPromise, actualPayment => deposit(actualPayment))`, - ); - } - - const amt = await retain(payment, optAmountShape); - - // The push must always succeed. - // - // NOTE: There is no potential recovery protocol for failed `.pushAmount`, - // there's no path to send a new payment back to the virtual purse holder. - // If we don't first retain the payment, we can't be guaranteed that it is - // the correct value, and that would be a race where somebody else might - // claim the payment before us. - return E(vpc) - .pushAmount(amt) - .then(_ => amt); - }, - }); - - /** @type {EOnly} */ - const purse = Far('Virtual Purse', { - deposit: depositFacet.receive, - getAllegedBrand() { - return brand; - }, - getCurrentAmount() { - return lastBalance; - }, - getCurrentAmountNotifier() { - return balanceNotifier; - }, - getDepositFacet() { - return depositFacet; - }, - async withdraw(amount) { - // Both ensure that the amount exists, and have the other side "send" it - // to us. If this fails, the balance is not affected and the withdraw - // (properly) fails, too. - await E(vpc).pullAmount(amount); - // Amount has been successfully received from the other side. - // Try to redeem the amount. - const pmt = await redeem(amount).catch(async e => { - // We can recover from failed redemptions... just send back what we - // received. - await E(vpc).pushAmount(amount); - throw e; - }); - return pmt; - }, - getRecoverySet: () => E(recoveryPurse).getRecoverySet(), - recoverAll: () => E(recoveryPurse).recoverAll(), - }); - return purse; -} -harden(makeVirtualPurse); + return makeVirtualPurse; +}; -export { makeVirtualPurse }; +harden(prepareVirtualPurse); diff --git a/packages/vats/test/bootstrapTests/supports.js b/packages/vats/test/bootstrapTests/supports.js index 2ff8d99ed45..e4bce7d91a1 100644 --- a/packages/vats/test/bootstrapTests/supports.js +++ b/packages/vats/test/bootstrapTests/supports.js @@ -349,6 +349,8 @@ export const makeSwingsetTestKit = async ( return marshal.fromCapData(capData); }; + let lastNonce = 0n; + /** * Mock the bridge outbound handler. The real one is implemented in Golang so * changes there will sometimes require changes here. @@ -358,34 +360,48 @@ export const makeSwingsetTestKit = async ( */ const bridgeOutbound = (bridgeId, obj) => { switch (bridgeId) { - case BridgeId.BANK: + case BridgeId.BANK: { // bridgeOutbound bank : { // moduleName: 'vbank/reserve', // type: 'VBANK_GET_MODULE_ACCOUNT_ADDRESS' // } - if ( - obj.moduleName === VBankAccount.reserve.module && - obj.type === 'VBANK_GET_MODULE_ACCOUNT_ADDRESS' - ) { - return VBankAccount.reserve.address; - } - if ( - obj.moduleName === VBankAccount.provision.module && - obj.type === 'VBANK_GET_MODULE_ACCOUNT_ADDRESS' - ) { - return VBankAccount.provision.address; - } + switch (obj.type) { + case 'VBANK_GET_MODULE_ACCOUNT_ADDRESS': { + const { moduleName } = obj; + const moduleDescriptor = Object.values(VBankAccount).find( + ({ module }) => module === moduleName, + ); + if (!moduleDescriptor) { + return 'undefined'; + } + return moduleDescriptor.address; + } - // Observed message: - // address: 'agoric1megzytg65cyrgzs6fvzxgrcqvwwl7ugpt62346', - // denom: 'ibc/toyatom', - // type: 'VBANK_GET_BALANCE' - if (obj.type === 'VBANK_GET_BALANCE') { - // empty balances for test, passed to `BigInt` - return '0'; - } + // Observed message: + // address: 'agoric1megzytg65cyrgzs6fvzxgrcqvwwl7ugpt62346', + // denom: 'ibc/toyatom', + // type: 'VBANK_GET_BALANCE' + case 'VBANK_GET_BALANCE': { + // empty balances for test. + return '0'; + } - return undefined; + case 'VBANK_GRAB': + case 'VBANK_GIVE': { + lastNonce += 1n; + // Also empty balances. + return harden({ + type: 'VBANK_BALANCE_UPDATE', + nonce: `${lastNonce}`, + updated: [], + }); + } + + default: { + return 'undefined'; + } + } + } case BridgeId.CORE: case BridgeId.DIBC: case BridgeId.PROVISION: diff --git a/packages/vats/test/setup-vat-data.js b/packages/vats/test/setup-vat-data.js new file mode 100644 index 00000000000..87aad92444f --- /dev/null +++ b/packages/vats/test/setup-vat-data.js @@ -0,0 +1,28 @@ +/* global globalThis */ +// This file produces the globalThis.VatData property outside of SwingSet so +// that it can be used by '@agoric/vat-data' (which only *consumes* +// `globalThis.VatData`) in code under test. +import { makeFakeVirtualStuff } from '@agoric/swingset-liveslots/tools/fakeVirtualSupport.js'; + +export const fakeVomKit = makeFakeVirtualStuff({ + relaxDurabilityRules: false, +}); + +globalThis.WeakMap = fakeVomKit.vom.VirtualObjectAwareWeakMap; +globalThis.WeakSet = fakeVomKit.vom.VirtualObjectAwareWeakSet; + +const { vom, wpm: watchedPromiseManager, cm: collectionManager } = fakeVomKit; +globalThis.VatData = harden({ + defineKind: vom.defineKind, + defineKindMulti: vom.defineKindMulti, + defineDurableKind: vom.defineDurableKind, + defineDurableKindMulti: vom.defineDurableKindMulti, + makeKindHandle: vom.makeKindHandle, + canBeDurable: vom.canBeDurable, + providePromiseWatcher: watchedPromiseManager.providePromiseWatcher, + watchPromise: watchedPromiseManager.watchPromise, + makeScalarBigMapStore: collectionManager.makeScalarBigMapStore, + makeScalarBigWeakMapStore: collectionManager.makeScalarBigWeakMapStore, + makeScalarBigSetStore: collectionManager.makeScalarBigSetStore, + makeScalarBigWeakSetStore: collectionManager.makeScalarBigWeakSetStore, +}); diff --git a/packages/vats/test/test-clientBundle.js b/packages/vats/test/test-clientBundle.js index 4e053af887e..88c3a141b33 100644 --- a/packages/vats/test/test-clientBundle.js +++ b/packages/vats/test/test-clientBundle.js @@ -106,7 +106,6 @@ test('connectFaucet produces payments', async t => { return amt; }, }), - // @ts-expect-error mock getAssetSubscription: () => null, }), }), diff --git a/packages/vats/test/test-provisionPool.js b/packages/vats/test/test-provisionPool.js index 5759aa69df0..982ceb39257 100644 --- a/packages/vats/test/test-provisionPool.js +++ b/packages/vats/test/test-provisionPool.js @@ -16,6 +16,7 @@ import { unsafeMakeBundleCache } from '@agoric/swingset-vat/tools/bundleTool.js' import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; import { E, Far } from '@endo/far'; import path from 'path'; +import { makeScalarBigMapStore } from '@agoric/vat-data'; import centralSupplyBundle from '../bundles/bundle-centralSupply.js'; import { makeBoard } from '../src/lib-board.js'; import { makeNameHubKit } from '../src/nameHub.js'; @@ -269,7 +270,12 @@ test('provisionPool trades provided assets for IST', async t => { * @param {string} address */ const makeWalletFactoryKitFor1 = async address => { - const bankManager = await buildBankRoot().makeBankManager(); + const baggage = makeScalarBigMapStore('bank baggage'); + const bankManager = await buildBankRoot( + undefined, + undefined, + baggage, + ).makeBankManager(); const fees = withAmountUtils(makeIssuerKit('FEE')); await bankManager.addAsset('ufee', 'FEE', 'FEE', fees); @@ -280,7 +286,7 @@ const makeWalletFactoryKitFor1 = async address => { }; const b1 = bankManager.getBankForAddress(address); - const p1 = b1.getPurse(fees.brand); + const p1 = E(b1).getPurse(fees.brand); /** @type {import('@agoric/smart-wallet/src/smartWallet.js').SmartWallet} */ // @ts-expect-error mock diff --git a/packages/vats/test/test-vat-bank-integration.js b/packages/vats/test/test-vat-bank-integration.js new file mode 100644 index 00000000000..39ed21d4829 --- /dev/null +++ b/packages/vats/test/test-vat-bank-integration.js @@ -0,0 +1,118 @@ +// @ts-check +import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; + +import { makeScalarMapStore } from '@agoric/vat-data'; + +import { E } from '@endo/far'; +import { makePromiseKit } from '@endo/promise-kit'; +import { makeZoeKit } from '@agoric/zoe'; +import { observeIteration } from '@agoric/notifier'; +import { buildRootObject } from '../src/vat-bank.js'; +import { + mintInitialSupply, + addBankAssets, + installBootContracts, + produceStartUpgradable, +} from '../src/core/basic-behaviors.js'; +import { makeAgoricNamesAccess } from '../src/core/utils.js'; +import { makePromiseSpace } from '../src/core/promise-space.js'; +import { makePopulatedFakeVatAdmin } from '../tools/boot-test-utils.js'; + +test('mintInitialSupply, addBankAssets bootstrap actions', async t => { + // Supply bootstrap prerequisites. + const space = /** @type { any } */ (makePromiseSpace(t.log)); + const { produce, consume } = + /** @type { BootstrapPowers & { consume: { loadCriticalVat: VatLoader }}} */ ( + space + ); + const { agoricNames, spaces } = await makeAgoricNamesAccess(); + produce.agoricNames.resolve(agoricNames); + + const { vatAdminService } = makePopulatedFakeVatAdmin(); + const { zoeService, feeMintAccess: fma } = makeZoeKit(vatAdminService); + produce.zoe.resolve(zoeService); + produce.feeMintAccess.resolve(fma); + produce.vatAdminSvc.resolve(vatAdminService); + await installBootContracts({ + consume, + produce, + ...spaces, + }); + + // Genesis RUN supply: 50 + const bootMsg = { + type: 'INIT@@', + chainID: 'ag', + storagePort: 1, + supplyCoins: [{ amount: '50000000', denom: 'uist' }], + vbankPort: 2, + vibcPort: 3, + }; + + // Now run the function under test. + await mintInitialSupply({ + vatParameters: { + argv: { + bootMsg, + ROLE: 'x', + hardcodedClientAddresses: [], + FIXME_GCI: '', + PROVISIONER_INDEX: 1, + }, + }, + consume, + produce, + devices: /** @type { any } */ ({}), + vats: /** @type { any } */ ({}), + vatPowers: /** @type { any } */ ({}), + runBehaviors: /** @type { any } */ ({}), + modules: {}, + ...spaces, + }); + + // check results: initialSupply + const runIssuer = await E(zoeService).getFeeIssuer(); + const runBrand = await E(runIssuer).getBrand(); + const pmt = await consume.initialSupply; + const amt = await E(runIssuer).getAmountOf(pmt); + t.deepEqual( + amt, + { brand: runBrand, value: 50_000_000n }, + 'initialSupply of 50 RUN', + ); + + const loadCriticalVat = async name => { + assert.equal(name, 'bank'); + return E(buildRootObject)( + null, + null, + makeScalarMapStore('addAssets baggage'), + ); + }; + produce.loadCriticalVat.resolve(loadCriticalVat); + produce.bridgeManager.resolve(undefined); + + await Promise.all([ + produceStartUpgradable({ consume, produce, ...spaces }), + addBankAssets({ consume, produce, ...spaces }), + ]); + + // check results: bankManager assets + const assets = E(consume.bankManager).getAssetSubscription(); + const expected = ['BLD', 'IST']; + const seen = new Set(); + const done = makePromiseKit(); + void observeIteration(assets, { + updateState: asset => { + seen.add(asset.issuerName); + if (asset.issuerName === 'IST') { + t.is(asset.issuer, runIssuer); + } + if (seen.size === expected.length) { + done.resolve(seen); + } + }, + }); + await done.promise; + t.deepEqual([...seen].sort(), expected); +}); diff --git a/packages/vats/test/test-vat-bank.js b/packages/vats/test/test-vat-bank.js index d7f2f2daef3..8a1e7f1abf7 100644 --- a/packages/vats/test/test-vat-bank.js +++ b/packages/vats/test/test-vat-bank.js @@ -1,22 +1,70 @@ // @ts-check -// eslint-disable-next-line import/no-extraneous-dependencies import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; -import { E, Far } from '@endo/far'; +// eslint-disable-next-line import/order +import { fakeVomKit } from './setup-vat-data.js'; + +import { E } from '@endo/far'; import { AmountMath, makeIssuerKit, AssetKind } from '@agoric/ertp'; +import { makeDurableZone } from '@agoric/zone/durable.js'; +import { heapZone } from '@agoric/zone'; +import { subscribeEach } from '@agoric/notifier'; import { buildRootObject } from '../src/vat-bank.js'; +const provideBaggage = key => { + const root = fakeVomKit.cm.provideBaggage(); + const zone = makeDurableZone(root); + return zone.mapStore(`${key} baggage`); +}; + +test('provideAssetSubscription - MapStore insertion order preserved', async t => { + const zones = { + durableZone: makeDurableZone(provideBaggage('key order')), + heapZone, + }; + for (const [name, zone] of Object.entries(zones)) { + const ids = harden(['a', 'b', 'c', 'd', 'e']); + const handleToId = new Map( + ids.map(id => [zone.exo(`${name} ${id} handle`, undefined, {}), id]), + ); + + const forwardMap = zone.mapStore(`${name} forward map`); + handleToId.forEach((id, h) => forwardMap.init(h, id)); + const forwardMapIds = [...forwardMap.values()]; + + const reverseMap = zone.mapStore(`${name} reverse map`); + [...handleToId.entries()] + .reverse() + .forEach(([h, id]) => reverseMap.init(h, id)); + const reverseMapIds = [...reverseMap.values()]; + + t.deepEqual( + forwardMapIds, + ids, + `${name} forward map insertion order preserved`, + ); + t.deepEqual( + reverseMapIds, + [...ids].reverse(), + `${name} reverse map insertion order preserved`, + ); + } +}); + test('communication', async t => { - t.plan(29); - const bankVat = E(buildRootObject)(); + t.plan(32); + const baggage = provideBaggage('communication'); + const bankVat = E(buildRootObject)(null, null, baggage); + + const zone = makeDurableZone(baggage); /** @type {undefined | ERef} */ let bankHandler; /** @type {import('../src/types.js').ScopedBridgeManager} */ - const bankBridgeMgr = Far('fakeBankBridgeManager', { - async fromBridge(_obj) { - t.fail('unexpected fromBridge'); + const bankBridgeMgr = zone.exo('fakeBankBridgeManager', undefined, { + async fromBridge(obj) { + t.is(typeof obj, 'string'); }, async toBridge(obj) { let ret; @@ -93,11 +141,11 @@ test('communication', async t => { const bank = E(bankMgr).getBankForAddress('agoricfoo'); const sub = await E(bank).getAssetSubscription(); - const it = sub[Symbol.asyncIterator](); + const it = subscribeEach(sub)[Symbol.asyncIterator](); const kit = makeIssuerKit('BLD', AssetKind.NAT, harden({ decimalPlaces: 6 })); await t.throwsAsync(() => E(bank).getPurse(kit.brand), { - message: /"brand" not found/, + message: /not found/, }); /** @type {undefined | IteratorResult<{brand: Brand, issuer: ERef, proposedName: string}>} */ diff --git a/packages/vats/test/test-vpurse.js b/packages/vats/test/test-vpurse.js index 391c20d9a0f..502ad5b9055 100644 --- a/packages/vats/test/test-vpurse.js +++ b/packages/vats/test/test-vpurse.js @@ -1,15 +1,37 @@ // @ts-check // eslint-disable-next-line import/no-extraneous-dependencies -import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; +import { test as rawTest } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; + +// eslint-disable-next-line import/order +import { fakeVomKit } from './setup-vat-data.js'; import { E } from '@endo/far'; import { AmountMath, makeIssuerKit } from '@agoric/ertp'; import { claim } from '@agoric/ertp/src/legacy-payment-helpers.js'; import { makeNotifierKit } from '@agoric/notifier'; -import { makeVirtualPurse } from '../src/virtual-purse.js'; +import { makeDurableZone } from '@agoric/zone/durable.js'; +import { prepareVirtualPurse } from '../src/virtual-purse.js'; + +/** @type {import('ava').TestFn>} */ +const test = rawTest; + +const makeTestContext = () => { + return { baggage: fakeVomKit.cm.provideBaggage() }; +}; + +test.before(t => { + t.context = makeTestContext(); +}); + +/** + * @param {*} t + * @param {import('@agoric/zone').Zone} zone + * @param {bigint} [escrowValue] + */ +const setup = (t, zone, escrowValue = 0n) => { + const makeVirtualPurse = prepareVirtualPurse(zone); -const setup = (t, escrowValue = 0n) => { const kit = makeIssuerKit('fungible'); const { brand } = kit; @@ -38,15 +60,10 @@ const setup = (t, escrowValue = 0n) => { }); /** @type {import('../src/virtual-purse').VirtualPurseController} */ - const vpcontroller = harden({ - async *getBalances(b) { + const vpcontroller = zone.exo('TestController', undefined, { + getBalances(b) { t.is(b, brand); - let record = await balanceNotifier.getUpdateSince(); - while (record.updateCount) { - yield record.value; - // eslint-disable-next-line no-await-in-loop - record = await balanceNotifier.getUpdateSince(record.updateCount); - } + return balanceNotifier; }, async pullAmount(amt) { t.is(amt.brand, brand); @@ -87,8 +104,14 @@ const setup = (t, escrowValue = 0n) => { }; test('makeVirtualPurse', async t => { - t.plan(16); - const { expected, balanceUpdater, issuer, mint, brand, vpurse } = setup(t); + t.plan(22); + const { baggage } = t.context; + const zone = makeDurableZone(baggage).subZone('makeVirtualPurse'); + + const { expected, balanceUpdater, issuer, mint, brand, vpurse } = setup( + t, + zone, + ); const payment = mint.mintPayment(AmountMath.make(brand, 837n)); @@ -159,9 +182,13 @@ test('makeVirtualPurse', async t => { }); test('makeVirtualPurse withdraw from escrowPurse', async t => { - t.plan(11); + const { baggage } = t.context; + const zone = makeDurableZone(baggage).subZone('withdraw from escrowPurse'); + + t.plan(16); const { expected, balanceUpdater, issuer, brand, vpurse } = setup( t, + zone, 987654321n, ); @@ -219,8 +246,11 @@ test('makeVirtualPurse withdraw from escrowPurse', async t => { }); test('vpurse.deposit', async t => { - t.plan(14); - const { balanceUpdater, mint, brand, vpurse, expected } = setup(t); + const { baggage } = t.context; + const zone = makeDurableZone(baggage).subZone('vpurse.deposit'); + + t.plan(19); + const { balanceUpdater, mint, brand, vpurse, expected } = setup(t, zone); const fungible0 = AmountMath.makeEmpty(brand); const fungible17 = AmountMath.make(brand, 17n); const fungible25 = AmountMath.make(brand, 25n); @@ -271,8 +301,11 @@ test('vpurse.deposit', async t => { }); test('vpurse.deposit promise', async t => { - t.plan(2); - const { issuer, mint, brand, vpurse } = setup(t); + const { baggage } = t.context; + const zone = makeDurableZone(baggage).subZone('vpurse.deposit promise'); + + t.plan(1); + const { issuer, mint, brand, vpurse } = setup(t, zone); const fungible25 = AmountMath.make(brand, 25n); const payment = mint.mintPayment(fungible25); @@ -281,14 +314,20 @@ test('vpurse.deposit promise', async t => { await t.throwsAsync( // @ts-expect-error deliberate invalid arguments for testing () => E(vpurse).deposit(exclusivePaymentP, fungible25), - { message: /deposit does not accept promises/ }, + { + message: + /deposit does not accept promises|promise .* Must be a remotable/i, + }, 'failed to reject a promise for a payment', ); }); test('vpurse.getDepositFacet', async t => { - t.plan(8); - const { balanceUpdater, mint, brand, vpurse, expected } = setup(t); + const { baggage } = t.context; + const zone = makeDurableZone(baggage).subZone('vpurse.getDepositFacet'); + + t.plan(11); + const { balanceUpdater, mint, brand, vpurse, expected } = setup(t, zone); const fungible25 = AmountMath.make(brand, 25n); const payment = mint.mintPayment(fungible25); @@ -321,6 +360,6 @@ test('vpurse.getDepositFacet', async t => { expected.pushAmount(fungible25); await E(vpurse) .getDepositFacet() - .then(({ receive }) => receive(payment)) + .then(df => df.receive(payment)) .then(checkDeposit); }); diff --git a/packages/wallet/api/test/test-lib-wallet.js b/packages/wallet/api/test/test-lib-wallet.js index bd14a6e0cee..5b39da98c9c 100644 --- a/packages/wallet/api/test/test-lib-wallet.js +++ b/packages/wallet/api/test/test-lib-wallet.js @@ -132,7 +132,7 @@ async function setupTest( /** * Run a thunk and wait for the notifier to fire. * - * @param {ERef>} notifier + * @param {ERef>} notifier * @param {() => Promise} thunk */ const waitForUpdate = async (notifier, thunk) => { diff --git a/packages/zoe/src/contracts/exported.js b/packages/zoe/src/contracts/exported.js index 3971fd68704..8111efe33d9 100644 --- a/packages/zoe/src/contracts/exported.js +++ b/packages/zoe/src/contracts/exported.js @@ -1,3 +1,4 @@ import './types.js'; +// eslint-disable-next-line no-restricted-syntax import './loan/types.js'; import './callSpread/types.js';