From 1a223bbc3d2b42a523d39829ef9cecb8dd1a6ca7 Mon Sep 17 00:00:00 2001 From: andreip136 <129227833+andreip136@users.noreply.github.com> Date: Thu, 2 Jul 2026 23:41:39 +0300 Subject: [PATCH 1/9] escrow page improvements --- src/components/button/copy-button.tsx | 2 +- src/components/escrow/authorization-form.tsx | 6 ++- .../escrow/create-authorization-modal.tsx | 18 ++++--- .../escrow/edit-authorization-modal.tsx | 34 ++++++++++++- .../escrow/escrow-token-panel.module.css | 46 +++++++---------- src/components/escrow/escrow-token-panel.tsx | 49 +++++++++++++------ src/lib/use-escrow-data.ts | 42 ++++++++++++++++ src/types/nodes.ts | 1 + 8 files changed, 147 insertions(+), 51 deletions(-) diff --git a/src/components/button/copy-button.tsx b/src/components/button/copy-button.tsx index b4ff1d3f..0241b2e9 100644 --- a/src/components/button/copy-button.tsx +++ b/src/components/button/copy-button.tsx @@ -32,7 +32,7 @@ const CopyButton: React.FC = ({
@@ -243,10 +270,7 @@ const EscrowTokenPanel = ({ token, spenders, loadingSpenders, onChange }: Escrow
{/* ── Left: balance + move funds ── */}
- {/* Token header */} -
- {token.symbol} -
+

{token.symbol}

{/* Primary balance */}
@@ -260,7 +284,7 @@ const EscrowTokenPanel = ({ token, spenders, loadingSpenders, onChange }: Escrow {/* Secondary balances */}
- Locked in escrow + Locked for running jobs {formatTokenAmount(token.locked, token.address)}{' '} {token.symbol} @@ -342,10 +366,10 @@ const EscrowTokenPanel = ({ token, spenders, loadingSpenders, onChange }: Escrow {/* ── Right: authorizations & locks (one card per authorized spender) ── */}
- Authorizations +

Authorizations

{spenders.length > 0 && ( - {spenders.length} {spenders.length === 1 ? 'consumer' : 'consumers'} + {spenders.length} {spenders.length === 1 ? 'node' : 'nodes'} )}
No authorization yet - - An authorization is created automatically the first time you pay for a compute job with {token.symbol}. -
)}
diff --git a/src/lib/use-escrow-data.ts b/src/lib/use-escrow-data.ts index 576a0383..df531a82 100644 --- a/src/lib/use-escrow-data.ts +++ b/src/lib/use-escrow-data.ts @@ -1,6 +1,9 @@ import { useOceanAccount } from '@/lib/use-ocean-account'; +import { getApiRoute } from '@/config'; import { getSupportedTokens } from '@/constants/tokens'; import { Authorizations, EscrowLock } from '@/types/payment'; +import { Node } from '@/types/nodes'; +import axios from 'axios'; import { useCallback, useEffect, useState } from 'react'; import { toast } from 'react-toastify'; @@ -22,6 +25,29 @@ export type EscrowSpenderInfo = { spender: string; authorizations: Authorizations; locks: EscrowLock[]; + // Resolved from the nodes API by matching the spender wallet to a node's `address`. + // Undefined while loading or if the wallet maps to no known node. + nodeId?: string; + nodeFriendlyName?: string; +}; + +// Look up the node whose payment wallet equals `wallet`, using the same filtered nodes +// endpoint as the leaderboard. Returns id + friendly name, or null if none matches. +const fetchNodeByWallet = async (wallet: string): Promise<{ id: string; friendlyName?: string } | null> => { + try { + const filters = { address: { operator: 'equals', value: wallet } }; + const res = await axios.get( + `${getApiRoute('nodes')}?page=0&size=1&filters=${encodeURIComponent(JSON.stringify(filters))}` + ); + const node: Node | undefined = res.data?.nodes?.[0]?._source; + if (!node?.id) { + return null; + } + return { id: node.id, friendlyName: node.friendlyName }; + } catch (err) { + console.error(`Failed to resolve node for wallet ${wallet}:`, err); + return null; + } }; export type UseEscrowDataReturn = { @@ -102,6 +128,22 @@ export const useEscrowData = (): UseEscrowDataReturn => { // Drop revoked authorizations — limits all zeroed, no longer usable. .filter((info) => !isRevokedAuthorization(info.authorizations) || info.locks.length > 0); setSpenders(spenderInfos); + + // Enrich each spender with its node id. One lookup per unique wallet (a wallet can + // appear across multiple tokens), then map results back onto every matching spender. + const uniqueWallets = [...new Set(spenderInfos.map((info) => info.spender))]; + const nodeByWallet = new Map(); + await Promise.all( + uniqueWallets.map(async (wallet) => { + nodeByWallet.set(wallet, await fetchNodeByWallet(wallet)); + }) + ); + setSpenders((current) => + current.map((info) => { + const node = nodeByWallet.get(info.spender); + return node ? { ...info, nodeId: node.id, nodeFriendlyName: node.friendlyName } : info; + }) + ); } catch (err) { console.error('Failed to load escrow data:', err); } finally { diff --git a/src/types/nodes.ts b/src/types/nodes.ts index d5eb5fc4..a859877c 100644 --- a/src/types/nodes.ts +++ b/src/types/nodes.ts @@ -22,6 +22,7 @@ export type NodeBanInfo = { }; export type Node = { + address?: string; allowedAdmins?: string[]; banned: boolean; bannedAt?: number; From 742e56d5945c73f6cac722255737b4ce5d4c2ad4 Mon Sep 17 00:00:00 2001 From: andreip136 <129227833+andreip136@users.noreply.github.com> Date: Thu, 2 Jul 2026 23:48:05 +0300 Subject: [PATCH 2/9] deposit/withdraw funds --- .../escrow/escrow-token-panel.module.css | 17 +- src/components/escrow/escrow-token-panel.tsx | 155 +++++++++--------- 2 files changed, 86 insertions(+), 86 deletions(-) diff --git a/src/components/escrow/escrow-token-panel.module.css b/src/components/escrow/escrow-token-panel.module.css index cb1c6e8c..8a3ad566 100644 --- a/src/components/escrow/escrow-token-panel.module.css +++ b/src/components/escrow/escrow-token-panel.module.css @@ -99,16 +99,23 @@ gap: 10px; } -.fundRow { - display: flex; -} - .fundInput { - flex: 1; + width: 100%; min-width: 0; } +.fundButtons { + display: flex; + flex-direction: column; + gap: 10px; + + @media (min-width: 576px) { + flex-direction: row; + } +} + .fundButton { + flex: 1; white-space: nowrap; } diff --git a/src/components/escrow/escrow-token-panel.tsx b/src/components/escrow/escrow-token-panel.tsx index 60eaf979..199596ce 100644 --- a/src/components/escrow/escrow-token-panel.tsx +++ b/src/components/escrow/escrow-token-panel.tsx @@ -20,9 +20,7 @@ import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; import { CircularProgress, Collapse, IconButton, ListItemIcon, MenuItem } from '@mui/material'; import classNames from 'classnames'; -import { useFormik } from 'formik'; import { useRef, useState } from 'react'; -import * as Yup from 'yup'; import styles from './escrow-token-panel.module.css'; type EscrowTokenPanelProps = { @@ -32,10 +30,6 @@ type EscrowTokenPanelProps = { onChange: () => void; }; -type AmountFormValues = { - amount: number | ''; -}; - // One spending-authorization card (right-hand side). A token can have multiple authorized // spenders, so each gets its own card with its own edit + locks-expansion state. const AuthorizationCard = ({ @@ -234,36 +228,52 @@ const AuthorizationCard = ({ const EscrowTokenPanel = ({ token, spenders, loadingSpenders, onChange }: EscrowTokenPanelProps) => { const [isCreateOpen, setIsCreateOpen] = useState(false); + // Single amount input drives both actions. amount > 0 is the base check; the per-action + // balance ceiling (wallet for deposit, escrow for withdraw) is validated at click time + // since it depends on which button was pressed. + const [amount, setAmount] = useState(''); + const [error, setError] = useState(); + const { handleDeposit, isDepositing } = useDepositTokens({ onSuccess: () => { - depositForm.resetForm(); + setAmount(''); onChange(); }, }); const { handleWithdraw, isWithdrawing } = useWithdrawTokens({ onSuccess: () => { - withdrawForm.resetForm(); + setAmount(''); onChange(); }, }); - const depositForm = useFormik({ - initialValues: { amount: '' }, - onSubmit: (values) => handleDeposit({ tokenAddress: token.address, amount: values.amount.toString() }), - validateOnMount: true, - validationSchema: Yup.object({ - amount: Yup.number().moreThan(0, 'Invalid amount').max(token.walletBalance, 'Exceeds wallet balance'), - }), - }); + const onDeposit = () => { + const value = Number(amount); + if (!(value > 0)) { + setError('Invalid amount'); + return; + } + if (value > token.walletBalance) { + setError('Exceeds wallet balance'); + return; + } + setError(undefined); + handleDeposit({ tokenAddress: token.address, amount: value.toString() }); + }; - const withdrawForm = useFormik({ - initialValues: { amount: '' }, - onSubmit: (values) => handleWithdraw({ tokenAddresses: [token.address], amounts: [values.amount.toString()] }), - validateOnMount: true, - validationSchema: Yup.object({ - amount: Yup.number().moreThan(0, 'Invalid amount').max(token.available, 'Exceeds available funds'), - }), - }); + const onWithdraw = () => { + const value = Number(amount); + if (!(value > 0)) { + setError('Invalid amount'); + return; + } + if (value > token.available) { + setError('Exceeds available funds'); + return; + } + setError(undefined); + handleWithdraw({ tokenAddresses: [token.address], amounts: [value.toString()] }); + }; return ( @@ -301,65 +311,48 @@ const EscrowTokenPanel = ({ token, spenders, loadingSpenders, onChange }: Escrow {/* Move funds */}
- Move funds - - } - disabled={!depositForm.isValid || !depositForm.values.amount} - loading={isDepositing} - size="sm" - type="submit" - variant="filled" - > - Deposit - - } - errorText={ - depositForm.touched.amount && depositForm.errors.amount ? depositForm.errors.amount : undefined - } - name="amount" - onBlur={depositForm.handleBlur} - onChange={depositForm.handleChange} + { + setAmount(e.target.value); + setError(undefined); + }} + size="md" + startAdornment={token.symbol} + type="number" + value={amount} + /> +
+ - } - errorText={ - withdrawForm.touched.amount && withdrawForm.errors.amount ? withdrawForm.errors.amount : undefined - } - name="amount" - onBlur={withdrawForm.handleBlur} - onChange={withdrawForm.handleChange} + type="button" + variant="outlined" + > + Withdraw + + +
From 8657c4f755273a4dc2da9347628e7fa37e84564c Mon Sep 17 00:00:00 2001 From: andreip136 <129227833+andreip136@users.noreply.github.com> Date: Fri, 3 Jul 2026 00:12:50 +0300 Subject: [PATCH 3/9] single request for fetch nodes by addresses --- src/lib/use-escrow-data.ts | 45 +++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/lib/use-escrow-data.ts b/src/lib/use-escrow-data.ts index df531a82..e55d2751 100644 --- a/src/lib/use-escrow-data.ts +++ b/src/lib/use-escrow-data.ts @@ -31,23 +31,32 @@ export type EscrowSpenderInfo = { nodeFriendlyName?: string; }; -// Look up the node whose payment wallet equals `wallet`, using the same filtered nodes -// endpoint as the leaderboard. Returns id + friendly name, or null if none matches. -const fetchNodeByWallet = async (wallet: string): Promise<{ id: string; friendlyName?: string } | null> => { +// Resolve node ids for a list of payment wallets in a single request. The nodes endpoint +// matches `address` against any value in a comma-separated list (backend splits it into an +// OR of case-insensitive matches). Returns a map keyed by lowercased wallet address; +// wallets with no matching node are simply absent. +const fetchNodesByWallets = async ( + wallets: string[] +): Promise> => { + const byWallet = new Map(); + if (wallets.length === 0) { + return byWallet; + } try { - const filters = { address: { operator: 'equals', value: wallet } }; + const filters = { address: { value: wallets.join(',') } }; const res = await axios.get( - `${getApiRoute('nodes')}?page=0&size=1&filters=${encodeURIComponent(JSON.stringify(filters))}` + `${getApiRoute('nodes')}?page=0&size=${wallets.length}&filters=${encodeURIComponent(JSON.stringify(filters))}` ); - const node: Node | undefined = res.data?.nodes?.[0]?._source; - if (!node?.id) { - return null; + const nodes: { _source: Node }[] = res.data?.nodes ?? []; + for (const { _source: node } of nodes) { + if (node?.id && node.address) { + byWallet.set(node.address.toLowerCase(), { id: node.id, friendlyName: node.friendlyName }); + } } - return { id: node.id, friendlyName: node.friendlyName }; } catch (err) { - console.error(`Failed to resolve node for wallet ${wallet}:`, err); - return null; + console.error('Failed to resolve nodes for wallets:', err); } + return byWallet; }; export type UseEscrowDataReturn = { @@ -129,18 +138,14 @@ export const useEscrowData = (): UseEscrowDataReturn => { .filter((info) => !isRevokedAuthorization(info.authorizations) || info.locks.length > 0); setSpenders(spenderInfos); - // Enrich each spender with its node id. One lookup per unique wallet (a wallet can - // appear across multiple tokens), then map results back onto every matching spender. + // Enrich each spender with its node id. Collect the unique wallets (a wallet can + // appear across multiple tokens) and resolve them all in a single request, then map + // results back onto every matching spender. const uniqueWallets = [...new Set(spenderInfos.map((info) => info.spender))]; - const nodeByWallet = new Map(); - await Promise.all( - uniqueWallets.map(async (wallet) => { - nodeByWallet.set(wallet, await fetchNodeByWallet(wallet)); - }) - ); + const nodeByWallet = await fetchNodesByWallets(uniqueWallets); setSpenders((current) => current.map((info) => { - const node = nodeByWallet.get(info.spender); + const node = nodeByWallet.get(info.spender.toLowerCase()); return node ? { ...info, nodeId: node.id, nodeFriendlyName: node.friendlyName } : info; }) ); From 30f9e88476668c453c5d5a14d411fc1972f439cb Mon Sep 17 00:00:00 2001 From: andreip136 <129227833+andreip136@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:22:19 +0300 Subject: [PATCH 4/9] remove unused class --- src/components/escrow/escrow-token-panel.module.css | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/escrow/escrow-token-panel.module.css b/src/components/escrow/escrow-token-panel.module.css index 8a3ad566..77b45442 100644 --- a/src/components/escrow/escrow-token-panel.module.css +++ b/src/components/escrow/escrow-token-panel.module.css @@ -154,14 +154,6 @@ gap: 12px; } -.authName { - font-size: 14px; - font-weight: 600; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - .copyRow { display: flex; align-items: center; From 7659a054323735902095143984e4ebd617ce576e Mon Sep 17 00:00:00 2001 From: andreip136 <129227833+andreip136@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:22:59 +0300 Subject: [PATCH 5/9] change endAdornment --- src/components/escrow/authorization-form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/escrow/authorization-form.tsx b/src/components/escrow/authorization-form.tsx index 4e384b50..d2eb5d2a 100644 --- a/src/components/escrow/authorization-form.tsx +++ b/src/components/escrow/authorization-form.tsx @@ -79,7 +79,7 @@ const AuthorizationForm = ({ value={formik.values.maxLockSeconds} /> Date: Fri, 3 Jul 2026 11:39:49 +0300 Subject: [PATCH 6/9] fix comment --- src/components/escrow/escrow-token-panel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/escrow/escrow-token-panel.tsx b/src/components/escrow/escrow-token-panel.tsx index 199596ce..92563b9e 100644 --- a/src/components/escrow/escrow-token-panel.tsx +++ b/src/components/escrow/escrow-token-panel.tsx @@ -249,7 +249,7 @@ const EscrowTokenPanel = ({ token, spenders, loadingSpenders, onChange }: Escrow const onDeposit = () => { const value = Number(amount); - if (!(value > 0)) { + if (value <= 0) { setError('Invalid amount'); return; } @@ -263,7 +263,7 @@ const EscrowTokenPanel = ({ token, spenders, loadingSpenders, onChange }: Escrow const onWithdraw = () => { const value = Number(amount); - if (!(value > 0)) { + if (value <= 0) { setError('Invalid amount'); return; } From 4942d66add872cff0d941c70e0d209ef116b94fe Mon Sep 17 00:00:00 2001 From: andreip136 <129227833+andreip136@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:59:54 +0300 Subject: [PATCH 7/9] add terms operator --- src/lib/use-escrow-data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/use-escrow-data.ts b/src/lib/use-escrow-data.ts index e55d2751..c857538a 100644 --- a/src/lib/use-escrow-data.ts +++ b/src/lib/use-escrow-data.ts @@ -43,7 +43,7 @@ const fetchNodesByWallets = async ( return byWallet; } try { - const filters = { address: { value: wallets.join(',') } }; + const filters = { address: { value: wallets.join(','), operator: 'terms' } }; const res = await axios.get( `${getApiRoute('nodes')}?page=0&size=${wallets.length}&filters=${encodeURIComponent(JSON.stringify(filters))}` ); From f5e54c17e1c034b9d371468eef8d0e92743ab7dd Mon Sep 17 00:00:00 2001 From: andreip136 <129227833+andreip136@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:35:15 +0300 Subject: [PATCH 8/9] add amount placeholder --- src/components/escrow/escrow-token-panel.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/escrow/escrow-token-panel.tsx b/src/components/escrow/escrow-token-panel.tsx index 92563b9e..d4a0aaaf 100644 --- a/src/components/escrow/escrow-token-panel.tsx +++ b/src/components/escrow/escrow-token-panel.tsx @@ -320,6 +320,7 @@ const EscrowTokenPanel = ({ token, spenders, loadingSpenders, onChange }: Escrow setAmount(e.target.value); setError(undefined); }} + placeholder="Amount" size="md" startAdornment={token.symbol} type="number" From c7cff0ceebdf3cc69129e0924e87a9c7eded5400 Mon Sep 17 00:00:00 2001 From: andreip136 <129227833+andreip136@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:52:01 +0300 Subject: [PATCH 9/9] refactor api call --- src/lib/use-escrow-data.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/lib/use-escrow-data.ts b/src/lib/use-escrow-data.ts index c857538a..6d6990c9 100644 --- a/src/lib/use-escrow-data.ts +++ b/src/lib/use-escrow-data.ts @@ -43,9 +43,19 @@ const fetchNodesByWallets = async ( return byWallet; } try { - const filters = { address: { value: wallets.join(','), operator: 'terms' } }; const res = await axios.get( - `${getApiRoute('nodes')}?page=0&size=${wallets.length}&filters=${encodeURIComponent(JSON.stringify(filters))}` + getApiRoute('nodes'), { + params: { + page: 0, + size: wallets.length, + filters: JSON.stringify({ + address: { + value: wallets.join(','), + operator: 'terms' + } + }), + } + } ); const nodes: { _source: Node }[] = res.data?.nodes ?? []; for (const { _source: node } of nodes) {