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}
+ />
+
+
+
+
+
+ );
+};
+
+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(() => ({}));