From c9ef92359f294d6079aab4b56d2dcb55e8ca6b4f Mon Sep 17 00:00:00 2001
From: 0xMosas
Date: Fri, 27 Feb 2026 00:00:21 +0100
Subject: [PATCH 1/2] feat(utils): add formatAddress and formatNumber shared
utilities
- Add formatAddress(addr, startChars, endChars) for consistent truncation
- Add formatNumber(n, options) for locale-aware number formatting
- Add 8 new tests for formatAddress and formatNumber functions
---
frontend/src/lib/utils.js | 22 +++++++++++++++
frontend/src/test/utils.test.js | 47 ++++++++++++++++++++++++++++++++-
2 files changed, 68 insertions(+), 1 deletion(-)
diff --git a/frontend/src/lib/utils.js b/frontend/src/lib/utils.js
index 7e08a0e0..8d45d25a 100644
--- a/frontend/src/lib/utils.js
+++ b/frontend/src/lib/utils.js
@@ -26,3 +26,25 @@ export function formatSTX(microStx, decimals = 6) {
export function toMicroSTX(stx) {
return Math.floor(parseFloat(stx) * MICRO_STX);
}
+
+/**
+ * Truncate a Stacks address for display.
+ * @param {string} address - Full address
+ * @param {number} [startChars=6] - Characters to show from start
+ * @param {number} [endChars=4] - Characters to show from end
+ * @returns {string} Truncated address
+ */
+export function formatAddress(address, startChars = 6, endChars = 4) {
+ if (!address || address.length <= startChars + endChars + 3) return address || '';
+ return `${address.slice(0, startChars)}...${address.slice(-endChars)}`;
+}
+
+/**
+ * Locale-aware number formatting.
+ * @param {number|string} n - Number to format
+ * @param {object} [options] - Intl.NumberFormat options
+ * @returns {string} Formatted number
+ */
+export function formatNumber(n, options = {}) {
+ return Number(n).toLocaleString(undefined, options);
+}
diff --git a/frontend/src/test/utils.test.js b/frontend/src/test/utils.test.js
index 9441e5b2..de571d5d 100644
--- a/frontend/src/test/utils.test.js
+++ b/frontend/src/test/utils.test.js
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
-import { formatSTX, toMicroSTX, cn } from '../lib/utils';
+import { formatSTX, toMicroSTX, cn, formatAddress, formatNumber } from '../lib/utils';
describe('formatSTX', () => {
it('converts micro-STX to STX string', () => {
@@ -59,3 +59,48 @@ describe('cn', () => {
expect(result).not.toContain('hidden');
});
});
+
+describe('formatAddress', () => {
+ it('truncates a standard Stacks address', () => {
+ const addr = 'SP31PKQVQZVZCK3FM3NH67CGD6G1FMR17VQVS2W5T';
+ expect(formatAddress(addr)).toBe('SP31PK...2W5T');
+ });
+
+ it('uses custom start and end lengths', () => {
+ const addr = 'SP31PKQVQZVZCK3FM3NH67CGD6G1FMR17VQVS2W5T';
+ expect(formatAddress(addr, 8, 6)).toBe('SP31PKQV...VS2W5T');
+ });
+
+ it('returns short addresses unmodified', () => {
+ expect(formatAddress('SP123')).toBe('SP123');
+ });
+
+ it('handles empty or null input', () => {
+ expect(formatAddress('')).toBe('');
+ expect(formatAddress(null)).toBe('');
+ expect(formatAddress(undefined)).toBe('');
+ });
+});
+
+describe('formatNumber', () => {
+ it('formats a number with locale separators', () => {
+ const result = formatNumber(1234567);
+ expect(result).toContain('1');
+ expect(result.length).toBeGreaterThan(3);
+ });
+
+ it('formats with decimal options', () => {
+ const result = formatNumber(1234.5, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+ expect(result).toContain('34');
+ expect(result).toContain('50');
+ });
+
+ it('handles zero', () => {
+ expect(formatNumber(0)).toBe('0');
+ });
+
+ it('handles string input', () => {
+ const result = formatNumber('999');
+ expect(result).toContain('999');
+ });
+});
From 8df89174f3c85b9eec71a948349d0964427cc4bb Mon Sep 17 00:00:00 2001
From: 0xMosas
Date: Fri, 27 Feb 2026 00:00:30 +0100
Subject: [PATCH 2/2] refactor: replace inline address truncation and STX
division with shared utils
- TipHistory: use formatSTX() instead of raw (value / 1000000).toFixed(2)
- TipHistory: use formatAddress() instead of inline slice
- Leaderboard: use formatAddress() instead of inline slice
- RecentTips: use formatAddress() instead of inline slice
- NotificationBell: use formatAddress() instead of inline slice
---
frontend/src/components/Leaderboard.jsx | 4 ++--
frontend/src/components/NotificationBell.jsx | 5 ++---
frontend/src/components/RecentTips.jsx | 4 ++--
frontend/src/components/TipHistory.jsx | 8 ++++----
4 files changed, 10 insertions(+), 11 deletions(-)
diff --git a/frontend/src/components/Leaderboard.jsx b/frontend/src/components/Leaderboard.jsx
index 2e8569bd..c3f59282 100644
--- a/frontend/src/components/Leaderboard.jsx
+++ b/frontend/src/components/Leaderboard.jsx
@@ -1,6 +1,6 @@
import { useEffect, useState, useCallback } from 'react';
import { CONTRACT_ADDRESS, CONTRACT_NAME } from '../config/contracts';
-import { formatSTX } from '../lib/utils';
+import { formatSTX, formatAddress } from '../lib/utils';
import CopyButton from './ui/copy-button';
const API_BASE = 'https://api.hiro.so';
@@ -72,7 +72,7 @@ export default function Leaderboard() {
return b.totalReceived - a.totalReceived;
}).slice(0, 20);
- const truncateAddress = (addr) => `${addr.slice(0, 8)}...${addr.slice(-6)}`;
+ const truncateAddress = (addr) => formatAddress(addr, 8, 6);
if (loading) {
return (
diff --git a/frontend/src/components/NotificationBell.jsx b/frontend/src/components/NotificationBell.jsx
index b0a6133b..4a5031e1 100644
--- a/frontend/src/components/NotificationBell.jsx
+++ b/frontend/src/components/NotificationBell.jsx
@@ -1,5 +1,5 @@
import { useState, useRef, useEffect } from 'react';
-import { formatSTX } from '../lib/utils';
+import { formatSTX, formatAddress } from '../lib/utils';
export default function NotificationBell({ notifications, unreadCount, onMarkRead, loading }) {
const [open, setOpen] = useState(false);
@@ -22,8 +22,7 @@ export default function NotificationBell({ notifications, unreadCount, onMarkRea
}
};
- const truncateAddr = (addr) =>
- addr ? `${addr.slice(0, 6)}...${addr.slice(-4)}` : '';
+ const truncateAddr = (addr) => formatAddress(addr, 6, 4);
return (
diff --git a/frontend/src/components/RecentTips.jsx b/frontend/src/components/RecentTips.jsx
index d201ac5b..683d80a3 100644
--- a/frontend/src/components/RecentTips.jsx
+++ b/frontend/src/components/RecentTips.jsx
@@ -2,7 +2,7 @@ import { useEffect, useState, useCallback, useMemo } from 'react';
import { openContractCall } from '@stacks/connect';
import { uintCV, stringUtf8CV, PostConditionMode, Pc } from '@stacks/transactions';
import { CONTRACT_ADDRESS, CONTRACT_NAME } from '../config/contracts';
-import { formatSTX, toMicroSTX } from '../lib/utils';
+import { formatSTX, toMicroSTX, formatAddress } from '../lib/utils';
import { network, appDetails, userSession } from '../utils/stacks';
import { useTipContext } from '../context/TipContext';
import CopyButton from './ui/copy-button';
@@ -102,7 +102,7 @@ export default function RecentTips({ addToast }) {
const truncateAddress = (address) => {
const addrStr = typeof address === 'string' ? address : (address.value || '');
- return `${addrStr.slice(0, 8)}...${addrStr.slice(-6)}`;
+ return formatAddress(addrStr, 8, 6);
};
const fullAddress = (address) => {
diff --git a/frontend/src/components/TipHistory.jsx b/frontend/src/components/TipHistory.jsx
index 2d6c266a..519a9812 100644
--- a/frontend/src/components/TipHistory.jsx
+++ b/frontend/src/components/TipHistory.jsx
@@ -2,7 +2,7 @@ import { useEffect, useState, useCallback } from 'react';
import { fetchCallReadOnlyFunction, cvToJSON, principalCV } from '@stacks/transactions';
import { network } from '../utils/stacks';
import { CONTRACT_ADDRESS, CONTRACT_NAME } from '../config/contracts';
-import { formatSTX } from '../lib/utils';
+import { formatSTX, formatAddress } from '../lib/utils';
import { useTipContext } from '../context/TipContext';
import CopyButton from './ui/copy-button';
import ShareTip from './ShareTip';
@@ -123,7 +123,7 @@ export default function TipHistory({ userAddress }) {
}
};
- const truncateAddr = (addr) => `${addr.slice(0, 8)}...${addr.slice(-6)}`;
+ const truncateAddr = (addr) => formatAddress(addr, 8, 6);
const filteredTips = tips.filter(t => {
if (tab === 'sent' && t.direction !== 'sent') return false;
@@ -196,7 +196,7 @@ export default function TipHistory({ userAddress }) {
{stats['tips-sent'].value}
- Total Volume: {(stats['total-sent'].value / 1000000).toFixed(2)} STX
+ Total Volume: {formatSTX(stats['total-sent'].value, 2)} STX
@@ -213,7 +213,7 @@ export default function TipHistory({ userAddress }) {
{stats['tips-received'].value}
- Total Earned: {(stats['total-received'].value / 1000000).toFixed(2)} STX
+ Total Earned: {formatSTX(stats['total-received'].value, 2)} STX