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({
-
- - - setFormData((prev) => ({ ...prev, allocatorSignature: e.target.value })) - } - placeholder="0x..." - className="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-gray-300 focus:outline-none focus:border-[#00ff00] transition-colors" - /> -
- -
- - setFormData((prev) => ({ ...prev, nonce: e.target.value }))} - placeholder="Enter nonce" - className={`w-full px-3 py-2 bg-gray-800 border ${ - fieldErrors.nonce ? 'border-red-500' : 'border-gray-700' - } rounded-lg text-gray-300 focus:outline-none focus:border-[#00ff00] transition-colors`} - /> - {fieldErrors.nonce && ( -

{fieldErrors.nonce}

- )} -
-
- + {!hasAllocation ? ( + + ) : ( + + )}
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, + }; +}