- {tip.direction === 'sent' ? '-' : '+'}
-
-
-
{tip.direction === 'sent' ? 'To' : 'From'}
-
{formatAddress(tip.direction === 'sent' ? tip.recipient : tip.sender, 8, 6)}
-
+
+
+
+
+ {tip.direction === 'sent' ? '-' : '+'}
+
+
+
+ {tip.direction === 'sent' ? 'To' : 'From'}
+ {formatAddress(tip.direction === 'sent' ? tip.recipient : tip.sender, 8, 6)}
+
+
+ {tip.message ? (
+
“{tip.message}”
+ ) : null}
- {tip.message ? (
-
“{tip.message}”
- ) : null}
+
+
+
+ {tip.direction === 'sent' ? '-' : '+'}{formatSTX(tip.amount, 2)} STX
+
+
-
-
- {tip.direction === 'sent' ? '-' : '+'}{formatSTX(tip.amount, 2)} STX
-
-
-
+ {tip.direction === 'sent' && (
+
+
+
+ )}
+ {tip.direction === 'received' && (
+
+
+
+ )}
))}
diff --git a/frontend/src/config/contracts.js b/frontend/src/config/contracts.js
index 58708f82..31bf00da 100644
--- a/frontend/src/config/contracts.js
+++ b/frontend/src/config/contracts.js
@@ -61,6 +61,15 @@ export const FN_GET_USER_STATS = 'get-user-stats';
export const FN_GET_PLATFORM_STATS = 'get-platform-stats';
export const FN_GET_CURRENT_FEE_BASIS_POINTS = 'get-current-fee-basis-points';
+// Refund
+export const FN_REQUEST_REFUND = 'request-refund';
+export const FN_APPROVE_REFUND = 'approve-refund';
+export const FN_REJECT_REFUND = 'reject-refund';
+export const FN_GET_REFUND_REQUEST = 'get-refund-request';
+export const FN_IS_TIP_REFUNDED = 'is-tip-refunded';
+export const FN_IS_REFUND_ELIGIBLE = 'is-refund-eligible';
+export const FN_GET_REFUND_WINDOW_BLOCKS = 'get-refund-window-blocks';
+
// Contract validation helper
export function validateContractDeployment() {
return {
diff --git a/frontend/src/config/routes.js b/frontend/src/config/routes.js
index 4bfb5035..2527c1c9 100644
--- a/frontend/src/config/routes.js
+++ b/frontend/src/config/routes.js
@@ -103,6 +103,12 @@ export const ROUTE_ADMIN = '/admin';
*/
export const ROUTE_TELEMETRY = '/telemetry';
+/**
+ * Refund requests management.
+ * @type {string}
+ */
+export const ROUTE_REFUNDS = '/refunds';
+
/**
* The route that "/" redirects to when the user is authenticated.
* Change this single value to alter the default landing page site-wide.
@@ -130,6 +136,7 @@ export const ROUTE_LABELS = {
[ROUTE_STATS]: 'Stats',
[ROUTE_ADMIN]: 'Admin',
[ROUTE_TELEMETRY]: 'Telemetry',
+ [ROUTE_REFUNDS]: 'Refunds',
};
/**
@@ -155,6 +162,7 @@ export const ROUTE_TITLES = {
[ROUTE_STATS]: 'Platform Stats -- TipStream',
[ROUTE_ADMIN]: 'Admin Dashboard -- TipStream',
[ROUTE_TELEMETRY]: 'Telemetry -- TipStream',
+ [ROUTE_REFUNDS]: 'Refunds -- TipStream',
};
/**
@@ -244,4 +252,9 @@ export const ROUTE_META = {
requiresAuth: true,
adminOnly: true,
},
+ [ROUTE_REFUNDS]: {
+ description: 'View and manage refund requests for sent and received tips.',
+ requiresAuth: true,
+ adminOnly: false,
+ },
};
diff --git a/frontend/src/hooks/useRefund.js b/frontend/src/hooks/useRefund.js
new file mode 100644
index 00000000..ec252609
--- /dev/null
+++ b/frontend/src/hooks/useRefund.js
@@ -0,0 +1,118 @@
+import { useState, useCallback } from 'react';
+
+const REFUND_WINDOW_MS = 24 * 60 * 60 * 1000;
+
+export function isWithinRefundWindow(tipTimestamp) {
+ if (!tipTimestamp) return false;
+ const ts = typeof tipTimestamp === 'number' ? tipTimestamp : new Date(tipTimestamp).getTime();
+ return Date.now() - ts < REFUND_WINDOW_MS;
+}
+
+export function useRefund() {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const requestRefund = useCallback(async ({ tipId, txId, sender, recipient, amount, reason }) => {
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await fetch('/api/refunds', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ tipId, txId, sender, recipient, amount, reason }),
+ });
+ const data = await response.json();
+ if (!response.ok) {
+ throw new Error(data.message || 'Failed to submit refund request');
+ }
+ return { ok: true, refundRequest: data.refundRequest };
+ } catch (err) {
+ const msg = err.message || 'Failed to submit refund request';
+ setError(msg);
+ return { ok: false, error: msg };
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ const resolveRefund = useCallback(async ({ tipId, action, recipient, refundTxId }) => {
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await fetch(`/api/refunds/${tipId}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ action, recipient, refundTxId }),
+ });
+ const data = await response.json();
+ if (!response.ok) {
+ throw new Error(data.message || 'Failed to update refund request');
+ }
+ return { ok: true, refundRequest: data.refundRequest };
+ } catch (err) {
+ const msg = err.message || 'Failed to update refund request';
+ setError(msg);
+ return { ok: false, error: msg };
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ const fetchRefundRequest = useCallback(async (tipId) => {
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await fetch(`/api/refunds/${tipId}`);
+ if (response.status === 404) {
+ return { ok: true, refundRequest: null };
+ }
+ const data = await response.json();
+ if (!response.ok) {
+ throw new Error(data.message || 'Failed to fetch refund request');
+ }
+ return { ok: true, refundRequest: data };
+ } catch (err) {
+ const msg = err.message || 'Failed to fetch refund request';
+ setError(msg);
+ return { ok: false, error: msg };
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ const fetchUserRefunds = useCallback(async ({ sender, recipient, status, limit, offset } = {}) => {
+ setLoading(true);
+ setError(null);
+ try {
+ const params = new URLSearchParams();
+ if (sender) params.set('sender', sender);
+ if (recipient) params.set('recipient', recipient);
+ if (status) params.set('status', status);
+ if (limit) params.set('limit', String(limit));
+ if (offset) params.set('offset', String(offset));
+
+ const response = await fetch(`/api/refunds?${params}`);
+ const data = await response.json();
+ if (!response.ok) {
+ throw new Error(data.message || 'Failed to fetch refund requests');
+ }
+ return { ok: true, refundRequests: data.refundRequests, total: data.total };
+ } catch (err) {
+ const msg = err.message || 'Failed to fetch refund requests';
+ setError(msg);
+ return { ok: false, error: msg };
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ return {
+ loading,
+ error,
+ requestRefund,
+ resolveRefund,
+ fetchRefundRequest,
+ fetchUserRefunds,
+ isWithinRefundWindow,
+ };
+}