From 426740afe63b157aec910df26f8c1ef09402f9be Mon Sep 17 00:00:00 2001
From: 0age <37939117+0age@users.noreply.github.com>
Date: Sun, 8 Dec 2024 08:34:58 -0800
Subject: [PATCH] progress on using POST /compact in the UI
---
frontend/src/App.tsx | 2 +-
frontend/src/components/BalanceDisplay.tsx | 7 +-
frontend/src/components/Transfer.tsx | 247 ++++++++++++---------
frontend/src/hooks/useRequestAllocation.ts | 69 ++++++
4 files changed, 222 insertions(+), 103 deletions(-)
create mode 100644 frontend/src/hooks/useRequestAllocation.ts
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 8088a90..2541068 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -126,7 +126,7 @@ function App() {
{/* Balance Display */}
{sessionToken && (
-
+
)}
diff --git a/frontend/src/components/BalanceDisplay.tsx b/frontend/src/components/BalanceDisplay.tsx
index 64fc8ca..ba880e3 100644
--- a/frontend/src/components/BalanceDisplay.tsx
+++ b/frontend/src/components/BalanceDisplay.tsx
@@ -9,6 +9,10 @@ import { ForcedWithdrawalDialog } from './ForcedWithdrawalDialog';
import { useCompact } from '../hooks/useCompact';
import { useNotification } from '../hooks/useNotification';
+interface BalanceDisplayProps {
+ sessionToken: string | null;
+}
+
// Interface for the selected lock data needed by ForcedWithdrawalDialog
interface SelectedLockData {
chainId: string;
@@ -45,7 +49,7 @@ const formatResetPeriod = (seconds: number): string => {
return `${Math.floor(seconds / 86400)} days`;
};
-export function BalanceDisplay(): JSX.Element | null {
+export function BalanceDisplay({ sessionToken }: BalanceDisplayProps): JSX.Element | null {
const { isConnected } = useAccount();
const { balances, error, isLoading } = useBalances();
const { data: resourceLocksData, isLoading: resourceLocksLoading } =
@@ -297,6 +301,7 @@ export function BalanceDisplay(): JSX.Element | null {
}}
tokenSymbol={balance.token?.symbol || ''}
withdrawalStatus={balance.withdrawalStatus}
+ sessionToken={sessionToken}
onForceWithdraw={() => {
setSelectedLockId(balance.lockId);
setIsWithdrawalDialogOpen(true);
diff --git a/frontend/src/components/Transfer.tsx b/frontend/src/components/Transfer.tsx
index 10531c0..6d6bbef 100644
--- a/frontend/src/components/Transfer.tsx
+++ b/frontend/src/components/Transfer.tsx
@@ -4,6 +4,7 @@ import { parseUnits, formatUnits } from 'viem';
import { useNotification } from '../hooks/useNotification';
import { useAllocatedTransfer } from '../hooks/useAllocatedTransfer';
import { useAllocatedWithdrawal } from '../hooks/useAllocatedWithdrawal';
+import { useRequestAllocation } from '../hooks/useRequestAllocation';
import { COMPACT_ADDRESS, COMPACT_ABI } from '../constants/contracts';
interface TransferProps {
@@ -18,16 +19,17 @@ interface TransferProps {
};
tokenSymbol: string;
withdrawalStatus: number;
+ sessionToken: string | null;
onForceWithdraw: () => void;
onDisableForceWithdraw: () => void;
}
interface FormData {
- nonce: string;
expires: string;
recipient: string;
amount: string;
- allocatorSignature: string;
+ allocatorSignature?: string;
+ nonce?: string;
}
interface WalletError extends Error {
@@ -46,6 +48,7 @@ export function Transfer({
tokenName,
tokenSymbol,
withdrawalStatus,
+ sessionToken,
onForceWithdraw,
onDisableForceWithdraw,
}: TransferProps) {
@@ -54,16 +57,17 @@ export function Transfer({
const [isOpen, setIsOpen] = useState(false);
const [isWithdrawal, setIsWithdrawal] = useState(false);
const [isWithdrawalLoading, setIsWithdrawalLoading] = useState(false);
+ const [isRequestingAllocation, setIsRequestingAllocation] = useState(false);
+ const [hasAllocation, setHasAllocation] = useState(false);
const [formData, setFormData] = useState({
- nonce: '',
expires: '',
recipient: '',
amount: '',
- allocatorSignature: '',
});
const { allocatedTransfer, isPending: isTransferLoading } = useAllocatedTransfer();
const { allocatedWithdrawal, isPending: isWithdrawalPending } = useAllocatedWithdrawal();
+ const { requestAllocation } = useRequestAllocation();
const { showNotification } = useNotification();
const [fieldErrors, setFieldErrors] = useState<{ [key: string]: string | undefined }>({});
@@ -151,18 +155,7 @@ export function Transfer({
const isFormValid = useMemo(() => {
// Basic form validation
- if (
- !formData.nonce ||
- !formData.expires ||
- !formData.recipient ||
- !formData.amount ||
- !formData.allocatorSignature
- ) {
- return false;
- }
-
- // Check if nonce has been consumed
- if (nonceError) {
+ if (!formData.expires || !formData.recipient || !formData.amount) {
return false;
}
@@ -178,7 +171,7 @@ export function Transfer({
}
return true;
- }, [formData, nonceError, fieldErrors]);
+ }, [formData, fieldErrors]);
const handleAction = async (action: 'transfer' | 'withdraw' | 'force' | 'disable') => {
// Check if we need to switch networks
@@ -248,30 +241,79 @@ export function Transfer({
}
};
+ const handleRequestAllocation = async () => {
+ if (!isFormValid || !sessionToken || !address) {
+ if (!sessionToken) {
+ showNotification({
+ type: 'error',
+ title: 'Session Required',
+ message: 'Please sign in to request allocation',
+ });
+ }
+ if (!address) {
+ showNotification({
+ type: 'error',
+ title: 'Wallet Required',
+ message: 'Please connect your wallet first',
+ });
+ }
+ return;
+ }
+
+ try {
+ setIsRequestingAllocation(true);
+
+ const params = {
+ chainId: targetChainId,
+ compact: {
+ // Set arbiter equal to sponsor (user's address)
+ arbiter: address,
+ sponsor: address,
+ nonce: null,
+ expires: formData.expires,
+ id: lockId.toString(),
+ amount: parseUnits(formData.amount, decimals).toString(),
+ witnessTypeString: null,
+ witnessHash: null,
+ },
+ };
+
+ const response = await requestAllocation(params, sessionToken);
+
+ setFormData(prev => ({
+ ...prev,
+ allocatorSignature: response.signature,
+ nonce: response.nonce,
+ }));
+
+ setHasAllocation(true);
+ showNotification({
+ type: 'success',
+ title: 'Allocation Requested',
+ message: 'Successfully received allocation. You can now submit the transfer.',
+ });
+ } catch (error) {
+ console.error('Error requesting allocation:', error);
+ showNotification({
+ type: 'error',
+ title: 'Allocation Request Failed',
+ message: error instanceof Error ? error.message : 'Failed to request allocation',
+ });
+ } finally {
+ setIsRequestingAllocation(false);
+ }
+ };
+
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
- if (!isFormValid) return;
+ if (!isFormValid || !formData.allocatorSignature || !formData.nonce) return;
try {
- // Validate inputs before creating transfer struct
- if (!formData.allocatorSignature?.startsWith('0x')) {
- throw new Error('Allocator signature must start with 0x');
- }
+ // Validate recipient
if (!formData.recipient?.startsWith('0x')) {
throw new Error('Recipient must be a valid address starting with 0x');
}
- // Validate numeric fields
- if (!formData.nonce || !/^\d+$/.test(formData.nonce)) {
- throw new Error('Nonce must be a valid number');
- }
- if (!formData.expires || !/^\d+$/.test(formData.expires)) {
- throw new Error('Expires must be a valid timestamp');
- }
- if (!formData.amount || isNaN(Number(formData.amount)) || Number(formData.amount) <= 0) {
- throw new Error('Amount must be a valid positive number');
- }
-
try {
// Convert values and prepare transfer struct
const transfer = {
@@ -300,12 +342,11 @@ export function Transfer({
// Reset form and close
setFormData({
- nonce: '',
expires: '',
recipient: '',
amount: '',
- allocatorSignature: '',
});
+ setHasAllocation(false);
setIsOpen(false);
} catch (conversionError) {
console.error('Error converting values:', conversionError);
@@ -419,39 +460,6 @@ export function Transfer({
diff --git a/frontend/src/hooks/useRequestAllocation.ts b/frontend/src/hooks/useRequestAllocation.ts
new file mode 100644
index 0000000..6309bf9
--- /dev/null
+++ b/frontend/src/hooks/useRequestAllocation.ts
@@ -0,0 +1,69 @@
+import { useCallback } from 'react';
+import { useNotification } from './useNotification';
+import { useAllocatorAPI } from './useAllocatorAPI';
+
+interface RequestAllocationParams {
+ chainId: string;
+ compact: {
+ arbiter: string;
+ sponsor: string;
+ expires: string;
+ id: string;
+ amount: string;
+ nonce: null;
+ witnessTypeString?: string | null;
+ witnessHash?: string | null;
+ };
+}
+
+interface AllocationResponse {
+ hash: string;
+ signature: string;
+ nonce: string;
+}
+
+export function useRequestAllocation() {
+ const { showNotification } = useNotification();
+ const { createAllocation } = useAllocatorAPI();
+
+ const requestAllocation = useCallback(async (params: RequestAllocationParams, sessionToken: string): Promise => {
+ try {
+ // Create API params, preserving any witness fields if present
+ const apiParams = {
+ chainId: params.chainId,
+ compact: {
+ arbiter: params.compact.arbiter,
+ sponsor: params.compact.sponsor,
+ expires: params.compact.expires,
+ id: params.compact.id,
+ amount: params.compact.amount,
+ nonce: null,
+ ...(params.compact.witnessTypeString && { witnessTypeString: params.compact.witnessTypeString }),
+ ...(params.compact.witnessHash && { witnessHash: params.compact.witnessHash }),
+ }
+ };
+
+ const response = await createAllocation(sessionToken, apiParams);
+
+ // The API response should include the nonce in the response
+ // If not, we'll throw an error since we need it
+ if (!('nonce' in response)) {
+ throw new Error('Server response missing required nonce field');
+ }
+
+ return response as AllocationResponse;
+ } catch (error) {
+ console.error('Request allocation error:', error);
+ showNotification({
+ type: 'error',
+ title: 'Allocation Request Failed',
+ message: error instanceof Error ? error.message : 'Failed to request allocation',
+ });
+ throw error;
+ }
+ }, [createAllocation, showNotification]);
+
+ return {
+ requestAllocation,
+ };
+}