Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions quantara/frontend/src/hooks/useClosePosition.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { axiosInstance } from '../utils/axios';
import { axiosInstance, getAuthHeaders } from '../utils/axios';
import { closePosition } from '../services/transaction';
import { useWalletStore } from '../stores/useWalletStore';
import { notify } from '../components/layout/notifier/Notifier';
Expand All @@ -21,19 +21,19 @@ export const useClosePosition = () => {
console.error('closePositionEvent: walletId is undefined');
return;
}
const response = await axiosInstance.get('/api/get-repay-data', {
params: {
wallet_id: walletId,
},
});
const authHeaders = await getAuthHeaders(walletId);
const response = await axiosInstance.post(
'/api/get-repay-data',
{},
{ headers: authHeaders },
);
const transactionResult = await closePosition(response.data);
console.log('TransactionResult', transactionResult);
await axiosInstance.get('/api/close-position', {
params: {
position_id: response.data.position_id,
transaction_hash: transactionResult.transaction_hash,
},
});
await axiosInstance.post(
`/api/close-position/${response.data.position_id}`,
{ transaction_hash: transactionResult.transaction_hash },
{ headers: authHeaders },
);
},
onError: (error) => {
console.error('Error during closePositionEvent', error);
Expand Down
18 changes: 13 additions & 5 deletions quantara/frontend/src/hooks/useWithdrawAll.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMutation } from '@tanstack/react-query';
import { axiosInstance } from '../utils/axios';
import { axiosInstance, getAuthHeaders } from '../utils/axios';
import { notify } from '../components/layout/notifier/Notifier';
import { sendWithdrawAllTransaction } from '../services/transaction';

Expand All @@ -14,16 +14,24 @@ const useWithdrawAll = () => {
mutationFn: async (walletId) => {
if (!walletId) throw new Error('Wallet ID is required.');

const { data: withdraw_data } = await axiosInstance.get(`/api/get-withdraw-all-data?wallet_id=${walletId}`);
const authHeaders = await getAuthHeaders(walletId);

const { data: withdraw_data } = await axiosInstance.post(
'/api/get-withdraw-all-data',
{},
{ headers: authHeaders },
);

const { transaction_hash } = await sendWithdrawAllTransaction(
withdraw_data,
withdraw_data.repay_data.contract_address
);

await axiosInstance.get('/api/close-position', {
params: { transaction_hash: transaction_hash, position_id: withdraw_data.repay_data.position_id },
});
await axiosInstance.post(
`/api/close-position/${withdraw_data.repay_data.position_id}`,
{ transaction_hash },
{ headers: authHeaders },
);
},
onSuccess: () => {
notify('Withdraw All operation completed successfully!', 'success');
Expand Down
11 changes: 5 additions & 6 deletions quantara/frontend/src/services/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
loopData,
});

const { transaction_hash, result } = await invokeSorobanContract(

Check warning on line 46 in quantara/frontend/src/services/transaction.js

View workflow job for this annotation

GitHub Actions / test

'result' is assigned a value but never used
contractAddress,
'loop_liquidity',
[
Expand Down Expand Up @@ -276,12 +276,11 @@
);

// Notify backend of position creation with real transaction hash
await axiosInstance.get(`/api/open-position`, {
params: {
position_id: transactionData.position_id,
transaction_hash,
},
});
await axiosInstance.post(
`/api/open-position/${transactionData.position_id}`,
{ transaction_hash },
{ headers: authHeaders },
);

setTokenAmount('');
} catch (err) {
Expand Down
83 changes: 83 additions & 0 deletions quantara/frontend/test/hooks/useClosePosition.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { renderHook, act, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

import { useClosePosition } from '../../src/hooks/useClosePosition';
import { useWalletStore } from '../../src/stores/useWalletStore';
import { axiosInstance, getAuthHeaders } from '../../src/utils/axios';
import { closePosition } from '../../src/services/transaction';

vi.mock('../../src/utils/axios', () => ({
axiosInstance: { get: vi.fn(), post: vi.fn() },
getAuthHeaders: vi.fn(),
}));

vi.mock('../../src/services/transaction', () => ({
closePosition: vi.fn(),
}));

vi.mock('../../src/components/layout/notifier/Notifier', () => ({
notify: vi.fn(),
ToastWithLink: vi.fn(),
}));

const AUTH_HEADERS = {
'x-wallet-id': 'GABC123',
'x-nonce': 'nonce',
'x-signature': 'sig',
};

const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: { mutations: { retry: false } },
});
return ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};

describe('useClosePosition', () => {
beforeEach(() => {
vi.clearAllMocks();
useWalletStore.setState({ walletId: 'GABC123' });
getAuthHeaders.mockResolvedValue(AUTH_HEADERS);
});

afterEach(() => {
useWalletStore.setState({ walletId: null });
});

it('fetches repay data and closes the position via POST with auth headers', async () => {
axiosInstance.post.mockResolvedValueOnce({
data: {
position_id: 'pos-1',
contract_address: 'C123',
supply_token: 'S',
debt_token: 'D',
},
});
closePosition.mockResolvedValue({ transaction_hash: 'txhash' });

const { result } = renderHook(() => useClosePosition(), {
wrapper: createWrapper(),
});

act(() => {
result.current.mutate();
});

await waitFor(() => expect(closePosition).toHaveBeenCalled());

expect(getAuthHeaders).toHaveBeenCalledWith('GABC123');
expect(axiosInstance.post).toHaveBeenCalledWith(
'/api/get-repay-data',
{},
{ headers: AUTH_HEADERS },
);
expect(axiosInstance.post).toHaveBeenCalledWith(
'/api/close-position/pos-1',
{ transaction_hash: 'txhash' },
{ headers: AUTH_HEADERS },
);
});
});
80 changes: 80 additions & 0 deletions quantara/frontend/test/hooks/useWithdrawAll.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { renderHook, act, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { describe, it, expect, vi, beforeEach } from 'vitest';

import useWithdrawAll from '../../src/hooks/useWithdrawAll';
import { axiosInstance, getAuthHeaders } from '../../src/utils/axios';
import { sendWithdrawAllTransaction } from '../../src/services/transaction';

vi.mock('../../src/utils/axios', () => ({
axiosInstance: { get: vi.fn(), post: vi.fn() },
getAuthHeaders: vi.fn(),
}));

vi.mock('../../src/services/transaction', () => ({
sendWithdrawAllTransaction: vi.fn(),
}));

vi.mock('../../src/components/layout/notifier/Notifier', () => ({
notify: vi.fn(),
ToastWithLink: vi.fn(),
}));

const AUTH_HEADERS = {
'x-wallet-id': 'GABC123',
'x-nonce': 'nonce',
'x-signature': 'sig',
};

const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: { mutations: { retry: false } },
});
return ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};

describe('useWithdrawAll', () => {
beforeEach(() => {
vi.clearAllMocks();
getAuthHeaders.mockResolvedValue(AUTH_HEADERS);
});

it('fetches withdraw-all data and closes the position via POST with auth headers', async () => {
axiosInstance.post.mockResolvedValueOnce({
data: {
repay_data: {
position_id: 'pos-1',
contract_address: 'C123',
supply_token: 'S',
debt_token: 'D',
},
tokens: [],
},
});
sendWithdrawAllTransaction.mockResolvedValue({ transaction_hash: 'txhash' });

const { result } = renderHook(() => useWithdrawAll(), {
wrapper: createWrapper(),
});

act(() => {
result.current.withdrawAll('GABC123');
});

await waitFor(() => expect(sendWithdrawAllTransaction).toHaveBeenCalled());

expect(getAuthHeaders).toHaveBeenCalledWith('GABC123');
expect(axiosInstance.post).toHaveBeenCalledWith(
'/api/get-withdraw-all-data',
{},
{ headers: AUTH_HEADERS },
);
expect(axiosInstance.post).toHaveBeenCalledWith(
'/api/close-position/pos-1',
{ transaction_hash: 'txhash' },
{ headers: AUTH_HEADERS },
);
});
});
15 changes: 7 additions & 8 deletions quantara/frontend/test/services/transaction.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,18 @@ describe('Transaction Functions', () => {
});

it('should handle successful transaction flow', async () => {
axiosInstance.get.mockResolvedValueOnce({
data: { status: 'open' },
});

await handleTransaction(mockWalletId, mockFormData, mockSetTokenAmount, mockSetLoading);

const authHeaders = { 'x-wallet-id': 'mock', 'x-nonce': 'mock', 'x-signature': 'mock' };
expect(mockSetLoading).toHaveBeenCalledWith(true);
expect(getWalletPublicKey).toHaveBeenCalled();
expect(axiosInstance.post).toHaveBeenCalledWith('/api/create-position', mockFormData, { headers: { 'x-wallet-id': 'mock', 'x-nonce': 'mock', 'x-signature': 'mock' } });
expect(axiosInstance.post).toHaveBeenCalledWith('/api/create-position', mockFormData, { headers: authHeaders });
expect(invokeSorobanContract).toHaveBeenCalled();
expect(axiosInstance.get).toHaveBeenCalledWith('/api/open-position', {
params: { position_id: 1, transaction_hash: mockTransactionHash },
});
expect(axiosInstance.post).toHaveBeenCalledWith(
'/api/open-position/1',
{ transaction_hash: mockTransactionHash },
{ headers: authHeaders },
);
expect(mockSetTokenAmount).toHaveBeenCalledWith('');
expect(mockSetLoading).toHaveBeenCalledWith(false);
});
Expand Down
13 changes: 13 additions & 0 deletions quantara/web_app/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,19 @@ async def api_error_handler(request: Request, exc: APIError) -> JSONResponse:
}
},
},
403: {
"description": "Forbidden — the authenticated wallet is not allowed to perform this action.",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/ErrorEnvelope"},
"example": {
"detail": "Wallet does not own this position.",
"code": "not_position_owner",
"request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
},
}
},
},
404: {
"description": "Not Found — the requested resource does not exist.",
"content": {
Expand Down
Loading
Loading