From ed9741fa0f98c866cc71c1005c6f553b8cc0dd4d Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 19 Dec 2022 18:34:54 -0600 Subject: [PATCH 1/3] test(smart-wallet): avoid O(wallets) storage writes for a new asset failing test case for #6652 --- packages/smart-wallet/test/test-addAsset.js | 87 +++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 packages/smart-wallet/test/test-addAsset.js diff --git a/packages/smart-wallet/test/test-addAsset.js b/packages/smart-wallet/test/test-addAsset.js new file mode 100644 index 00000000000..f0733e87e07 --- /dev/null +++ b/packages/smart-wallet/test/test-addAsset.js @@ -0,0 +1,87 @@ +// @ts-check +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +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/zoe/tools/eventLoopIteration.js'; +import { makeDefaultTestContext } from './contexts.js'; +import { makeMockTestSpace } from './supports.js'; + +/** @type {import('ava').TestFn>>} */ +const test = anyTest; + +const TODO = undefined; + +test.before(async t => { + const withBankManager = async () => { + const bridge = TODO; + const bankManager = E(buildBankVatRoot()).makeBankManager(bridge); + const noop = () => {}; + const space0 = await makeMockTestSpace(noop); + space0.produce.bankManager.reset(); + space0.produce.bankManager.resolve(bankManager); + return space0; + }; + t.context = await makeDefaultTestContext(t, withBankManager); +}); + +const DEBUG = false; +const bigIntReplacer = (_key, val) => + typeof val === 'bigint' ? Number(val) : val; + +const range = qty => [...Array(qty).keys()]; + +/** + * NOTE: this doesn't test all forms of work. + * A better test would measure inter-vat messages or some such. + */ +test.failing('avoid O(wallets) storage writes for a new asset', async t => { + const bankManager = t.context.consume.bankManager; + + let chainStorageWrites = 0; + + const startUser = async ix => { + const address = `agoric1u${ix}`; + const smartWallet = t.context.simpleProvideWallet(address); + + // stick around waiting for things to happen + const current = await E(smartWallet).getCurrentSubscriber(); + /** @type {bigint | undefined} */ + let publishCount; + for (;;) { + // eslint-disable-next-line no-await-in-loop + const news = await E(current).subscribeAfter(publishCount); + publishCount = news.publishCount; + chainStorageWrites += 1; + if (DEBUG) { + console.log(JSON.stringify(news.head, bigIntReplacer, 2)); + } + } + }; + + const simulate = async (qty, denom, name) => { + range(qty).forEach(startUser); + await eventLoopIteration(); + const initialWrites = chainStorageWrites; + + const kit = makeIssuerKit(name); + await E(bankManager).addAsset(denom, name, name, kit); + await eventLoopIteration(); + return { + qty, + initialWrites, + addedWrites: chainStorageWrites - initialWrites, + }; + }; + const base = await simulate(2, 'ibc/dia1', 'DAI_axl'); + const exp = await simulate(6, 'ibc/dia2', 'DAI_grv'); + + t.log({ + base: { wallets: base.qty, writes: base.addedWrites }, + test: { wallets: exp.qty + base.qty, writes: exp.addedWrites }, + }); + t.true( + exp.addedWrites <= (base.addedWrites * exp.qty) / base.qty / 2, + 'actual writes should be less than half of linear growth', + ); +}); From de8bb0874fdbf22ecb140a627a63d966ba11f029 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 10 Jan 2023 16:49:23 -0600 Subject: [PATCH 2/3] docs(smart-wallet): typo --- packages/smart-wallet/src/walletFactory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/smart-wallet/src/walletFactory.js b/packages/smart-wallet/src/walletFactory.js index 4edc688f086..dd910d27f5b 100644 --- a/packages/smart-wallet/src/walletFactory.js +++ b/packages/smart-wallet/src/walletFactory.js @@ -66,7 +66,7 @@ export const publishDepositFacet = async ( */ // NB: even though all the wallets share this contract, they -// 1. they should rely on that; they may be partitioned later +// 1. they should not rely on that; they may be partitioned later // 2. they should never be able to detect behaviors from another wallet /** * From e241ba03a7d9f441436b3d987f9327060d7dd8ce Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 10 Jan 2023 16:50:05 -0600 Subject: [PATCH 3/3] fix(smart-wallet): create purses for new assets lazily - subscribe for new vbank assets once, in walletFactory.js - provide a read-only registry of the assets - no change to RPC protocol: still publish all brand descriptors in all wallet states - refactor: clarify invitationIssuer promise - add invitationDisplayInfo to synchronously-provided SharedParams - refine typeguards, esp. M.any() => M.remotable('...') - hoist makeAssetRegistry() to module scope to reduce in-scope authority feat(smart-wallet): initialize with existing vbank assets - add anyBank to walletFactory terms so we can subscribe to vbank assets without waiting for creation of the 1st wallet - refine customTermsShape from M.not(M.undefined()) to M.eref(M.remotable()) - attenuate poolBank to {getAssetSubscription} - 'want stable' test: don't assume incremental updates include all assets; get the full current state to start with - constrain BrandDescriptor to take settled issuers, since publishing records that include promises doesn't let off-chain callers compare identities - losen addBrand() typeguard to allow M.eref(PurseShape) since we await the purse inside the function - static type for purseForBrand motivates tweak to keywordPaymentPromises - fixup: Promise isn't durable a promise for agoricNames isn't durable. so I need deeplyFulfilledObject after all. but the types don't work out. so back to deeplyFulfilled I guess --- .../test/smartWallet/contexts.js | 4 + .../test/smartWallet/test-psm-integration.js | 8 +- packages/smart-wallet/src/offers.js | 2 +- packages/smart-wallet/src/payments.js | 14 +- packages/smart-wallet/src/smartWallet.js | 143 +++++++++--------- packages/smart-wallet/src/walletFactory.js | 96 +++++++++--- packages/smart-wallet/test/contexts.js | 4 + packages/smart-wallet/test/test-addAsset.js | 12 +- .../test/test-startWalletFactory.js | 6 +- packages/vats/src/core/startWalletFactory.js | 5 +- 10 files changed, 185 insertions(+), 109 deletions(-) diff --git a/packages/inter-protocol/test/smartWallet/contexts.js b/packages/inter-protocol/test/smartWallet/contexts.js index 392687d40e5..0f62d720cfa 100644 --- a/packages/inter-protocol/test/smartWallet/contexts.js +++ b/packages/inter-protocol/test/smartWallet/contexts.js @@ -35,6 +35,9 @@ export const makeDefaultTestContext = async (t, makeSpace) => { 'wallet', ); + const assetPublisher = await E(consume.bankManager).getBankForAddress( + 'anyAddress', + ); const bridgeManager = await consume.bridgeManager; const walletBridgeManager = await (bridgeManager && E(bridgeManager).register(BridgeId.WALLET)); @@ -44,6 +47,7 @@ export const makeDefaultTestContext = async (t, makeSpace) => { { agoricNames, board: consume.board, + assetPublisher, }, { storageNode, walletBridgeManager }, ); diff --git a/packages/inter-protocol/test/smartWallet/test-psm-integration.js b/packages/inter-protocol/test/smartWallet/test-psm-integration.js index 5b217bc7ee2..d47df0f9032 100644 --- a/packages/inter-protocol/test/smartWallet/test-psm-integration.js +++ b/packages/inter-protocol/test/smartWallet/test-psm-integration.js @@ -131,6 +131,9 @@ test('want stable', async t => { const stableBrand = await E(agoricNames).lookup('brand', Stable.symbol); const wallet = await t.context.simpleProvideWallet('agoric1wantstable'); + const current = await E(E(wallet).getCurrentSubscriber()) + .subscribeAfter() + .then(pub => pub.head.value); const computedState = coalesceUpdates(E(wallet).getUpdatesSubscriber()); const offersFacet = wallet.getOffersFacet(); @@ -138,7 +141,10 @@ test('want stable', async t => { // let promises settle to notify brands and create purses await eventLoopIteration(); - t.is(purseBalance(computedState, anchor.brand), 0n); + t.deepEqual(current.purses.find(b => b.brand === anchor.brand).balance, { + brand: anchor.brand, + value: 0n, + }); t.log('Fund the wallet'); assert(anchor.mint); diff --git a/packages/smart-wallet/src/offers.js b/packages/smart-wallet/src/offers.js index d9bede8bb44..b926dfdc24b 100644 --- a/packages/smart-wallet/src/offers.js +++ b/packages/smart-wallet/src/offers.js @@ -35,7 +35,7 @@ export const UNPUBLISHED_RESULT = 'UNPUBLISHED'; * @param {object} opts.powers * @param {Pick} opts.powers.logger * @param {(spec: import('./invitations').InvitationSpec) => ERef} opts.powers.invitationFromSpec - * @param {(brand: Brand) => import('./types').RemotePurse} opts.powers.purseForBrand + * @param {(brand: Brand) => Promise} opts.powers.purseForBrand * @param {(status: OfferStatus) => void} opts.onStatusChange * @param {(offerId: OfferId, invitationAmount: Amount<'set'>, continuation: import('./types').RemoteInvitationMakers) => void} opts.onNewContinuingOffer */ diff --git a/packages/smart-wallet/src/payments.js b/packages/smart-wallet/src/payments.js index 844f6b3b861..904d1062d03 100644 --- a/packages/smart-wallet/src/payments.js +++ b/packages/smart-wallet/src/payments.js @@ -4,7 +4,7 @@ import { E } from '@endo/far'; /** * Used in an offer execution to manage payments state safely. * - * @param {(brand: Brand) => import('./types').RemotePurse} purseForBrand + * @param {(brand: Brand) => Promise} purseForBrand * @param {{ receive: (payment: *) => Promise }} depositFacet */ export const makePaymentsHelper = (purseForBrand, depositFacet) => { @@ -29,14 +29,14 @@ export const makePaymentsHelper = (purseForBrand, depositFacet) => { 'withdrawPayments can be called once per helper', ); keywordPaymentPromises = objectMap(give, amount => { - /** @type {import('./types').RemotePurse} */ - const purse = purseForBrand(amount.brand); - return E(purse) - .withdraw(amount) - .then(payment => { + /** @type {Promise>} */ + const purseP = purseForBrand(amount.brand); + return Promise.all([purseP, E(purseP).withdraw(amount)]).then( + ([purse, payment]) => { paymentToPurse.set(payment, purse); return payment; - }); + }, + ); }); return keywordPaymentPromises; }, diff --git a/packages/smart-wallet/src/smartWallet.js b/packages/smart-wallet/src/smartWallet.js index 03f7a8877c6..2b5a49a5490 100644 --- a/packages/smart-wallet/src/smartWallet.js +++ b/packages/smart-wallet/src/smartWallet.js @@ -2,15 +2,12 @@ import { AmountMath, AmountShape, BrandShape, + DisplayInfoShape, IssuerShape, PaymentShape, PurseShape, } from '@agoric/ertp'; -import { - makeStoredPublishKit, - observeIteration, - observeNotifier, -} from '@agoric/notifier'; +import { makeStoredPublishKit, observeNotifier } from '@agoric/notifier'; import { fit, M, makeScalarMapStore } from '@agoric/store'; import { defineVirtualFarClassKit, @@ -80,7 +77,7 @@ const mapToRecord = map => Object.fromEntries(map.entries()); * @typedef {{ * brand: Brand, * displayInfo: DisplayInfo, - * issuer: ERef, + * issuer: Issuer, * petname: import('./types').Petname * }} BrandDescriptor * For use by clients to describe brands to users. Includes `displayInfo` to save a remote call. @@ -98,8 +95,13 @@ const mapToRecord = map => Object.fromEntries(map.entries()); * * @typedef {{ * agoricNames: ERef, - * invitationIssuer: ERef>, + * registry: { + * getRegisteredAsset: (b: Brand) => BrandDescriptor, + * getRegisteredBrands: () => BrandDescriptor[], + * }, + * invitationIssuer: Issuer<'set'>, * invitationBrand: Brand<'set'>, + * invitationDisplayInfo: DisplayInfo, * publicMarshaller: Marshaller, * storageNode: ERef, * zoe: ERef, @@ -108,7 +110,6 @@ const mapToRecord = map => Object.fromEntries(map.entries()); * @typedef {ImmutableState & MutableState} State * - `brandPurses` is precious and closely held. defined as late as possible to reduce its scope. * - `offerToInvitationMakers` is precious and closely held. - * - `brandDescriptors` will be precious. Currently it includes invitation brand and what we've received from the bank manager. * - `purseBalances` is a cache of what we've received from purses. Held so we can publish all balances on change. * * @typedef {UniqueParams & SharedParams} HeldParams @@ -117,7 +118,6 @@ const mapToRecord = map => Object.fromEntries(map.entries()); * paymentQueues: MapStore>>, * offerToInvitationMakers: MapStore, * offerToUsedInvitation: MapStore, - * brandDescriptors: MapStore, * brandPurses: MapStore, * purseBalances: MapStore, * updatePublishKit: StoredPublishKit, @@ -135,24 +135,26 @@ const mapToRecord = map => Object.fromEntries(map.entries()); * @returns {State} */ export const initState = (unique, shared) => { - // Some validation of inputs. "any" erefs because this synchronous call can't check more than that. + // Some validation of inputs. fit( unique, harden({ address: M.string(), - bank: M.eref(M.any()), - invitationPurse: M.eref(M.any()), + bank: M.eref(M.remotable()), + invitationPurse: PurseShape, }), ); fit( shared, harden({ - agoricNames: M.eref(M.any()), - invitationIssuer: M.eref(M.any()), + agoricNames: M.eref(M.remotable('agoricNames')), + invitationIssuer: IssuerShape, invitationBrand: BrandShape, - publicMarshaller: M.any(), - storageNode: M.eref(M.any()), - zoe: M.eref(M.any()), + invitationDisplayInfo: DisplayInfoShape, + publicMarshaller: M.remotable('Marshaller'), + storageNode: M.eref(M.remotable('StorageNode')), + zoe: M.eref(M.remotable('ZoeService')), + registry: M.remotable('AssetRegistry'), }), ); @@ -185,7 +187,6 @@ export const initState = (unique, shared) => { }; const nonpreciousState = { - brandDescriptors: makeScalarMapStore(), // What purses have reported on construction and by getCurrentAmountNotifier updates. purseBalances: makeScalarMapStore(), /** @type {StoredPublishKit} */ @@ -215,8 +216,9 @@ const behaviorGuards = { brand: BrandShape, issuer: M.eref(IssuerShape), petname: M.string(), + displayInfo: DisplayInfoShape, }, - PurseShape, + M.eref(PurseShape), ).returns(M.promise()), }), deposit: M.interface('depositFacetI', { @@ -230,10 +232,10 @@ const behaviorGuards = { handleBridgeAction: M.call(shape.StringCapData, M.boolean()).returns( M.promise(), ), - getDepositFacet: M.call().returns(M.eref(M.any())), - getOffersFacet: M.call().returns(M.eref(M.any())), - getCurrentSubscriber: M.call().returns(M.eref(M.any())), - getUpdatesSubscriber: M.call().returns(M.eref(M.any())), + getDepositFacet: M.call().returns(M.remotable()), + getOffersFacet: M.call().returns(M.remotable()), + getCurrentSubscriber: M.call().returns(M.remotable()), + getUpdatesSubscriber: M.call().returns(M.remotable()), }), }; @@ -265,13 +267,13 @@ const SmartWalletKit = defineVirtualFarClassKit( publishCurrentState() { const { - brandDescriptors, currentPublishKit, offerToUsedInvitation, purseBalances, + registry, } = this.state; currentPublishKit.publisher.publish({ - brands: [...brandDescriptors.values()], + brands: registry.getRegisteredBrands(), purses: [...purseBalances.values()].map(a => ({ brand: a.brand, balance: a, @@ -282,39 +284,15 @@ const SmartWalletKit = defineVirtualFarClassKit( }); }, - /** @type {(desc: Omit, purse: RemotePurse) => Promise} */ + /** @type {(desc: BrandDescriptor, purse: ERef) => Promise} */ async addBrand(desc, purseRef) { - const { - address, - brandDescriptors, - brandPurses, - paymentQueues, - updatePublishKit, - } = this.state; - // assert haven't received this issuer before. - const descriptorsHas = brandDescriptors.has(desc.brand); + const { address, brandPurses, paymentQueues, updatePublishKit } = + this.state; const pursesHas = brandPurses.has(desc.brand); - assert( - !(descriptorsHas && pursesHas), - 'repeated brand from bank asset subscription', - ); - assert( - !(descriptorsHas || pursesHas), - 'corrupted state; one store has brand already', - ); + assert(!pursesHas, 'repeated brand from bank asset subscription'); + + const purse = await purseRef; // promises don't fit in durable storage - const [purse, displayInfo] = await Promise.all([ - purseRef, - E(desc.brand).getDisplayInfo(), - ]); - - // save all five of these in a collection (indexed by brand?) so that when - // it's time to take an offer description you know where to get the - // relevant purse. when it's time to make an offer, you know how to make - // payments. REMEMBER when doing that, need to handle every exception to - // put the money back in the purse if anything fails. - const descriptor = { ...desc, displayInfo }; - brandDescriptors.init(desc.brand, descriptor); brandPurses.init(desc.brand, purse); const { helper } = this.facets; @@ -334,7 +312,10 @@ const SmartWalletKit = defineVirtualFarClassKit( }, }); - updatePublishKit.publisher.publish({ updated: 'brand', descriptor }); + updatePublishKit.publisher.publish({ + updated: 'brand', + descriptor: desc, + }); // deposit queued payments const payments = paymentQueues.has(desc.brand) @@ -405,6 +386,7 @@ const SmartWalletKit = defineVirtualFarClassKit( invitationIssuer, offerToInvitationMakers, offerToUsedInvitation, + registry, updatePublishKit, } = this.state; @@ -424,7 +406,22 @@ const SmartWalletKit = defineVirtualFarClassKit( invitationPurse, offerToInvitationMakers.get, ), - purseForBrand: brandPurses.get, + /** + * @param {Brand} brand + * @returns {Promise} + */ + purseForBrand: async brand => { + if (brandPurses.has(brand)) { + return brandPurses.get(brand); + } + const desc = registry.getRegisteredAsset(brand); + const { bank } = this.state; + /** @type {RemotePurse} */ + // @ts-expect-error cast to RemotePurse + const purse = E(bank).getPurse(desc.brand); + facets.helper.addBrand(desc, purse); + return purse; + }, logger, }, onStatusChange: offerStatus => { @@ -491,8 +488,12 @@ const SmartWalletKit = defineVirtualFarClassKit( }, { finish: ({ state, facets }) => { - const { invitationBrand, invitationIssuer, invitationPurse, bank } = - state; + const { + invitationBrand, + invitationDisplayInfo, + invitationIssuer, + invitationPurse, + } = state; const { helper } = facets; // Ensure a purse for each issuer helper.addBrand( @@ -500,25 +501,21 @@ const SmartWalletKit = defineVirtualFarClassKit( brand: invitationBrand, issuer: invitationIssuer, petname: 'invitations', + displayInfo: invitationDisplayInfo, }, // @ts-expect-error cast to RemotePurse /** @type {RemotePurse} */ (invitationPurse), ); - // watch the bank for new issuers to make purses out of - void observeIteration(E(bank).getAssetSubscription(), { - async updateState(desc) { - /** @type {RemotePurse} */ - // @ts-expect-error cast to RemotePurse - const purse = await E(bank).getPurse(desc.brand); - await helper.addBrand( - { - brand: desc.brand, - issuer: desc.issuer, - petname: desc.issuerName, - }, - purse, + + // Schedule creation of a purse for each registered brand. + state.registry.getRegisteredBrands().forEach(desc => { + // In this sync method, we can't await the outcome. + void E(desc.issuer) + .makeEmptyPurse() + // @ts-expect-error cast + .then((/** @type {RemotePurse} */ purse) => + helper.addBrand(desc, purse), ); - }, }); }, }, diff --git a/packages/smart-wallet/src/walletFactory.js b/packages/smart-wallet/src/walletFactory.js index dd910d27f5b..a4284a8768e 100644 --- a/packages/smart-wallet/src/walletFactory.js +++ b/packages/smart-wallet/src/walletFactory.js @@ -5,11 +5,12 @@ */ import { WalletName } from '@agoric/internal'; -import { fit, M, makeHeapFarInstance } from '@agoric/store'; +import { fit, M, makeHeapFarInstance, makeScalarMapStore } from '@agoric/store'; import { makeAtomicProvider } from '@agoric/store/src/stores/store-utils.js'; import { makeScalarBigMapStore } from '@agoric/vat-data'; import { makeMyAddressNameAdminKit } from '@agoric/vats/src/core/basic-behaviors.js'; -import { E } from '@endo/far'; +import { observeIteration } from '@agoric/notifier'; +import { E, Far } from '@endo/far'; import { makeSmartWallet } from './smartWallet.js'; import { shape } from './typeGuards.js'; @@ -18,14 +19,15 @@ import '@agoric/vats/exported.js'; export const privateArgsShape = harden( M.splitRecord( - { storageNode: M.eref(M.any()) }, - { walletBridgeManager: M.eref(M.any()) }, + { storageNode: M.eref(M.remotable('StorageNode')) }, + { walletBridgeManager: M.eref(M.remotable('walletBridgeManager')) }, ), ); export const customTermsShape = harden({ - agoricNames: M.not(M.undefined()), - board: M.not(M.undefined()), + agoricNames: M.eref(M.remotable('agoricNames')), + board: M.eref(M.remotable('board')), + assetPublisher: M.eref(M.remotable('Bank')), }); /** @@ -56,13 +58,64 @@ export const publishDepositFacet = async ( ); }; +/** + * @param {AssetPublisher} assetPublisher + */ +const makeAssetRegistry = assetPublisher => { + /** + * @typedef {{ + * brand: Brand, + * displayInfo: DisplayInfo, + * issuer: Issuer, + * petname: import('./types').Petname + * }} BrandDescriptor + * For use by clients to describe brands to users. Includes `displayInfo` to save a remote call. + */ + /** @type {MapStore} */ + const brandDescriptors = makeScalarMapStore(); + + // watch the bank for new issuers to make purses out of + void observeIteration(E(assetPublisher).getAssetSubscription(), { + async updateState(desc) { + const { brand, issuer: issuerP, issuerName: petname } = desc; + // await issuer identity for use in chainStorage + const [issuer, displayInfo] = await Promise.all([ + issuerP, + E(brand).getDisplayInfo(), + ]); + + brandDescriptors.init(desc.brand, { + brand, + issuer, + petname, + displayInfo, + }); + }, + }); + + // XXX marshal requires Far, but clients make sync calls + const registry = Far('AssetRegistry', { + /** @param {Brand} brand */ + getRegisteredAsset: brand => { + return brandDescriptors.get(brand); + }, + getRegisteredBrands: () => [...brandDescriptors.values()], + }); + return registry; +}; + /** * @typedef {{ * agoricNames: ERef, * board: ERef, + * assetPublisher: AssetPublisher, * }} SmartWalletContractTerms * * @typedef {import('@agoric/vats').NameHub} NameHub + * + * @typedef {{ + * getAssetSubscription: () => ERef> + * }} AssetPublisher */ // NB: even though all the wallets share this contract, they @@ -77,7 +130,7 @@ export const publishDepositFacet = async ( * }} privateArgs */ export const start = async (zcf, privateArgs) => { - const { agoricNames, board } = zcf.getTerms(); + const { agoricNames, board, assetPublisher } = zcf.getTerms(); const zoe = zcf.getZoeService(); const { storageNode, walletBridgeManager } = privateArgs; @@ -122,19 +175,28 @@ export const start = async (zcf, privateArgs) => { E(walletBridgeManager).setHandler(handleWalletAction)); // Resolve these first because the wallet maker must be synchronous - const getInvitationIssuer = E(zoe).getInvitationIssuer(); - const [invitationIssuer, invitationBrand, publicMarshaller] = - await Promise.all([ - getInvitationIssuer, - E(getInvitationIssuer).getBrand(), - E(board).getReadonlyMarshaller(), - ]); + const invitationIssuerP = E(zoe).getInvitationIssuer(); + const [ + invitationIssuer, + invitationBrand, + invitationDisplayInfo, + publicMarshaller, + ] = await Promise.all([ + invitationIssuerP, + E(invitationIssuerP).getBrand(), + E(E(invitationIssuerP).getBrand()).getDisplayInfo(), + E(board).getReadonlyMarshaller(), + ]); + + const registry = makeAssetRegistry(assetPublisher); const shared = harden({ agoricNames, invitationBrand, + invitationDisplayInfo, invitationIssuer, publicMarshaller, + registry, storageNode, zoe, }); @@ -144,9 +206,9 @@ export const start = async (zcf, privateArgs) => { M.interface('walletFactoryCreatorI', { provideSmartWallet: M.callWhen( M.string(), - M.await(M.remotable()), - M.await(M.remotable()), - ).returns([M.remotable(), M.boolean()]), + M.await(M.remotable('Bank')), + M.await(M.remotable('namesByAddressAdmin')), + ).returns([M.remotable('SmartWallet'), M.boolean()]), }), { /** diff --git a/packages/smart-wallet/test/contexts.js b/packages/smart-wallet/test/contexts.js index 33d055a3970..4f3f7891167 100644 --- a/packages/smart-wallet/test/contexts.js +++ b/packages/smart-wallet/test/contexts.js @@ -34,6 +34,9 @@ export const makeDefaultTestContext = async (t, makeSpace) => { 'wallet', ); + const assetPublisher = await E(consume.bankManager).getBankForAddress( + 'anyAddress', + ); const bridgeManager = await consume.bridgeManager; const walletBridgeManager = await (bridgeManager && E(bridgeManager).register(BridgeId.WALLET)); @@ -43,6 +46,7 @@ export const makeDefaultTestContext = async (t, makeSpace) => { { agoricNames, board: consume.board, + assetPublisher, }, { storageNode, walletBridgeManager }, ); diff --git a/packages/smart-wallet/test/test-addAsset.js b/packages/smart-wallet/test/test-addAsset.js index f0733e87e07..28ddccac10b 100644 --- a/packages/smart-wallet/test/test-addAsset.js +++ b/packages/smart-wallet/test/test-addAsset.js @@ -10,12 +10,10 @@ import { makeMockTestSpace } from './supports.js'; /** @type {import('ava').TestFn>>} */ const test = anyTest; -const TODO = undefined; - test.before(async t => { const withBankManager = async () => { - const bridge = TODO; - const bankManager = E(buildBankVatRoot()).makeBankManager(bridge); + const noBridge = undefined; + const bankManager = E(buildBankVatRoot()).makeBankManager(noBridge); const noop = () => {}; const space0 = await makeMockTestSpace(noop); space0.produce.bankManager.reset(); @@ -35,7 +33,7 @@ const range = qty => [...Array(qty).keys()]; * NOTE: this doesn't test all forms of work. * A better test would measure inter-vat messages or some such. */ -test.failing('avoid O(wallets) storage writes for a new asset', async t => { +test('avoid O(wallets) storage writes for a new asset', async t => { const bankManager = t.context.consume.bankManager; let chainStorageWrites = 0; @@ -73,8 +71,8 @@ test.failing('avoid O(wallets) storage writes for a new asset', async t => { addedWrites: chainStorageWrites - initialWrites, }; }; - const base = await simulate(2, 'ibc/dia1', 'DAI_axl'); - const exp = await simulate(6, 'ibc/dia2', 'DAI_grv'); + const base = await simulate(2, 'ibc/dai1', 'DAI_axl'); + const exp = await simulate(6, 'ibc/dai2', 'DAI_grv'); t.log({ base: { wallets: base.qty, writes: base.addedWrites }, diff --git a/packages/smart-wallet/test/test-startWalletFactory.js b/packages/smart-wallet/test/test-startWalletFactory.js index c3b49338e9f..7c359e2a676 100644 --- a/packages/smart-wallet/test/test-startWalletFactory.js +++ b/packages/smart-wallet/test/test-startWalletFactory.js @@ -64,6 +64,7 @@ test('customTermsShape', async t => { { agoricNames, board, + assetPublisher: /** @type {any} */ ({}), // @ts-expect-error extra term extra: board, }, @@ -71,7 +72,7 @@ test('customTermsShape', async t => { ), { message: - '{"agoricNames":"[Promise]","board":"[Promise]","extra":"[Seen]"} - Must not have unexpected properties: ["extra"]', + '{"agoricNames":"[Promise]","assetPublisher":{},"board":"[Promise]","extra":"[Seen]"} - Must not have unexpected properties: ["extra"]', }, ); @@ -88,7 +89,7 @@ test('customTermsShape', async t => { ), { message: - '{"agoricNames":"[Promise]"} - Must have missing properties ["board"]', + '{"agoricNames":"[Promise]"} - Must have missing properties ["board","assetPublisher"]', }, ); }); @@ -99,6 +100,7 @@ test('privateArgsShape', async t => { const terms = { agoricNames, board, + assetPublisher: /** @type {any} */ ({}), }; // missing an arg diff --git a/packages/vats/src/core/startWalletFactory.js b/packages/vats/src/core/startWalletFactory.js index 4181c6c15bf..1666b51e65f 100644 --- a/packages/vats/src/core/startWalletFactory.js +++ b/packages/vats/src/core/startWalletFactory.js @@ -184,10 +184,14 @@ export const startWalletFactory = async ( feeIssuerP, ]); + const poolBank = E(bankManager).getBankForAddress(poolAddr); const terms = await deeplyFulfilled( harden({ agoricNames, board, + assetPublisher: Far('AssetPublisher', { + getAssetSubscription: () => E(poolBank).getAssetSubscription(), + }), }), ); /** @type {WalletFactoryStartResult} */ @@ -202,7 +206,6 @@ export const startWalletFactory = async ( ); walletFactoryStartResult.resolve(wfFacets); instanceProduce.walletFactory.resolve(wfFacets.instance); - const poolBank = E(bankManager).getBankForAddress(poolAddr); const ppFacets = await startGovernedInstance( {