diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 52029992..9d71b0a9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -115,4 +115,3 @@ jobs: run: npm test --if-present # Workflow run retention settings -retention-days: 30 \ No newline at end of file diff --git a/.github/workflows/contract-release.yml b/.github/workflows/contract-release.yml index 2655fcbc..3f66b1a5 100644 --- a/.github/workflows/contract-release.yml +++ b/.github/workflows/contract-release.yml @@ -29,4 +29,3 @@ jobs: release_token: ${{ secrets.GITHUB_TOKEN }} # Workflow run retention settings -retention-days: 90 \ No newline at end of file diff --git a/.github/workflows/dapp-ipfs.yml b/.github/workflows/dapp-ipfs.yml index 29d16e55..42248532 100644 --- a/.github/workflows/dapp-ipfs.yml +++ b/.github/workflows/dapp-ipfs.yml @@ -67,4 +67,3 @@ jobs: echo "- URL: ${{ steps.storacha.outputs.url }}" >> "$GITHUB_STEP_SUMMARY" # Workflow run retention settings -retention-days: 30 \ No newline at end of file diff --git a/.github/workflows/secrets-check.yml b/.github/workflows/secrets-check.yml index b8d3b7e8..c7a34807 100644 --- a/.github/workflows/secrets-check.yml +++ b/.github/workflows/secrets-check.yml @@ -22,4 +22,3 @@ jobs: run: ./scripts/check-k8s-secrets.sh # Workflow run retention settings -retention-days: 30 \ No newline at end of file diff --git a/frontend/src/components/WithdrawalFlow.tsx b/frontend/src/components/WithdrawalFlow.tsx index a19daaa6..eb117e67 100644 --- a/frontend/src/components/WithdrawalFlow.tsx +++ b/frontend/src/components/WithdrawalFlow.tsx @@ -55,8 +55,8 @@ const WithdrawalFlow: React.FC = ({ }; const handleConfirmWithdrawal = async () => { - await initiateWithdrawal(destinationType, destinationDetails); - if (state.step !== 'failed') { + const initiated = await initiateWithdrawal(destinationType, destinationDetails); + if (initiated) { onSuccess(); } }; diff --git a/frontend/src/hooks/useWithdrawal.ts b/frontend/src/hooks/useWithdrawal.ts index 09dd0fc3..d3aa0ed4 100644 --- a/frontend/src/hooks/useWithdrawal.ts +++ b/frontend/src/hooks/useWithdrawal.ts @@ -33,7 +33,7 @@ interface UseWithdrawalReturn { initiateWithdrawal: ( destinationType: 'bank_account' | 'mobile_money', destinationDetails: Record - ) => Promise; + ) => Promise; openInteractiveUrl: () => void; pollTransactionStatus: () => Promise; cancelWithdrawal: () => Promise; @@ -122,18 +122,18 @@ export function useWithdrawal( ) => { if (!state.selectedAnchor || !state.amount) { setState((prev) => ({ ...prev, error: 'Please select an anchor and enter an amount' })); - return; + return false; } const amount = parseFloat(state.amount); if (isNaN(amount) || amount <= 0) { setState((prev) => ({ ...prev, error: 'Please enter a valid amount' })); - return; + return false; } if (amount > balance) { setState((prev) => ({ ...prev, error: 'Insufficient balance' })); - return; + return false; } setState((prev) => ({ ...prev, isLoading: true, error: null, step: 'confirm' })); @@ -165,6 +165,7 @@ export function useWithdrawal( isLoading: false, step: 'processing', })); + return true; } catch (err) { setState((prev) => ({ ...prev, @@ -172,6 +173,7 @@ export function useWithdrawal( error: err instanceof Error ? err.message : 'Failed to initiate withdrawal', step: 'enter_amount', })); + return false; } }, [state.selectedAnchor, state.amount, balance] diff --git a/frontend/src/services/__tests__/withdrawal.test.ts b/frontend/src/services/__tests__/withdrawal.test.ts new file mode 100644 index 00000000..af0c89bc --- /dev/null +++ b/frontend/src/services/__tests__/withdrawal.test.ts @@ -0,0 +1,48 @@ +import axios from 'axios'; +import { describe, expect, it, vi } from 'vitest'; +import withdrawalService from '../withdrawal'; + +vi.mock('axios', () => ({ + default: { + get: vi.fn(), + post: vi.fn(), + }, +})); + +describe('withdrawalService', () => { + it('propagates anchor discovery failures instead of returning fallback data', async () => { + const failure = new Error('Network Error'); + vi.spyOn(axios, 'get').mockRejectedValueOnce(failure); + + await expect(withdrawalService.getAvailableAnchors()).rejects.toBe(failure); + }); + + it('propagates withdrawal initiation failures instead of returning a fake transaction', async () => { + const failure = new Error('Request failed with status code 500'); + vi.spyOn(axios, 'post').mockRejectedValueOnce(failure); + + await expect( + withdrawalService.initiateWithdrawal({ + anchorDomain: 'anchor.ng', + assetCode: 'ORGUSD', + amount: 25, + destinationType: 'bank_account', + destinationDetails: { account: '0123456789' }, + }) + ).rejects.toBe(failure); + }); + + it('propagates transaction status failures instead of returning a fake success', async () => { + const failure = new Error('Request failed with status code 500'); + vi.spyOn(axios, 'get').mockRejectedValueOnce(failure); + + await expect(withdrawalService.getTransactionStatus('tx-1', 'anchor')).rejects.toBe(failure); + }); + + it('propagates cancellation failures instead of resolving silently', async () => { + const failure = new Error('Network Error'); + vi.spyOn(axios, 'post').mockRejectedValueOnce(failure); + + await expect(withdrawalService.cancelWithdrawal('tx-1')).rejects.toBe(failure); + }); +}); diff --git a/frontend/src/services/withdrawal.ts b/frontend/src/services/withdrawal.ts index 633d2dcf..879adb42 100644 --- a/frontend/src/services/withdrawal.ts +++ b/frontend/src/services/withdrawal.ts @@ -55,105 +55,39 @@ export interface WithdrawalResponse { } const withdrawalService = { - /** - * Fetch available anchors for withdrawal - */ + /** Fetch available anchors for withdrawal. */ getAvailableAnchors: async (assetCode: string = 'ORGUSD'): Promise => { - try { - const response = await axios.get<{ anchors: AnchorInfo[] }>( - `${API_BASE_URL}/withdrawal/anchors`, - { params: { assetCode } } - ); - return response.data.anchors; - } catch { - // Mock data for development until backend is ready - return [ - { - domain: 'anchor.ng', - name: 'Anchor Nigeria', - supportedCurrencies: ['NGN'], - withdrawFee: '1%', - withdrawMinAmount: 10, - withdrawMaxAmount: 10000, - }, - { - domain: 'flutterwave.com', - name: 'Flutterwave', - supportedCurrencies: ['NGN', 'KES', 'GHS', 'ZAR'], - withdrawFee: '0.5%', - withdrawMinAmount: 5, - withdrawMaxAmount: 50000, - }, - { - domain: 'stearn.com', - name: 'Stearn Financial', - supportedCurrencies: ['EUR', 'GBP'], - withdrawFee: '1.2%', - withdrawMinAmount: 20, - withdrawMaxAmount: 25000, - }, - ]; - } + const response = await axios.get<{ anchors: AnchorInfo[] }>( + `${API_BASE_URL}/withdrawal/anchors`, + { params: { assetCode } } + ); + return response.data.anchors; }, - /** - * Initiate a withdrawal via backend SEP-24 endpoint - */ + /** Initiate a withdrawal via backend SEP-24 endpoint. */ initiateWithdrawal: async (request: WithdrawalRequest): Promise => { - try { - const response = await axios.post( - `${API_BASE_URL}/withdrawal/initiate`, - request - ); - return response.data; - } catch { - // Mock response for development - const mockTxId = `wd-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`; - return { - transactionId: mockTxId, - interactiveUrl: `https://${request.anchorDomain}/withdraw?tx=${mockTxId}`, - status: 'pending_user_transfer', - }; - } + const response = await axios.post( + `${API_BASE_URL}/withdrawal/initiate`, + request + ); + return response.data; }, - /** - * Get withdrawal transaction status - */ + /** Get withdrawal transaction status. */ getTransactionStatus: async ( transactionId: string, anchorDomain: string ): Promise => { - try { - const response = await axios.get( - `${API_BASE_URL}/withdrawal/status/${transactionId}`, - { params: { anchorDomain } } - ); - return response.data; - } catch { - // Mock status for development - return { - id: transactionId, - anchorDomain, - status: 'pending_user_transfer', - amountIn: 100, - assetCode: 'ORGUSD', - interactiveUrl: `https://${anchorDomain}/withdraw?tx=${transactionId}`, - startedAt: new Date().toISOString(), - }; - } + const response = await axios.get( + `${API_BASE_URL}/withdrawal/status/${transactionId}`, + { params: { anchorDomain } } + ); + return response.data; }, - /** - * Cancel a pending withdrawal - */ + /** Cancel a pending withdrawal. */ cancelWithdrawal: async (transactionId: string): Promise => { - try { - await axios.post(`${API_BASE_URL}/withdrawal/cancel`, { transactionId }); - } catch { - // Mock success for development - console.log('Mock: Withdrawal cancelled', transactionId); - } + await axios.post(`${API_BASE_URL}/withdrawal/cancel`, { transactionId }); }, };