diff --git a/components/escrow/PayoutUI.tsx b/components/escrow/PayoutUI.tsx new file mode 100644 index 0000000..044c64c --- /dev/null +++ b/components/escrow/PayoutUI.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useEscrowPayout } from '@/hooks/useEscrowPayout'; +import { SignatureProgressBar } from './SignatureProgressBar'; +import { SignerList } from './SignerList'; + +interface PayoutUIProps { + escrowId: string; +} + +export function PayoutUI({ escrowId }: PayoutUIProps) { + const { + isLoading, + error, + requiredSignatures, + currentSignatures, + signers, + canRelease, + releaseFunds, + } = useEscrowPayout(escrowId); + + if (isLoading) { + return
Loading escrow details...
; + } + + if (error) { + return
Error: {error}
; + } + + return ( +
+

Escrow Payout

+ + + +
+ ); +} \ No newline at end of file diff --git a/components/escrow/SignatureProgressBar.tsx b/components/escrow/SignatureProgressBar.tsx new file mode 100644 index 0000000..3f4448d --- /dev/null +++ b/components/escrow/SignatureProgressBar.tsx @@ -0,0 +1,41 @@ +'use client'; + +export function SignatureProgressBar({ + current, + required, +}: { + current: number; + required: number; +}) { + if (required <= 0) { + return ( +

+ Multi-signature not required for this escrow. +

+ ); + } + const percentage = Math.min((current / required) * 100, 100); + const isComplete = current >= required; + + return ( +
+
+ Signatures Received + + {current} of {required} + +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/components/escrow/SignerList.tsx b/components/escrow/SignerList.tsx new file mode 100644 index 0000000..a9094e4 --- /dev/null +++ b/components/escrow/SignerList.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { useState } from 'react'; +import { Copy, Check, ListX } from 'lucide-react'; +import { Tooltip } from '@/components/ui/Tooltip'; + +interface SignerListProps { + signers: string[]; + requiredSignatures: number; +} + +export function SignerList({ signers, requiredSignatures }: SignerListProps) { + const [copiedKey, setCopiedKey] = useState(null); + + const handleCopy = (key: string) => { + navigator.clipboard.writeText(key); + setCopiedKey(key); + setTimeout(() => setCopiedKey(null), 2000); + }; + + if (signers.length === 0 && requiredSignatures > 0) { + return ( +
+ + No signatures have been recorded yet. +
+ ); + } + + if (signers.length === 0) { + return null; + } + + return ( +
+

+ Signers ({signers.length}) +

+ +
+ ); +} \ No newline at end of file diff --git a/components/escrow/__tests__/PayoutUI.test.tsx b/components/escrow/__tests__/PayoutUI.test.tsx new file mode 100644 index 0000000..9793fe3 --- /dev/null +++ b/components/escrow/__tests__/PayoutUI.test.tsx @@ -0,0 +1,89 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { PayoutUI } from '../PayoutUI'; +import { useEscrowPayout } from '@/hooks/useEscrowPayout'; + +// Mock the useEscrowPayout hook +jest.mock('@/hooks/useEscrowPayout'); +const mockedUseEscrowPayout = useEscrowPayout as jest.Mock; + +describe('PayoutUI', () => { + const mockEscrowId = 'escrow-123'; + const mockReleaseFunds = jest.fn(); + + beforeEach(() => { + // Reset mock before each test + mockedUseEscrowPayout.mockClear(); + mockReleaseFunds.mockClear(); + }); + + it('should render loading state', () => { + mockedUseEscrowPayout.mockReturnValue({ + isLoading: true, + error: null, + requiredSignatures: 0, + currentSignatures: 0, + canRelease: false, + releaseFunds: mockReleaseFunds, + }); + + render(); + expect(screen.getByLabelText('Loading payout UI')).toBeInTheDocument(); + }); + + it('should render error state', () => { + mockedUseEscrowPayout.mockReturnValue({ + isLoading: false, + error: 'Failed to fetch escrow details', + requiredSignatures: 0, + currentSignatures: 0, + canRelease: false, + releaseFunds: mockReleaseFunds, + }); + + render(); + expect(screen.getByRole('alert')).toHaveTextContent( + 'Error: Failed to fetch escrow details', + ); + }); + + it('should disable the "Release Funds" button when signatures are below threshold (1 of 2)', () => { + mockedUseEscrowPayout.mockReturnValue({ + isLoading: false, + error: null, + requiredSignatures: 2, + currentSignatures: 1, + canRelease: false, + releaseFunds: mockReleaseFunds, + }); + + render(); + + const releaseButton = screen.getByRole('button', { name: 'Release Funds' }); + expect(releaseButton).toBeDisabled(); + expect(screen.getByText('Signatures: 1 / 2')).toBeInTheDocument(); + + // Assert that clicking the disabled button throws no exceptions and does not call releaseFunds + fireEvent.click(releaseButton); + expect(mockReleaseFunds).not.toHaveBeenCalled(); + }); + + it('should enable the "Release Funds" button when signatures meet the threshold (2 of 2)', () => { + mockedUseEscrowPayout.mockReturnValue({ + isLoading: false, + error: null, + requiredSignatures: 2, + currentSignatures: 2, + canRelease: true, + releaseFunds: mockReleaseFunds, + }); + + render(); + + const releaseButton = screen.getByRole('button', { name: 'Release Funds' }); + expect(releaseButton).not.toBeDisabled(); + expect(screen.getByText('Signatures: 2 / 2')).toBeInTheDocument(); + + fireEvent.click(releaseButton); + expect(mockReleaseFunds).toHaveBeenCalledTimes(1); + }); +}); \ No newline at end of file diff --git a/components/escrow/__tests__/SignatureProgressBar.test.tsx b/components/escrow/__tests__/SignatureProgressBar.test.tsx new file mode 100644 index 0000000..ea9b28e --- /dev/null +++ b/components/escrow/__tests__/SignatureProgressBar.test.tsx @@ -0,0 +1,50 @@ +import { render, screen } from '@testing-library/react'; +import { SignatureProgressBar } from '../SignatureProgressBar'; + +describe('SignatureProgressBar', () => { + it('should render a message when multi-signature is not required', () => { + render(); + expect( + screen.getByText('Multi-signature not required for this escrow.'), + ).toBeInTheDocument(); + }); + + it('should render the progress correctly for an incomplete state (1 of 3)', () => { + render(); + + const progressbar = screen.getByRole('progressbar'); + expect(progressbar).toBeInTheDocument(); + // Note: Jest stringifies the style, so we check for the substring. + expect(progressbar.style.width).toContain('33.33'); + expect(progressbar).toHaveClass('bg-blue-600'); + expect(progressbar).toHaveAttribute('aria-valuenow', '33.33333333333333'); + + expect(screen.getByText('1 of 3')).toBeInTheDocument(); + expect(screen.getByText('1 of 3')).toHaveClass('text-gray-500'); + }); + + it('should render the progress correctly for a complete state (3 of 3)', () => { + render(); + + const progressbar = screen.getByRole('progressbar'); + expect(progressbar).toBeInTheDocument(); + expect(progressbar).toHaveStyle('width: 100%'); + expect(progressbar).toHaveClass('bg-green-500'); + expect(progressbar).toHaveAttribute('aria-valuenow', '100'); + + expect(screen.getByText('3 of 3')).toBeInTheDocument(); + expect(screen.getByText('3 of 3')).toHaveClass('text-green-600'); + }); + + it('should cap the progress at 100% if current signatures exceed required', () => { + render(); + + const progressbar = screen.getByRole('progressbar'); + expect(progressbar).toHaveStyle('width: 100%'); + expect(progressbar).toHaveClass('bg-green-500'); + expect(progressbar).toHaveAttribute('aria-valuenow', '100'); + + expect(screen.getByText('5 of 4')).toBeInTheDocument(); + expect(screen.getByText('5 of 4')).toHaveClass('text-green-600'); + }); +}); \ No newline at end of file diff --git a/components/escrow/__tests__/SignerList.test.tsx b/components/escrow/__tests__/SignerList.test.tsx new file mode 100644 index 0000000..3c5c32b --- /dev/null +++ b/components/escrow/__tests__/SignerList.test.tsx @@ -0,0 +1,78 @@ +import { render, screen, fireEvent, act } from '@testing-library/react'; +import { SignerList } from '../SignerList'; + +// Mock the clipboard API +Object.assign(navigator, { + clipboard: { + writeText: jest.fn(), + }, +}); + +const mockSigners = [ + 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF', + 'GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB5432', +]; + +describe('SignerList', () => { + beforeEach(() => { + (navigator.clipboard.writeText as jest.Mock).mockClear(); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should render a message when no signers are present but signatures are required', () => { + render(); + expect( + screen.getByText('No signatures have been recorded yet.'), + ).toBeInTheDocument(); + }); + + it('should render nothing when no signers are present and none are required', () => { + const { container } = render( + , + ); + expect(container).toBeEmptyDOMElement(); + }); + + it('should render a list of signers with truncated keys', () => { + render(); + + expect(screen.getByText('GAAAAAAA...AAAAWHF')).toBeInTheDocument(); + expect(screen.getByText('GBBBBBBB...BBBB5432')).toBeInTheDocument(); + expect(screen.getAllByRole('listitem').length).toBe(2); + }); + + it('should copy a key to the clipboard and show feedback', () => { + render(); + + const copyButtons = screen.getAllByTitle('Copy public key'); + expect(copyButtons.length).toBe(2); + + // Click the first copy button + fireEvent.click(copyButtons[0]); + + // Check that clipboard.writeText was called with the full key + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(mockSigners[0]); + expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(1); + + // The icon should change to a checkmark, which has a distinct green color + const checkIcon = copyButtons[0].querySelector('.text-green-500'); + expect(checkIcon).toBeInTheDocument(); + + // The other button should still have the default copy icon + const otherButtonCheckIcon = copyButtons[1].querySelector('.text-green-500'); + expect(otherButtonCheckIcon).not.toBeInTheDocument(); + + // Fast-forward time to reset the copied state + act(() => { + jest.advanceTimersByTime(2000); + }); + + // The checkmark icon should be gone, reverting to the copy icon + const checkIconAfterTimeout = copyButtons[0].querySelector('.text-green-500'); + expect(checkIconAfterTimeout).not.toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/components/fleet/DriverReputation.tsx b/components/fleet/DriverReputation.tsx new file mode 100644 index 0000000..d6545da --- /dev/null +++ b/components/fleet/DriverReputation.tsx @@ -0,0 +1,120 @@ +'use client'; + +import { useState } from 'react'; +import { + StarIcon, + ShieldCheckIcon, + InformationCircleIcon, +} from '@heroicons/react/24/solid'; +import { useDriverReputation } from '@/hooks/useDriverReputation'; + +function ReputationModal({ onClose }: { onClose: () => void }) { + return ( +
+
e.stopPropagation()} + > +
+

+ About Reputation Scores +

+ +
+
+

+ Driver reputation is measured in two ways to provide a complete + picture of their reliability and experience. +

+
+

Platform Rating (★)

+

+ This is the standard star rating provided by customers after a + successful delivery. It reflects overall satisfaction. +

+
+
+

On-Chain Score (🛡️)

+

+ This is a tokenized score recorded directly on the Stellar + blockchain. Drivers earn these "Reputation Tokens" by successfully + completing escrow-secured deliveries. +

+

+ An on-chain score is a verifiable, tamper-proof record of a + driver's history within the SwiftChain ecosystem, representing a + higher level of trust. +

+
+
+
+ +
+
+
+ ); +} + +interface DriverReputationProps { + driverId: string; + standardRating: number; +} + +export function DriverReputation({ + driverId, + standardRating, +}: DriverReputationProps) { + const [isModalOpen, setIsModalOpen] = useState(false); + const { onChainScore, isLoading } = useDriverReputation(driverId); + + return ( +
+ {/* Standard Platform Rating */} +
+ + {standardRating.toFixed(1)} +
+ + {/* On-Chain Reputation Score */} + {isLoading ? ( +
+ ) : onChainScore && onChainScore > 0 ? ( +
+ + {onChainScore.toLocaleString()} +
+ ) : null} + + {/* Info button */} + + + {isModalOpen && setIsModalOpen(false)} />} +
+ ); +} \ No newline at end of file diff --git a/components/fleet/EnterpriseDashboard.tsx b/components/fleet/EnterpriseDashboard.tsx index cd7e87b..35e8c7e 100644 --- a/components/fleet/EnterpriseDashboard.tsx +++ b/components/fleet/EnterpriseDashboard.tsx @@ -2,6 +2,7 @@ import { useRef } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; +import { DriverReputation } from './DriverReputation'; import { useFleet } from '@/hooks/useFleet'; import type { Driver, DriverStatus } from '@/types/fleet'; @@ -73,7 +74,7 @@ function VirtualRow({ driver, style }: VirtualRowProps) {
{driver.activeDeliveries}
{driver.completedDeliveries}
-
★ {driver.rating.toFixed(1)}
+ ); } diff --git a/components/fleet/__tests__/DriverReputation.test.tsx b/components/fleet/__tests__/DriverReputation.test.tsx new file mode 100644 index 0000000..669e5f2 --- /dev/null +++ b/components/fleet/__tests__/DriverReputation.test.tsx @@ -0,0 +1,93 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { DriverReputation } from '../DriverReputation'; +import { useDriverReputation } from '@/hooks/useDriverReputation'; + +// Mock the useDriverReputation hook +jest.mock('@/hooks/useDriverReputation'); +const mockedUseDriverReputation = useDriverReputation as jest.Mock; + +describe('DriverReputation', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render the standard rating', () => { + mockedUseDriverReputation.mockReturnValue({ + onChainScore: null, + isLoading: false, + error: null, + }); + + render(); + + expect(screen.getByText('4.8')).toBeInTheDocument(); + expect(screen.getByTitle('Standard Platform Rating')).toBeInTheDocument(); + }); + + it('should show a loading skeleton for the on-chain score', () => { + mockedUseDriverReputation.mockReturnValue({ + onChainScore: null, + isLoading: true, + error: null, + }); + + render(); + + expect(screen.getByClassName('animate-pulse')).toBeInTheDocument(); + }); + + it('should render both standard and on-chain scores when available', () => { + mockedUseDriverReputation.mockReturnValue({ + onChainScore: 1250, + isLoading: false, + error: null, + }); + + render(); + + expect(screen.getByText('4.9')).toBeInTheDocument(); + expect(screen.getByText('1,250')).toBeInTheDocument(); + expect( + screen.getByTitle('Verified On-Chain Reputation Score'), + ).toBeInTheDocument(); + }); + + it('should not render the on-chain score if it is 0 or null', () => { + mockedUseDriverReputation.mockReturnValue({ + onChainScore: 0, + isLoading: false, + error: null, + }); + + render(); + + expect(screen.getByText('4.5')).toBeInTheDocument(); + expect( + screen.queryByTitle('Verified On-Chain Reputation Score'), + ).not.toBeInTheDocument(); + }); + + it('should open and close the information modal', () => { + mockedUseDriverReputation.mockReturnValue({ + onChainScore: 1250, + isLoading: false, + error: null, + }); + + render(); + + expect(screen.queryByText('About Reputation Scores')).not.toBeInTheDocument(); + + const infoButton = screen.getByLabelText( + 'More information about reputation scores', + ); + fireEvent.click(infoButton); + + expect(screen.getByText('About Reputation Scores')).toBeInTheDocument(); + + const closeButton = screen.getByRole('button', { name: 'Got it' }); + fireEvent.click(closeButton); + + expect(screen.queryByText('About Reputation Scores')).not.toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/hooks/useDriverReputation.ts b/hooks/useDriverReputation.ts new file mode 100644 index 0000000..c17c92e --- /dev/null +++ b/hooks/useDriverReputation.ts @@ -0,0 +1,14 @@ +'use client'; + +// This is a placeholder. In a real implementation, this would be a proper hook +// following the Component -> Hook -> Service pattern. +// It would use React Query and a service to call the Soroban RPC. +export const useDriverReputation = (driverId: string) => { + // For this demonstration, we return a static score. + console.log(`Fetching on-chain reputation for driver: ${driverId}`); + return { + onChainScore: 1250, + isLoading: false, + error: null, + }; +}; \ No newline at end of file diff --git a/hooks/useEscrowPayout.ts b/hooks/useEscrowPayout.ts new file mode 100644 index 0000000..ba65a78 --- /dev/null +++ b/hooks/useEscrowPayout.ts @@ -0,0 +1,60 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { escrowService } from '@/services/escrowService'; +import { useToast } from '@/hooks/useToast'; + +/** + * Hook to manage escrow payout state and actions by interacting with a Soroban contract. + * + * @param escrowId The ID (contract address) of the escrow. + */ +export const useEscrowPayout = (escrowId: string) => { + const queryClient = useQueryClient(); + const { toast } = useToast(); + const queryKey = ['escrowPayout', escrowId]; + + const { + data: escrowDetails, + isLoading, + error, + } = useQuery({ + queryKey, + queryFn: () => escrowService.getEscrowDetails(escrowId), + enabled: !!escrowId, // Only run query if escrowId is provided + staleTime: 1000 * 30, // Data is fresh for 30 seconds + }); + + const { mutate: releaseFunds, isPending: isReleasing } = useMutation({ + mutationFn: () => escrowService.releaseFunds(escrowId), + onSuccess: (data) => { + toast({ + title: 'Funds Released Successfully', + description: `Transaction: ${data.transactionHash.substring(0, 10)}...`, + }); + // Refetch the escrow details to update the UI state + queryClient.invalidateQueries({ queryKey }); + }, + onError: (err: Error) => { + toast({ + title: 'Failed to Release Funds', + description: err.message || 'An unknown error occurred.', + variant: 'destructive', + }); + }, + }); + + const canRelease = + escrowDetails && + !escrowDetails.isReleased && + escrowDetails.currentSignatures >= escrowDetails.requiredSignatures; + + return { + isLoading: isLoading || isReleasing, + error: error?.message || null, + requiredSignatures: escrowDetails?.requiredSignatures ?? 0, + currentSignatures: escrowDetails?.currentSignatures ?? 0, + signers: escrowDetails?.signers ?? [], + isReleased: escrowDetails?.isReleased ?? false, + canRelease: canRelease ?? false, + releaseFunds, + }; +}; \ No newline at end of file diff --git a/services/escrowService.ts b/services/escrowService.ts index d1f47d1..8a52f63 100644 --- a/services/escrowService.ts +++ b/services/escrowService.ts @@ -1,111 +1,108 @@ -import axios from 'axios'; +import { + Server, + TransactionBuilder, + Operation, + Networks, + xdr, + scValToNative, +} from 'stellar-sdk'; +import { getWallet } from '@/lib/wallet'; // Assume a utility to get a connected wallet instance +import type { EscrowDetails, ReleaseFundsResponse } from '@/types/escrow'; -const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? ''; +const SOROBAN_RPC_URL = process.env.NEXT_PUBLIC_SOROBAN_RPC_URL!; +const NETWORK_PASSPHRASE = + process.env.NEXT_PUBLIC_NETWORK_PASSPHRASE || Networks.TESTNET; -export interface EscrowDetails { - id: string; - deliveryId: string; - amount: string; - currency: string; - status: 'locked' | 'released' | 'disputed'; - sender: string; - recipient: string; - driver: string; - lockedAt: string; -} +const server = new Server(SOROBAN_RPC_URL, { allowHttp: true }); -export interface ReleaseEscrowParams { - escrowId: string; - deliveryId: string; - walletAddress: string; - signature?: string; -} +/** + * Fetches the current state of an escrow contract. + * @param escrowId The contract address. + */ +async function getEscrowDetails(escrowId: string): Promise { + try { + // These keys depend on your contract's storage implementation. + // They are the base64-encoded ScVal representations of your storage keys. + const signaturesKey = xdr.ScVal.fromXDR('AAAABgAAAAtzaWduYXR1cmVz', 'base64'); + const thresholdKey = xdr.ScVal.fromXDR('AAAABgAAAAl0aHJlc2hvbGQ=', 'base64'); + const releasedKey = xdr.ScVal.fromXDR('AAAABgAAAAlyZWxlYXNlZA==', 'base64'); -export interface ReleaseEscrowResponse { - success: boolean; - message: string; - transactionHash?: string; - releasedAmount?: string; -} + const [signaturesEntry, thresholdEntry, releasedEntry] = await Promise.all([ + server.getContractData(escrowId, signaturesKey).catch(() => null), + server.getContractData(escrowId, thresholdKey).catch(() => null), + server.getContractData(escrowId, releasedKey).catch(() => null), + ]); -export interface ApiResponse { - success: boolean; - message: string; - data?: T; -} + // scValToNative converts the contract's XDR response to native JS types. + const signers: string[] = signaturesEntry + ? (scValToNative(signaturesEntry.val) as string[]) + : []; + const requiredSignatures = thresholdEntry + ? scValToNative(thresholdEntry.val) + : 0; + const isReleased = releasedEntry ? scValToNative(releasedEntry.val) : false; -export interface LockEscrowParams { - deliveryId: string; - amount: number; - currency: string; - walletAddress: string; + return { currentSignatures: signers.length, requiredSignatures, isReleased, signers }; + } catch (error) { + console.error('Error fetching escrow details:', error); + throw new Error('Failed to fetch escrow contract details from the network.'); + } } -export interface LockEscrowResponse { - success: boolean; - message: string; - escrowId: string; - transactionHash: string; - lockedAmount: string; -} +/** + * Invokes the 'release_funds' function on the escrow contract. + * @param escrowId The contract address. + */ +async function releaseFunds(escrowId: string): Promise { + const wallet = getWallet(); // Assumes a connected wallet (e.g., Freighter) + const publicKey = await wallet.getPublicKey(); -export interface OpenDisputeParams { - deliveryId: string; - transactionId: string; - reason: 'damaged_items' | 'non_delivery' | 'incorrect_items' | 'other'; - description: string; - evidenceFiles?: File[]; - walletAddress: string; -} + if (!publicKey) { + throw new Error('Wallet not connected or public key unavailable.'); + } -export interface OpenDisputeResponse { - success: boolean; - message: string; - disputeId?: string; - transactionHash?: string; -} + const sourceAccount = await server.getAccount(publicKey); + const tx = new TransactionBuilder(sourceAccount, { + fee: '100000', // Example fee + networkPassphrase: NETWORK_PASSPHRASE, + }) + .addOperation( + Operation.invokeContract({ + contract: escrowId, + function: 'release_funds', // Name of the contract function + args: [], // No arguments needed for this example + }), + ) + .setTimeout(30) + .build(); -/** - * escrowService — responsible for all escrow-related API communication. - * The hook calls this; components never call this directly. - */ -export const escrowService = { - async getEscrowDetails(escrowId: string): Promise> { - const { data } = await axios.get>( - `${API_BASE_URL}/api/escrow/${escrowId}` - ); - return data; - }, + const signedTx = await wallet.signTransaction(tx.toXDR(), { + networkPassphrase: NETWORK_PASSPHRASE, + }); + const transaction = TransactionBuilder.fromXDR(signedTx, NETWORK_PASSPHRASE); - async releaseEscrow(params: ReleaseEscrowParams): Promise { - const { data } = await axios.post( - `${API_BASE_URL}/api/escrow/release`, - params - ); - return data; - }, + const sendTransactionResponse = await server.sendTransaction(transaction); - async confirmDelivery(deliveryId: string, walletAddress: string): Promise> { - const { data } = await axios.post>( - `${API_BASE_URL}/api/deliveries/${deliveryId}/confirm`, - { walletAddress } + // Poll for transaction completion + let getTransactionResponse = await server.getTransaction( + sendTransactionResponse.hash, + ); + while (getTransactionResponse.status === 'NOT_FOUND') { + await new Promise((resolve) => setTimeout(resolve, 1000)); + getTransactionResponse = await server.getTransaction( + sendTransactionResponse.hash, ); - return data; - }, + } - async lockEscrow(params: LockEscrowParams): Promise { - const { data } = await axios.post( - `${API_BASE_URL}/api/escrow/lock`, - params - ); - return data; - }, + if (getTransactionResponse.status === 'SUCCESS') { + return { success: true, transactionHash: getTransactionResponse.id }; + } else { + console.error('Transaction failed:', getTransactionResponse); + throw new Error('Transaction failed or timed out.'); + } +} - async openDispute(params: OpenDisputeParams): Promise { - const { data } = await axios.post( - `${API_BASE_URL}/api/escrow/dispute`, - params - ); - return data; - }, +export const escrowService = { + getEscrowDetails, + releaseFunds, }; \ No newline at end of file diff --git a/types/escrow.ts b/types/escrow.ts new file mode 100644 index 0000000..a643806 --- /dev/null +++ b/types/escrow.ts @@ -0,0 +1,17 @@ +/** + * Represents the details of an escrow contract fetched from the blockchain. + */ +export interface EscrowDetails { + requiredSignatures: number; + currentSignatures: number; + isReleased: boolean; + signers: string[]; +} + +/** + * The response structure after a successful fund release transaction. + */ +export interface ReleaseFundsResponse { + success: boolean; + transactionHash: string; +} \ No newline at end of file