diff --git a/contract/.gitignore b/contract/.gitignore index dade5bf3..7fa9bd69 100644 --- a/contract/.gitignore +++ b/contract/.gitignore @@ -1,3 +1,4 @@ start-sell-concert-tickets-permit.json start-sell-concert-tickets.js bundles/ +,tx.json diff --git a/contract/src/postal-service.contract.js b/contract/src/postal-service.contract.js index 2bd08ff2..e0b4b237 100644 --- a/contract/src/postal-service.contract.js +++ b/contract/src/postal-service.contract.js @@ -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; @@ -19,9 +20,10 @@ export const { customTermsShape } = meta; /** @param {ZCF} 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 @@ -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 => { diff --git a/contract/src/postal-service.proposal.js b/contract/src/postal-service.proposal.js index 45187a03..5e085f2f 100644 --- a/contract/src/postal-service.proposal.js +++ b/contract/src/postal-service.proposal.js @@ -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; @@ -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, { @@ -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 }, }); }; diff --git a/contract/src/swaparoo.contract.js b/contract/src/swaparoo.contract.js index dcccb8aa..20905141 100644 --- a/contract/src/swaparoo.contract.js +++ b/contract/src/swaparoo.contract.js @@ -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, @@ -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, @@ -134,6 +131,8 @@ export const start = async (zcf, privateArgs, baggage) => { }; })(); + let issuerNumber = 1; + /** * @param { ZCFSeat } firstSeat * @param {{ addr: string }} offerArgs diff --git a/contract/test/market-actors.js b/contract/test/market-actors.js index 53102e72..a0817a86 100644 --- a/contract/test/market-actors.js +++ b/contract/test/market-actors.js @@ -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} */ @@ -55,7 +56,7 @@ export const payerPete = async ( source: 'contract', instance, publicInvitationMaker: 'makeSendInvitation', - invitationArgs: [rxAddr], + invitationArgs: [rxAddr, issuers], }, proposal: { give: toSend }, }; diff --git a/contract/test/snapshots/test-postalSvc.js.md b/contract/test/snapshots/test-postalSvc.js.md index ee2f600e..0e833ce6 100644 --- a/contract/test/snapshots/test-postalSvc.js.md +++ b/contract/test/snapshots/test-postalSvc.js.md @@ -14,6 +14,9 @@ Generated by [AVA](https://avajs.dev). instance: Object @Alleged: InstanceHandle {}, invitationArgs: [ 'agoric1aap7m84dt0rwhhfw49d4kv2gqetzl56vn8aaxj', + [ + Object @Alleged: ATOM issuer {}, + ], ], publicInvitationMaker: 'makeSendInvitation', source: 'contract', diff --git a/contract/test/snapshots/test-postalSvc.js.snap b/contract/test/snapshots/test-postalSvc.js.snap index 674ff436..eb00cca2 100644 Binary files a/contract/test/snapshots/test-postalSvc.js.snap and b/contract/test/snapshots/test-postalSvc.js.snap differ diff --git a/contract/test/test-postalSvc.js b/contract/test/test-postalSvc.js index 4d38410d..922cf23c 100644 --- a/contract/test/test-postalSvc.js +++ b/contract/test/test-postalSvc.js @@ -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 } }, }, }); @@ -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 = { @@ -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 }, }, }); diff --git a/ui/package.json b/ui/package.json index 6ad5cf57..98532397 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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", diff --git a/ui/src/components/swap/AmountSelectorDialog.tsx b/ui/src/components/AmountSelectorDialog.tsx similarity index 100% rename from ui/src/components/swap/AmountSelectorDialog.tsx rename to ui/src/components/AmountSelectorDialog.tsx diff --git a/ui/src/components/swap/DisplayAmount.tsx b/ui/src/components/DisplayAmount.tsx similarity index 97% rename from ui/src/components/swap/DisplayAmount.tsx rename to ui/src/components/DisplayAmount.tsx index a6014316..fa289e2c 100644 --- a/ui/src/components/swap/DisplayAmount.tsx +++ b/ui/src/components/DisplayAmount.tsx @@ -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, diff --git a/ui/src/components/swap/ProposalAmountsBox.tsx b/ui/src/components/ProposalAmountsBox.tsx similarity index 100% rename from ui/src/components/swap/ProposalAmountsBox.tsx rename to ui/src/components/ProposalAmountsBox.tsx diff --git a/ui/src/components/swap/PurseAmountInput.tsx b/ui/src/components/PurseAmountInput.tsx similarity index 98% rename from ui/src/components/swap/PurseAmountInput.tsx rename to ui/src/components/PurseAmountInput.tsx index b204b220..dbb1ecc0 100644 --- a/ui/src/components/swap/PurseAmountInput.tsx +++ b/ui/src/components/PurseAmountInput.tsx @@ -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 = { diff --git a/ui/src/components/swap/RecipientInput.tsx b/ui/src/components/RecipientInput.tsx similarity index 100% rename from ui/src/components/swap/RecipientInput.tsx rename to ui/src/components/RecipientInput.tsx diff --git a/ui/src/components/swap/SelectedAmountRow.tsx b/ui/src/components/SelectedAmountRow.tsx similarity index 96% rename from ui/src/components/swap/SelectedAmountRow.tsx rename to ui/src/components/SelectedAmountRow.tsx index c8e2d913..af434ad8 100644 --- a/ui/src/components/swap/SelectedAmountRow.tsx +++ b/ui/src/components/SelectedAmountRow.tsx @@ -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 = { diff --git a/ui/src/components/Tabs.tsx b/ui/src/components/Tabs.tsx index d28e11f4..235431b0 100644 --- a/ui/src/components/Tabs.tsx +++ b/ui/src/components/Tabs.tsx @@ -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 = [ @@ -65,7 +66,7 @@ const Tabs = () => { activeTab={activeTab} handleTabClick={handleTabClick} > -
TBD
+ { + const { addNotification } = useContext(NotificationContext); + const { purses, chainStorageWatcher, makeOffer } = useAgoric(); + const [recipientAddr, setRecipientAddr] = useState(''); + const [recipientError, setRecipientError] = useState(''); + const [myAmounts, setMyAmounts] = useState([]); + 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 ( +
+
+

Send Payment

+
+ setRecipientAddr(addr)} + error={recipientError} + /> +
+

Give

+ +
+ +
+
+
+ ); +}; + +export default Pay; diff --git a/ui/src/components/swap/FeeInfo.tsx b/ui/src/components/swap/FeeInfo.tsx index 96aa823d..99fc1cd4 100644 --- a/ui/src/components/swap/FeeInfo.tsx +++ b/ui/src/components/swap/FeeInfo.tsx @@ -1,5 +1,5 @@ import type { Amount } from '@agoric/web-components'; -import { AmountValue } from './DisplayAmount'; +import { AmountValue } from '../DisplayAmount'; type Props = { fee: Amount; diff --git a/ui/src/components/swap/IncomingOffer.tsx b/ui/src/components/swap/IncomingOffer.tsx index 046b8cbf..f697968c 100644 --- a/ui/src/components/swap/IncomingOffer.tsx +++ b/ui/src/components/swap/IncomingOffer.tsx @@ -1,5 +1,5 @@ import { type Amount, AssetKind, type Brand } from '@agoric/web-components'; -import { AmountValue } from './DisplayAmount'; +import { AmountValue } from '../DisplayAmount'; import { useAgoric } from '@agoric/react-components'; import { useContext, useEffect, useState } from 'react'; import { useDisplayInfo } from '../../store/displayInfo'; diff --git a/ui/src/components/swap/Swap.tsx b/ui/src/components/swap/Swap.tsx index 679719e9..80b87e90 100644 --- a/ui/src/components/swap/Swap.tsx +++ b/ui/src/components/swap/Swap.tsx @@ -1,6 +1,6 @@ import { type PurseJSONState, useAgoric } from '@agoric/react-components'; -import ProposalAmountsBox from './ProposalAmountsBox'; -import RecipientInput from './RecipientInput'; +import ProposalAmountsBox from '../ProposalAmountsBox'; +import RecipientInput from '../RecipientInput'; import { queryPurses } from '../../utils/queryPurses'; import { useContext, useEffect, useState } from 'react'; import type { Amount, AssetKind } from '@agoric/web-components'; @@ -99,10 +99,12 @@ const Swap = () => { try { const brandPetnameToIssuer = await queryIssuers(chainStorageWatcher); const issuers = new Set( - [...myAmounts, ...recipientAmounts].map(amount => { - const { petname } = brandToDisplayInfo.get(amount.brand)!; - return brandPetnameToIssuer.get(petname); - }), + [...myAmounts, ...recipientAmounts, ...(fee ? [fee] : [])].map( + amount => { + const { petname } = brandToDisplayInfo.get(amount.brand)!; + return brandPetnameToIssuer.get(petname); + }, + ), ); const invitationSpec = { diff --git a/ui/src/providers/Contract.tsx b/ui/src/providers/Contract.tsx index fbee3c3d..3ceaf8f9 100644 --- a/ui/src/providers/Contract.tsx +++ b/ui/src/providers/Contract.tsx @@ -28,16 +28,6 @@ const watchContract = (watcher: ChainStorageWatcher) => { }); }, ); - - watcher.watchLatest>( - [Kind.Data, 'published.agoricNames.vbankAsset'], - vbank => { - console.log('Got vbank', vbank); - useContractStore.setState({ - vbank: fromEntries(vbank), - }); - }, - ); }; export const ContractProvider = ({ children }: PropsWithChildren) => { diff --git a/ui/src/store/contract.ts b/ui/src/store/contract.ts index f95d1b64..c3e9cc87 100644 --- a/ui/src/store/contract.ts +++ b/ui/src/store/contract.ts @@ -3,7 +3,6 @@ import { create } from 'zustand'; interface ContractState { instances?: Record; brands?: Record; - vbank?: Record; } export const useContractStore = create(() => ({}));