Skip to content

Commit

Permalink
feat: pay ui (#45)
Browse files Browse the repository at this point in the history
* feat: pay ui

* feat: handle any issuer in postal service

* build: avoid ui heap error

* fix: make issuerNumber non-static
  • Loading branch information
samsiegart authored Jun 4, 2024
1 parent 8d801d9 commit 3729be1
Show file tree
Hide file tree
Showing 22 changed files with 197 additions and 50 deletions.
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));

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(
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
File renamed without changes.
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
File renamed without changes.
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
File renamed without changes.
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

0 comments on commit 3729be1

Please sign in to comment.