Skip to content

Commit 0eb378b

Browse files
authored
feat: allow dapps to suggest petnames for issuer/brand, instance, installation (#1308)
* feat: allow dapps to suggest petnames for issuer/brand, instance, installation * chore: call updatePursesState and updateInboxState on every potential petname change. Inefficient but correct.
1 parent 354a3ae commit 0eb378b

File tree

5 files changed

+760
-76
lines changed

5 files changed

+760
-76
lines changed

packages/cosmic-swingset/lib/ag-solo/vats/lib-dehydrate.js

+34-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { makeMarshal } from '@agoric/marshal';
44
import makeStore from '@agoric/store';
5-
import { assert, details } from '@agoric/assert';
5+
import { assert, details, q } from '@agoric/assert';
66

77
// Marshalling for the UI should only use the user's petnames. We will
88
// call marshalling for the UI "dehydration" and "hydration" to distinguish it from
@@ -27,10 +27,43 @@ export const makeDehydrator = (initialUnnamedCount = 0) => {
2727
petnameToVal.init(petname, val);
2828
valToPetname.init(val, petname);
2929
};
30+
const renamePetname = (petname, val) => {
31+
assert(
32+
valToPetname.has(val),
33+
details`val ${val} has not been previously named, would you like to add it instead?`,
34+
);
35+
assert(
36+
!petnameToVal.has(petname),
37+
details`petname ${petname} is already in use`,
38+
);
39+
// Delete the old mappings.
40+
const oldPetname = valToPetname.get(val);
41+
petnameToVal.delete(oldPetname);
42+
valToPetname.delete(val);
43+
44+
// Add the new mappings.
45+
petnameToVal.init(petname, val);
46+
valToPetname.init(val, petname);
47+
};
48+
const deletePetname = petname => {
49+
assert(
50+
petnameToVal.has(petname),
51+
details`petname ${q(
52+
petname,
53+
)} has not been previously named, would you like to add it instead?`,
54+
);
55+
56+
// Delete the mappings.
57+
const val = petnameToVal.get(petname);
58+
petnameToVal.delete(petname);
59+
valToPetname.delete(val);
60+
};
3061
const mapping = harden({
3162
valToPetname,
3263
petnameToVal,
3364
addPetname,
65+
renamePetname,
66+
deletePetname,
3467
kind,
3568
});
3669
petnameKindToMapping.init(kind, mapping);

packages/cosmic-swingset/lib/ag-solo/vats/lib-wallet.js

+191-40
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export async function makeWallet({
2828
const { makeMapping, dehydrate } = makeDehydrator();
2929
const purseMapping = makeMapping('purse');
3030
const brandMapping = makeMapping('brand');
31+
const instanceMapping = makeMapping('instance');
32+
const installationMapping = makeMapping('installation');
3133

3234
// Brand Table
3335
// Columns: key:brand | issuer | amountMath
@@ -75,6 +77,7 @@ export async function makeWallet({
7577
});
7678
},
7779
getBrandForIssuer: issuerToBrand.get,
80+
hasIssuer: issuerToBrand.has,
7881
});
7982
const brandTable = makeTable(
8083
validateSomewhat,
@@ -147,12 +150,83 @@ export async function makeWallet({
147150
pursesStateChangeHandler(getPursesState());
148151
}
149152

153+
async function updateAllPurseState() {
154+
return Promise.all(
155+
purseMapping.petnameToVal
156+
.entries()
157+
.map(([petname, purse]) => updatePursesState(petname, purse)),
158+
);
159+
}
160+
161+
const display = value => fillInSlots(dehydrate(harden(value)));
162+
163+
const displayProposal = proposalTemplate => {
164+
const { want, give, exit = { onDemand: null } } = proposalTemplate;
165+
const compile = pursePetnameExtentKeywordRecord => {
166+
if (pursePetnameExtentKeywordRecord === undefined) {
167+
return undefined;
168+
}
169+
return Object.fromEntries(
170+
Object.entries(pursePetnameExtentKeywordRecord).map(
171+
([keyword, { pursePetname, extent }]) => {
172+
// eslint-disable-next-line no-use-before-define
173+
const purse = getPurse(pursePetname);
174+
const brand = purseToBrand.get(purse);
175+
const amount = { brand, extent };
176+
return [keyword, { pursePetname, amount: display(amount) }];
177+
},
178+
),
179+
);
180+
};
181+
const proposal = {
182+
want: compile(want),
183+
give: compile(give),
184+
exit,
185+
};
186+
return proposal;
187+
};
188+
150189
async function updateInboxState(id, offer) {
151190
// Only sent the uncompiled offer to the client.
152-
inboxState.set(id, offer);
191+
const {
192+
instanceHandleBoardId,
193+
installationHandleBoardId,
194+
proposalTemplate,
195+
} = offer;
196+
// We could get the instanceHandle and installationHandle from the
197+
// board and store them to prevent having to make this call each
198+
// time, but if we want the offers to be able to sent to the
199+
// frontend, we cannot store the instanceHandle and
200+
// installationHandle in these offer objects because the handles
201+
// are presences and we don't wish to send presences to the
202+
// frontend.
203+
const instanceHandle = await E(board).getValue(instanceHandleBoardId);
204+
const installationHandle = await E(board).getValue(
205+
installationHandleBoardId,
206+
);
207+
const offerForDisplay = {
208+
...offer,
209+
instancePetname: display(instanceHandle).petname,
210+
installationPetname: display(installationHandle).petname,
211+
proposalForDisplay: displayProposal(proposalTemplate),
212+
};
213+
214+
inboxState.set(id, offerForDisplay);
153215
inboxStateChangeHandler(getInboxState());
154216
}
155217

218+
async function updateAllInboxState() {
219+
return Array.from(inboxState.entries()).map(([id, offer]) =>
220+
updateInboxState(id, offer),
221+
);
222+
}
223+
224+
// TODO: fix this horribly inefficient update on every potential
225+
// petname change.
226+
async function updateAllState() {
227+
return Promise.all([updateAllPurseState(), updateAllInboxState()]);
228+
}
229+
156230
// handle the update, which has already resolved to a record. If the offer is
157231
// 'done', mark the offer 'complete', otherwise resubscribe to the notifier.
158232
function updateOrResubscribe(id, offerHandle, update) {
@@ -293,7 +367,27 @@ export async function makeWallet({
293367
brandMapping.addPetname(petnameForBrand, brand);
294368
return `issuer ${q(petnameForBrand)} successfully added to wallet`;
295369
};
296-
return issuerSavedP.then(addBrandPetname);
370+
return issuerSavedP.then(addBrandPetname).then(updateAllState);
371+
};
372+
373+
const addInstance = (petname, instanceHandle) => {
374+
// We currently just add the petname mapped to the instanceHandle
375+
// value, but we could have a list of known instances for
376+
// possible display in the wallet.
377+
instanceMapping.addPetname(petname, instanceHandle);
378+
// We don't wait for the update before returning.
379+
updateAllState();
380+
return `instance ${q(petname)} successfully added to wallet`;
381+
};
382+
383+
const addInstallation = (petname, installationHandle) => {
384+
// We currently just add the petname mapped to the installationHandle
385+
// value, but we could have a list of known installations for
386+
// possible display in the wallet.
387+
installationMapping.addPetname(petname, installationHandle);
388+
// We don't wait for the update before returning.
389+
updateAllState();
390+
return `installation ${q(petname)} successfully added to wallet`;
297391
};
298392

299393
const makeEmptyPurse = async (brandPetname, petnameForPurse) => {
@@ -382,34 +476,6 @@ export async function makeWallet({
382476
return { proposal, purseKeywordRecord };
383477
};
384478

385-
const display = value => fillInSlots(dehydrate(harden(value)));
386-
387-
const displayProposal = proposalTemplate => {
388-
const {
389-
want = {},
390-
give = {},
391-
exit = { onDemand: null },
392-
} = proposalTemplate;
393-
const compile = pursePetnameExtentKeywordRecord => {
394-
return Object.fromEntries(
395-
Object.entries(pursePetnameExtentKeywordRecord).map(
396-
([keyword, { pursePetname, extent }]) => {
397-
const purse = getPurse(pursePetname);
398-
const brand = purseToBrand.get(purse);
399-
const amount = { brand, extent };
400-
return [keyword, { pursePetname, amount: display(amount) }];
401-
},
402-
),
403-
);
404-
};
405-
const proposal = {
406-
want: compile(want),
407-
give: compile(give),
408-
exit,
409-
};
410-
return proposal;
411-
};
412-
413479
const compileOffer = async offer => {
414480
const { inviteHandleBoardId } = offer;
415481
const { proposal, purseKeywordRecord } = compileProposal(
@@ -437,7 +503,6 @@ export async function makeWallet({
437503
id: rawId,
438504
instanceHandleBoardId,
439505
installationHandleBoardId,
440-
proposalTemplate,
441506
} = rawOffer;
442507
const id = `${requestContext.origin}#${rawId}`;
443508
assert(
@@ -448,24 +513,17 @@ export async function makeWallet({
448513
typeof installationHandleBoardId === 'string',
449514
details`installationHandleBoardId must be a string`,
450515
);
451-
const instanceHandle = await E(board).getValue(instanceHandleBoardId);
452-
const installationHandle = await E(board).getValue(
453-
installationHandleBoardId,
454-
);
455516
const offer = harden({
456517
...rawOffer,
457518
id,
458519
requestContext,
459520
status: undefined,
460-
instancePetname: display(instanceHandle).petname,
461-
installationPetname: display(installationHandle).petname,
462-
proposalForDisplay: displayProposal(proposalTemplate),
463521
});
464522
idToOffer.init(id, offer);
465523
updateInboxState(id, offer);
466524

467-
// Start compiling the template, saving a promise for it.
468-
idToCompiledOfferP.set(id, compileOffer(offer));
525+
// Compile the offer
526+
idToCompiledOfferP.set(id, await compileOffer(offer));
469527

470528
// Our inbox state may have an enriched offer.
471529
updateInboxState(id, idToOffer.get(id));
@@ -615,10 +673,100 @@ export async function makeWallet({
615673
.then(saveAsDefault);
616674
}
617675

676+
function acceptPetname(acceptFn, suggestedPetname, boardId) {
677+
return E(board)
678+
.getValue(boardId)
679+
.then(value => acceptFn(suggestedPetname, value));
680+
}
681+
682+
async function suggestIssuer(suggestedPetname, issuerBoardId) {
683+
// TODO: add an approval step in the wallet UI in which
684+
// suggestion can be rejected and the suggested petname can be
685+
// changed
686+
// eslint-disable-next-line no-use-before-define
687+
return acceptPetname(wallet.addIssuer, suggestedPetname, issuerBoardId);
688+
}
689+
690+
async function suggestInstance(suggestedPetname, instanceHandleBoardId) {
691+
// TODO: add an approval step in the wallet UI in which
692+
// suggestion can be rejected and the suggested petname can be
693+
// changed
694+
695+
return acceptPetname(
696+
// eslint-disable-next-line no-use-before-define
697+
wallet.addInstance,
698+
suggestedPetname,
699+
instanceHandleBoardId,
700+
);
701+
}
702+
703+
async function suggestInstallation(
704+
suggestedPetname,
705+
installationHandleBoardId,
706+
) {
707+
// TODO: add an approval step in the wallet UI in which
708+
// suggestion can be rejected and the suggested petname can be
709+
// changed
710+
711+
return acceptPetname(
712+
// eslint-disable-next-line no-use-before-define
713+
wallet.addInstallation,
714+
suggestedPetname,
715+
installationHandleBoardId,
716+
);
717+
}
718+
719+
function renameIssuer(petname, issuer) {
720+
assert(
721+
brandTable.hasIssuer(issuer),
722+
`issuer has not been previously added`,
723+
);
724+
const brand = brandTable.getBrandForIssuer(issuer);
725+
brandMapping.renamePetname(petname, brand);
726+
// We don't wait for the update before returning.
727+
updateAllState();
728+
return `issuer ${q(petname)} successfully renamed in wallet`;
729+
}
730+
731+
function renameInstance(petname, instance) {
732+
instanceMapping.renamePetname(petname, instance);
733+
// We don't wait for the update before returning.
734+
updateAllState();
735+
return `instance ${q(petname)} successfully renamed in wallet`;
736+
}
737+
738+
function renameInstallation(petname, installation) {
739+
installationMapping.renamePetname(petname, installation);
740+
// We don't wait for the update before returning.
741+
updateAllState();
742+
return `installation ${q(petname)} successfully renamed in wallet`;
743+
}
744+
745+
function getIssuer(petname) {
746+
const brand = brandMapping.petnameToVal.get(petname);
747+
return brandTable.get(brand).issuer;
748+
}
749+
750+
function getInstance(petname) {
751+
return instanceMapping.petnameToVal.get(petname);
752+
}
753+
754+
function getInstallation(petname) {
755+
return installationMapping.petnameToVal.get(petname);
756+
}
757+
618758
const wallet = harden({
619759
addIssuer,
760+
addInstance,
761+
addInstallation,
762+
renameIssuer,
763+
renameInstance,
764+
renameInstallation,
765+
getInstance,
766+
getInstallation,
620767
makeEmptyPurse,
621768
deposit,
769+
getIssuer,
622770
getIssuers,
623771
getPurses,
624772
getPurse,
@@ -632,6 +780,9 @@ export async function makeWallet({
632780
getOfferHandles: ids => ids.map(wallet.getOfferHandle),
633781
addDepositFacet,
634782
getDepositFacetId,
783+
suggestIssuer,
784+
suggestInstance,
785+
suggestInstallation,
635786
});
636787

637788
// Make Zoe invite purse

0 commit comments

Comments
 (0)