From d76f42b1deb2a4fc280faef4ce74046b4b7cded0 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Thu, 19 May 2022 14:58:51 -0600 Subject: [PATCH 1/7] fix(notifier): reject iteration if an observer throws --- packages/notifier/src/asyncIterableAdaptor.js | 4 +- .../notifier/test/test-notifier-adaptor.js | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/notifier/src/asyncIterableAdaptor.js b/packages/notifier/src/asyncIterableAdaptor.js index 819fc4d2362..c003e2efc86 100644 --- a/packages/notifier/src/asyncIterableAdaptor.js +++ b/packages/notifier/src/asyncIterableAdaptor.js @@ -99,7 +99,7 @@ export const makeAsyncIterableFromNotifier = notifierP => { * @returns {Promise} */ export const observeIterator = (asyncIteratorP, iterationObserver) => { - return new Promise(ack => { + return new Promise((ack, observerError) => { const recur = () => { E.when( E(asyncIteratorP).next(), @@ -117,7 +117,7 @@ export const observeIterator = (asyncIteratorP, iterationObserver) => { iterationObserver.fail && iterationObserver.fail(reason); ack(undefined); }, - ); + ).catch(observerError); }; recur(); }); diff --git a/packages/notifier/test/test-notifier-adaptor.js b/packages/notifier/test/test-notifier-adaptor.js index 60c51333e79..59d38e5fdc5 100644 --- a/packages/notifier/test/test-notifier-adaptor.js +++ b/packages/notifier/test/test-notifier-adaptor.js @@ -49,6 +49,52 @@ test('observeIteration - update from iterator fails', t => { return observeIteration(explodingStream, u); }); +test('observeIteration - synchronous throw from observer stops hard', async t => { + t.plan(2); + await t.throwsAsync( + () => + observeIteration(finiteStream, { + updateState: state => { + t.is(state, 1); + throw new Error('sync propagates'); + }, + finish: state => { + t.fail(`unexpected finish with state ${state}`); + }, + fail: reason => { + t.is(reason.message, 'sync propagates'); + }, + }), + { message: 'sync propagates' }, + ); +}); + +test('observeIteration - rejection from observer does nothing', async t => { + const u = makeTestIterationObserver(t, true, false); + const handledRejection = reason => { + const r = Promise.reject(reason); + r.catch(() => {}); + return r; + }; + await observeIteration(finiteStream, { + ...u, + updateState: state => { + u.updateState(state); + return handledRejection( + Error('updateState rejection does not propagate'), + ); + }, + finish: state => { + u.finish(state); + return handledRejection(Error('finish rejection does not propagate')); + }, + fail: reason => { + u.fail(reason); + return handledRejection(Error('fail rejection does not propagate')); + }, + }); +}); + // /////////////////////////////// NotifierKit ///////////////////////////////// test('notifier adaptor - manual finishes', async t => { From 980ac06a4cb31e2b226ffc060828ce6e69109676 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Thu, 19 May 2022 14:59:40 -0600 Subject: [PATCH 2/7] feat(board): `getPublishingMarshaller` and `getReadonlyMarshaller` --- packages/vats/src/lib-board.js | 41 +++++++++++++++ packages/vats/test/test-lib-board.js | 75 ++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/packages/vats/src/lib-board.js b/packages/vats/src/lib-board.js index e6239f69d0e..885cc6ad8d6 100644 --- a/packages/vats/src/lib-board.js +++ b/packages/vats/src/lib-board.js @@ -2,6 +2,7 @@ import { assert, details as X, q } from '@agoric/assert'; import { E, Far } from '@endo/far'; +import { makeMarshal } from '@endo/marshal'; import { makeStore } from '@agoric/store'; import { crc6 } from './crc.js'; @@ -44,8 +45,48 @@ function makeBoard( const idToVal = makeStore('boardId'); const valToId = makeStore('value'); + const ifaceAllegedPrefix = 'Alleged: '; + const ifaceInaccessiblePrefix = 'INACCESSIBLE: '; + const slotToVal = (slot, iface) => { + if (slot !== null) { + // eslint-disable-next-line no-use-before-define + return board.getValue(slot); + } + + // Private object. + if (typeof iface === 'string' && iface.startsWith(ifaceAllegedPrefix)) { + iface = iface.slice(ifaceAllegedPrefix.length); + } + return Far(`${ifaceInaccessiblePrefix}${iface}`, {}); + }; + + // Create a marshaller that just looks up objects, not publish them. + const readonlyMarshaller = Far('board readonly marshaller', { + ...makeMarshal(val => { + if (!valToId.has(val)) { + // Unpublished value. + return null; + } + + // Published value. + return valToId.get(val); + }, slotToVal), + }); + + // Create a marshaller useful for publishing all ocaps. + const publishingMarshaller = Far('board publishing marshaller', { + ...makeMarshal( + // Always put the value in the board. + // eslint-disable-next-line no-use-before-define + val => board.getId(val), + slotToVal, + ), + }); + /** @type {Board} */ const board = Far('Board', { + getPublishingMarshaller: () => publishingMarshaller, + getReadonlyMarshaller: () => readonlyMarshaller, // Add if not already present getId: value => { if (!valToId.has(value)) { diff --git a/packages/vats/test/test-lib-board.js b/packages/vats/test/test-lib-board.js index 3655b2862dc..f43dd50d33c 100644 --- a/packages/vats/test/test-lib-board.js +++ b/packages/vats/test/test-lib-board.js @@ -48,3 +48,78 @@ test('makeBoard', async t => { const idObj2b = board2.getId(obj2); t.is(idObj2b, 'tooboard012'); }); + +const testBoardMarshaller = async (t, board, marshaller, publishing) => { + const published = Far('published', {}); + const unpublished = Far('unpublished', {}); + + const published1id = board.getId(published); + const ser = marshaller.serialize( + harden({ + published1: published, + unpublished1: unpublished, + published2: published, + unpublished2: unpublished, + }), + ); + const pub1ser = `{"@qclass":"slot","iface":"Alleged: published","index":0}`; + const pub2ser = `{"@qclass":"slot","index":0}`; + const unpub1ser = `{"@qclass":"slot","iface":"Alleged: unpublished","index":1}`; + const unpub2ser = `{"@qclass":"slot","index":1}`; + t.is( + ser.body, + `{"published1":${pub1ser},"published2":${pub2ser},"unpublished1":${unpub1ser},"unpublished2":${unpub2ser}}`, + ); + t.is(ser.slots.length, 2); + t.is(ser.slots[0], published1id); + if (publishing) { + t.assert(ser.slots[1].startsWith('board0')); + } else { + t.is(ser.slots[1], null); + } + + const { published1, unpublished1, published2, unpublished2 } = + marshaller.unserialize(ser); + t.is(published1, published); + t.is(published2, published); + t.is(published1.toString(), '[object Alleged: published]'); + t.is(published2.toString(), '[object Alleged: published]'); + t.is(unpublished1, unpublished2); + if (publishing) { + t.is(unpublished1, unpublished); + t.is(unpublished2, unpublished); + t.is(unpublished1.toString(), '[object Alleged: unpublished]'); + t.is(unpublished2.toString(), '[object Alleged: unpublished]'); + } else { + t.not(unpublished1, unpublished); + t.not(unpublished2, unpublished); + t.is( + unpublished1.toString(), + '[object Alleged: INACCESSIBLE: unpublished]', + ); + t.is( + unpublished2.toString(), + '[object Alleged: INACCESSIBLE: unpublished]', + ); + + // Separate marshals do not compare. + const unpublished3 = marshaller.unserialize( + marshaller.serialize(unpublished), + ); + t.not(unpublished3, unpublished); + t.not(unpublished3, unpublished1); + t.not(unpublished3, unpublished2); + } +}; + +test('getPublishingMarshaller round trips unpublished objects', async t => { + const board = makeBoard(); + const marshaller = board.getPublishingMarshaller(); + await testBoardMarshaller(t, board, marshaller, true); +}); + +test(`getReadonlyMarshaller doesn't leak unpublished objects`, async t => { + const board = makeBoard(); + const marshaller = board.getReadonlyMarshaller(); + await testBoardMarshaller(t, board, marshaller, false); +}); From 82062910b9d8c57f76851e3f6bd5f405a47e04eb Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Thu, 19 May 2022 15:06:41 -0600 Subject: [PATCH 3/7] feat(notifier): `makeStoredSubscription` --- packages/notifier/src/index.js | 1 + packages/notifier/src/storesub.js | 78 ++++++++++++++++++ packages/notifier/src/subscriber.js | 1 + packages/notifier/src/types.js | 26 ++++++ packages/notifier/test/marshal-corpus.js | 73 +++++++++++++++++ .../notifier/test/test-stored-subscription.js | 79 +++++++++++++++++++ 6 files changed, 258 insertions(+) create mode 100644 packages/notifier/src/storesub.js create mode 100644 packages/notifier/test/marshal-corpus.js create mode 100644 packages/notifier/test/test-stored-subscription.js diff --git a/packages/notifier/src/index.js b/packages/notifier/src/index.js index 2252bf8e2df..3aff3d84733 100644 --- a/packages/notifier/src/index.js +++ b/packages/notifier/src/index.js @@ -16,3 +16,4 @@ export { // Consider deprecating or not reexporting makeAsyncIterableFromNotifier, } from './asyncIterableAdaptor.js'; +export * from './storesub.js'; diff --git a/packages/notifier/src/storesub.js b/packages/notifier/src/storesub.js new file mode 100644 index 00000000000..94f36efae8d --- /dev/null +++ b/packages/notifier/src/storesub.js @@ -0,0 +1,78 @@ +// @ts-check +import { E } from '@endo/eventual-send'; +import { Far, makeMarshal } from '@endo/marshal'; +import { observeIteration } from './asyncIterableAdaptor.js'; + +/** + * Begin iterating the source, storing serialized iteration values. If the + * storageNode's `setValue` operation rejects, the iteration will be terminated. + * + * Returns a StoredSubscription that can be used by a client to directly follow + * the iteration themselves, or obtain information to subscribe to the stored + * data out-of-band. + * + * @template T + * @param {Subscription} subscription + * @param {ERef} [storageNode] + * @param {ERef>} [marshaller] + * @returns {StoredSubscription} + */ +export const makeStoredSubscription = ( + subscription, + storageNode, + marshaller = makeMarshal(undefined, undefined, { + marshalSaveError: () => {}, + }), +) => { + /** @type {Unserializer} */ + const unserializer = Far('unserializer', { + unserialize: E(marshaller).unserialize, + }); + + // Abort the iteration on the next observation if the publisher ever fails. + let publishFailed = false; + let publishException; + + const fail = err => { + publishFailed = true; + publishException = err; + }; + + // Must *not* be an async function, because it sometimes must throw to abort + // the iteration. + const publishValue = obj => { + assert(storageNode); + if (publishFailed) { + // To properly abort the iteration, this must be a synchronous exception. + throw publishException; + } + + // Publish the value, capturing any error. + E(marshaller) + .serialize(obj) + .then(serialized => { + const encoded = JSON.stringify(serialized); + return E(storageNode).setValue(encoded); + }) + .catch(fail); + }; + + if (storageNode) { + // Start publishing the source. + observeIteration(subscription, { + updateState: publishValue, + finish: publishValue, + }).catch(fail); + } + + /** @type {StoredSubscription} */ + const storesub = Far('StoredSubscription', { + getStoreKey: () => storageNode && E(storageNode).getStoreKey(), + getUnserializer: () => unserializer, + getSharableSubscriptionInternals: + subscription.getSharableSubscriptionInternals, + [Symbol.asyncIterator]: subscription[Symbol.asyncIterator], + }); + return storesub; +}; +harden(makeStoredSubscription); diff --git a/packages/notifier/src/subscriber.js b/packages/notifier/src/subscriber.js index 5e6c6aeef30..f933943bf76 100644 --- a/packages/notifier/src/subscriber.js +++ b/packages/notifier/src/subscriber.js @@ -46,6 +46,7 @@ const makeSubscriptionIterator = tailP => { next: () => { const resultP = E.get(tailP).head; tailP = E.get(tailP).tail; + Promise.resolve(tailP).catch(() => {}); // suppress unhandled rejection error return resultP; }, }); diff --git a/packages/notifier/src/types.js b/packages/notifier/src/types.js index 627d14e8bff..ce07b5f84d2 100644 --- a/packages/notifier/src/types.js +++ b/packages/notifier/src/types.js @@ -158,3 +158,29 @@ * @property {IterationObserver} publication * @property {Subscription} subscription */ + +/** @typedef {ReturnType} Marshaller */ + +/** + * @typedef {object} Unserializer + * @property {Marshaller['unserialize']} unserialize + */ + +/** + * @typedef {object} StorageNode + * @property {(data: string) => void} setValue publishes some data + * @property {() => unknown} getStoreKey get the externally-reachable store key + * for this storage item + * @property {(subPath: string) => StorageNode} getChildNode TODO: makeChildNode + */ + +/** + * @typedef {object} StoredFacet + * @property {StorageNode['getStoreKey']} getStoreKey get the externally-reachable store key + * @property {() => Unserializer} getUnserializer get the unserializer for the stored data + */ + +/** + * @template T + * @typedef {Subscription & StoredFacet} StoredSubscription + */ diff --git a/packages/notifier/test/marshal-corpus.js b/packages/notifier/test/marshal-corpus.js new file mode 100644 index 00000000000..6c2c8523ec6 --- /dev/null +++ b/packages/notifier/test/marshal-corpus.js @@ -0,0 +1,73 @@ +/** + * Based on roundTripPairs from test-marshal.js + * + * A list of `[body, justinSrc]` pairs, where the body parses into + * an encoding that decodes to a Justin expression that evaluates to something + * that has the same encoding. + */ +export const jsonPairs = harden([ + // Justin is the same as the JSON encoding but without unnecessary quoting + ['[1,2]', '[1,2]'], + ['{"foo":1}', '{foo:1}'], + ['{"a":1,"b":2}', '{a:1,b:2}'], + ['{"a":1,"b":{"c":3}}', '{a:1,b:{c:3}}'], + ['true', 'true'], + ['1', '1'], + ['"abc"', '"abc"'], + ['null', 'null'], + + // Primitives not representable in JSON + ['{"@qclass":"undefined"}', 'undefined'], + // FIGME: Uncomment when this is in agoric-sdk: https://github.com/endojs/endo/pull/1204 + // ['{"@qclass":"NaN"}', 'NaN'], // TODO: uncomment + ['{"@qclass":"Infinity"}', 'Infinity'], + ['{"@qclass":"-Infinity"}', '-Infinity'], + ['{"@qclass":"bigint","digits":"4"}', '4n'], + ['{"@qclass":"bigint","digits":"9007199254740993"}', '9007199254740993n'], + ['{"@qclass":"symbol","name":"@@asyncIterator"}', 'Symbol.asyncIterator'], + ['{"@qclass":"symbol","name":"@@match"}', 'Symbol.match'], + ['{"@qclass":"symbol","name":"foo"}', 'Symbol.for("foo")'], + ['{"@qclass":"symbol","name":"@@@@foo"}', 'Symbol.for("@@foo")'], + + // Arrays and objects + ['[{"@qclass":"undefined"}]', '[undefined]'], + ['{"foo":{"@qclass":"undefined"}}', '{foo:undefined}'], + ['{"@qclass":"error","message":"","name":"Error"}', 'Error("")'], + [ + '{"@qclass":"error","message":"msg","name":"ReferenceError"}', + 'ReferenceError("msg")', + ], + + // The one case where JSON is not a semantic subset of JS + ['{"__proto__":8}', '{["__proto__"]:8}'], + + // The Hilbert Hotel is always tricky + ['{"@qclass":"hilbert","original":8}', '{"@qclass":8}'], + ['{"@qclass":"hilbert","original":"@qclass"}', '{"@qclass":"@qclass"}'], + [ + '{"@qclass":"hilbert","original":{"@qclass":"hilbert","original":8}}', + '{"@qclass":{"@qclass":8}}', + ], + [ + '{"@qclass":"hilbert","original":{"@qclass":"hilbert","original":8,"rest":{"foo":"foo1"}},"rest":{"bar":{"@qclass":"hilbert","original":{"@qclass":"undefined"}}}}', + '{"@qclass":{"@qclass":8,foo:"foo1"},bar:{"@qclass":undefined}}', + ], + + // tagged + ['{"@qclass":"tagged","tag":"x","payload":8}', 'makeTagged("x",8)'], + [ + '{"@qclass":"tagged","tag":"x","payload":{"@qclass":"undefined"}}', + 'makeTagged("x",undefined)', + ], + + // Slots + [ + '[{"@qclass":"slot","iface":"Alleged: for testing Justin","index":0}]', + '[slot(0,"Alleged: for testing Justin")]', + ], + // Tests https://github.com/endojs/endo/issues/1185 fix + [ + '[{"@qclass":"slot","iface":"Alleged: for testing Justin","index":0},{"@qclass":"slot","index":0}]', + '[slot(0,"Alleged: for testing Justin"),slot(0)]', + ], +]); diff --git a/packages/notifier/test/test-stored-subscription.js b/packages/notifier/test/test-stored-subscription.js new file mode 100644 index 00000000000..af20be24a08 --- /dev/null +++ b/packages/notifier/test/test-stored-subscription.js @@ -0,0 +1,79 @@ +// @ts-check + +// eslint-disable-next-line import/order +import { test } from './prepare-test-env-ava.js'; + +import { E } from '@endo/eventual-send'; +import { Far, makeMarshal } from '@endo/marshal'; +import { makeSubscriptionKit, makeStoredSubscription } from '../src/index.js'; + +import '../src/types.js'; +import { jsonPairs } from './marshal-corpus.js'; + +const makeFakeStorage = (path, publication) => { + const fullPath = `publish.${path}`; + const storeKey = harden({ + storeName: 'swingset', + storeSubkey: `swingset/data:${fullPath}`, + }); + /** @type {StorageNode} */ + const storage = Far('fakeStorage', { + getStoreKey: () => storeKey, + setValue: value => { + assert.typeof(value, 'string'); + publication.updateState(value); + }, + getChildNode: () => storage, + }); + return storage; +}; + +test('stored subscription', async t => { + t.plan((jsonPairs.length + 1) * 4 + 1); + const { publication: pubStorage, subscription: subStorage } = + makeSubscriptionKit(); + const storage = makeFakeStorage('publish.foo.bar', pubStorage); + const { subscription, publication } = makeSubscriptionKit(); + const storesub = makeStoredSubscription(subscription, storage); + + t.is(await E(storesub).getStoreKey(), await E(storage).getStoreKey()); + const unserializer = E(storesub).getUnserializer(); + + const ait = E(storesub)[Symbol.asyncIterator](); + const storeAit = subStorage[Symbol.asyncIterator](); + + const { unserialize } = makeMarshal(); + const updateAndCheck = async (description, origValue, expectDone) => { + const nextP = E(ait).next(); + if (expectDone) { + await E(publication).finish(origValue); + } else { + await E(publication).updateState(origValue); + } + + const { done, value } = await nextP; + t.is(done, expectDone, `check doneness for ${description}`); + t.deepEqual( + value, + origValue, + `check value against original ${description}`, + ); + + const { done: storedDone, value: storedEncoded } = await storeAit.next(); + t.assert(!storedDone, `check not stored done for ${description}`); + const storedDecoded = JSON.parse(storedEncoded); + const storedValue = await E(unserializer).unserialize(storedDecoded); + t.deepEqual( + storedValue, + origValue, + `check stored value against original ${description}`, + ); + }; + + for await (const [encoded] of jsonPairs) { + const origValue = unserialize({ body: encoded, slots: [] }); + await updateAndCheck(encoded, origValue, false); + } + + await updateAndCheck('terminal', null, true); +}); From 1ba086b836e30afe7a4c8d22972f24525ebf8740 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 3 Jun 2022 21:48:25 -0500 Subject: [PATCH 4/7] chore(vats): refine ChainStorage type and infer Board type from makeBoard --- packages/vats/src/core/types.js | 4 +++- packages/vats/src/lib-board.js | 2 -- packages/vats/src/types.js | 7 +------ 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/vats/src/core/types.js b/packages/vats/src/core/types.js index cc35d1dc8fc..16a02cdb3d2 100644 --- a/packages/vats/src/core/types.js +++ b/packages/vats/src/core/types.js @@ -189,7 +189,7 @@ * bldIssuerKit: RemoteIssuerKit, * board: Board, * bridgeManager: OptionalBridgeManager, - * chainStorage: unknown, + * chainStorage: ChainStorageNode | undefined, * chainTimerService: TimerService, * client: ClientManager, * clientCreator: ClientCreator, @@ -208,6 +208,8 @@ * zoe: ZoeService, * }>} ChainBootstrapSpace * + * @typedef {ReturnType} ChainStorageNode + * * IDEA/TODO: make types of demo stuff invisible in production behaviors * @typedef {{ * argv: { diff --git a/packages/vats/src/lib-board.js b/packages/vats/src/lib-board.js index 885cc6ad8d6..50c1f4a6d57 100644 --- a/packages/vats/src/lib-board.js +++ b/packages/vats/src/lib-board.js @@ -34,7 +34,6 @@ const calcCrc = (data, crcDigits) => { * @param {object} [options] * @param {string} [options.prefix] * @param {number} [options.crcDigits] - * @returns {Board} */ function makeBoard( initSequence = 0, @@ -83,7 +82,6 @@ function makeBoard( ), }); - /** @type {Board} */ const board = Far('Board', { getPublishingMarshaller: () => publishingMarshaller, getReadonlyMarshaller: () => readonlyMarshaller, diff --git a/packages/vats/src/types.js b/packages/vats/src/types.js index ba682fa84c6..fc0fcb7546e 100644 --- a/packages/vats/src/types.js +++ b/packages/vats/src/types.js @@ -6,12 +6,7 @@ */ /** - * @typedef {object} Board - * @property {(id: string) => any} getValue - * @property {(value: unknown) => string} getId - * @property {(value: unknown) => boolean} has - * @property {() => string[]} ids - * @property {(...path: string[]) => Promise} lookup + * @typedef {ReturnType} Board */ /** From 784d7cf06cbf350798f50f7340f3344502204533 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 3 Jun 2022 21:50:44 -0500 Subject: [PATCH 5/7] feat(run-protocol): publish AMM metrics via chainStorage top level metrics only: amm.metrics --- .../src/proposals/core-proposal.js | 2 ++ .../src/proposals/econ-behaviors.js | 15 ++++++++++++- .../src/vpool-xyk-amm/multipoolMarketMaker.js | 22 +++++++++++++++---- .../run-protocol/src/vpool-xyk-amm/types.js | 4 ++-- .../test/amm/vpool-xyk-amm/setup.js | 1 + packages/run-protocol/test/supports.js | 1 + 6 files changed, 38 insertions(+), 7 deletions(-) diff --git a/packages/run-protocol/src/proposals/core-proposal.js b/packages/run-protocol/src/proposals/core-proposal.js index a3e830e4bc0..07c92d219e4 100644 --- a/packages/run-protocol/src/proposals/core-proposal.js +++ b/packages/run-protocol/src/proposals/core-proposal.js @@ -26,6 +26,8 @@ const ECON_COMMITTEE_MANIFEST = harden({ const SHARED_MAIN_MANIFEST = harden({ [econBehaviors.setupAmm.name]: { consume: { + board: 'board', + chainStorage: true, chainTimerService: 'timer', zoe: 'zoe', economicCommitteeCreatorFacet: 'economicCommittee', diff --git a/packages/run-protocol/src/proposals/econ-behaviors.js b/packages/run-protocol/src/proposals/econ-behaviors.js index 2148eb9c741..7af621917fb 100644 --- a/packages/run-protocol/src/proposals/econ-behaviors.js +++ b/packages/run-protocol/src/proposals/econ-behaviors.js @@ -166,6 +166,8 @@ export const startInterchainPool = async ( }; harden(startInterchainPool); +const AMM_STORAGE_PATH = 'amm'; // TODO: share with agoricNames? + /** * @param { EconomyBootstrapPowers } powers * @param {{ options?: { minInitialPoolLiquidity?: bigint }}} opts @@ -173,9 +175,11 @@ harden(startInterchainPool); export const setupAmm = async ( { consume: { + board, chainTimerService, zoe, economicCommitteeCreatorFacet: committeeCreator, + chainStorage, }, produce: { ammCreatorFacet, @@ -214,6 +218,11 @@ export const setupAmm = async ( AmountMath.make(runBrand, minInitialPoolLiquidity), ); + const chainStoragePresence = await chainStorage; + const ammChainStorage = await (chainStoragePresence && + E(chainStoragePresence).getChildNode(AMM_STORAGE_PATH)); + const marshaller = E(board).getPublishingMarshaller(); + const ammGovernorTerms = { timer, electorateInstance, @@ -221,7 +230,11 @@ export const setupAmm = async ( governed: { terms: ammTerms, issuerKeywordRecord: { Central: centralIssuer }, - privateArgs: { initialPoserInvitation: poserInvitation }, + privateArgs: { + initialPoserInvitation: poserInvitation, + storageNode: ammChainStorage, + marshaller, + }, }, }; /** @type {{ creatorFacet: GovernedContractFacetAccess, publicFacet: GovernorPublic, instance: Instance }} */ diff --git a/packages/run-protocol/src/vpool-xyk-amm/multipoolMarketMaker.js b/packages/run-protocol/src/vpool-xyk-amm/multipoolMarketMaker.js index cee9a6b311f..33a2427c5a9 100644 --- a/packages/run-protocol/src/vpool-xyk-amm/multipoolMarketMaker.js +++ b/packages/run-protocol/src/vpool-xyk-amm/multipoolMarketMaker.js @@ -5,7 +5,7 @@ import { Far } from '@endo/marshal'; import { AssetKind, makeIssuerKit } from '@agoric/ertp'; import { handleParamGovernance, ParamTypes } from '@agoric/governance'; -import { makeSubscriptionKit } from '@agoric/notifier'; +import { makeStoredSubscription, makeSubscriptionKit } from '@agoric/notifier'; import { assertIssuerKeywords, @@ -111,7 +111,11 @@ const trace = makeTracer('XykAmm'); * creator. * * @param {ZCF} zcf - * @param {{initialPoserInvitation: Invitation}} privateArgs + * @param {{ + * initialPoserInvitation: Invitation, + * storageNode?: ERef, + * marshaller?: ERef, + * }} privateArgs */ const start = async (zcf, privateArgs) => { /** @@ -161,8 +165,18 @@ const start = async (zcf, privateArgs) => { const quoteIssuerKit = makeIssuerKit('Quote', AssetKind.SET); /** @type {SubscriptionRecord} */ - const { publication: metricsPublication, subscription: metricsSubscription } = - makeSubscriptionKit(); + const { + publication: metricsPublication, + subscription: rawMetricsSubscription, + } = makeSubscriptionKit(); + const { storageNode, marshaller } = privateArgs; + const metricsStorageNode = + storageNode && E(storageNode).getChildNode('metrics'); // TODO: magic string + const metricsSubscription = makeStoredSubscription( + rawMetricsSubscription, + metricsStorageNode, + marshaller, + ); const updateMetrics = () => { metricsPublication.updateState( harden({ XYK: Array.from(secondaryBrandToPool.keys()) }), diff --git a/packages/run-protocol/src/vpool-xyk-amm/types.js b/packages/run-protocol/src/vpool-xyk-amm/types.js index 6b9b5601cc3..5088202d83b 100644 --- a/packages/run-protocol/src/vpool-xyk-amm/types.js +++ b/packages/run-protocol/src/vpool-xyk-amm/types.js @@ -82,7 +82,7 @@ * @property {() => PriceAuthority} getToCentralPriceAuthority * @property {() => PriceAuthority} getFromCentralPriceAuthority * @property {() => VirtualPool} getVPool - * @property {() => Subscription} getMetrics + * @property {() => StoredSubscription} getMetrics */ /** @@ -136,7 +136,7 @@ * Prices and notifications about changing prices. * @property {() => Brand[]} getAllPoolBrands * @property {() => Allocation} getProtocolPoolBalance - * @property {() => Subscription} getMetrics + * @property {() => StoredSubscription} getMetrics * @property {(brand: Brand) => Subscription} getPoolMetrics */ diff --git a/packages/run-protocol/test/amm/vpool-xyk-amm/setup.js b/packages/run-protocol/test/amm/vpool-xyk-amm/setup.js index 2b879137eef..8c2b4f0f9ad 100644 --- a/packages/run-protocol/test/amm/vpool-xyk-amm/setup.js +++ b/packages/run-protocol/test/amm/vpool-xyk-amm/setup.js @@ -71,6 +71,7 @@ export const setupAMMBootstrap = async ( produce.agoricNamesAdmin.resolve(agoricNamesAdmin); installGovernance(zoe, spaces.installation.produce); + produce.chainStorage.resolve(undefined); return { produce, consume, ...spaces }; }; diff --git a/packages/run-protocol/test/supports.js b/packages/run-protocol/test/supports.js index a8dde00a3ab..7f901279911 100644 --- a/packages/run-protocol/test/supports.js +++ b/packages/run-protocol/test/supports.js @@ -71,6 +71,7 @@ export const setupBootstrap = (t, optTimer = undefined) => { const timer = optTimer || buildManualTimer(t.log); produce.chainTimerService.resolve(timer); + produce.chainStorage.resolve(undefined); const { zoe, From 9c2a8689cd02d028d00f286d67da2b5de9f85083 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Fri, 3 Jun 2022 23:18:59 -0600 Subject: [PATCH 6/7] fix(priceAuthorityTransform): ask for the sourceQuoteIssuer on demand --- .../zoe/src/contractSupport/priceAuthorityTransform.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/zoe/src/contractSupport/priceAuthorityTransform.js b/packages/zoe/src/contractSupport/priceAuthorityTransform.js index 2dc35299254..9e130363082 100644 --- a/packages/zoe/src/contractSupport/priceAuthorityTransform.js +++ b/packages/zoe/src/contractSupport/priceAuthorityTransform.js @@ -35,10 +35,6 @@ export const makePriceAuthorityTransform = async ({ }) => { const quoteIssuer = E(quoteMint).getIssuer(); const quoteBrand = await E(quoteIssuer).getBrand(); - const sourceQuoteIssuer = E(sourcePriceAuthority).getQuoteIssuer( - sourceBrandIn, - sourceBrandOut, - ); /** * Ensure that the brandIn/brandOut pair is supported. @@ -66,6 +62,11 @@ export const makePriceAuthorityTransform = async ({ const scaleQuote = async sourceQuote => { const { quotePayment: sourceQuotePayment } = sourceQuote; + const sourceQuoteIssuer = E(sourcePriceAuthority).getQuoteIssuer( + sourceBrandIn, + sourceBrandOut, + ); + /** @type {Amount<'set'>} */ const { value: sourceQuoteValue } = await E(sourceQuoteIssuer).getAmountOf( sourceQuotePayment, From affccabfb3d4534792f1e9234a50d9f97db2c11c Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Sat, 4 Jun 2022 09:46:21 -0600 Subject: [PATCH 7/7] feat(notifier): allow `makeSubscriptionKit(initialState)` --- packages/notifier/src/subscriber.js | 7 +++- .../notifier/test/test-stored-subscription.js | 40 +++++++++---------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/packages/notifier/src/subscriber.js b/packages/notifier/src/subscriber.js index f933943bf76..4d3944a46cc 100644 --- a/packages/notifier/src/subscriber.js +++ b/packages/notifier/src/subscriber.js @@ -57,9 +57,10 @@ const makeSubscriptionIterator = tailP => { * distributed pub/sub. * * @template T + * @param {T[]} optionalInitialState * @returns {SubscriptionRecord} */ -const makeSubscriptionKit = () => { +const makeSubscriptionKit = (...optionalInitialState) => { /** @type {((internals: ERef>) => void) | undefined} */ let rear; const hp = new HandledPromise(r => (rear = r)); @@ -96,6 +97,10 @@ const makeSubscriptionKit = () => { rear = undefined; }, }); + + if (optionalInitialState.length > 0) { + publication.updateState(optionalInitialState[0]); + } return harden({ publication, subscription }); }; harden(makeSubscriptionKit); diff --git a/packages/notifier/test/test-stored-subscription.js b/packages/notifier/test/test-stored-subscription.js index af20be24a08..5c091ac3690 100644 --- a/packages/notifier/test/test-stored-subscription.js +++ b/packages/notifier/test/test-stored-subscription.js @@ -29,11 +29,14 @@ const makeFakeStorage = (path, publication) => { }; test('stored subscription', async t => { - t.plan((jsonPairs.length + 1) * 4 + 1); + t.plan((jsonPairs.length + 2) * 4 + 1); + + /** @type {any} */ + const initialValue = 'first value'; const { publication: pubStorage, subscription: subStorage } = makeSubscriptionKit(); const storage = makeFakeStorage('publish.foo.bar', pubStorage); - const { subscription, publication } = makeSubscriptionKit(); + const { subscription, publication } = makeSubscriptionKit(initialValue); const storesub = makeStoredSubscription(subscription, storage); t.is(await E(storesub).getStoreKey(), await E(storage).getStoreKey()); @@ -43,22 +46,7 @@ test('stored subscription', async t => { const storeAit = subStorage[Symbol.asyncIterator](); const { unserialize } = makeMarshal(); - const updateAndCheck = async (description, origValue, expectDone) => { - const nextP = E(ait).next(); - if (expectDone) { - await E(publication).finish(origValue); - } else { - await E(publication).updateState(origValue); - } - - const { done, value } = await nextP; - t.is(done, expectDone, `check doneness for ${description}`); - t.deepEqual( - value, - origValue, - `check value against original ${description}`, - ); - + const check = async (description, origValue, expectedDone) => { const { done: storedDone, value: storedEncoded } = await storeAit.next(); t.assert(!storedDone, `check not stored done for ${description}`); const storedDecoded = JSON.parse(storedEncoded); @@ -68,12 +56,24 @@ test('stored subscription', async t => { origValue, `check stored value against original ${description}`, ); + + const { done, value } = await E(ait).next(); + t.is(done, expectedDone, `check doneness for ${description}`); + t.deepEqual( + value, + origValue, + `check value against original ${description}`, + ); }; + await check(initialValue, initialValue, false); for await (const [encoded] of jsonPairs) { const origValue = unserialize({ body: encoded, slots: [] }); - await updateAndCheck(encoded, origValue, false); + await E(publication).updateState(origValue); + await check(encoded, origValue, false); } - await updateAndCheck('terminal', null, true); + const terminalNull = null; + await E(publication).finish(terminalNull); + await check('terminal null', terminalNull, true); });