diff --git a/.claude/settings.local.json b/.claude/settings.local.json index dcd3a34..d7c87bd 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -19,7 +19,14 @@ "Bash(bun x:*)", "Bash(git commit -m ':*)", "Bash(git push:*)", - "Bash(gh pr:*)" + "Bash(gh pr:*)", + "Bash(sed -i \"57s/title='Pool Registry'/title='No Pools Loaded'/\" ui/ts/components/SecurityPoolsOverviewSection.tsx)", + "Bash(sed -i \"65s|title={}|title={getQuestionTitle\\(pool.marketDetails\\)}|\" ui/ts/components/SecurityPoolsOverviewSection.tsx)", + "Bash(echo \"EXIT:$?\")", + "WebFetch(domain:docs.uniswap.org)", + "Bash(bunx:*)", + "WebFetch(domain:app.uniswap.org)", + "WebFetch(domain:gateway.thegraph.com)" ] } } diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/AGENTS.md b/AGENTS.md index f2b031f..7e01305 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,15 +11,20 @@ Run each command individually and address any issues before proceeding to the ne bun tsc ``` - **UI-only exception**: If the change only affects `ui/` and does not touch `solidity/`, generated contract artifacts, or anything that depends on refreshed contract output, run: - ```bash - bun x tsc --noEmit - ``` + ```bash + bun x tsc + ``` + (We emit JS even for UI-only changes so the live-reload watcher notices the rebuild.) - **Contract exception**: If contracts or generated contract outputs changed, run the full `bun tsc`. 2. **Tests**: Run all tests ```bash bun test ``` + - If tests require Anvil and the `anvil` executable is missing, install it with: + ```bash + bun run install:anvil + ``` 3. **Code formatting**: ```bash @@ -84,7 +89,7 @@ This test-driven approach ensures: ## Notes -- This is a TypeScript project. Do not inspect or work from `js/` files anywhere in the repository when there is a corresponding TypeScript source file. -- The project compiles TypeScript sources to JavaScript before testing (`bun tsc`). This happens automatically via the test scripts. -- The root `bun tsc` runs the full generation pipeline before type-checking. Use `bun x tsc --noEmit` only for `ui/`-only changes that do not require regenerated contract outputs. -- Never edit files directly in any `js/` directory. Changes may be overwritten by TypeScript compilation. Always use the corresponding `.ts` or `.tsx` source files. +- - This is a TypeScript project. Do not inspect or work from `js/` files anywhere in the repository when there is a corresponding TypeScript source file. +- - The project compiles TypeScript sources to JavaScript before testing (`bun tsc`). This happens automatically via the test scripts. +- - The root `bun tsc` runs the full generation pipeline before type-checking. UI-only changes still need to emit JS so the watcher reloads correctly; use `bun x tsc` instead of `--noEmit`. +- - Never edit files directly in any `js/` directory. Changes may be overwritten by TypeScript compilation. Always use the corresponding `.ts` or `.tsx` source files. diff --git a/solidity/ts/testsuite/simulator/AnvilWindowEthereum.ts b/solidity/ts/testsuite/simulator/AnvilWindowEthereum.ts index bbeda45..ec39e0f 100644 --- a/solidity/ts/testsuite/simulator/AnvilWindowEthereum.ts +++ b/solidity/ts/testsuite/simulator/AnvilWindowEthereum.ts @@ -56,22 +56,6 @@ function parseSnapshotId(value: unknown) { return value } -function parseBlock(value: unknown): GetBlockReturn { - if (typeof value !== 'object' || value === null) { - throw new Error('Invalid eth_getBlockByNumber response: block is not an object') - } - if (!('timestamp' in value) || typeof value.timestamp !== 'string') { - throw new Error('Invalid eth_getBlockByNumber response: missing timestamp') - } - if (!/^0x([a-fA-F0-9]{1,64})$/.test(value.timestamp)) { - throw new Error(`Invalid eth_getBlockByNumber response: invalid timestamp ${value.timestamp}`) - } - - return { - timestamp: BigInt(value.timestamp), - } -} - export interface AnvilWindowEthereum { addStateOverrides: (stateOverrides: StateOverrides) => Promise manipulateTime: (blockTimeManipulation: BlockTimeManipulation) => Promise @@ -94,6 +78,8 @@ export const getDefaultAnvilRpcUrl = (): string => (process.platform === 'win32' export const getMockedEthSimulateWindowEthereum = async (rpcUrl?: string): Promise => { const ANVIL_RPC = rpcUrl ?? process.env['ANVIL_RPC'] ?? getDefaultAnvilRpcUrl() + let currentTimestamp = 0n + let snapshotTimestamp = 0n // Validate RPC endpoint points to localhost only for test security const validateLocalhostUrl = (url: string): void => { @@ -115,6 +101,7 @@ export const getMockedEthSimulateWindowEthereum = async (rpcUrl?: string): Promi // Make JSON-RPC request to Anvil let requestId = 0 const request = async (args: { method: string; params?: unknown[] | unknown | undefined }): Promise => { + let nextBlockTimestamp: bigint | undefined // For eth_sendTransaction, simulate first to catch reverts early const params = ensureArray(args.params) if (args.method === 'eth_sendTransaction' && params[0]) { @@ -126,10 +113,10 @@ export const getMockedEthSimulateWindowEthereum = async (rpcUrl?: string): Promi throw simulationError } - const latestBlock = parseBlock(await request({ method: 'eth_getBlockByNumber', params: ['latest', false] })) + nextBlockTimestamp = currentTimestamp + 1n await request({ method: 'evm_setNextBlockTimestamp', - params: [`0x${(latestBlock.timestamp + 1n).toString(16)}`], + params: [`0x${nextBlockTimestamp.toString(16)}`], }) } @@ -164,6 +151,9 @@ export const getMockedEthSimulateWindowEthereum = async (rpcUrl?: string): Promi // For eth_getTransactionReceipt, return the receipt even if status === '0x0' (reverted) // Callers can check the status field themselves ensureDefined(json.result, 'json.result is undefined') + if (nextBlockTimestamp !== undefined) { + currentTimestamp = nextBlockTimestamp + } return json.result } @@ -218,6 +208,7 @@ export const getMockedEthSimulateWindowEthereum = async (rpcUrl?: string): Promi params: [`0x${blockTimeManipulation.deltaToAdd.toString(16)}`], }) await request({ method: 'evm_mine', params: [] }) + currentTimestamp += blockTimeManipulation.deltaToAdd } else if (blockTimeManipulation.type === 'SetTimestamp') { const hexTimestamp = `0x${blockTimeManipulation.timeToSet.toString(16)}` try { @@ -236,17 +227,16 @@ export const getMockedEthSimulateWindowEthereum = async (rpcUrl?: string): Promi }) } await request({ method: 'evm_mine', params: [] }) + currentTimestamp = blockTimeManipulation.timeToSet } } const getTime = async (): Promise => { - const block = await getBlock() - return block.timestamp + return currentTimestamp } const getBlock = async (): Promise => { - const raw = await request({ method: 'eth_getBlockByNumber', params: ['latest', false] }) - return parseBlock(raw) + return { timestamp: currentTimestamp } } const advanceTime = async (amountInSeconds: bigint) => { @@ -275,17 +265,21 @@ export const getMockedEthSimulateWindowEthereum = async (rpcUrl?: string): Promi } const anvilSnapshot = async (): Promise => { + snapshotTimestamp = currentTimestamp const result = await request({ method: 'anvil_snapshot', params: [] }) return parseSnapshotId(result) } const anvilRevert = async (snapshotId: string): Promise => { await request({ method: 'anvil_revert', params: [snapshotId] }) + currentTimestamp = snapshotTimestamp } const resetToCleanState = async (): Promise => { await request({ method: 'anvil_reset', params: [] }) await request({ method: 'anvil_setNextBlockBaseFeePerGas', params: ['0x0'] }) + currentTimestamp = 0n + snapshotTimestamp = 0n } const setNextBlockBaseFeePerGasToZero = async (): Promise => { diff --git a/solidity/ts/testsuite/simulator/useIsolatedAnvilNode.ts b/solidity/ts/testsuite/simulator/useIsolatedAnvilNode.ts index 7d7e513..67f6b03 100644 --- a/solidity/ts/testsuite/simulator/useIsolatedAnvilNode.ts +++ b/solidity/ts/testsuite/simulator/useIsolatedAnvilNode.ts @@ -214,7 +214,13 @@ export const useIsolatedAnvilNode = () => { beforeEach(async () => { const currentEthereum = ensureDefined(anvilWindowEthereum, 'Isolated Anvil node was not initialized') const currentSnapshotId = ensureDefined(snapshotId, 'Missing Anvil snapshot for test isolation') - await currentEthereum.anvilRevert(currentSnapshotId) + try { + await currentEthereum.anvilRevert(currentSnapshotId) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + if (!errorMessage.includes('Resource not found')) throw error + await currentEthereum.resetToCleanState() + } await currentEthereum.setNextBlockBaseFeePerGasToZero() snapshotId = await currentEthereum.anvilSnapshot() }) diff --git a/ui/build/watch.mts b/ui/build/watch.mts index 7d77c2b..b224dec 100644 --- a/ui/build/watch.mts +++ b/ui/build/watch.mts @@ -9,6 +9,8 @@ const UI_ROOT_PATH = path.join(directoryOfThisFile, '..') const REPOSITORY_ROOT_PATH = path.join(UI_ROOT_PATH, '..') const DEV_SERVER_PATH = path.join(UI_ROOT_PATH, 'dev-server.mjs') const INDEX_HTML_PATH = path.join(UI_ROOT_PATH, 'index.html') +const TYPE_SCRIPT_OUTPUT_PATH = path.join(UI_ROOT_PATH, 'js') +const TYPE_SCRIPT_SOURCE_PATH = path.join(UI_ROOT_PATH, 'ts') const VENDOR_INPUT_PATHS = [path.join(UI_ROOT_PATH, 'build', 'vendor.mts'), path.join(UI_ROOT_PATH, 'package.json'), path.join(UI_ROOT_PATH, 'tsconfig.vendor.json'), path.join(UI_ROOT_PATH, 'bun.lock')] const LIVE_RELOAD_ENDPOINT = 'http://127.0.0.1:12345/__live-reload' @@ -23,9 +25,10 @@ let vendorBuildRunning = false let vendorBuildQueued = false let liveReloadQueued = false let liveReloadTimeout: NodeJS.Timeout | undefined -let typeScriptWatchStdoutBuffer = '' const unwatchCallbacks: Array<() => void> = [] +let typeScriptOutputUnwatchCallbacks: Array<() => void> = [] +let typeScriptSourceUnwatchCallbacks: Array<() => void> = [] const waitForProcessExit = async (childProcess: ManagedProcess) => { return await new Promise<{ exitCode: number | null; signalCode: NodeJS.Signals | null }>((resolve, reject) => { @@ -101,6 +104,16 @@ const getAllFiles = async (dirPath: string, fileList: string[] = []) => { return fileList } +const getAllDirectories = async (dirPath: string, directoryList: string[] = []) => { + directoryList.push(dirPath) + const entries = await fs.promises.readdir(dirPath, { withFileTypes: true }) + for (const entry of entries) { + if (!entry.isDirectory()) continue + await getAllDirectories(path.join(dirPath, entry.name), directoryList) + } + return directoryList +} + const queueLiveReload = (reason: string) => { if (shuttingDown) return if (liveReloadTimeout !== undefined) clearTimeout(liveReloadTimeout) @@ -117,6 +130,7 @@ const sendLiveReload = async (reason: string) => { liveReloadQueued = false try { await fetch(`${LIVE_RELOAD_ENDPOINT}?reason=${encodeURIComponent(reason)}`, { method: 'POST' }) + console.log(`[ui:watch] Reload requested (${reason})`) } catch (error) { console.error(`[ui:watch] Failed to signal browser reload because ${reason} changed`) console.error(error) @@ -146,24 +160,121 @@ const spawnServer = () => { } const onTypeScriptWatchStdout = (chunk: Buffer) => { - const output = chunk.toString('utf8') - process.stdout.write(output) - typeScriptWatchStdoutBuffer += output - let newlineIndex = typeScriptWatchStdoutBuffer.indexOf('\n') - while (newlineIndex !== -1) { - const line = typeScriptWatchStdoutBuffer.slice(0, newlineIndex).trim() - typeScriptWatchStdoutBuffer = typeScriptWatchStdoutBuffer.slice(newlineIndex + 1) - if (line.includes('Found 0 errors')) { - queueLiveReload('TypeScript rebuild') - } - newlineIndex = typeScriptWatchStdoutBuffer.indexOf('\n') - } + process.stdout.write(chunk) } const onTypeScriptWatchStderr = (chunk: Buffer) => { process.stderr.write(chunk) } +const watchFileWithCleanup = (filePath: string, onChange: (relativePath: string) => void, registerUnwatch: (callback: () => void) => void) => { + let debounceTimeout: NodeJS.Timeout | undefined + const listener = (currentStat: fs.Stats, previousStat: fs.Stats) => { + if (currentStat.mtimeMs === previousStat.mtimeMs) return + if (debounceTimeout !== undefined) clearTimeout(debounceTimeout) + debounceTimeout = setTimeout(() => { + debounceTimeout = undefined + const relativePath = path.relative(UI_ROOT_PATH, filePath).replaceAll('\\', '/') + onChange(relativePath) + }, 120) + } + fs.watchFile(filePath, { interval: 250 }, listener) + registerUnwatch(() => { + if (debounceTimeout !== undefined) clearTimeout(debounceTimeout) + fs.unwatchFile(filePath, listener) + }) +} + +const clearTypeScriptOutputWatchers = () => { + for (const unwatch of typeScriptOutputUnwatchCallbacks) { + unwatch() + } + typeScriptOutputUnwatchCallbacks = [] +} + +const clearTypeScriptSourceWatchers = () => { + for (const unwatch of typeScriptSourceUnwatchCallbacks) { + unwatch() + } + typeScriptSourceUnwatchCallbacks = [] +} + +const watchDirectoryForTypeScriptOutputs = (directoryPath: string, refreshWatchers: () => void) => { + let debounceTimeout: NodeJS.Timeout | undefined + const watcher = fs.watch(directoryPath, (_eventType, filename) => { + if (debounceTimeout !== undefined) clearTimeout(debounceTimeout) + debounceTimeout = setTimeout(() => { + debounceTimeout = undefined + refreshWatchers() + const changedPath = typeof filename === 'string' && filename.length > 0 ? path.join(directoryPath, filename) : directoryPath + queueLiveReload(path.relative(UI_ROOT_PATH, changedPath).replaceAll('\\', '/')) + }, 120) + }) + typeScriptOutputUnwatchCallbacks.push(() => { + if (debounceTimeout !== undefined) clearTimeout(debounceTimeout) + watcher.close() + }) +} + +const watchDirectoryForTypeScriptSources = (directoryPath: string, refreshWatchers: () => void) => { + let debounceTimeout: NodeJS.Timeout | undefined + const watcher = fs.watch(directoryPath, (_eventType, _filename) => { + if (debounceTimeout !== undefined) clearTimeout(debounceTimeout) + debounceTimeout = setTimeout(() => { + debounceTimeout = undefined + refreshWatchers() + }, 120) + }) + typeScriptSourceUnwatchCallbacks.push(() => { + if (debounceTimeout !== undefined) clearTimeout(debounceTimeout) + watcher.close() + }) +} + +const refreshTypeScriptOutputWatchers = async () => { + clearTypeScriptOutputWatchers() + const directories = await getAllDirectories(TYPE_SCRIPT_OUTPUT_PATH) + for (const directoryPath of directories) { + watchDirectoryForTypeScriptOutputs(directoryPath, () => { + void refreshTypeScriptOutputWatchers() + }) + } + const files = await getAllFiles(TYPE_SCRIPT_OUTPUT_PATH) + for (const filePath of files) { + watchFileWithCleanup( + filePath, + relativePath => { + queueLiveReload(relativePath) + }, + callback => { + typeScriptOutputUnwatchCallbacks.push(callback) + }, + ) + } +} + +const refreshTypeScriptSourceWatchers = async () => { + clearTypeScriptSourceWatchers() + const directories = await getAllDirectories(TYPE_SCRIPT_SOURCE_PATH) + for (const directoryPath of directories) { + watchDirectoryForTypeScriptSources(directoryPath, () => { + void refreshTypeScriptSourceWatchers() + }) + } + const files = await getAllFiles(TYPE_SCRIPT_SOURCE_PATH) + for (const filePath of files) { + watchFileWithCleanup( + filePath, + relativePath => { + queueLiveReload(relativePath) + }, + callback => { + typeScriptSourceUnwatchCallbacks.push(callback) + }, + ) + } +} + const restartServer = async (reason: string) => { if (shuttingDown) return console.log(`[ui:watch] Restarting ui:serve because ${reason} changed`) @@ -226,20 +337,8 @@ const runVendorBuild = async (reason: string) => { } const watchFile = (filePath: string, onChange: (relativePath: string) => void) => { - let debounceTimeout: NodeJS.Timeout | undefined - const listener = (currentStat: fs.Stats, previousStat: fs.Stats) => { - if (currentStat.mtimeMs === previousStat.mtimeMs) return - if (debounceTimeout !== undefined) clearTimeout(debounceTimeout) - debounceTimeout = setTimeout(() => { - debounceTimeout = undefined - const relativePath = path.relative(UI_ROOT_PATH, filePath).replaceAll('\\', '/') - onChange(relativePath) - }, 120) - } - fs.watchFile(filePath, { interval: 250 }, listener) - unwatchCallbacks.push(() => { - if (debounceTimeout !== undefined) clearTimeout(debounceTimeout) - fs.unwatchFile(filePath, listener) + watchFileWithCleanup(filePath, onChange, callback => { + unwatchCallbacks.push(callback) }) } @@ -249,6 +348,8 @@ const shutdown = async (exitCode: number) => { for (const unwatch of unwatchCallbacks) { unwatch() } + clearTypeScriptOutputWatchers() + clearTypeScriptSourceWatchers() await stopProcess(vendorBuildProcess) await stopProcess(serverProcess) await stopProcess(typeScriptWatchProcess) @@ -296,6 +397,18 @@ const main = () => { }) } + void refreshTypeScriptOutputWatchers().catch(error => { + console.error('[ui:watch] Failed to watch TypeScript output files') + console.error(error) + void shutdown(1) + }) + + void refreshTypeScriptSourceWatchers().catch(error => { + console.error('[ui:watch] Failed to watch TypeScript source files') + console.error(error) + void shutdown(1) + }) + void (async () => { try { const cssFiles = await getAllFiles(path.join(UI_ROOT_PATH, 'css')) diff --git a/ui/css/index.css b/ui/css/index.css index f01402b..3cf5521 100644 --- a/ui/css/index.css +++ b/ui/css/index.css @@ -93,6 +93,15 @@ --glow-inset: 0 0 0 1px rgba(255, 255, 255, 0.03) inset; --glow-inset-subtle: 0 0 0 1px rgba(255, 255, 255, 0.02) inset; --divider: linear-gradient(90deg, rgba(56, 213, 255, 0.24), var(--border-subtle) 50%, rgba(124, 108, 255, 0.2)); + --slider-thumb-border: rgba(231, 243, 255, 0.8); + + /* notice error */ + --danger-text: #ffe5eb; + --notice-error-gradient: linear-gradient(180deg, rgba(87, 18, 33, 0.94) 0%, rgba(59, 11, 22, 0.94) 100%); + + /* elevated surfaces */ + --surface-dropdown: rgba(5, 11, 21, 0.97); + --surface-modal: rgba(7, 17, 31, 0.96); } html { @@ -200,6 +209,13 @@ p { overflow-wrap: anywhere; } +h4 { + margin: 0; + font-size: var(--font-body); + line-height: 1.2; + overflow-wrap: anywhere; +} + button { max-width: 100%; border: 1px solid var(--interactive-border); @@ -313,9 +329,7 @@ button.quiet:hover:not(:disabled) { .not-found-shell { display: grid; - grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr); - gap: 1.25rem; - align-items: end; + gap: 1rem; padding-top: 0.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border-default); @@ -480,10 +494,9 @@ button.destructive:hover:not(:disabled) { border: 1px solid var(--danger-border-strong); border-top-color: var(--danger-border-strong); border-bottom-color: var(--danger-border-strong); - background: linear-gradient(180deg, rgba(87, 18, 33, 0.94) 0%, rgba(59, 11, 22, 0.94) 100%); - color: #ffe5eb; + background: var(--notice-error-gradient); + color: var(--danger-text); font-weight: 600; - border-radius: 0.4rem; box-shadow: 0 0 0 1px rgba(255, 112, 143, 0.14) inset, 0 10px 30px rgba(255, 112, 143, 0.08); @@ -506,6 +519,14 @@ button.destructive:hover:not(:disabled) { word-break: normal; } +.form-validation-inline { + flex: 1 1 18rem; + margin: 0; + color: var(--danger); + font-size: var(--font-caption); + line-height: 1.45; +} + .spinner { display: inline-block; width: 0.9rem; @@ -994,7 +1015,7 @@ button.currency-value.copyable:hover:not(:disabled) { .migration-outcome-metrics { display: grid; - gap: 0.12rem; + gap: 0.25rem; color: var(--text-secondary); font-size: var(--font-caption); line-height: 1.35; @@ -1107,10 +1128,6 @@ button.currency-value.copyable:hover:not(:disabled) { border-image: var(--divider) 1; } -.workflow-snapshot { - gap: 1rem; -} - .workflow-section-header { display: flex; justify-content: space-between; @@ -1152,6 +1169,21 @@ button.currency-value.copyable:hover:not(:disabled) { overflow-wrap: anywhere; } +.migration-approval-row { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; + padding-top: 0.5rem; + border-top: 1px solid var(--border-default); +} + +.migration-approval-row strong { + display: block; + margin-top: 0.25rem; +} + .question-summary { display: grid; gap: 0.7rem; @@ -1249,8 +1281,8 @@ button.currency-value.copyable:hover:not(:disabled) { } .workflow-vault-topline h4 { - margin: 0; - font-size: var(--font-body); + line-height: 1.15; + letter-spacing: -0.01em; } .overview-summary-grid { @@ -1327,14 +1359,31 @@ button.currency-value.copyable:hover:not(:disabled) { color: var(--accent); } -.market-scalar-deploy .field.scalar-invalid-toggle { +.scalar-slider-with-invalid { display: flex; align-items: center; - gap: 0.65rem; - padding: 0.05rem 0 0.1rem; + gap: 0.75rem; +} + +.scalar-slider-with-invalid .scalar-slider-rail { + flex: 1; } -.market-scalar-deploy .field.scalar-invalid-toggle > input[type="checkbox"] { +.scalar-or-divider { + flex-shrink: 0; + color: var(--muted); + font-size: var(--font-body); +} + +.scalar-invalid-toggle { + display: flex; + flex-shrink: 0; + align-items: center; + gap: 0.5rem; + cursor: pointer; +} + +.scalar-invalid-toggle > input[type="checkbox"] { width: 1rem; height: 1rem; margin: 0; @@ -1342,12 +1391,12 @@ button.currency-value.copyable:hover:not(:disabled) { cursor: pointer; } -.market-scalar-deploy .field.scalar-invalid-toggle > input[type="checkbox"]:disabled { +.scalar-invalid-toggle > input[type="checkbox"]:disabled { opacity: 0.7; cursor: not-allowed; } -.market-scalar-deploy .scalar-invalid-toggle > span { +.scalar-invalid-toggle > span { color: var(--text); font-family: inherit; font-size: var(--font-body); @@ -1384,11 +1433,18 @@ button.currency-value.copyable:hover:not(:disabled) { pointer-events: none; } +.scalar-slider-input-wrapper { + position: relative; + width: 100%; +} + .scalar-slider-fill { position: absolute; - top: calc(50% - 0.2rem); - left: 0.85rem; + top: 50%; + left: 0; + width: var(--slider-fill, 0%); height: 0.4rem; + transform: translateY(-50%); background: linear-gradient(90deg, rgba(56, 213, 255, 0.92) 0%, rgba(124, 108, 255, 0.88) 100%); box-shadow: 0 0 16px rgba(56, 213, 255, 0.16); pointer-events: none; @@ -1428,7 +1484,7 @@ button.currency-value.copyable:hover:not(:disabled) { width: 1rem; height: 1rem; margin-top: -0.3rem; - border: 1px solid rgba(233, 246, 255, 0.8); + border: 1px solid var(--slider-thumb-border); border-radius: 50%; background: linear-gradient(180deg, var(--text) 0%, rgba(56, 213, 255, 0.8) 100%); box-shadow: @@ -1441,7 +1497,7 @@ button.currency-value.copyable:hover:not(:disabled) { .scalar-slider-field input[type="range"]::-moz-range-thumb { width: 1rem; height: 1rem; - border: 1px solid rgba(233, 246, 255, 0.8); + border: 1px solid var(--slider-thumb-border); border-radius: 50%; background: linear-gradient(180deg, var(--text) 0%, rgba(56, 213, 255, 0.8) 100%); box-shadow: @@ -1461,8 +1517,6 @@ button.currency-value.copyable:hover:not(:disabled) { } .entity-card-subsection-header h4 { - margin: 0; - font-size: var(--font-body); line-height: 1.15; letter-spacing: -0.02em; } @@ -1659,6 +1713,38 @@ button.currency-value.copyable:hover:not(:disabled) { margin-top: 0.25rem; } +.workflow-stack > .entity-card.selected-pool-card { + padding: 1rem 1.05rem; + gap: 0.85rem; +} + +.workflow-stack > .entity-card.selected-pool-card .entity-card-body { + gap: 0.75rem; +} + +.workflow-stack > .entity-card.selected-pool-card .entity-card-subsection { + gap: 0.65rem; + padding-top: 0.45rem; +} + +.workflow-stack > .entity-card.selected-pool-card .entity-card-subsection-header { + margin-bottom: -0.05rem; +} + +.workflow-stack > .entity-card.selected-pool-card .workflow-metric-grid, +.workflow-stack > .entity-card.selected-pool-card .workflow-vault-grid { + gap: 0.6rem 0.85rem; +} + +.workflow-stack > .entity-card.selected-pool-card .workflow-metric-grid > div, +.workflow-stack > .entity-card.selected-pool-card .workflow-vault-grid > div { + padding-top: 0.4rem; +} + +.workflow-stack > .entity-card.selected-pool-card .detail { + line-height: 1.45; +} + .workflow-create-section { border-top-color: var(--border-strong); padding-top: 1.25rem; @@ -1668,11 +1754,6 @@ button.currency-value.copyable:hover:not(:disabled) { padding-bottom: 0.9rem; } -.section-actions-row { - padding-bottom: 0.25rem; - border-bottom: 0; -} - .entity-card .pool-vaults { margin-top: 0.25rem; padding-top: 0.85rem; @@ -1682,7 +1763,6 @@ button.currency-value.copyable:hover:not(:disabled) { .badge.muted { color: var(--muted); border-color: var(--border-default); - background: var(--control-bg); } .modal-backdrop { @@ -1704,8 +1784,7 @@ button.currency-value.copyable:hover:not(:disabled) { gap: 1rem; padding: 1.25rem; border: 1px solid var(--border-strong); - border-radius: 4px; - background: rgba(7, 17, 31, 0.96); + background: var(--surface-modal); box-shadow: 0 24px 72px rgba(0, 0, 0, 0.45); } @@ -1852,7 +1931,7 @@ button.currency-value.copyable:hover:not(:disabled) { padding: 0.35rem; border: 1px solid var(--border-strong); border-radius: 3px; - background: rgba(5, 11, 21, 0.97); + background: var(--surface-dropdown); box-shadow: 0 20px 40px rgba(0, 0, 0, 0.35); } @@ -1961,6 +2040,23 @@ button:focus-visible { resize: vertical; } +.categorical-outcomes { + display: grid; + gap: 0.75rem; +} + +.categorical-outcome-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.75rem; + align-items: end; +} + +.categorical-outcome-add, +.categorical-outcome-remove { + justify-self: start; +} + .escalation-metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -2022,6 +2118,7 @@ button:focus-visible { .grid, .escalation-metrics, .escalation-sides, + .categorical-outcome-row, .field-row, .market-grid { grid-template-columns: 1fr; diff --git a/ui/ts/App.tsx b/ui/ts/App.tsx index 8267b66..0bfa2a8 100644 --- a/ui/ts/App.tsx +++ b/ui/ts/App.tsx @@ -18,6 +18,7 @@ import { useOpenOracleOperations } from './hooks/useOpenOracleOperations.js' import { useReportingOperations } from './hooks/useReportingOperations.js' import { useSecurityPoolCreation } from './hooks/useSecurityPoolCreation.js' import { useSecurityPoolsOverview } from './hooks/useSecurityPoolsOverview.js' +import { useRepPrices } from './hooks/useRepPrices.js' import { useSecurityVaultOperations } from './hooks/useSecurityVaultOperations.js' import { useTradingOperations } from './hooks/useTradingOperations.js' import { useUrlState } from './hooks/useUrlState.js' @@ -100,12 +101,34 @@ export function App() { zoltarUniverse, zoltarUniverseMissing, } = useMarketCreation({ ...baseHookConfig, activeUniverseId, autoLoadInitialData: walletBootstrapComplete, deploymentStatuses }) - const { checkingDuplicateOriginPool, createPool, duplicateOriginPoolExists, loadMarket, loadMarketById, loadingMarketDetails, marketDetails, poolCreationMarketDetails, securityPoolCreating, securityPoolError, securityPoolForm, securityPoolResult, setSecurityPoolForm } = useSecurityPoolCreation({ - ...baseHookConfig, - deploymentStatuses, - }) - const { approveRep, depositRep, loadSecurityVault, loadingSecurityVault, redeemFees, redeemRep, securityVaultDetails, securityVaultError, securityVaultForm, securityVaultResult, setSecurityVaultForm, updateVaultFees } = useSecurityVaultOperations(baseHookConfig) - const { approveToken1, approveToken2, loadOracleManager, loadingOracleManager, onQueueOperation, onRequestPrice, openOracleError, openOracleForm, openOracleResult, oracleManagerDetails, setOpenOracleForm, settleReport, submitInitialReport } = useOpenOracleOperations(baseHookConfig) + const zoltarUniverseHasForked = zoltarUniverse?.hasForked === true + const { checkingDuplicateOriginPool, createPool, duplicateOriginPoolExists, loadMarket, loadMarketById, loadingMarketDetails, marketDetails, poolCreationMarketDetails, resetSecurityPoolCreation, securityPoolCreating, securityPoolError, securityPoolForm, securityPoolResult, setSecurityPoolForm } = + useSecurityPoolCreation({ + ...baseHookConfig, + deploymentStatuses, + zoltarUniverseHasForked, + }) + const { approveRep, depositRep, loadSecurityVault, loadingSecurityVault, redeemFees, securityVaultDetails, securityVaultError, securityVaultForm, securityVaultRepAllowance, securityVaultRepBalance, securityVaultResult, setSecurityBondAllowance, setSecurityVaultForm, withdrawRep } = + useSecurityVaultOperations(baseHookConfig) + const { + approveToken1, + approveToken2, + disputeReport, + loadOracleManager, + loadOracleReport, + loadingOracleManager, + loadingOracleReport, + onQueueOperation, + onRequestPrice, + openOracleError, + openOracleForm, + openOracleReportDetails, + openOracleResult, + oracleManagerDetails, + setOpenOracleForm, + settleReport, + submitInitialReport, + } = useOpenOracleOperations(baseHookConfig) const { loadingReportingDetails, loadReporting, onReportOutcome, reportingDetails, reportingError, reportingForm, reportingResult, setReportingForm, withdrawEscalation } = useReportingOperations(baseHookConfig) const { closeLiquidationModal, @@ -147,9 +170,9 @@ export function App() { submitBid, withdrawBids, } = useForkAuctionOperations(baseHookConfig) + const { repEthPrice, repEthSource, repUsdcPrice, repUsdcSource, isLoadingRepPrices } = useRepPrices() const deploymentSections = getDeploymentSections(deploymentStatuses) const errorMessage = deploymentErrorMessage ?? walletErrorMessage - const lastCreatedQuestionId = marketResult?.questionId const isMainnet = isMainnetChain(accountState.chainId) const wrongNetworkMessage = accountState.address !== undefined && accountState.chainId !== undefined && !isMainnet ? 'Switch your wallet to Ethereum mainnet.' : undefined const augurPlaceHolderDeploymentMissing = augurPlaceHolderDeployed === false @@ -158,7 +181,7 @@ export function App() { const showZoltarUniverseWarning = zoltarUniverseMissing const showZoltarUniverseForkedWarning = zoltarUniverse?.hasForked === true const disableRouteContent = route !== 'deploy' && (augurPlaceHolderDeploymentMissing || showZoltarUniverseWarning) - const isRouteContentDisabled = transactionState.value.transactionInFlightCount > 0 || disableRouteContent || !walletBootstrapComplete + const isRouteContentDisabled = transactionState.value.transactionInFlightCount > 0 || disableRouteContent const universeLabel = formatUniverseCollectionLabel([activeUniverseId]) const universeErrorMessage = showZoltarUniverseWarning ? 'The universe does not exist.' : undefined const renderRouteContent = () => { @@ -197,7 +220,7 @@ export function App() { marketCreating={marketCreating} marketError={marketError} marketResult={marketResult} - onApproveZoltarForkRep={() => void approveZoltarForkRep()} + onApproveZoltarForkRep={amount => void approveZoltarForkRep(amount)} onCreateMarket={() => void createMarket()} onForkZoltar={() => void forkZoltar()} onLoadZoltarQuestions={() => void loadZoltarQuestions()} @@ -236,25 +259,18 @@ export function App() { duplicateOriginPoolExists, poolCreationMarketDetails, onCreateSecurityPool: () => void createPool(), - lastCreatedQuestionId, onLoadMarket: () => void loadMarket(), onLoadMarketById: loadMarketById, loadingMarketDetails, marketDetails, + onResetSecurityPoolCreation: resetSecurityPoolCreation, onSecurityPoolFormChange: update => setSecurityPoolForm(current => ({ ...current, ...update })), + zoltarUniverseHasForked, securityPools, securityPoolCreating, securityPoolError, securityPoolForm, securityPoolResult, - onLoadLatestMarket: () => { - if (lastCreatedQuestionId === undefined) return - setSecurityPoolForm(current => ({ - ...current, - marketId: lastCreatedQuestionId, - })) - void loadMarketById(lastCreatedQuestionId) - }, }} overview={{ accountState, @@ -276,6 +292,7 @@ export function App() { }} workflow={{ accountState, + activeUniverseId, closeLiquidationModal: () => closeLiquidationModal(), forkAuction: { accountState, @@ -329,16 +346,18 @@ export function App() { securityVault: { accountState, loadingSecurityVault, - onApproveRep: () => void approveRep(), + onApproveRep: amount => void approveRep(amount), onDepositRep: () => void depositRep(), onLoadSecurityVault: () => void loadSecurityVault(), onRedeemFees: () => void redeemFees(), - onRedeemRep: () => void redeemRep(), + onSetSecurityBondAllowance: () => void setSecurityBondAllowance(), onSecurityVaultFormChange: update => setSecurityVaultForm(current => ({ ...current, ...update })), - onUpdateVaultFees: () => void updateVaultFees(), + onWithdrawRep: () => void withdrawRep(), securityVaultDetails, securityVaultError, securityVaultForm, + securityVaultRepAllowance, + securityVaultRepBalance, securityVaultResult, }, trading: { @@ -360,9 +379,12 @@ export function App() { void approveToken1()} onApproveToken2={() => void approveToken2()} + onDisputeReport={() => void disputeReport()} onLoadOracleManager={() => void loadOracleManager()} + onLoadOracleReport={() => void loadOracleReport()} onOpenOracleFormChange={update => setOpenOracleForm(current => ({ ...current, ...update }))} onQueueOperation={() => void onQueueOperation()} onRequestPrice={() => void onRequestPrice()} @@ -370,6 +392,7 @@ export function App() { onSubmitInitialReport={() => void submitInitialReport()} openOracleError={openOracleError} openOracleForm={openOracleForm} + openOracleReportDetails={openOracleReportDetails} openOracleResult={openOracleResult} oracleManagerDetails={oracleManagerDetails} /> @@ -449,10 +472,15 @@ export function App() { void connectWallet()} onGoToGenesisUniverse={() => setActiveUniverseId(0n)} onRefresh={() => refreshState()} + repEthPrice={repEthPrice} + repEthSource={repEthSource} + repUsdcPrice={repUsdcPrice} + repUsdcSource={repUsdcSource} universeErrorMessage={universeErrorMessage} universeLabel={universeLabel} universeRepBalance={zoltarForkRepBalance} diff --git a/ui/ts/components/DeploymentSection.tsx b/ui/ts/components/DeploymentSection.tsx index 16fb4fd..72e93ca 100644 --- a/ui/ts/components/DeploymentSection.tsx +++ b/ui/ts/components/DeploymentSection.tsx @@ -86,7 +86,7 @@ export function DeploymentSection({ title, steps, allSteps, accountAddress, isMa

{step.address}

{stepStatus.detail}

- diff --git a/ui/ts/components/ForkAuctionSection.tsx b/ui/ts/components/ForkAuctionSection.tsx index 03de394..d924891 100644 --- a/ui/ts/components/ForkAuctionSection.tsx +++ b/ui/ts/components/ForkAuctionSection.tsx @@ -257,7 +257,7 @@ export function ForkAuctionSection({
-
-
-
diff --git a/ui/ts/components/ForkZoltarSection.tsx b/ui/ts/components/ForkZoltarSection.tsx index 003dbd6..6cd1df0 100644 --- a/ui/ts/components/ForkZoltarSection.tsx +++ b/ui/ts/components/ForkZoltarSection.tsx @@ -11,7 +11,7 @@ type ForkZoltarSectionProps = { loadingZoltarForkAccess: boolean loadingZoltarQuestions: boolean loadingZoltarUniverse: boolean - onApproveZoltarForkRep: () => void + onApproveZoltarForkRep: (amount?: bigint) => void onForkZoltar: () => void onZoltarForkQuestionIdChange: (questionId: string) => void zoltarForkActiveAction: 'approve' | 'fork' | undefined @@ -102,18 +102,19 @@ export function ForkZoltarSection({
{hasForked ? undefined : ( - )}
diff --git a/ui/ts/components/LiquidationModal.tsx b/ui/ts/components/LiquidationModal.tsx index 0c8ca30..932e350 100644 --- a/ui/ts/components/LiquidationModal.tsx +++ b/ui/ts/components/LiquidationModal.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'preact/hooks' import type { Address } from 'viem' import { AddressInfo } from './AddressInfo.js' @@ -16,6 +17,17 @@ type LiquidationModalProps = { } export function LiquidationModal({ accountAddress, closeLiquidationModal, isMainnet, liquidationAmount, liquidationManagerAddress, liquidationModalOpen, liquidationSecurityPoolAddress, liquidationTargetVault, onLiquidationAmountChange, onLiquidationTargetVaultChange, onQueueLiquidation }: LiquidationModalProps) { + useEffect(() => { + if (!liquidationModalOpen) return + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') closeLiquidationModal() + } + document.addEventListener('keydown', handleKeyDown) + return () => { + document.removeEventListener('keydown', handleKeyDown) + } + }, [liquidationModalOpen, closeLiquidationModal]) + if (!liquidationModalOpen) return undefined return ( @@ -52,6 +64,7 @@ export function LiquidationModal({ accountAddress, closeLiquidationModal, isMain Cancel