From 5032cc1929f202444042d02645ae243368478f5b Mon Sep 17 00:00:00 2001 From: Bogdan Fazakas Date: Thu, 2 Jul 2026 22:05:17 +0300 Subject: [PATCH 1/5] Escrow contract switch --- README.md | 7 +- src/components/escrow/escrow-page.module.css | 17 ++ src/components/escrow/escrow-page.tsx | 30 +- .../escrow/escrow-token-panel.module.css | 6 + src/components/escrow/escrow-token-panel.tsx | 264 ++++++++++-------- src/constants/escrow.ts | 13 + src/lib/ocean-provider.ts | 35 ++- src/lib/use-escrow-data.ts | 36 ++- src/lib/use-withdraw-tokens.ts | 11 +- 9 files changed, 280 insertions(+), 139 deletions(-) create mode 100644 src/constants/escrow.ts diff --git a/README.md b/README.md index 4120f0ea..53170e62 100644 --- a/README.md +++ b/README.md @@ -98,9 +98,10 @@ Create a `.env.local` file (or configure your deployment environment) with the f ### App -| Variable | Description | -| --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `NEXT_PUBLIC_APP_ENV` | `"development"` or `"production"`. Controls which backend URLs are used and which chain tokens are on (ETH Sepolia or BASE). | +| Variable | Description | +| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `NEXT_PUBLIC_APP_ENV` | `"development"` or `"production"`. Controls which backend URLs are used and which chain tokens are on (ETH Sepolia or BASE). | +| `NEXT_PUBLIC_LEGACY_ESCROW_ADDRESS` | Optional. Address of the previous Escrow deployment on Base. When set, the escrow page shows a contract selector so users can view and withdraw funds left in it. Ignored on Sepolia. | ### Alchemy diff --git a/src/components/escrow/escrow-page.module.css b/src/components/escrow/escrow-page.module.css index c81d8aa8..ba6e4966 100644 --- a/src/components/escrow/escrow-page.module.css +++ b/src/components/escrow/escrow-page.module.css @@ -3,6 +3,23 @@ flex-direction: column; } +.contractSelector { + display: flex; + align-items: flex-start; + gap: 12px; + margin-bottom: 16px; +} + +.contractSelect { + width: 340px; + max-width: 100%; +} + +/* Line the chip up with the select control, below its label. */ +.legacyChip { + margin-top: 38px; +} + .panels { display: flex; flex-direction: column; diff --git a/src/components/escrow/escrow-page.tsx b/src/components/escrow/escrow-page.tsx index 547b8078..4426d22c 100644 --- a/src/components/escrow/escrow-page.tsx +++ b/src/components/escrow/escrow-page.tsx @@ -1,13 +1,20 @@ import Container from '@/components/container/container'; import EscrowTokenPanel from '@/components/escrow/escrow-token-panel'; +import Select from '@/components/input/select'; import SectionTitle from '@/components/section-title/section-title'; +import { EscrowContractVersion, LEGACY_ESCROW_ADDRESS } from '@/constants/escrow'; import { useEscrowData } from '@/lib/use-escrow-data'; +import { formatWalletAddress } from '@/utils/formatters'; import { CircularProgress } from '@mui/material'; import classNames from 'classnames'; +import { useState } from 'react'; import styles from './escrow-page.module.css'; const EscrowPage = () => { - const { tokens, spenders, loading, reload } = useEscrowData(); + const [contractVersion, setContractVersion] = useState('current'); + const isLegacy = contractVersion === 'legacy'; + const escrowAddress = isLegacy ? LEGACY_ESCROW_ADDRESS : undefined; + const { tokens, spenders, loading, reload } = useEscrowData(escrowAddress); return ( @@ -17,6 +24,26 @@ const EscrowPage = () => { subTitle="Deposit and withdraw escrow funds, and manage the spending authorizations used to pay for compute jobs." />
+ {LEGACY_ESCROW_ADDRESS && ( +
+ + className={styles.contractSelect} + hint={ + isLegacy + ? 'Viewing the previous escrow deployment. You can withdraw your funds; deposits and authorization changes are disabled.' + : undefined + } + label="Escrow contract" + onChange={(e) => setContractVersion(e.target.value as EscrowContractVersion)} + options={[ + { label: 'Current contract', value: 'current' }, + { label: `Legacy contract (${formatWalletAddress(LEGACY_ESCROW_ADDRESS)})`, value: 'legacy' }, + ]} + value={contractVersion} + /> + {isLegacy && Withdraw only} +
+ )} {loading && tokens.length === 0 ? ( ) : ( @@ -27,6 +54,7 @@ const EscrowPage = () => { ); return ( void; + // Set when the panel shows the legacy escrow deployment: reads and withdrawals target this + // address, and deposits/authorization changes are disabled. + escrowAddress?: string; }; type AmountFormValues = { @@ -41,10 +44,12 @@ const AuthorizationCard = ({ spender, token, onChange, + readOnly, }: { spender: EscrowSpenderInfo; token: EscrowTokenInfo; onChange: () => void; + readOnly?: boolean; }) => { const [isEditOpen, setIsEditOpen] = useState(false); const [isRevokeOpen, setIsRevokeOpen] = useState(false); @@ -67,48 +72,52 @@ const AuthorizationCard = ({
Consumer {formatWalletAddress(spender.spender)}
- setMenuAnchor(menuButtonRef.current)} - ref={menuButtonRef} - size="small" - sx={{ color: 'var(--text-secondary)' }} - > - - - - { - setIsEditOpen(true); - closeMenu(); - }} - > - - - - Edit - - { - setIsRevokeOpen(true); - closeMenu(); - }} - sx={{ color: 'var(--error-darker)' }} - > - - - - Revoke - - + {!readOnly && ( + <> + setMenuAnchor(menuButtonRef.current)} + ref={menuButtonRef} + size="small" + sx={{ color: 'var(--text-secondary)' }} + > + + + + { + setIsEditOpen(true); + closeMenu(); + }} + > + + + + Edit + + { + setIsRevokeOpen(true); + closeMenu(); + }} + sx={{ color: 'var(--error-darker)' }} + > + + + + Revoke + + + + )}
{/* Stats grid */} @@ -181,31 +190,36 @@ const AuthorizationCard = ({ No active locks )} - setIsEditOpen(false)} - onSuccess={() => { - setIsEditOpen(false); - onChange(); - }} - spender={spender} - /> + {!readOnly && ( + <> + setIsEditOpen(false)} + onSuccess={() => { + setIsEditOpen(false); + onChange(); + }} + spender={spender} + /> - setIsRevokeOpen(false)} - onSuccess={() => { - setIsRevokeOpen(false); - onChange(); - }} - spender={spender} - /> + setIsRevokeOpen(false)} + onSuccess={() => { + setIsRevokeOpen(false); + onChange(); + }} + spender={spender} + /> + + )} ); }; -const EscrowTokenPanel = ({ token, spenders, loadingSpenders, onChange }: EscrowTokenPanelProps) => { +const EscrowTokenPanel = ({ token, spenders, loadingSpenders, onChange, escrowAddress }: EscrowTokenPanelProps) => { const [isCreateOpen, setIsCreateOpen] = useState(false); + const isLegacy = !!escrowAddress; const { handleDeposit, isDepositing } = useDepositTokens({ onSuccess: () => { @@ -231,7 +245,8 @@ const EscrowTokenPanel = ({ token, spenders, loadingSpenders, onChange }: Escrow const withdrawForm = useFormik({ initialValues: { amount: '' }, - onSubmit: (values) => handleWithdraw({ tokenAddresses: [token.address], amounts: [values.amount.toString()] }), + onSubmit: (values) => + handleWithdraw({ tokenAddresses: [token.address], amounts: [values.amount.toString()], escrowAddress }), validateOnMount: true, validationSchema: Yup.object({ amount: Yup.number().moreThan(0, 'Invalid amount').max(token.available, 'Exceeds available funds'), @@ -278,35 +293,42 @@ 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} - size="md" - startAdornment={token.symbol} - type="number" - value={depositForm.values.amount} - /> -
+ {isLegacy ? ( + + This is the previous escrow contract — you can only withdraw from it. Deposits go to the current + contract. + + ) : ( +
+ } + 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} + size="md" + startAdornment={token.symbol} + type="number" + value={depositForm.values.amount} + /> +
+ )}
)} - + {!isLegacy && ( + + )}
{loadingSpenders && spenders.length === 0 ? (
@@ -365,33 +389,43 @@ const EscrowTokenPanel = ({ token, spenders, loadingSpenders, onChange }: Escrow
) : spenders.length > 0 ? ( spenders.map((spender) => ( - + )) ) : (
- No authorization yet + {isLegacy ? 'No authorizations' : 'No authorization yet'} - An authorization is created automatically the first time you pay for a compute job with {token.symbol}. + {isLegacy + ? `No ${token.symbol} authorizations were found on the legacy contract.` + : `An authorization is created automatically the first time you pay for a compute job with ${token.symbol}.`}
)} - s.spender)} - isOpen={isCreateOpen} - onClose={() => setIsCreateOpen(false)} - onSuccess={() => { - setIsCreateOpen(false); - onChange(); - }} - tokenAddress={token.address} - tokenSymbol={token.symbol} - /> + {!isLegacy && ( + s.spender)} + isOpen={isCreateOpen} + onClose={() => setIsCreateOpen(false)} + onSuccess={() => { + setIsCreateOpen(false); + onChange(); + }} + tokenAddress={token.address} + tokenSymbol={token.symbol} + /> + )} ); }; diff --git a/src/constants/escrow.ts b/src/constants/escrow.ts new file mode 100644 index 00000000..73697d37 --- /dev/null +++ b/src/constants/escrow.ts @@ -0,0 +1,13 @@ +import { BASE_CHAIN_ID, CHAIN_ID } from '@/constants/chains'; +import { ethers } from 'ethers'; + +export type EscrowContractVersion = 'current' | 'legacy'; + +// The Escrow contract was redeployed on Base, so users may still hold funds in the previous +// deployment. When this address is set, the escrow page offers a contract selector to view and +// withdraw those funds. Legacy access is Base-only: there is no old deployment with user funds +// on Sepolia, so the variable is ignored there. +const legacyEscrowEnv = process.env.NEXT_PUBLIC_LEGACY_ESCROW_ADDRESS; + +export const LEGACY_ESCROW_ADDRESS: string | undefined = + CHAIN_ID === BASE_CHAIN_ID && legacyEscrowEnv && ethers.isAddress(legacyEscrowEnv) ? legacyEscrowEnv : undefined; diff --git a/src/lib/ocean-provider.ts b/src/lib/ocean-provider.ts index ec783f44..cfc1f36d 100644 --- a/src/lib/ocean-provider.ts +++ b/src/lib/ocean-provider.ts @@ -42,7 +42,12 @@ export class OceanProvider { return new BigNumber(number).multipliedBy(new BigNumber(10).pow(decimals)).toFixed(0); } - private async getEscrowContract(chainId: number) { + // `escrowAddress` overrides the deployment from address.json — used by the escrow page to read + // from and withdraw out of the legacy (pre-redeploy) contract. + private async getEscrowContract(chainId: number, escrowAddress?: string) { + if (escrowAddress) { + return new ethers.Contract(escrowAddress, Escrow.abi, this.provider); + } const config = await this.getConfigByChainId(chainId); if (!config.Escrow) { throw new Error('No escrow found for chainId'); @@ -158,12 +163,12 @@ export class OceanProvider { // Returns every authorization the payer has granted for a token, across all payees. // The Escrow contract treats a zero payee address as a wildcard. - async getAllAuthorizations(tokenAddress: string, payer: string): Promise { + async getAllAuthorizations(tokenAddress: string, payer: string, escrowAddress?: string): Promise { if (!ethers.isAddress(tokenAddress) || !ethers.isAddress(payer)) { console.error('Invalid address passed to getAllAuthorizations', { tokenAddress, payer }); return []; } - const escrow = await this.getEscrowContract(this.chainId); + const escrow = await this.getEscrowContract(this.chainId, escrowAddress); const tokenDecimals = await getTokenDecimals(tokenAddress); const authorizations = await escrow.getAuthorizations(tokenAddress, payer, ethers.ZeroAddress); if (!authorizations || authorizations.length === 0) { @@ -188,12 +193,16 @@ export class OceanProvider { return parseFloat(balanceString); } - async getUserFundsDetailed(tokenAddress: string, address: string): Promise<{ available: number; locked: number }> { + async getUserFundsDetailed( + tokenAddress: string, + address: string, + escrowAddress?: string + ): Promise<{ available: number; locked: number }> { if (!ethers.isAddress(tokenAddress) || !ethers.isAddress(address)) { console.error('Invalid address passed to getUserFundsDetailed', { tokenAddress, address }); return { available: 0, locked: 0 }; } - const escrow = await this.getEscrowContract(this.chainId); + const escrow = await this.getEscrowContract(this.chainId, escrowAddress); const funds = await escrow.getUserFunds(address, tokenAddress); const tokenDecimals = await getTokenDecimals(tokenAddress); return { @@ -202,12 +211,12 @@ export class OceanProvider { }; } - async getLocks(tokenAddress: string, payer: string, payee: string): Promise { + async getLocks(tokenAddress: string, payer: string, payee: string, escrowAddress?: string): Promise { if (!ethers.isAddress(tokenAddress) || !ethers.isAddress(payer) || !ethers.isAddress(payee)) { console.error('Invalid address passed to getLocks', { tokenAddress, payer, payee }); return []; } - const escrow = await this.getEscrowContract(this.chainId); + const escrow = await this.getEscrowContract(this.chainId, escrowAddress); const tokenDecimals = await getTokenDecimals(tokenAddress); const locks = await escrow.getLocks(tokenAddress, payer, payee); if (!locks || locks.length === 0) { @@ -271,12 +280,20 @@ export class OceanProvider { return deposit; } - async withdrawTokensEoa({ tokenAddresses, amounts }: { tokenAddresses: string[]; amounts: string[] }): Promise { + async withdrawTokensEoa({ + tokenAddresses, + amounts, + escrowAddress, + }: { + tokenAddresses: string[]; + amounts: string[]; + escrowAddress?: string; + }): Promise { if (!tokenAddresses.every((addr) => ethers.isAddress(addr))) { console.error('Invalid address passed to withdrawTokensEoa', { tokenAddresses }); throw new Error('Invalid address'); } - const escrow = await this.getEscrowContract(this.chainId); + const escrow = await this.getEscrowContract(this.chainId, escrowAddress); const signer = await this.provider.getSigner(); const escrowWithSigner = escrow.connect(signer) as ethers.Contract; const normalizedAmounts = await Promise.all( diff --git a/src/lib/use-escrow-data.ts b/src/lib/use-escrow-data.ts index 576a0383..b984708f 100644 --- a/src/lib/use-escrow-data.ts +++ b/src/lib/use-escrow-data.ts @@ -1,7 +1,7 @@ import { useOceanAccount } from '@/lib/use-ocean-account'; import { getSupportedTokens } from '@/constants/tokens'; import { Authorizations, EscrowLock } from '@/types/payment'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { toast } from 'react-toastify'; export type EscrowTokenInfo = { @@ -31,17 +31,30 @@ export type UseEscrowDataReturn = { reload: () => void; }; -export const useEscrowData = (): UseEscrowDataReturn => { +// `escrowAddress` points the reads at a different escrow deployment than the one in address.json +// (the legacy contract). Omit it for the current deployment. +export const useEscrowData = (escrowAddress?: string): UseEscrowDataReturn => { const { account, ocean } = useOceanAccount(); const [tokens, setTokens] = useState([]); const [spenders, setSpenders] = useState([]); const [loading, setLoading] = useState(false); + // Switching contracts can leave a previous load in flight; only the latest load may set state, + // so data from one contract never lands while another is selected. + const loadIdRef = useRef(0); + // load() reads the selected contract from a ref rather than closing over it, so a reload captured + // before a contract switch (e.g. by an in-flight transaction's onSuccess) refreshes the contract + // selected now instead of resurrecting the old one's data. + const escrowAddressRef = useRef(escrowAddress); const load = useCallback(async () => { + const loadId = ++loadIdRef.current; + const isCurrent = () => loadIdRef.current === loadId; + const contractAddress = escrowAddressRef.current; if (!ocean || !account?.address) { setTokens([]); setSpenders([]); + setLoading(false); return; } setLoading(true); @@ -51,7 +64,7 @@ export const useEscrowData = (): UseEscrowDataReturn => { const tokenInfos = await Promise.all( tokenList.map(async (token) => { - const funds = await ocean.getUserFundsDetailed(token.address, account.address!); + const funds = await ocean.getUserFundsDetailed(token.address, account.address!, contractAddress); const walletBalance = Number(await ocean.getBalance(token.address, account.address!)); return { symbol: token.symbol, @@ -62,6 +75,7 @@ export const useEscrowData = (): UseEscrowDataReturn => { }; }) ); + if (!isCurrent()) return; setTokens(tokenInfos); // List spenders (payees) straight from the Escrow contract: `getAllAuthorizations` queries @@ -69,13 +83,13 @@ export const useEscrowData = (): UseEscrowDataReturn => { // event indexing needed, so this works on any chain (including sepolia). const settled = await Promise.allSettled( tokenList.map(async (token) => { - const allAuthorizations = await ocean.getAllAuthorizations(token.address, account.address!); + const allAuthorizations = await ocean.getAllAuthorizations(token.address, account.address!, contractAddress); return Promise.all( allAuthorizations.map(async (authorizations) => { const spender = authorizations.payee; // Contract getLocks filters payer/token with OR, so it leaks other payers' locks and // other tokens. Narrow to this user's locks for this token client-side. - const allLocks = await ocean.getLocks(token.address, account.address!, spender); + const allLocks = await ocean.getLocks(token.address, account.address!, spender, contractAddress); const locks = allLocks.filter( (lock) => lock.payer.toLowerCase() === account.address!.toLowerCase() && @@ -92,6 +106,7 @@ export const useEscrowData = (): UseEscrowDataReturn => { ); }) ); + if (!isCurrent()) return; const failCount = settled.filter((r) => r.status === 'rejected').length; if (failCount > 0) { toast.warning(`Failed to load authorization data for ${failCount} token${failCount > 1 ? 's' : ''}`); @@ -105,13 +120,20 @@ export const useEscrowData = (): UseEscrowDataReturn => { } catch (err) { console.error('Failed to load escrow data:', err); } finally { - setLoading(false); + if (isCurrent()) { + setLoading(false); + } } }, [ocean, account?.address]); + // On contract switch (and initial mount): clear the previous contract's data so a loader shows + // instead of the other contract's balances, then fetch from the newly selected contract. useEffect(() => { + escrowAddressRef.current = escrowAddress; + setTokens([]); + setSpenders([]); load(); - }, [load]); + }, [escrowAddress, load]); return { tokens, spenders, loading, reload: load }; }; diff --git a/src/lib/use-withdraw-tokens.ts b/src/lib/use-withdraw-tokens.ts index 34a5bd2a..23da4b4d 100644 --- a/src/lib/use-withdraw-tokens.ts +++ b/src/lib/use-withdraw-tokens.ts @@ -12,6 +12,8 @@ import { encodeFunctionData } from 'viem'; export interface WithdrawTokensParams { tokenAddresses: string[]; amounts: string[]; + // Withdraw from this escrow deployment instead of the one in address.json (legacy contract). + escrowAddress?: string; } export interface UseWithdrawTokensParams { @@ -34,12 +36,12 @@ export const useWithdrawTokens = ({ onSuccess }: UseWithdrawTokensParams = {}): const chainId = CHAIN_ID; const handleWithdraw = useCallback( - async ({ tokenAddresses, amounts }: WithdrawTokensParams) => { + async ({ tokenAddresses, amounts, escrowAddress }: WithdrawTokensParams) => { if (user?.type === 'eoa') { try { setIsWithdrawing(true); if (!ocean) return; - const tx = await ocean.withdrawTokensEoa({ tokenAddresses, amounts }); + const tx = await ocean.withdrawTokensEoa({ tokenAddresses, amounts, escrowAddress }); await tx.wait(); setIsWithdrawing(false); setError(undefined); @@ -62,7 +64,8 @@ export const useWithdrawTokens = ({ onSuccess }: UseWithdrawTokensParams = {}): setError(undefined); const config = Object.values(Address).find((chainConfig: any) => chainConfig.chainId === chainId); - if (!config || !(config as any).Escrow) { + const escrowTarget = escrowAddress ?? (config as any)?.Escrow; + if (!escrowTarget) { throw new Error('No escrow found for chainId'); } @@ -81,7 +84,7 @@ export const useWithdrawTokens = ({ onSuccess }: UseWithdrawTokensParams = {}): }); await sendTransaction({ - to: (config as any).Escrow as `0x${string}`, + to: escrowTarget as `0x${string}`, data: data as `0x${string}`, }); From d61ab2c044b8f06d40fd6eafefa827ae1597e228 Mon Sep 17 00:00:00 2001 From: Bogdan Fazakas Date: Fri, 3 Jul 2026 15:01:37 +0300 Subject: [PATCH 2/5] add alert --- src/components/homepage/hero-section.tsx | 2 + .../homepage/legacy-escrow-banner.module.css | 43 ++++++++++++++ .../homepage/legacy-escrow-banner.tsx | 57 +++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 src/components/homepage/legacy-escrow-banner.module.css create mode 100644 src/components/homepage/legacy-escrow-banner.tsx diff --git a/src/components/homepage/hero-section.tsx b/src/components/homepage/hero-section.tsx index 0091cce6..4a8f38e8 100644 --- a/src/components/homepage/hero-section.tsx +++ b/src/components/homepage/hero-section.tsx @@ -5,6 +5,7 @@ import { TransitionGroup } from 'react-transition-group'; import Button from '../button/button'; import Container from '../container/container'; import styles from './hero-section.module.css'; +import LegacyEscrowBanner from './legacy-escrow-banner'; const videoSrc = '/hero.mp4'; // const posterSrc = '/hero.jpg'; @@ -34,6 +35,7 @@ export default function HeroSection() { src={videoSrc} /> +

Global
diff --git a/src/components/homepage/legacy-escrow-banner.module.css b/src/components/homepage/legacy-escrow-banner.module.css new file mode 100644 index 00000000..390b6277 --- /dev/null +++ b/src/components/homepage/legacy-escrow-banner.module.css @@ -0,0 +1,43 @@ +.banner { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 12px; + background: var(--background-glass); + backdrop-filter: var(--backdrop-filter-glass); + /* The hero pads its top for the nav overlap (see hero-section.module.css); pull the banner up + through most of that padding so it sits just below the header. Breakpoints mirror the hero's. */ + margin-top: -120px; + margin-bottom: 48px; +} + +@media (max-width: 992px) { + .banner { + margin-top: -90px; + } +} + +@media (max-width: 768px) { + .banner { + margin-top: -80px; + } +} + +.icon { + color: var(--warning-darker); + flex-shrink: 0; +} + +.message { + flex: 1; + min-width: 240px; + font-size: 14px; + line-height: 1.5; +} + +.actions { + display: flex; + align-items: center; + gap: 4px; + margin-left: auto; +} diff --git a/src/components/homepage/legacy-escrow-banner.tsx b/src/components/homepage/legacy-escrow-banner.tsx new file mode 100644 index 00000000..c7a513d1 --- /dev/null +++ b/src/components/homepage/legacy-escrow-banner.tsx @@ -0,0 +1,57 @@ +import Button from '@/components/button/button'; +import Card from '@/components/card/card'; +import { LEGACY_ESCROW_ADDRESS } from '@/constants/escrow'; +import CloseIcon from '@mui/icons-material/Close'; +import ErrorOutlinedIcon from '@mui/icons-material/ErrorOutlined'; +import { IconButton } from '@mui/material'; +import { useEffect, useState } from 'react'; +import styles from './legacy-escrow-banner.module.css'; + +const DISMISSED_KEY = 'legacyEscrowBannerDismissed'; + +// Announces the escrow contract redeployment and points users with funds left in the old +// deployment to the legacy-contract withdraw flow on the escrow page. Renders only while a legacy +// address is configured, and stays hidden once dismissed. +const LegacyEscrowBanner = () => { + // Start hidden and reveal after mount, so the server render matches the first client render and + // the dismissed flag is only read on the client. + const [visible, setVisible] = useState(false); + + useEffect(() => { + setVisible(localStorage.getItem(DISMISSED_KEY) !== 'true'); + }, []); + + if (!LEGACY_ESCROW_ADDRESS || !visible) { + return null; + } + + const dismiss = () => { + localStorage.setItem(DISMISSED_KEY, 'true'); + setVisible(false); + }; + + return ( + + + + A new version of the Ocean contracts was deployed. If you have funds in the old escrow contract, open the + Manage escrow page, select the legacy contract and withdraw your funds. + +
+ + + + +
+
+ ); +}; + +export default LegacyEscrowBanner; From 1d4f3ad2fdca65d23ee7998f929f2547a615104f Mon Sep 17 00:00:00 2001 From: andreip136 <129227833+andreip136@users.noreply.github.com> Date: Mon, 6 Jul 2026 18:53:40 +0300 Subject: [PATCH 3/5] update style --- src/components/escrow/escrow-page.tsx | 2 + .../homepage/hero-section.module.css | 15 +++++ src/components/homepage/hero-section.tsx | 2 +- .../homepage/legacy-escrow-banner.module.css | 60 +++++++------------ .../homepage/legacy-escrow-banner.tsx | 40 +++++++------ 5 files changed, 63 insertions(+), 56 deletions(-) diff --git a/src/components/escrow/escrow-page.tsx b/src/components/escrow/escrow-page.tsx index 4426d22c..9f43c7d6 100644 --- a/src/components/escrow/escrow-page.tsx +++ b/src/components/escrow/escrow-page.tsx @@ -1,5 +1,6 @@ import Container from '@/components/container/container'; import EscrowTokenPanel from '@/components/escrow/escrow-token-panel'; +import LegacyEscrowBanner from '@/components/homepage/legacy-escrow-banner'; import Select from '@/components/input/select'; import SectionTitle from '@/components/section-title/section-title'; import { EscrowContractVersion, LEGACY_ESCROW_ADDRESS } from '@/constants/escrow'; @@ -24,6 +25,7 @@ const EscrowPage = () => { subTitle="Deposit and withdraw escrow funds, and manage the spending authorizations used to pay for compute jobs." />
+ {LEGACY_ESCROW_ADDRESS && (
diff --git a/src/components/homepage/hero-section.module.css b/src/components/homepage/hero-section.module.css index dd506783..98841c23 100644 --- a/src/components/homepage/hero-section.module.css +++ b/src/components/homepage/hero-section.module.css @@ -69,6 +69,21 @@ color: #d54335; } +.legacyEscrowBanner { + /* The hero pads its top for the nav overlap (see hero-section.module.css); pull the banner up + through most of that padding so it sits just below the header. Breakpoints mirror the hero's. */ + margin-top: -80px; + margin-bottom: 48px; + + @media (min-width: 768px) { + margin-top: -90px; + } + + @media (min-width: 992px) { + margin-top: -120px; + } +} + @media (max-width: 1200px) { .title { font-size: 72px; diff --git a/src/components/homepage/hero-section.tsx b/src/components/homepage/hero-section.tsx index 4a8f38e8..05eee2c6 100644 --- a/src/components/homepage/hero-section.tsx +++ b/src/components/homepage/hero-section.tsx @@ -35,7 +35,7 @@ export default function HeroSection() { src={videoSrc} /> - +

Global
diff --git a/src/components/homepage/legacy-escrow-banner.module.css b/src/components/homepage/legacy-escrow-banner.module.css index 390b6277..e490c8f0 100644 --- a/src/components/homepage/legacy-escrow-banner.module.css +++ b/src/components/homepage/legacy-escrow-banner.module.css @@ -1,43 +1,27 @@ -.banner { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 12px; - background: var(--background-glass); - backdrop-filter: var(--backdrop-filter-glass); - /* The hero pads its top for the nav overlap (see hero-section.module.css); pull the banner up - through most of that padding so it sits just below the header. Breakpoints mirror the hero's. */ - margin-top: -120px; - margin-bottom: 48px; -} +.root { + padding: 16px; + text-align: left; -@media (max-width: 992px) { - .banner { - margin-top: -90px; - } -} + .row { + display: flex; + align-items: flex-start; + gap: 8px; -@media (max-width: 768px) { - .banner { - margin-top: -80px; - } -} + .icon { + flex-shrink: 0; + } -.icon { - color: var(--warning-darker); - flex-shrink: 0; -} - -.message { - flex: 1; - min-width: 240px; - font-size: 14px; - line-height: 1.5; -} + .message { + flex: 1; + min-width: 240px; + font-size: 14px; + line-height: 1.5; + } -.actions { - display: flex; - align-items: center; - gap: 4px; - margin-left: auto; + .close { + position: relative; + top: -4px; + right: -4px; + } + } } diff --git a/src/components/homepage/legacy-escrow-banner.tsx b/src/components/homepage/legacy-escrow-banner.tsx index c7a513d1..ccdbf779 100644 --- a/src/components/homepage/legacy-escrow-banner.tsx +++ b/src/components/homepage/legacy-escrow-banner.tsx @@ -4,6 +4,7 @@ import { LEGACY_ESCROW_ADDRESS } from '@/constants/escrow'; import CloseIcon from '@mui/icons-material/Close'; import ErrorOutlinedIcon from '@mui/icons-material/ErrorOutlined'; import { IconButton } from '@mui/material'; +import classNames from 'classnames'; import { useEffect, useState } from 'react'; import styles from './legacy-escrow-banner.module.css'; @@ -12,7 +13,11 @@ const DISMISSED_KEY = 'legacyEscrowBannerDismissed'; // Announces the escrow contract redeployment and points users with funds left in the old // deployment to the legacy-contract withdraw flow on the escrow page. Renders only while a legacy // address is configured, and stays hidden once dismissed. -const LegacyEscrowBanner = () => { +const LegacyEscrowBanner: React.FC<{ + className?: string; + /** Whether to show/mention the link to the escrow page */ + escrowPageLink?: boolean; +}> = ({ className, escrowPageLink }) => { // Start hidden and reveal after mount, so the server render matches the first client render and // the dismissed flag is only read on the client. const [visible, setVisible] = useState(false); @@ -31,25 +36,26 @@ const LegacyEscrowBanner = () => { }; return ( - - - - A new version of the Ocean contracts was deployed. If you have funds in the old escrow contract, open the - Manage escrow page, select the legacy contract and withdraw your funds. - -
- - + +
+ + + A new version of the Ocean contracts was deployed. +
+ If you have funds in the old escrow contract, {escrowPageLink ? 'open the Manage escrow page,' : null} select + the legacy contract and withdraw your funds. +
+
+ {escrowPageLink ? ( +
+ +
+ ) : null}
); }; From 20bf262ebb6e4b4b522ba44fb25f83bcbce4d346 Mon Sep 17 00:00:00 2001 From: andreip136 <129227833+andreip136@users.noreply.github.com> Date: Mon, 6 Jul 2026 19:02:44 +0300 Subject: [PATCH 4/5] update style --- src/components/escrow/escrow-page.module.css | 7 +++- src/components/escrow/escrow-page.tsx | 41 +++++++++++--------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/components/escrow/escrow-page.module.css b/src/components/escrow/escrow-page.module.css index ba6e4966..7703736b 100644 --- a/src/components/escrow/escrow-page.module.css +++ b/src/components/escrow/escrow-page.module.css @@ -11,10 +11,15 @@ } .contractSelect { - width: 340px; + width: 350px; max-width: 100%; } +.hint { + color: var(--text-secondary); + padding: 0 16px; +} + /* Line the chip up with the select control, below its label. */ .legacyChip { margin-top: 38px; diff --git a/src/components/escrow/escrow-page.tsx b/src/components/escrow/escrow-page.tsx index 9f43c7d6..f8a08b46 100644 --- a/src/components/escrow/escrow-page.tsx +++ b/src/components/escrow/escrow-page.tsx @@ -1,3 +1,4 @@ +import Card from '@/components/card/card'; import Container from '@/components/container/container'; import EscrowTokenPanel from '@/components/escrow/escrow-token-panel'; import LegacyEscrowBanner from '@/components/homepage/legacy-escrow-banner'; @@ -27,24 +28,28 @@ const EscrowPage = () => {
{LEGACY_ESCROW_ADDRESS && ( -
- - className={styles.contractSelect} - hint={ - isLegacy - ? 'Viewing the previous escrow deployment. You can withdraw your funds; deposits and authorization changes are disabled.' - : undefined - } - label="Escrow contract" - onChange={(e) => setContractVersion(e.target.value as EscrowContractVersion)} - options={[ - { label: 'Current contract', value: 'current' }, - { label: `Legacy contract (${formatWalletAddress(LEGACY_ESCROW_ADDRESS)})`, value: 'legacy' }, - ]} - value={contractVersion} - /> - {isLegacy && Withdraw only} -
+ +
+ + className={styles.contractSelect} + label="Escrow contract" + onChange={(e) => setContractVersion(e.target.value as EscrowContractVersion)} + options={[ + { label: 'Current contract', value: 'current' }, + { label: `Legacy contract (${formatWalletAddress(LEGACY_ESCROW_ADDRESS)})`, value: 'legacy' }, + ]} + value={contractVersion} + /> + {isLegacy && Withdraw only} +
+ {isLegacy ? ( +
+ Viewing the previous escrow deployment. +
+ You can withdraw your funds; deposits and authorization changes are disabled. +
+ ) : undefined} +
)} {loading && tokens.length === 0 ? ( From 9524f71b88b3e1b238396157077fad9868f5a06a Mon Sep 17 00:00:00 2001 From: Bogdan Fazakas Date: Tue, 7 Jul 2026 14:56:42 +0300 Subject: [PATCH 5/5] fixed suggestions review --- .../escrow/escrow-token-panel.module.css | 19 +++++++++++++++++++ src/components/escrow/escrow-token-panel.tsx | 4 ++-- .../homepage/legacy-escrow-banner.tsx | 13 +++++++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/components/escrow/escrow-token-panel.module.css b/src/components/escrow/escrow-token-panel.module.css index 9e69fbd8..13566297 100644 --- a/src/components/escrow/escrow-token-panel.module.css +++ b/src/components/escrow/escrow-token-panel.module.css @@ -102,6 +102,25 @@ .fundInput { width: 100%; min-width: 0; + + /* Solid background instead of the translucent glass default, so the field reads as editable + rather than disabled. */ + :global(.MuiTextField-root) { + background: var(--background-primary); + } +} + +/* Token-symbol prefix, separated from the value area so it doesn't read as a pre-filled value. */ +.fundInputSymbol { + align-self: stretch; + display: flex; + align-items: center; + padding-right: 12px; + border-right: 1px solid var(--border); + color: var(--text-secondary); + font-size: 14px; + font-weight: 600; + white-space: nowrap; } .fundButtons { diff --git a/src/components/escrow/escrow-token-panel.tsx b/src/components/escrow/escrow-token-panel.tsx index 1c3762d0..8699d583 100644 --- a/src/components/escrow/escrow-token-panel.tsx +++ b/src/components/escrow/escrow-token-panel.tsx @@ -340,9 +340,9 @@ const EscrowTokenPanel = ({ token, spenders, loadingSpenders, onChange, escrowAd setAmount(e.target.value); setError(undefined); }} - placeholder="Amount" + placeholder="Enter amount" size="md" - startAdornment={token.symbol} + startAdornment={{token.symbol}} type="number" value={amount} /> diff --git a/src/components/homepage/legacy-escrow-banner.tsx b/src/components/homepage/legacy-escrow-banner.tsx index ccdbf779..cc30d080 100644 --- a/src/components/homepage/legacy-escrow-banner.tsx +++ b/src/components/homepage/legacy-escrow-banner.tsx @@ -23,7 +23,12 @@ const LegacyEscrowBanner: React.FC<{ const [visible, setVisible] = useState(false); useEffect(() => { - setVisible(localStorage.getItem(DISMISSED_KEY) !== 'true'); + // localStorage can throw (Safari private mode, restricted iframes) — fall back to showing it. + try { + setVisible(localStorage.getItem(DISMISSED_KEY) !== 'true'); + } catch { + setVisible(true); + } }, []); if (!LEGACY_ESCROW_ADDRESS || !visible) { @@ -31,7 +36,11 @@ const LegacyEscrowBanner: React.FC<{ } const dismiss = () => { - localStorage.setItem(DISMISSED_KEY, 'true'); + try { + localStorage.setItem(DISMISSED_KEY, 'true'); + } catch { + // Persistence is best-effort; still hide the banner for this session. + } setVisible(false); };