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

feat: pay ui #45

Merged
merged 4 commits into from
Jun 4, 2024
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
1 change: 1 addition & 0 deletions contract/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
start-sell-concert-tickets-permit.json
start-sell-concert-tickets.js
bundles/
,tx.json
20 changes: 16 additions & 4 deletions contract/src/postal-service.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { E, Far } from '@endo/far';
import { M, mustMatch } from '@endo/patterns';
import { withdrawFromSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js';
import { IssuerShape } from '@agoric/ertp/src/typeGuards.js';

const { keys, values } = Object;

Expand All @@ -19,9 +20,10 @@ export const { customTermsShape } = meta;

/** @param {ZCF<PostalSvcTerms>} zcf */
export const start = zcf => {
const { namesByAddress, issuers } = zcf.getTerms();
const { namesByAddress } = zcf.getTerms();
mustMatch(namesByAddress, M.remotable('namesByAddress'));
console.log('postal-service issuers', Object.keys(issuers));

let issuerNumber = 1;

/**
* @param {string} addr
Expand All @@ -38,9 +40,19 @@ export const start = zcf => {
*/
const sendTo = (addr, pmt) => E(getDepositFacet(addr)).receive(pmt);

/** @param {string} recipient */
const makeSendInvitation = recipient => {
/**
* @param {string} recipient
* @param {Issuer[]} issuers
*/
const makeSendInvitation = (recipient, issuers) => {
assert.typeof(recipient, 'string');
mustMatch(issuers, M.arrayOf(IssuerShape));
Copy link
Member

Choose a reason for hiding this comment

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

Did you consider making the issuers arg optional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The UI is always just going to send the issuers anyway so it doesn't seem worth the complexity. We'd have to query and check which issuers are already added first, I'm not sure if they're published anywhere either.


for (const i of issuers) {
if (!Object.values(zcf.getTerms().issuers).includes(i)) {
zcf.saveIssuer(i, `Issuer${(issuerNumber += 1)}`);
}
}

/** @type {OfferHandler} */
const handleSend = async seat => {
Expand Down
14 changes: 2 additions & 12 deletions contract/src/postal-service.proposal.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@
*/
// @ts-check

import { E } from '@endo/far';
import { fixHub } from './fixHub.js';
import {
installContract,
startContract,
} from './platform-goals/start-contract.js';
import { allValues } from './objectTools.js';

const { Fail } = assert;

Expand All @@ -23,17 +21,15 @@ const contractName = 'postalService';
* @param {BootstrapPowers} powers
* @param {{ options?: { postalService: {
* bundleID: string;
* issuerNames?: string[];
* }}}} [config]
*/
export const startPostalService = async (powers, config) => {
const {
consume: { namesByAddressAdmin, agoricNames },
consume: { namesByAddressAdmin },
} = powers;
const {
// must be supplied by caller or template-replaced
bundleID = Fail`no bundleID`,
issuerNames = ['IST', 'Invitation', 'BLD', 'ATOM'],
} = config?.options?.[contractName] ?? {};

const installation = await installContract(powers, {
Expand All @@ -44,15 +40,9 @@ export const startPostalService = async (powers, config) => {
const namesByAddress = await fixHub(namesByAddressAdmin);
const terms = harden({ namesByAddress });

const issuerKeywordRecord = await allValues(
Copy link
Member

Choose a reason for hiding this comment

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

This regression is a little surprising. But I guess I don't mind too much.

Object.fromEntries(
issuerNames.map(n => [n, E(agoricNames).lookup('issuer', n)]),
),
);

await startContract(powers, {
name: contractName,
startArgs: { installation, issuerKeywordRecord, terms },
startArgs: { installation, terms },
});
};

Expand Down
7 changes: 3 additions & 4 deletions contract/src/swaparoo.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { E, Far } from '@endo/far';
import '@agoric/zoe/exported.js';
import { atomicRearrange } from '@agoric/zoe/src/contractSupport/atomicTransfer.js';
import '@agoric/zoe/src/contracts/exported.js';
import { AmountShape } from '@agoric/ertp/src/typeGuards.js';
import { AmountShape, IssuerShape } from '@agoric/ertp/src/typeGuards.js';
import {
InstanceHandleShape,
InvitationShape,
Expand Down Expand Up @@ -49,9 +49,6 @@ export const swapWithFee = (zcf, firstSeat, secondSeat, feeSeat, feeAmount) => {
return 'success';
};

let issuerNumber = 1;
const IssuerShape = M.remotable('Issuer');

const paramTypes = harden(
/** @type {const} */ ({
Fee: ParamTypes.AMOUNT,
Expand Down Expand Up @@ -134,6 +131,8 @@ export const start = async (zcf, privateArgs, baggage) => {
};
})();

let issuerNumber = 1;

/**
* @param { ZCFSeat } firstSeat
* @param {{ addr: string }} offerArgs
Expand Down
7 changes: 4 additions & 3 deletions contract/test/market-actors.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ const { entries, fromEntries, keys } = Object;
* }} mine
* @param {{
* rxAddr: string,
* toSend: AmountKeywordRecord;
* toSend: AmountKeywordRecord,
* issuers: Issuer[]
* }} shared
*/
export const payerPete = async (
t,
{ wallet, queryTool },
{ rxAddr, toSend },
{ rxAddr, toSend, issuers },
) => {
const hub = await makeAgoricNames(queryTool);
/** @type {WellKnown} */
Expand All @@ -55,7 +56,7 @@ export const payerPete = async (
source: 'contract',
instance,
publicInvitationMaker: 'makeSendInvitation',
invitationArgs: [rxAddr],
invitationArgs: [rxAddr, issuers],
},
proposal: { give: toSend },
};
Expand Down
3 changes: 3 additions & 0 deletions contract/test/snapshots/test-postalSvc.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Generated by [AVA](https://avajs.dev).
instance: Object @Alleged: InstanceHandle {},
invitationArgs: [
'agoric1aap7m84dt0rwhhfw49d4kv2gqetzl56vn8aaxj',
[
Object @Alleged: ATOM issuer {},
],
],
publicInvitationMaker: 'makeSendInvitation',
source: 'contract',
Expand Down
Binary file modified contract/test/snapshots/test-postalSvc.js.snap
Binary file not shown.
5 changes: 3 additions & 2 deletions contract/test/test-postalSvc.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ test.serial('deploy contract with core eval: postalService / send', async t => {
behavior: startPostalService,
entryFile: scriptRoots.postalService,
config: {
options: { postalService: { bundleID, issuerNames: ['ATOM', 'Item'] } },
options: { postalService: { bundleID } },
},
});

Expand Down Expand Up @@ -160,6 +160,7 @@ test.serial('deliver payment using offer', async t => {
toSend: {
Pmt: amt(await agoricNames.brand.ATOM, 3n),
},
issuers: [await agoricNames.issuer.ATOM],
};

const wallet = {
Expand Down Expand Up @@ -192,7 +193,7 @@ test('send invitation* from contract using publicFacet of postalService', async
const postalPowers = extract(permit, powers);
await startPostalService(postalPowers, {
options: {
postalService: { bundleID, issuerNames: ['IST', 'Invitation'] },
postalService: { bundleID },
},
});

Expand Down
2 changes: 1 addition & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build": "tsc && NODE_OPTIONS=--max-old-space-size=4096 vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint:fix": "yarn lint --fix",
"preview": "vite preview",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { PurseJSONState } from '@agoric/react-components';
import type { DisplayInfoForBrand } from '../../store/displayInfo';
import type { DisplayInfoForBrand } from '../store/displayInfo';
import { stringifyValue, type AssetKind } from '@agoric/web-components';
import type { Amount } from '@agoric/ertp/src/types';
import { isCopyBagValue } from '@agoric/ertp';
import { useEffect, useRef, useState } from 'react';
import { stringifyData } from '../../utils/stringify';
import { stringifyData } from '../utils/stringify';

export const PurseValue = ({
purse,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AmountInput, type PurseJSONState } from '@agoric/react-components';
import type { Amount, AssetKind } from '@agoric/web-components';
import { useState } from 'react';
import { CopyBagEntry, PurseValue, SetEntry } from './DisplayAmount';
import { stringifyData } from '../../utils/stringify';
import { stringifyData } from '../utils/stringify';
import { makeCopyBag } from '@endo/patterns';

type Props = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Amount } from '@agoric/web-components';
import { useDisplayInfo } from '../../store/displayInfo';
import { useDisplayInfo } from '../store/displayInfo';
import { AmountValue } from './DisplayAmount';

type Props = {
Expand Down
3 changes: 2 additions & 1 deletion ui/src/components/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TabWrapper } from './TabWrapper';
import { Notifications } from './Notifications';
import { NotificationContext } from '../context/NotificationContext';
import Swap from './swap/Swap';
import Pay from './pay/Pay';

// notification related types
const dynamicToastChildStatuses = [
Expand Down Expand Up @@ -65,7 +66,7 @@ const Tabs = () => {
activeTab={activeTab}
handleTabClick={handleTabClick}
>
<div>TBD</div>
<Pay />
</TabWrapper>
<TabWrapper
tab="Vote"
Expand Down
148 changes: 148 additions & 0 deletions ui/src/components/pay/Pay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { useAgoric } from '@agoric/react-components';
import ProposalAmountsBox from '../ProposalAmountsBox';
import RecipientInput from '../RecipientInput';
import { queryPurses } from '../../utils/queryPurses';
import { useContext, useEffect, useState } from 'react';
import type { Amount } from '@agoric/web-components';
import { useDisplayInfo } from '../../store/displayInfo';
import { NotificationContext } from '../../context/NotificationContext';
import { queryIssuers } from '../../utils/queryIssuers';

const Pay = () => {
const { addNotification } = useContext(NotificationContext);
const { purses, chainStorageWatcher, makeOffer } = useAgoric();
const [recipientAddr, setRecipientAddr] = useState('');
const [recipientError, setRecipientError] = useState('');
const [myAmounts, setMyAmounts] = useState<Amount[]>([]);
const { brandToDisplayInfo } = useDisplayInfo(({ brandToDisplayInfo }) => ({
brandToDisplayInfo,
}));

useEffect(() => {
let isCancelled = false;
const checkRecipientSmartWallet = async () => {
if (chainStorageWatcher && recipientAddr) {
try {
await queryPurses(chainStorageWatcher, recipientAddr);
} catch (e) {
if (!isCancelled) {
setRecipientError('Failed to fetch recipient wallet.');
}
}
}
};

if (!recipientAddr.length) {
setRecipientError('');
} else if (
recipientAddr.startsWith('agoric') &&
recipientAddr.length === 45
) {
setRecipientError('');
checkRecipientSmartWallet();
} else {
setRecipientError('Invalid address format');
}

return () => {
isCancelled = true;
};
}, [chainStorageWatcher, recipientAddr]);

const sendOffer = async () => {
assert(chainStorageWatcher && makeOffer);

assert(chainStorageWatcher && makeOffer);
try {
const brandPetnameToIssuer = await queryIssuers(chainStorageWatcher);
const issuers = new Set(
[...myAmounts].map(amount => {
const { petname } = brandToDisplayInfo.get(amount.brand)!;
return brandPetnameToIssuer.get(petname);
}),
);

const invitationSpec = {
source: 'agoricContract',
instancePath: ['postalService'],
callPipe: [['makeSendInvitation', [recipientAddr, [...issuers]]]],
};

const gives = myAmounts.map(amount => {
const { petname } = brandToDisplayInfo.get(amount.brand)!;
return [petname, amount];
});
const proposal = {
give: { ...Object.fromEntries(gives) },
want: {},
};

makeOffer(
invitationSpec,
proposal,
undefined,
(update: { status: string; data?: unknown }) => {
if (update.status === 'error') {
addNotification!({
text: `Payment Error: ${update.data}`,
status: 'error',
});
}
if (update.status === 'accepted') {
addNotification!({
text: 'Payment Sent',
status: 'success',
});
}
if (update.status === 'refunded') {
addNotification!({
text: 'Payment Refunded',
status: 'warning',
});
}
},
);
} catch (e) {
addNotification!({
text: `Offer error: ${e}`,
status: 'error',
});
}
};

const isButtonDisabled = !makeOffer || !recipientAddr || !myAmounts.length;

return (
<div className="items-top flex w-full flex-col justify-around lg:flex-row">
<div>
<h2 className="daisyui-card-title mb-2 w-full">Send Payment</h2>
<div className="daisyui-card h-fit w-96 bg-base-300 px-4 py-4 shadow-xl">
<RecipientInput
address={recipientAddr}
onChange={addr => setRecipientAddr(addr)}
error={recipientError}
/>
<div className="my-1">
<h2 className="mb-2 text-lg font-medium">Give</h2>
<ProposalAmountsBox
actionLabel="Add from Your Purse"
amounts={myAmounts}
purses={purses}
onChange={setMyAmounts}
warning={purses ? undefined : 'Wallet Not Connected'}
/>
</div>
<button
onClick={sendOffer}
disabled={isButtonDisabled}
className="daisyui-btn daisyui-btn-primary mt-4 w-full self-center text-lg"
>
Send Payment
</button>
</div>
</div>
</div>
);
};

export default Pay;
2 changes: 1 addition & 1 deletion ui/src/components/swap/FeeInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Amount } from '@agoric/web-components';
import { AmountValue } from './DisplayAmount';
import { AmountValue } from '../DisplayAmount';

type Props = {
fee: Amount;
Expand Down
Loading