Skip to content

Commit

Permalink
fix(smart-wallet): create purses for new assets lazily
Browse files Browse the repository at this point in the history
 - 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<NameHub> 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
  • Loading branch information
dckc committed Jan 20, 2023
1 parent de8bb08 commit e241ba0
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 109 deletions.
4 changes: 4 additions & 0 deletions packages/inter-protocol/test/smartWallet/contexts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -44,6 +47,7 @@ export const makeDefaultTestContext = async (t, makeSpace) => {
{
agoricNames,
board: consume.board,
assetPublisher,
},
{ storageNode, walletBridgeManager },
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,20 @@ 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();
t.assert(offersFacet, 'undefined offersFacet');
// 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);
Expand Down
2 changes: 1 addition & 1 deletion packages/smart-wallet/src/offers.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const UNPUBLISHED_RESULT = 'UNPUBLISHED';
* @param {object} opts.powers
* @param {Pick<Console, 'info'| 'error'>} opts.powers.logger
* @param {(spec: import('./invitations').InvitationSpec) => ERef<Invitation>} opts.powers.invitationFromSpec
* @param {(brand: Brand) => import('./types').RemotePurse} opts.powers.purseForBrand
* @param {(brand: Brand) => Promise<import('./types').RemotePurse>} opts.powers.purseForBrand
* @param {(status: OfferStatus) => void} opts.onStatusChange
* @param {(offerId: OfferId, invitationAmount: Amount<'set'>, continuation: import('./types').RemoteInvitationMakers) => void} opts.onNewContinuingOffer
*/
Expand Down
14 changes: 7 additions & 7 deletions packages/smart-wallet/src/payments.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<import('./types').RemotePurse>} purseForBrand
* @param {{ receive: (payment: *) => Promise<Amount> }} depositFacet
*/
export const makePaymentsHelper = (purseForBrand, depositFacet) => {
Expand All @@ -29,14 +29,14 @@ export const makePaymentsHelper = (purseForBrand, depositFacet) => {
'withdrawPayments can be called once per helper',
);
keywordPaymentPromises = objectMap(give, amount => {
/** @type {import('./types').RemotePurse<any>} */
const purse = purseForBrand(amount.brand);
return E(purse)
.withdraw(amount)
.then(payment => {
/** @type {Promise<import('./types').RemotePurse<any>>} */
const purseP = purseForBrand(amount.brand);
return Promise.all([purseP, E(purseP).withdraw(amount)]).then(
([purse, payment]) => {
paymentToPurse.set(payment, purse);
return payment;
});
},
);
});
return keywordPaymentPromises;
},
Expand Down
143 changes: 70 additions & 73 deletions packages/smart-wallet/src/smartWallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -80,7 +77,7 @@ const mapToRecord = map => Object.fromEntries(map.entries());
* @typedef {{
* brand: Brand,
* displayInfo: DisplayInfo,
* issuer: ERef<Issuer>,
* issuer: Issuer,
* petname: import('./types').Petname
* }} BrandDescriptor
* For use by clients to describe brands to users. Includes `displayInfo` to save a remote call.
Expand All @@ -98,8 +95,13 @@ const mapToRecord = map => Object.fromEntries(map.entries());
*
* @typedef {{
* agoricNames: ERef<import('@agoric/vats').NameHub>,
* invitationIssuer: ERef<Issuer<'set'>>,
* registry: {
* getRegisteredAsset: (b: Brand) => BrandDescriptor,
* getRegisteredBrands: () => BrandDescriptor[],
* },
* invitationIssuer: Issuer<'set'>,
* invitationBrand: Brand<'set'>,
* invitationDisplayInfo: DisplayInfo,
* publicMarshaller: Marshaller,
* storageNode: ERef<StorageNode>,
* zoe: ERef<ZoeService>,
Expand All @@ -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
Expand All @@ -117,7 +118,6 @@ const mapToRecord = map => Object.fromEntries(map.entries());
* paymentQueues: MapStore<Brand, Array<import('@endo/far').FarRef<Payment>>>,
* offerToInvitationMakers: MapStore<string, import('./types').RemoteInvitationMakers>,
* offerToUsedInvitation: MapStore<string, Amount>,
* brandDescriptors: MapStore<Brand, BrandDescriptor>,
* brandPurses: MapStore<Brand, RemotePurse>,
* purseBalances: MapStore<RemotePurse, Amount>,
* updatePublishKit: StoredPublishKit<UpdateRecord>,
Expand All @@ -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'),
}),
);

Expand Down Expand Up @@ -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<UpdateRecord>} */
Expand Down Expand Up @@ -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', {
Expand All @@ -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()),
}),
};

Expand Down Expand Up @@ -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,
Expand All @@ -282,39 +284,15 @@ const SmartWalletKit = defineVirtualFarClassKit(
});
},

/** @type {(desc: Omit<BrandDescriptor, 'displayInfo'>, purse: RemotePurse) => Promise<void>} */
/** @type {(desc: BrandDescriptor, purse: ERef<RemotePurse>) => Promise<void>} */
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;
Expand All @@ -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)
Expand Down Expand Up @@ -405,6 +386,7 @@ const SmartWalletKit = defineVirtualFarClassKit(
invitationIssuer,
offerToInvitationMakers,
offerToUsedInvitation,
registry,
updatePublishKit,
} = this.state;

Expand All @@ -424,7 +406,22 @@ const SmartWalletKit = defineVirtualFarClassKit(
invitationPurse,
offerToInvitationMakers.get,
),
purseForBrand: brandPurses.get,
/**
* @param {Brand} brand
* @returns {Promise<RemotePurse>}
*/
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 => {
Expand Down Expand Up @@ -491,34 +488,34 @@ 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(
{
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),
);
},
});
},
},
Expand Down
Loading

0 comments on commit e241ba0

Please sign in to comment.