// Wallet — HCR + HCC balances + transaction history.
// Source: GET /api/v1/healthcoin/wallet + GET /api/v1/hcc/wallet
// GET /api/v1/healthcoin/transactions?limit=200
const Wallet = () => {
const [tab, setTab] = React.useState('all');
const [hcr, setHcr] = React.useState(undefined); // undefined=loading, null=missing, object=loaded
const [hcc, setHcc] = React.useState(undefined);
const [txns, setTxns] = React.useState(null);
React.useEffect(() => {
let dead = false;
const get = async (path) => {
try {
const r = await fetch(window.CHVault.apiRoot + path, { credentials: 'include', headers: { 'Accept': 'application/json' } });
if (r.status === 404) return null;
if (!r.ok) throw 0;
return await r.json();
} catch { return null; }
};
(async () => {
const [a, b, hcrTx, hccTx] = await Promise.all([
get('/api/v1/healthcoin/wallet'),
get('/api/v1/hcc/wallet'),
get('/api/v1/healthcoin/transactions?limit=200'),
get('/api/v1/hcc/transactions?limit=200'),
]);
if (dead) return;
setHcr(a);
setHcc(b);
// Normalize both streams into one list, tagged with coin.
// HCR shape: { transactions: [{tx_hash, tx_type, amount, timestamp(float seconds), direction, reward_reason}], total }
// HCC shape: { events: [{event_id, tier, final_pulses, final_hcc, era, created_at}], count }
const hcrRows = hcrTx
? (Array.isArray(hcrTx) ? hcrTx : (hcrTx.transactions || hcrTx.items || []))
: [];
const hccRows = hccTx
? (Array.isArray(hccTx) ? hccTx : (hccTx.events || hccTx.transactions || hccTx.items || []))
: [];
const epochToISO = (sec) => {
if (sec == null) return null;
const n = Number(sec);
if (!Number.isFinite(n)) return null;
// Heuristic: if it's < 1e12 it's seconds, else already ms.
return new Date(n < 1e12 ? n * 1000 : n).toISOString();
};
const merged = [].sort((x, y) => {
const tx = new Date(x.recorded_at || 0).getTime();
const ty = new Date(y.recorded_at || 0).getTime();
return ty - tx;
});
setTxns(merged);
})();
return () => { dead = true; };
}, []);
// HCR (/api/v1/healthcoin/wallet) returns { balance, balance_usd, total_earned, … }
// HCC (/api/v1/hcc/wallet) returns { balance_hcc, balance_pulses, total_mined_hcc, … }
const toNum = (x) => {
if (x == null) return null;
const n = Number(x);
return Number.isFinite(n) ? n : null;
};
const hcrBal = hcr && toNum(hcr.balance ?? hcr.hcr_balance ?? hcr.amount);
const hcrUsd = hcr && toNum(hcr.balance_usd ?? hcr.usd_value);
const hcrEarned = hcr && toNum(hcr.total_earned);
const hccBal = hcc && toNum(hcc.balance_hcc ?? hcc.balance ?? hcc.hcc_balance ?? hcc.amount);
const hccPulses = hcc && toNum(hcc.balance_pulses);
const hccMined = hcc && toNum(hcc.total_mined_hcc ?? hcc.total_mined);
const hccUsd = hcc && toNum(hcc.usd_value ?? hcc.amount_usd ?? hcc.balance_usd);
const filtered = (txns || []).filter(t => {
if (tab === 'all') return true;
return (t.coin || t.currency || 'HCR').toLowerCase() === tab;
});
const kindColor = (k) => k === 'mint' ? 'var(--accent-clinical)' : k === 'hcc' ? 'var(--accent-plasma)' : k === 'redeem' ? 'var(--bracket-poor)' : 'var(--accent-coral)';
const kindLabel = (k) => ({ mint: 'Mint', hcc: 'HCC', redeem: 'Redeem', reverse: 'Reverse' })[k] || k;
return (
Wallet
Your reserve and your data economy.
HCR is what you've earned through axis-lift. HCC is what researchers paid you to access opt-in records. Two coins. One wallet.
window.openQuickLog && window.openQuickLog(null) }}
/>
0 ? `${Number(hccMined).toLocaleString(undefined, { maximumFractionDigits: 6 })} HCC lifetime mined` : null}
actions={[
{ label: 'Cash out', primary: true, href: 'https://hc.exchange/redeem' },
{ label: 'Trade on hc.exchange', href: 'https://hc.exchange' },
{ label: 'Mining stats', href: 'https://hc.exchange/mining' },
]}
emptyTitle="No HCC yet."
emptyDetail="HCC is paid out when you opt your records into a researcher cohort and the query is accepted. Browse offers under Research."
ctaPrimary={{ label: 'Browse research offers', href: 'https://hc.exchange/research' }}
/>
Activity · last 200
Transaction history
{['all', 'hcr', 'hcc'].map(t => (
))}
{txns === null && (
Loading transactions…
)}
{txns && filtered.length === 0 && txns.length === 0 && (
No transactions yet.
The first mint will appear here the moment your first signal event is accepted.
)}
{txns && txns.length > 0 && filtered.length === 0 && (
No {tab.toUpperCase()} transactions in the last 200. Try the "All" or "{tab === 'hcr' ? 'HCC' : 'HCR'}" tab.
)}
{filtered.map((t, i) => {
const coin = (t.coin || t.currency || 'HCR').toUpperCase();
const kind = t.kind || t.type || (Number(t.amount || 0) < 0 ? 'redeem' : 'mint');
const amtNum = Number(t.amount ?? t.value ?? 0);
const when = t.recorded_at || t.created_at || (t.timestamp ? new Date((Number(t.timestamp) < 1e12 ? Number(t.timestamp) * 1000 : Number(t.timestamp))).toISOString() : null);
const precision = coin === 'HCC' ? 6 : 2;
return (
{kindLabel(kind)}
{t.description || t.detail || (t.signal_id ? 'Signal · ' + t.signal_id : 'Transaction')}
{when ? new Date(when).toLocaleString() : ''}{t.source ? ' · ' + t.source : ''}
{coin}
{amtNum > 0 ? '+' : ''}{amtNum.toLocaleString(undefined, { maximumFractionDigits: precision })}
);
})}
);
};
const BalanceCard = ({ tint, eyebrow, subtitle, value, unitLabel, secondary, actions, loading, emptyTitle, emptyDetail, ctaPrimary }) => {
const hasBalance = !loading && value != null && !Number.isNaN(Number(value));
const renderAction = (a, i) => {
const style = { textDecoration: 'none', padding: '10px 16px', borderRadius: 8, background: a.primary ? tint : 'transparent', color: a.primary ? 'white' : 'var(--fg-1)', border: a.primary ? 'none' : '1px solid var(--border-1)', fontWeight: 600, fontSize: 13, cursor: 'pointer' };
if (a.href) return {a.label};
if (a.onClick) return ;
return ;
};
return (
{loading ? (
Loading wallet…
) : hasBalance ? (
{Number(value).toLocaleString(undefined, { maximumFractionDigits: 6 })}
{unitLabel}
{secondary && {secondary}
}
) : (
{emptyTitle}
{emptyDetail}
)}
{hasBalance && (actions || []).map(renderAction)}
{!hasBalance && ctaPrimary && renderAction({ ...ctaPrimary, primary: true }, 'cta')}
);
};
window.Wallet = Wallet;