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 = ({
@@ -207,46 +228,59 @@ 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 (
{/* ── Left: balance + move funds ── */}
- {/* Token header */} -
- {token.symbol} -
+

{token.symbol}

{/* Primary balance */}
@@ -260,7 +294,7 @@ const EscrowTokenPanel = ({ token, spenders, loadingSpenders, onChange }: Escrow {/* Secondary balances */}
- Locked in escrow + Locked for running jobs {formatTokenAmount(token.locked, token.address)}{' '} {token.symbol} @@ -277,75 +311,59 @@ 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); + }} + placeholder="Amount" + 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 + + +
{/* ── 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..6d6990c9 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,48 @@ 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; +}; + +// 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 res = await axios.get( + 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) { + if (node?.id && node.address) { + byWallet.set(node.address.toLowerCase(), { id: node.id, friendlyName: node.friendlyName }); + } + } + } catch (err) { + console.error('Failed to resolve nodes for wallets:', err); + } + return byWallet; }; export type UseEscrowDataReturn = { @@ -102,6 +147,18 @@ 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. 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 = await fetchNodesByWallets(uniqueWallets); + setSpenders((current) => + current.map((info) => { + const node = nodeByWallet.get(info.spender.toLowerCase()); + 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;