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/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); + try { + const invitationSpec = { + source: 'agoricContract', + instancePath: ['postalService'], + callPipe: [['makeSendInvitation', [recipientAddr]]], + }; + + 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..91225284 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'; 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(() => ({}));