Skip to content

Commit

Permalink
feat: render non-vbank NFTs in offers (#125) (#126)
Browse files Browse the repository at this point in the history
Co-authored-by: Samuel Siegart <[email protected]>
  • Loading branch information
iomekam and samsiegart authored Sep 21, 2023
1 parent 0fd1b4a commit 03c1244
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 78 deletions.
3 changes: 2 additions & 1 deletion wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
"homepage": "/wallet",
"type": "module",
"dependencies": {
"@agoric/rpc": "^0.0.2-dev-2c6fbc5.0",
"@agoric/casting": "^0.4.3-dev-6bce049.0",
"@agoric/cosmic-proto": "0.2.2-dev-a5437cf.0",
"@agoric/ertp": "^0.16.3-dev-6bce049.0",
"@agoric/internal": "0.3.2",
"@agoric/nat": "^4.1.0",
"@agoric/notifier": "^0.6.2",
"@agoric/rpc": "^0.6.0",
"@agoric/smart-wallet": "^0.5.4-dev-6bce049.0",
"@agoric/ui-components": "^0.3.9-dev-2c6fbc5.0",
"@agoric/web-components": "^0.6.4-dev-2c6fbc5.0",
Expand All @@ -29,6 +29,7 @@
"@endo/far": "^0.2.18",
"@endo/init": "^0.5.56",
"@endo/marshal": "^0.8.5",
"@endo/patterns": "^0.2.6",
"@mui/icons-material": "^5.1.0",
"@mui/lab": "^5.0.0-alpha.67",
"@mui/material": "^5.1.0",
Expand Down
47 changes: 25 additions & 22 deletions wallet/src/components/Proposal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,40 @@ import { stringify } from '../util/marshal';
import { Typography } from '@mui/material';

import './Offer.scss';
import type { Amount, DisplayInfo } from '@agoric/ertp/src/types';

const OfferEntryFromTemplate = (
type,
[role, { value: stringifiedValue, pursePetname }],
purses,
type: { header: string; move: string },
entry: [
any,
{ amount: Amount; brandPetname: string; displayInfo: DisplayInfo },
],
) => {
const value = BigInt(stringifiedValue);
const purse = purses.find(p => p.pursePetname === pursePetname);
if (!purse) {
return null;
}
const [role, { amount, brandPetname, displayInfo }] = entry;

return (
<div className="OfferEntry" key={purse.brandPetname}>
<div className="OfferEntry" key={brandPetname}>
<h6>
{type.header} {role}
</h6>
<div className="Token">
<BrandIcon brandPetname={purse.brandPetname} />
<BrandIcon brandPetname={brandPetname} />
<div>
<PurseValue
value={value}
displayInfo={purse.displayInfo}
brandPetname={purse.brandPetname}
value={amount.value}
displayInfo={displayInfo}
brandPetname={brandPetname}
/>
{type.move} <PetnameSpan name={purse.pursePetname} />
{type.move} <PetnameSpan name={brandPetname} />
</div>
</div>
</div>
);
};

const OfferEntryFromDisplayInfo = (type, [role, { amount, pursePetname }]) => {
const OfferEntryFromDisplayInfo = (type, entry) => {
const [role, { amount, pursePetname }] = entry;

const value =
amount.displayInfo.assetKind === 'nat' ? Nat(amount.value) : amount.value;
return (
Expand Down Expand Up @@ -77,10 +79,10 @@ const GiveFromDisplayInfo = entry =>
const WantFromDisplayInfo = entry =>
OfferEntryFromDisplayInfo(entryTypes.want, entry);

const GiveFromTemplate = (entry, purses) =>
OfferEntryFromTemplate(entryTypes.give, entry, purses);
const WantFromTemplate = (entry, purses) =>
OfferEntryFromTemplate(entryTypes.want, entry, purses);
const GiveFromTemplate = entry =>
OfferEntryFromTemplate(entryTypes.give, entry);
const WantFromTemplate = entry =>
OfferEntryFromTemplate(entryTypes.want, entry);

const cmp = (a, b) => {
if (a < b) {
Expand Down Expand Up @@ -237,10 +239,10 @@ const Proposal = ({ offer, purses, swingsetParams, beansOwing }) => {
);

const Gives = sortedEntries(give).map(g =>
hasDisplayInfo ? GiveFromDisplayInfo(g) : GiveFromTemplate(g, purses),
hasDisplayInfo ? GiveFromDisplayInfo(g) : GiveFromTemplate(g),
);
const Wants = sortedEntries(want).map(w =>
hasDisplayInfo ? WantFromDisplayInfo(w) : WantFromTemplate(w, purses),
hasDisplayInfo ? WantFromDisplayInfo(w) : WantFromTemplate(w),
);

return (
Expand Down Expand Up @@ -283,9 +285,10 @@ const Proposal = ({ offer, purses, swingsetParams, beansOwing }) => {

export default withApplicationContext(
Proposal,
({ purses, swingsetParams, beansOwing }) => ({
({ purses, swingsetParams, beansOwing, watcher }) => ({
swingsetParams,
purses,
beansOwing,
watcher,
}),
);
11 changes: 7 additions & 4 deletions wallet/src/components/PurseValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { useState, useEffect, useRef } from 'react';
import PetnameSpan from './PetnameSpan';
import { stringify } from '../util/marshal';
import { isCopyBag } from '@endo/patterns';

const Item = ({ showDivider, children }) => {
const [expanded, setExpanded] = useState(false);
Expand Down Expand Up @@ -189,9 +190,9 @@ const RichAmountDisplay = ({ text, items }) => {
const PurseValue = ({ value, displayInfo, brandPetname }) => {
const isNat = displayInfo?.assetKind === AssetKind.NAT;
const isSet = displayInfo?.assetKind === AssetKind.SET;
const isCopyBag = displayInfo?.assetKind === AssetKind.COPY_BAG;
const isCopyBagResult = displayInfo?.assetKind === AssetKind.COPY_BAG;

if (isCopyBag && Object.prototype.hasOwnProperty.call(value, 'payload')) {
if (isCopyBagResult && isCopyBag(value)) {
value = value.payload;
}

Expand All @@ -216,7 +217,7 @@ const PurseValue = ({ value, displayInfo, brandPetname }) => {
));

const copyBagItems =
isCopyBag &&
isCopyBagResult &&
value.map((entry, index) => (
<CopyBagItem
key={stringify(entry[0], true)}
Expand All @@ -230,7 +231,9 @@ const PurseValue = ({ value, displayInfo, brandPetname }) => {
<Box sx={{ fontWeight: 600 }}>
{isNat && text}
{isSet && <RichAmountDisplay text={text} items={setItems} />}
{isCopyBag && <RichAmountDisplay text={text} items={copyBagItems} />}
{isCopyBagResult && (
<RichAmountDisplay text={text} items={copyBagItems} />
)}
</Box>
);
};
Expand Down
2 changes: 1 addition & 1 deletion wallet/src/components/SmartWalletConnection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ const SmartWalletConnection = ({
return makeAgoricChainStorageWatcher(
keplrConnection.rpc,
keplrConnection.chainId,
context.fromBoard.unserialize,
backendError,
context.fromBoard,
);
}, [keplrConnection, context]);

Expand Down
111 changes: 82 additions & 29 deletions wallet/src/service/Offers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import type { Notifier } from '@agoric/notifier/src/types';
import type { Petname } from '@agoric/smart-wallet/src/types';
import type { Amount, Brand, DisplayInfo } from '@agoric/ertp/src/types';
import type { InvitationSpec } from '@agoric/smart-wallet/src/invitations';
import { AgoricChainStoragePathKind, ChainStorageWatcher } from '@agoric/rpc';
import { deeplyFulfilledObject, objectMap } from '@agoric/internal';

// XXX These should be imported from @agoric/web-components.
export type PurseInfo = {
Expand All @@ -33,12 +35,14 @@ export type PurseInfo = {
denom?: string;
};

// XXX better name?
type PurseDisplayInfo = {
value?: number | string; // Localstorage cannot serialize BigInt.
pursePetname?: Petname;
amount?: CapData<string>;
};
type GiveOrWantEntries = {
[keyword: string]: {
value?: number | string; // Localstorage cannot serialize BigInt.
pursePetname?: Petname;
amount?: CapData<string>;
};
[keyword: string]: PurseDisplayInfo;
};

const sourceDescriptionForSpec = (spec: InvitationSpec) => {
Expand All @@ -58,11 +62,17 @@ export const getOfferService = (
offerUpdatesNotifier: Notifier<OfferStatus>,
pendingOffersNotifier: Notifier<OfferStatus>,
boardIdMarshaller: Marshal<string>,
watcher: ChainStorageWatcher,
) => {
const offers = new Map<number, Offer>();
const { notifier, updater } = makeNotifierKit<Offer[]>();
const broadcastUpdates = () => updater.updateState([...offers.values()]);

const brandsP = watcher.queryOnce<[string, unknown][]>([
AgoricChainStoragePathKind.Data,
'published.agoricNames.brand',
]);

// Takes an offer object from storage and augments it with a spend action and
// everything needed to display it in the UI.
const unserializeOfferFromStorage = async (
Expand Down Expand Up @@ -109,26 +119,41 @@ export const getOfferService = (
return Object.fromEntries(entries);
};

const readDisplayInfo = async (entry: PurseDisplayInfo) => {
const amount: Amount = await E(boardIdMarshaller).unserialize(
entry.amount as CapData<string>,
);
const purse = brandToPurse.get(amount.brand);

if (purse) {
const { pursePetname: brandPetname, displayInfo } = purse;
return harden({ amount, brandPetname, displayInfo });
}

const [brands, boardAux] = await Promise.all([
brandsP,
watcher.queryBoardAux<{
displayInfo: DisplayInfo;
}>([amount.brand])[0],
]);

const brandPetname: string = brands
.find(([_, brand]) => brand === brand)
?.at(0) as string;
const displayInfo: DisplayInfo | undefined = boardAux?.displayInfo;

return harden({ amount, brandPetname, displayInfo });
};

// Takes give/want entries from dapps and augments them with data needed to
// display them in the UI, namely a pursePetname and value.
const makeProposalTemplateDisplayable = async (
const makeProposalTemplateDisplayable = (
proposalTemplate: GiveOrWantEntries,
) =>
Object.fromEntries(
await Promise.all(
Object.entries(proposalTemplate).map(async ([kw, entry]) => {
if (entry.amount && !(entry.pursePetname && entry.value)) {
const unserializedAmount = await E(boardIdMarshaller).unserialize(
entry.amount,
);
entry.pursePetname = brandToPurse.get(unserializedAmount.brand)
?.pursePetname;
entry.value = String(unserializedAmount.value);
}
return [kw, entry];
}),
),
) => {
return deeplyFulfilledObject(
objectMap(proposalTemplate, readDisplayInfo),
);
};

const [give, want, displayableGiveTemplate, displayableWantTemplate] =
await Promise.all([
Expand Down Expand Up @@ -257,15 +282,43 @@ export const getOfferService = (
};

const watchPendingOffers = async (brandToPurse: Map<Brand, PurseInfo>) => {
const makeProposalEntriesDisplayable = (proposalEntries: {
const makeProposalEntriesDisplayable = async (proposalEntries: {
[key: string]: Amount;
}) =>
Object.fromEntries(
Object.entries(proposalEntries).map(([kw, entry]) => {
const pursePetname = brandToPurse.get(entry.brand)?.pursePetname;
const value = String(entry.value);
return [kw, { pursePetname, value }];
}),
await Promise.all(
Object.entries(proposalEntries).map(
async ([kw, unserializedAmount]) => {
assert(unserializedAmount);
const purse = brandToPurse.get(unserializedAmount.brand);
if (purse) {
const { pursePetname: brandPetname, displayInfo } = purse;
return [
kw,
{ amount: unserializedAmount, brandPetname, displayInfo },
];
}

const [brands, boardAux] = await Promise.all([
brandsP,
watcher.queryBoardAux<{
displayInfo: DisplayInfo;
}>([unserializedAmount.brand])[0],
]);

const brandPetname: string = brands
.find(([_, brand]) => brand === brand)
?.at(0) as string;
const displayInfo: DisplayInfo | undefined =
boardAux?.displayInfo;

return [
kw,
{ amount: unserializedAmount, brandPetname, displayInfo },
];
},
),
),
);

for await (const pendingOffers of makeAsyncIterableFromNotifier(
Expand All @@ -282,10 +335,10 @@ export const getOfferService = (
proposalTemplate: {
give:
o.proposal.give &&
makeProposalEntriesDisplayable(o.proposal.give),
(await makeProposalEntriesDisplayable(o.proposal.give)),
want:
o.proposal.want &&
makeProposalEntriesDisplayable(o.proposal.want),
(await makeProposalEntriesDisplayable(o.proposal.want)),
},
sourceDescription:
'Source: ' + sourceDescriptionForSpec(o.invitationSpec),
Expand Down
11 changes: 9 additions & 2 deletions wallet/src/util/WalletBackendAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { querySwingsetParams } from '../util/querySwingsetParams';
import { getDappService } from '../service/Dapps';
import { getIssuerService } from '../service/Issuers';
import { getOfferService } from '../service/Offers';
import { isCopyBag } from '@endo/patterns';

import type {
Brand,
Expand Down Expand Up @@ -242,6 +243,7 @@ export const makeWalletBridgeFromFollowers = (
offerUpdatesNotifer,
pendingOffersNotifier,
marshaller,
watcher,
);

const { notifier: beansOwingNotifier, updater: beansOwingUpdater } =
Expand Down Expand Up @@ -402,7 +404,10 @@ export const makeWalletBridgeFromFollowers = (
// If we ever add non 'set' amount purses that aren't in the vbank, it's
// not currently possible to read their decimalPlaces, so this code
// will need updating.
if (!Array.isArray(purse.balance.value)) {

const isCopyBagResult = isCopyBag(purse.balance.value);

if (!Array.isArray(purse.balance.value) && !isCopyBagResult) {
console.debug('skipping non-set amount', purse.balance.value);
continue;
}
Expand All @@ -412,7 +417,9 @@ export const makeWalletBridgeFromFollowers = (
}
const brandDescriptor = {
petname: agoricBrands.get(purse.brand) as Petname,
displayInfo: { assetKind: AssetKind.SET },
displayInfo: isCopyBagResult
? { assetKind: AssetKind.COPY_BAG }
: { assetKind: AssetKind.SET },
};
const purseInfo: PurseInfo = {
brand: purse.brand,
Expand Down
Loading

0 comments on commit 03c1244

Please sign in to comment.