Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6b4ff83
manage escrow
andreip136 Jun 15, 2026
3a10fc3
add authorizations spinner
andreip136 Jun 16, 2026
325dfd2
style improvements
andreip136 Jun 16, 2026
a4dee08
style improvements
andreip136 Jun 16, 2026
78a8575
Merge branch 'main' into feat/escrow-management
andreip136 Jun 18, 2026
409f626
get authorizations from node + contract; support for multiple spenders
andreip136 Jun 18, 2026
93fe2af
deposit/withdraw form improvements
andreip136 Jun 18, 2026
3ff2062
style improvements
andreip136 Jun 18, 2026
bf42264
fix formatter
andreip136 Jun 18, 2026
a719f99
create authorization
andreip136 Jun 18, 2026
d4145be
remove escrow withdraw section from node info balance
andreip136 Jun 18, 2026
c697cbb
fix summary style
andreip136 Jun 18, 2026
eea4278
fix deposit/withdraw form clearing if unsuccessful
andreip136 Jun 18, 2026
223a937
clear state if no account
andreip136 Jun 18, 2026
1223412
handle partial loading fail
andreip136 Jun 18, 2026
ed0947f
refactor get logs from node
andreip136 Jun 18, 2026
03ae23f
use pinned getRpc instead of wallet RPC
andreip136 Jun 19, 2026
6830430
use node id when creating authorization
andreip136 Jun 19, 2026
9b52ff8
add eth address in node info
andreip136 Jun 19, 2026
df214fa
fix style
andreip136 Jun 19, 2026
a894243
Update src/components/escrow/create-authorization-modal.tsx
andreip136 Jun 19, 2026
6a6be19
Revert "use pinned getRpc instead of wallet RPC"
andreip136 Jun 19, 2026
7fe8035
fix node url
andreip136 Jun 19, 2026
e4d2cb1
implement revoke authorization
andreip136 Jun 23, 2026
c7baf6d
move authorization actions in dropdown
andreip136 Jun 23, 2026
da191b1
add escrow history tab
andreip136 Jun 23, 2026
d298fbf
fetch authorizations from contract
andreip136 Jun 23, 2026
56fb0f2
fix animation
andreip136 Jun 23, 2026
a357bf9
fix comment
andreip136 Jun 23, 2026
592dfbe
Merge branch 'feat/escrow-management' into feat/534-escrow-history
andreip136 Jun 23, 2026
5fb46c0
Merge branch 'main' into feat/534-escrow-history
andreip136 Jun 24, 2026
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
142 changes: 142 additions & 0 deletions src/components/escrow/escrow-history.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
.header {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 16px;
justify-content: space-between;
}

.headerSub {
color: var(--text-secondary);
font-size: 13px;
}

.typeCell {
align-items: center;
display: flex;
gap: 12px;
height: 100%;

.typeIcon {
align-items: center;
border-radius: 10px;
display: flex;
flex-shrink: 0;
height: 36px;
justify-content: center;
width: 36px;

&.iconCredit {
background: color-mix(in srgb, var(--success) 30%, transparent 70%);
color: var(--success-darker);
}

&.iconDebit {
background: color-mix(in srgb, var(--error) 22%, transparent 78%);
color: var(--error-darker);
}

&.iconLock {
background: color-mix(in srgb, var(--warning) 30%, transparent 70%);
color: var(--warning-darker);
}

&.iconAuth {
background: var(--background-glass-secondary);
color: var(--text-secondary);
}
}

.typeText {
display: flex;
flex-direction: column;
line-height: 1.3;

.typeLabel {
color: var(--text-primary);
font-weight: 600;
}

.typeToken {
color: var(--text-secondary);
font-size: 13px;
}
}
}

.amount {
font-weight: 600;

&.amountPositive {
color: var(--success-darker);
}

&.amountNegative {
color: var(--error-darker);
}
}

.amountEmpty {
color: var(--text-secondary);
}

.detailCell {
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
line-height: 1.3;
}

.detailPrimary {
color: var(--text-primary);
}

.payeeRow {
align-items: center;
color: var(--text-secondary);
display: flex;
font-size: 13px;
gap: 4px;
}

.mono {
color: var(--text-secondary);
font-family: var(--font-mono, monospace);
font-size: 13px;
}

.copyButton {
align-items: center;
background: none;
border: none;
border-radius: 4px;
color: var(--text-secondary);
cursor: pointer;
display: inline-flex;
opacity: 0.5;
padding: 2px;
transition: opacity 0.15s;

&:hover {
opacity: 1;
}
}

.copyIcon {
font-size: 14px !important;
}

.txLink {
color: var(--accent1);
font-size: 13px;
text-decoration: none;

&:hover {
text-decoration: underline;
}
}

.date {
color: var(--text-secondary);
}
229 changes: 229 additions & 0 deletions src/components/escrow/escrow-history.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import Button from '@/components/button/button';
import Card from '@/components/card/card';
import TabBar from '@/components/tab-bar/tab-bar';
import { Table } from '@/components/table/table';
import { TableTypeEnum } from '@/components/table/table-type';
import { BASE_CHAIN_ID, CHAIN_ID, ETH_SEPOLIA_CHAIN_ID } from '@/constants/chains';
import { getSupportedTokens } from '@/constants/tokens';
import { EscrowHistoryEntry, EscrowHistoryKind, useEscrowHistory } from '@/lib/use-escrow-history';
import { formatDateTime, formatTokenAmount, formatWalletAddress } from '@/utils/formatters';
import BlockOutlinedIcon from '@mui/icons-material/BlockOutlined';
import BoltIcon from '@mui/icons-material/Bolt';
import CachedIcon from '@mui/icons-material/Cached';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
import FileUploadOutlinedIcon from '@mui/icons-material/FileUploadOutlined';
import LockOpenOutlinedIcon from '@mui/icons-material/LockOpenOutlined';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import VpnKeyOutlinedIcon from '@mui/icons-material/VpnKeyOutlined';
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
import classNames from 'classnames';
import { useMemo, useState } from 'react';
import { toast } from 'react-toastify';
import styles from './escrow-history.module.css';

const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
toast.success('Copied to clipboard');
};

const KIND_META: Record<EscrowHistoryKind, { label: string; icon: React.ReactNode; iconClass: string }> = {
deposit: { label: 'Deposit', icon: <FileUploadOutlinedIcon fontSize="small" />, iconClass: styles.iconCredit },
withdraw: { label: 'Withdraw', icon: <FileDownloadOutlinedIcon fontSize="small" />, iconClass: styles.iconDebit },
lock: { label: 'Lock created', icon: <LockOutlinedIcon fontSize="small" />, iconClass: styles.iconLock },
release: { label: 'Lock released', icon: <LockOpenOutlinedIcon fontSize="small" />, iconClass: styles.iconCredit },
charge: { label: 'Job charge', icon: <BoltIcon fontSize="small" />, iconClass: styles.iconDebit },
'auth-created': { label: 'Auth. created', icon: <VpnKeyOutlinedIcon fontSize="small" />, iconClass: styles.iconAuth },
'auth-updated': { label: 'Auth. updated', icon: <EditOutlinedIcon fontSize="small" />, iconClass: styles.iconAuth },
'auth-revoked': { label: 'Auth. revoked', icon: <BlockOutlinedIcon fontSize="small" />, iconClass: styles.iconDebit },
};

// Authorization changes — amount means a max-locked allowance, not a signed fund movement.
const AUTH_KINDS: EscrowHistoryKind[] = ['auth-created', 'auth-updated', 'auth-revoked'];

const getExplorerUrl = () => {
if (CHAIN_ID === BASE_CHAIN_ID) return 'https://basescan.org';
if (CHAIN_ID === ETH_SEPOLIA_CHAIN_ID) return 'https://sepolia.etherscan.io';
return 'https://etherscan.io';
};

const escrowHistoryColumns: GridColDef<EscrowHistoryEntry>[] = [
{
field: 'kind',
headerName: 'Type',
flex: 1.4,
minWidth: 170,
sortable: false,
renderCell: ({ row }: GridRenderCellParams<EscrowHistoryEntry>) => {
const meta = KIND_META[row.kind];
return (
<div className={styles.typeCell}>
<span className={classNames(styles.typeIcon, meta.iconClass)}>{meta.icon}</span>
<div className={styles.typeText}>
<span className={styles.typeLabel}>{meta.label}</span>
{row.tokenSymbol && <span className={styles.typeToken}>{row.tokenSymbol}</span>}
</div>
</div>
);
},
},
{
field: 'amount',
headerName: 'Amount',
flex: 0.9,
minWidth: 120,
align: 'right',
headerAlign: 'right',
renderCell: ({ row }: GridRenderCellParams<EscrowHistoryEntry>) => {
const isAuth = AUTH_KINDS.includes(row.kind);
// Revoke has no meaningful limit; auth amounts are a max allowance, not a signed movement.
if (row.kind === 'auth-revoked') {
return <span className={styles.amountEmpty}>—</span>;
}
const formatted = formatTokenAmount(Math.abs(row.amount), row.tokenAddress);
if (isAuth) {
return <span className={styles.amount}>{formatted}</span>;
}
const positive = row.amount >= 0;
return (
<span className={classNames(styles.amount, positive ? styles.amountPositive : styles.amountNegative)}>
{positive ? '+' : '-'}
{formatted} {row.tokenSymbol}
</span>
);
},
},
{
field: 'detail',
headerName: 'Details',
flex: 1.6,
minWidth: 200,
sortable: false,
renderCell: ({ row }: GridRenderCellParams<EscrowHistoryEntry>) => (
<div className={styles.detailCell}>
<span className={styles.detailPrimary}>{row.detail}</span>
{row.counterparty && (
<span className={styles.payeeRow}>
Consumer:{' '}
<span className={styles.mono} title={row.counterparty}>
{formatWalletAddress(row.counterparty)}
</span>
<button
aria-label="Copy payee address"
className={styles.copyButton}
onClick={() => copyToClipboard(row.counterparty!)}
type="button"
>
<ContentCopyIcon className={styles.copyIcon} />
</button>
</span>
)}
</div>
),
},
{
field: 'txHash',
headerName: 'Transaction',
flex: 1,
minWidth: 140,
sortable: false,
renderCell: ({ row }: GridRenderCellParams<EscrowHistoryEntry>) => (
<a
className={styles.txLink}
href={`${getExplorerUrl()}/tx/${row.txHash}`}
rel="noopener noreferrer"
target="_blank"
title={row.txHash}
>
{formatWalletAddress(row.txHash)}
</a>
),
},
{
field: 'timestamp',
headerName: 'Date',
flex: 1,
minWidth: 140,
renderCell: ({ row }: GridRenderCellParams<EscrowHistoryEntry>) => (
<span className={styles.date}>{row.timestamp ? formatDateTime(row.timestamp) : `Block ${row.block}`}</span>
),
},
{
field: 'status',
headerName: 'Status',
flex: 0.8,
minWidth: 110,
align: 'right',
headerAlign: 'right',
sortable: false,
renderCell: ({ row }: GridRenderCellParams<EscrowHistoryEntry>) => (
<span
className={classNames('chip', {
chipSuccess: row.status === 'confirmed',
chipWarning: row.status === 'pending',
chipError: row.status === 'failed',
})}
>
{row.status.charAt(0).toUpperCase() + row.status.slice(1)}
</span>
),
},
];

const ALL_FILTER = 'ALL';

const EscrowHistory = () => {
const { entries, loading, reload } = useEscrowHistory();
const [tokenFilter, setTokenFilter] = useState<string>(ALL_FILTER);

// Token filter chips: All + every supported token symbol.
const tokenSymbols = useMemo(() => Object.keys(getSupportedTokens()), []);

const filtered = useMemo(
() => (tokenFilter === ALL_FILTER ? entries : entries.filter((entry) => entry.tokenSymbol === tokenFilter)),
[entries, tokenFilter]
);

const tabs = useMemo(
() => [
{ key: ALL_FILTER, label: 'All', onClick: () => setTokenFilter(ALL_FILTER) },
...tokenSymbols.map((symbol) => ({
key: symbol,
label: symbol,
onClick: () => setTokenFilter(symbol),
})),
],
[tokenSymbols]
);

return (
<Card direction="column" padding="md" radius="lg" shadow="black" spacing="md" variant="glass-shaded">
<div className={styles.header}>
<h3>Escrow history</h3>
{tokenSymbols.length > 0 && <TabBar activeKey={tokenFilter} size="sm" tabs={tabs} />}
</div>
<Table<EscrowHistoryEntry>
autoHeight
columns={escrowHistoryColumns}
data={filtered}
getRowId={(row) => row.id}
loading={loading}
paginationType="none"
tableType={TableTypeEnum.ESCROW_HISTORY}
/>
<Button
className="alignSelfEnd"
color="accent1"
contentBefore={<CachedIcon />}
onClick={reload}
size="sm"
variant="transparent"
>
Refresh
</Button>
</Card>
);
};

export default EscrowHistory;
5 changes: 5 additions & 0 deletions src/components/escrow/escrow-manage.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.panels {
display: flex;
flex-direction: column;
gap: 16px;
}
Loading
Loading