Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(smart-wallet): create purses for new assets lazily #6774

Merged
merged 4 commits into from
Jan 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why remove this explanation for M.any?

btw, should some of these M.remotable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why remove this explanation for M.any?

Because I got rid of all the M.any cases. Oops... but then I reverted that change without putting the comment back.

btw, should some of these M.remotable?

Yes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refining the typeguards isn't necessary to fix #6652, so arguably it should go in a separate PR. You don't mind if I include it here, do you?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM, makes sense with the anys gone :)

// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

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