From 83fe416b84245bc27bd713ad0407ed8e338c2a70 Mon Sep 17 00:00:00 2001 From: "Benjamin O. Ajayi" Date: Mon, 5 Oct 2026 15:52:22 +0100 Subject: [PATCH 1/3] fix(ci): reconcile the ledger-reorg/main merge across every suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merging main into this branch unioned both sides of every conflicting file instead of combining them, so the tree did not build: `lib.rs` had an unclosed `match`, `errors.rs`/`test.rs` declared duplicate variants and fields, `sorobanService.ts` and `stream-simulation.test.ts` each contained two copies of a function, `indexerService.ts`/`pg-pool.ts`/`health.routes.ts` duplicated declarations, and several frontend files mixed both revisions (duplicate JSX props, hooks and imports). The Docker and preview jobs also failed on `EOVERRIDE` because the backend override pinned `@types/pg@8.23.1` while main had added it as a direct `8.20.0` devDependency. Take main's revision where it supersedes the branch's reconciliation (sorobanService, pg-pool, health routes, and the matching test suites), keep the branch's intentional fixes (actual-toast helper, useSyncExternalStore network provider, `nativeToScVal` vec element type, webhook fetch cancellation), and align the `@types/pg` override with the direct dependency so the standalone container install resolves a single copy again. cargo fmt/clippy/test, the wasm release build, backend build + vitest + OpenAPI drift, and frontend lint/tsc/vitest are all green. 🤖 Generated with Codebuff Co-Authored-By: Codebuff # Conflicts: # contracts/stream_contract/src/errors.rs # contracts/stream_contract/src/lib.rs # frontend/src/app/streams/[id]/__tests__/stream-details-content.test.tsx --- backend/package.json | 2 +- backend/src/lib/pg-pool.ts | 18 ------ backend/src/routes/health.routes.ts | 29 +--------- backend/src/services/indexerService.ts | 15 ----- backend/src/services/sorobanService.ts | 56 +------------------ backend/tests/indexer-service.test.ts | 14 ----- backend/tests/stream-simulation.test.ts | 21 ------- contracts/stream_contract/src/errors.rs | 6 -- contracts/stream_contract/src/lib.rs | 15 +---- contracts/stream_contract/src/test.rs | 6 +- frontend/src/app/settings/webhooks/page.tsx | 4 -- .../__tests__/stream-details-content.test.tsx | 3 - .../components/dashboard/dashboard-view.tsx | 3 - .../stream-creation/ScheduleStep.tsx | 6 -- .../__tests__/StreamCreationWizard.test.tsx | 3 - .../ui/__tests__/StellarExplorerLink.test.tsx | 12 ---- frontend/src/context/NetworkContext.tsx | 7 --- frontend/src/lib/soroban.ts | 1 - frontend/src/lib/stream-validation.ts | 2 - frontend/src/utils/cashflowCalculations.ts | 6 +- 20 files changed, 8 insertions(+), 221 deletions(-) diff --git a/backend/package.json b/backend/package.json index 2a45a6a7..84f92d60 100644 --- a/backend/package.json +++ b/backend/package.json @@ -29,7 +29,7 @@ "author": "", "license": "ISC", "overrides": { - "@types/pg": "8.23.1" + "@types/pg": "8.20.0" }, "dependencies": { "@opentelemetry/api": "^1.9.1", diff --git a/backend/src/lib/pg-pool.ts b/backend/src/lib/pg-pool.ts index 6f517b1e..c6cb1f73 100644 --- a/backend/src/lib/pg-pool.ts +++ b/backend/src/lib/pg-pool.ts @@ -62,24 +62,6 @@ export function publishPoolMetrics(pool: pg.Pool): void { dbPoolMaxConnections.set(pool.options?.max ?? 0); } -/** - * Snapshot of the pool's utilisation counters for the admin metrics payload. - * - * Kept separate from `publishPoolMetrics` so callers that only want the numbers - * (e.g. `GET /v1/admin/metrics`) don't have to mutate Prometheus gauges. - */ -export function getPoolMetrics(pool: pg.Pool): { - totalCount: number; - idleCount: number; - waitingCount: number; -} { - return { - totalCount: pool.totalCount ?? 0, - idleCount: pool.idleCount ?? 0, - waitingCount: pool.waitingCount ?? 0, - }; -} - /** * Reduce a SQL statement to a low-cardinality operation label. * diff --git a/backend/src/routes/health.routes.ts b/backend/src/routes/health.routes.ts index 7fd40bd9..7986457d 100644 --- a/backend/src/routes/health.routes.ts +++ b/backend/src/routes/health.routes.ts @@ -79,20 +79,6 @@ router.get('/', async (_req: Request, res: Response) => { } } - // Event-processing failures surface independently of lag: `updatedAt` is - // bumped by the IndexerState upsert on every poll, so a broken indexer can - // still look "fresh". Read the worker's sliding-window counters for that. - const counters = sorobanEventWorker.getEventCounters(); - const indexerFailureDegraded = indexerEnabled && counters.degraded; - const indexerLagDegraded = indexerEnabled && indexerLag > 60; - // `indexerDegraded` reports the failure-rate signal specifically (#1294): - // `checks.indexer.status` folds in lag degradation, but the top-level flag - // must stay false for a lag-only incident. - const indexerDegraded = indexerFailureDegraded; - - // 503 when: DB is down, OR the indexer is enabled and its state row is stale - // (lag > 60), OR its recent failure rate spiked. A missing state row - // (lag === -1) is a cold-start condition, not a failure, even when enabled. // 503 only when: DB is down, OR the indexer is enabled and its state row is // stale (lag > 60). A missing state row (lag === -1) is a cold-start // condition, not a failure, even when the indexer is enabled. @@ -108,11 +94,6 @@ router.get('/', async (_req: Request, res: Response) => { dbStatus === 'connected' && !indexerLagDegraded && !indexerFailureDegraded; const status = isHealthy ? 'ok' : 'degraded'; - const redisStatus = isRedisAvailable() ? 'ok' : 'unavailable'; - // Reuse the ledger tip already resolved above instead of issuing a second - // RPC probe; an unresolved tip (0) counts as unreachable only when the - // indexer is actually enabled. - const sorobanRpcOk = !indexerEnabled || networkLedger > 0; // Redis is optional (single-instance SSE mode falls back gracefully when it's // absent), so its status never affects the top-level `isHealthy` verdict. const redisConfigured = !!process.env.REDIS_URL; @@ -135,10 +116,6 @@ router.get('/', async (_req: Request, res: Response) => { db: dbStatus, indexerEnabled, indexerLag: indexerLag === -1 ? null : indexerLag, - eventsProcessed: counters.eventsProcessed, - eventsFailed: counters.eventsFailed, - lastErrorAt: counters.lastErrorAt, - indexerDegraded, eventsProcessed: eventCounters.eventsProcessed, eventsFailed: eventCounters.eventsFailed, lastErrorAt: eventCounters.lastErrorAt, @@ -153,11 +130,7 @@ router.get('/', async (_req: Request, res: Response) => { status: dbStatus === 'connected' ? 'ok' : 'down', }, indexer: { - status: !indexerEnabled - ? 'disabled' - : indexerLagDegraded || indexerFailureDegraded - ? 'degraded' - : 'ok', + status: !indexerEnabled ? 'disabled' : indexerFailureDegraded || indexerLagDegraded ? 'degraded' : 'ok', enabled: indexerEnabled, lagSeconds: indexerLag === -1 ? null : indexerLag, }, diff --git a/backend/src/services/indexerService.ts b/backend/src/services/indexerService.ts index 07490a8e..abb7cda3 100644 --- a/backend/src/services/indexerService.ts +++ b/backend/src/services/indexerService.ts @@ -39,8 +39,6 @@ export async function getIndexerStatus(): Promise { } export async function resetIndexer(toLedger: number): Promise { - // Acquire the same mutex that serialises poll/replay batches so an in-flight - // poll cannot overwrite the reset cursor after we write it (#1221). // Acquire the same mutex that serialises poll/replay batches so that an // in-flight poll cannot overwrite the reset cursor after we write it (#1221). await sorobanEventWorker.runExclusive(async () => { @@ -55,8 +53,6 @@ export async function resetIndexer(toLedger: number): Promise { } /** - * Preview what a reset would do without mutating state, so operators can - * verify the intended scope before committing (admin `dryRun`). * Preview what a reset would do without mutating state. * Returns the current cursor and the target ledger so operators can * verify the intended scope before committing. @@ -79,9 +75,6 @@ export async function previewReset(targetLedger: number): Promise } /** - * Preview what a replay from a given ledger would do without mutating state: - * the event count, ledger range, and current cursor for sanity-checking a - * destructive replay before it commits. * Preview what a replay from a given ledger would do without mutating state. * Returns the event count, ledger range, and current cursor so operators can * sanity-check before committing a destructive replay. @@ -139,18 +132,12 @@ export async function replayFromLedger( fromLedger: number, customRequestId?: string, ): Promise { - const requestId = - customRequestId || requestContext.getStore()?.requestId || randomUUID(); - - return requestContext.run({ requestId }, async () => { const requestId = customRequestId || requestContext.getStore()?.requestId || randomUUID(); await requestContext.run({ requestId }, async () => { await resetIndexer(fromLedger); // Kick off an immediate poll cycle without waiting for the next interval. await sorobanEventWorker.triggerPoll(requestId); logger.info(`[IndexerService] Replay triggered from ledger ${fromLedger}`); - return requestId; - }); }); return requestId; } @@ -302,8 +289,6 @@ function eventTypeOf(event: rpc.Api.EventResponse): string { const topic0 = event.topic?.[0]; if (!topic0) return 'unknown'; try { - // SDK v17 exposes the symbol as a property (`.sym`), not a method. - return (topic0 as xdr.ScValSymbol).sym.toString(); // `ScVal` is a union; only the symbol arm carries `sym`, and in recent // stellar-sdk versions it is a value (not a method). Read the property and // stringify it so this survives across SDK generations. diff --git a/backend/src/services/sorobanService.ts b/backend/src/services/sorobanService.ts index 13a6d216..87092baf 100644 --- a/backend/src/services/sorobanService.ts +++ b/backend/src/services/sorobanService.ts @@ -8,7 +8,6 @@ import { rpcFailoversTotal, } from '../lib/metrics.js'; import { withSpan } from '../lib/tracing.js'; -import { rpcPool } from '../lib/rpc-pool.js'; const RPC_URL = process.env.SOROBAN_RPC_URL ?? 'https://soroban-testnet.stellar.org'; @@ -357,41 +356,6 @@ export async function submitContractCall(method: string, args: xdr.ScVal[], send } /** - * Poll the network until a submitted transaction reaches a terminal status. - * - * `NOT_FOUND` is the mempool/in-flight signal and is retried until the deadline; - * `SUCCESS` resolves with the final response and `FAILED` throws immediately. - * The deadline is enforced with a strict `<` check against `Date.now()` so a - * zero timeout still performs exactly one poll. - */ -export async function pollTransactionStatus( - hash: string, - timeoutMs: number = getTxConfirmationTimeoutMs(), - intervalMs: number = getTxPollIntervalMs(), -): Promise { - const deadline = Date.now() + Math.max(0, timeoutMs); - - for (;;) { - const response = await withRpcRetry('getTransaction', () => - withRpcTimeout('getTransaction', () => - executeRpc('getTransaction', (server) => server.getTransaction(hash)), - ), - ); - - if (response.status === rpc.Api.GetTransactionStatus.SUCCESS) { - return response; - } - if (response.status === rpc.Api.GetTransactionStatus.FAILED) { - throw new Error(`Transaction failed on-chain: ${hash}`); - } - - if (Date.now() >= deadline) { - throw new Error( - `Transaction confirmation timed out after ${timeoutMs}ms: ${hash}`, - ); - } - await new Promise((resolve) => setTimeout(resolve, intervalMs)); - } * Poll Soroban RPC getTransaction until the transaction reaches a terminal * status (SUCCESS or FAILED) or until the bounded timeout expires. */ @@ -442,9 +406,6 @@ export async function pollTransactionStatus( export async function getLatestLedger(): Promise { try { const response = await withRpcRetry('getLatestLedger', () => - withRpcTimeout('getLatestLedger', () => - executeRpc('getLatestLedger', (server) => server.getLatestLedger()), - ), withRpcTimeout('getLatestLedger', () => executeRpc('getLatestLedger', (server) => server.getLatestLedger())), ); return Number(response.sequence); @@ -937,7 +898,6 @@ function decodeSimulatedReturn(result: rpc.Api.SimulateTransactionSuccessRespons if (!retval) return ''; try { - // SDK v17 exposes ScVal fields as properties discriminated by `type`. switch (retval.type) { case 'scvI128': return decodeI128(retval); @@ -947,22 +907,14 @@ function decodeSimulatedReturn(result: rpc.Api.SimulateTransactionSuccessRespons return retval.u32.toString(); case 'scvI64': return retval.i64.toString(); - case 'scvI32': - return retval.i32.toString(); - case 'scvU128': { - const parts = retval.u128; - const hi = BigInt.asUintN(64, BigInt(parts.hi.toString())); - const lo = BigInt.asUintN(64, BigInt(parts.lo.toString())); case 'scvU128': { const parts = retval.u128; const hi = BigInt.asUintN(64, parts.hi); const lo = BigInt.asUintN(64, parts.lo); return ((hi << 64n) | lo).toString(); } - case 'scvVoid': - return ''; default: - // Non-numeric returns (addresses, maps, event markers) are surfaced as + // Non-numeric returns (addresses, maps, void markers) are surfaced as // base64 XDR so the client can decode them with the SDK if it needs to. return Buffer.from(retval.toXDR()).toString('base64'); } @@ -1002,9 +954,6 @@ export async function simulateStreamAction( let sourceAccount: Account; try { sourceAccount = await withRpcRetry('getAccount', () => - withRpcTimeout('getAccount', () => - executeRpc('getAccount', (server) => server.getAccount(senderPublicKey)), - ), withRpcTimeout('getAccount', () => executeRpc('getAccount', (server) => server.getAccount(senderPublicKey))), ); } catch (err) { @@ -1028,9 +977,6 @@ export async function simulateStreamAction( const tx = builder.setTimeout(TX_TIMEOUT_SECONDS).build(); const simulation = await withRpcRetry('simulateTransaction', () => - withRpcTimeout('simulateTransaction', () => - executeRpc('simulateTransaction', (server) => server.simulateTransaction(tx)), - ), withRpcTimeout('simulateTransaction', () => executeRpc('simulateTransaction', (server) => server.simulateTransaction(tx))), ); diff --git a/backend/tests/indexer-service.test.ts b/backend/tests/indexer-service.test.ts index 031cd8eb..dff5ecf3 100644 --- a/backend/tests/indexer-service.test.ts +++ b/backend/tests/indexer-service.test.ts @@ -10,8 +10,6 @@ const hoisted = vi.hoisted(() => ({ delete: vi.fn(), triggerPoll: vi.fn(), processEvent: vi.fn(), - // Pass-through so the reset/replay mutex wrapper executes its callback. - runExclusive: vi.fn((fn: () => Promise) => fn()), runExclusive: vi.fn(), sendDeadLetterAlert: vi.fn(), })); @@ -41,18 +39,6 @@ vi.mock('../src/workers/soroban-event-worker.js', () => ({ }, })); -vi.mock('../src/logger.js', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - // Keep the real requestContext (AsyncLocalStorage) so replay can bind IDs. - default: { - info: vi.fn(), - error: vi.fn(), - warn: vi.fn(), - }, - }; -}); vi.mock('../src/logger.js', () => ({ default: { info: vi.fn(), diff --git a/backend/tests/stream-simulation.test.ts b/backend/tests/stream-simulation.test.ts index 7a3c7a0a..7f69b019 100644 --- a/backend/tests/stream-simulation.test.ts +++ b/backend/tests/stream-simulation.test.ts @@ -71,17 +71,6 @@ function simulationError(error: string): rpc.Api.SimulateTransactionErrorRespons /** * Extract the invoke args from the transaction handed to the RPC mock. * - * `Transaction.operations` exposes marshalled attribute objects. In SDK v17 the - * host function is a `func.invokeContract` property whose `contractAddress`, - * `functionName` and `args` are read as properties too. - */ -function invokedOps(tx: Transaction): Array<{ contractHex: string; fn: string; args: xdr.ScVal[] }> { - return tx.operations.map((op) => { - const func = (op as unknown as { func: xdr.HostFunctionInvokeContract }).func; - const ico = func.invokeContract; - const contractId = (ico.contractAddress as xdr.ScAddressContract).contractId; - return { - contractHex: Buffer.from(contractId.value as unknown as Uint8Array).toString('hex'), * `Transaction.operations` exposes marshalled operation bodies, so the contract * call is reached via `body.invokeHostFunctionOp.hostFunction.invokeContract` * and its fields are read as plain properties. @@ -112,11 +101,6 @@ function contractHex(address: string): string { /** Decode a returned envelope and assert it carries no signatures. */ function expectUnsignedEnvelope(unsignedXdr: string): xdr.Transaction { - const envelope = xdr.TransactionEnvelope.fromXDR(unsignedXdr, 'base64'); - expect(envelope.type).toBe('envelopeTypeTx'); - const txEnvelope = envelope as xdr.TransactionEnvelopeTx; - expect(txEnvelope.v1.signatures).toHaveLength(0); - return txEnvelope.v1.tx; const envelope = xdr.TransactionEnvelope.fromXDR( unsignedXdr, 'base64', @@ -195,7 +179,6 @@ describe('simulateStreamAction', () => { expect(Address.fromScVal(op!.args[1]!).toString()).toBe(recipientKp.publicKey()); expect(Address.fromScVal(op!.args[2]!).toString()).toBe(tokenAddress); expect(service.decodeI128(op!.args[3]!)).toBe('1000000'); - expect((op!.args[4]! as xdr.ScValU64).u64.toString()).toBe('3600'); expect((op!.args[4] as xdr.ScValU64).u64.toString()).toBe('3600'); }); @@ -254,7 +237,6 @@ describe('simulateStreamAction', () => { const [op] = invokedOps(lastSimulatedTx()); expect(op!.fn).toBe('withdraw'); expect(Address.fromScVal(op!.args[0]!).toString()).toBe(recipientKp.publicKey()); - expect((op!.args[1]! as xdr.ScValU64).u64.toString()).toBe('42'); expect((op!.args[1] as xdr.ScValU64).u64.toString()).toBe('42'); }); @@ -263,7 +245,6 @@ describe('simulateStreamAction', () => { const [op] = invokedOps(lastSimulatedTx()); expect(op!.fn).toBe('cancel_stream'); - expect((op!.args[1]! as xdr.ScValU64).u64.toString()).toBe('7'); expect((op!.args[1] as xdr.ScValU64).u64.toString()).toBe('7'); }); @@ -275,7 +256,6 @@ describe('simulateStreamAction', () => { const [op] = invokedOps(lastSimulatedTx()); expect(op!.fn).toBe('top_up_stream'); - expect((op!.args[1]! as xdr.ScValU64).u64.toString()).toBe('7'); expect((op!.args[1] as xdr.ScValU64).u64.toString()).toBe('7'); expect(service.decodeI128(op!.args[2]!)).toBe('2500'); }); @@ -302,7 +282,6 @@ describe('simulateStreamAction', () => { const ops = invokedOps(lastSimulatedTx()); expect(ops).toHaveLength(3); expect(ops.map((o) => o.fn)).toEqual(['withdraw', 'withdraw', 'withdraw']); - expect(ops.map((o) => (o.args[1]! as xdr.ScValU64).u64.toString())).toEqual(['1', '2', '3']); expect(ops.map((o) => (o.args[1] as xdr.ScValU64).u64.toString())).toEqual(['1', '2', '3']); }); diff --git a/contracts/stream_contract/src/errors.rs b/contracts/stream_contract/src/errors.rs index 63003060..67ff7cfd 100644 --- a/contracts/stream_contract/src/errors.rs +++ b/contracts/stream_contract/src/errors.rs @@ -92,10 +92,4 @@ pub enum StreamError { /// `close_stream` was called on a stream that is still active or still /// holds unwithdrawn funds, so its record cannot be pruned yet. StreamStillActive = 36, - /// Checked arithmetic overflow (fee, withdrawal total, or end-time projection). - ArithmeticOverflow = 34, - /// Stream is still active / holds funds and cannot be pruned yet. - StreamStillActive = 35, - /// Operation requires an active stream, but the stream is inactive. - StreamNotActive = 36, } diff --git a/contracts/stream_contract/src/lib.rs b/contracts/stream_contract/src/lib.rs index 1c191d2f..bd91be6d 100644 --- a/contracts/stream_contract/src/lib.rs +++ b/contracts/stream_contract/src/lib.rs @@ -39,7 +39,7 @@ mod property_tests; mod test; use soroban_sdk::{ - contract, contractimpl, token, vec, Address, BytesN, Env, IntoVal, InvokeError, Symbol, Vec, + contract, contractimpl, token, vec, Address, BytesN, Env, InvokeError, Symbol, Vec, }; use errors::StreamError; @@ -2008,19 +2008,6 @@ impl StreamContract { let stream_id = next_stream_id(&env); let start_time = env.ledger().timestamp(); - // Check allowance: just verify it's callable, don't lock it yet. - // Try to get allowance to validate approval was made - match env.try_invoke_contract::( - &token_address, - &Symbol::new(&env, "allowance"), - vec![ - &env, - sender.into_val(&env), - env.current_contract_address().into_val(&env), - ], - ) { - Ok(Ok(allowance)) if allowance > 0 => {} - _ => return Err(StreamError::AllowanceLocked), // Check allowance: just verify it's callable, don't lock it yet let token_client = token::Client::new(&env, &token_address); // Use the generated client: avoids manual Val conversion for try_invoke. diff --git a/contracts/stream_contract/src/test.rs b/contracts/stream_contract/src/test.rs index 34eae48b..905041e1 100644 --- a/contracts/stream_contract/src/test.rs +++ b/contracts/stream_contract/src/test.rs @@ -2327,14 +2327,14 @@ fn test_fuzz_claimable_overflow_and_cancel_invariants() { None }, schedule: VestingSchedule::Linear, + arbiter: None, + dispute_status: DisputeStatus::None, + is_allowance_based: false, status: if paused { StreamStatus::Paused } else { StreamStatus::Active }, - arbiter: None, - dispute_status: DisputeStatus::None, - is_allowance_based: false, }; let claimable = StreamContract::calculate_claimable(&stream, elapsed); diff --git a/frontend/src/app/settings/webhooks/page.tsx b/frontend/src/app/settings/webhooks/page.tsx index ca01f4da..cdc78b70 100644 --- a/frontend/src/app/settings/webhooks/page.tsx +++ b/frontend/src/app/settings/webhooks/page.tsx @@ -19,10 +19,6 @@ export default function WebhooksPage() { .catch((error) => { toast.error(error instanceof Error ? error.message : "Failed to load webhooks"); }); return () => { cancelled = true; }; }, [session?.publicKey]); - // Refetch when the connected session changes; setState only runs after the - // async list resolves, so this is not a synchronous cascading update. - // eslint-disable-next-line react-hooks/set-state-in-effect - useEffect(() => { void refresh(); }, [session?.publicKey]); const save = async (data: { targetUrl: string; eventTypes: string[] }) => { if (!session?.publicKey) return; if (editing) await updateWebhook(editing.id, session.publicKey, data); else { const result = await createWebhook({ userAddress: session.publicKey, ...data }); setSecretKey(result.secretKey); setEditing(result.subscription); } await refresh(); }; return

Developer tools

Webhooks

Register HTTPS receivers, manage event subscriptions, and inspect delivery attempts.

{session?.publicKey ?
{ setEditing(subscription); setSecretKey(undefined); setCreating(true); }} onToggle={async (subscription) => { await updateWebhook(subscription.id, session.publicKey, { isActive: !subscription.isActive }); await refresh(); }} onDelete={async (subscription) => { if (window.confirm("Delete this webhook endpoint?")) { await deleteWebhook(subscription.id, session.publicKey); await refresh(); } }} onDeliveries={setDelivery} />
:
Connect a wallet to manage developer webhooks.
}
{creating && session?.publicKey && { setCreating(false); setEditing(null); setSecretKey(undefined); }} onSave={async (data) => { await save(data); }} onRegenerate={editing ? async () => { const next = await regenerateWebhookSecret(editing.id, session.publicKey); setSecretKey(next); return next; } : undefined} />}{delivery && session?.publicKey && setDelivery(null)} />}
; } \ No newline at end of file diff --git a/frontend/src/app/streams/[id]/__tests__/stream-details-content.test.tsx b/frontend/src/app/streams/[id]/__tests__/stream-details-content.test.tsx index e4607bda..ba007d14 100644 --- a/frontend/src/app/streams/[id]/__tests__/stream-details-content.test.tsx +++ b/frontend/src/app/streams/[id]/__tests__/stream-details-content.test.tsx @@ -206,9 +206,6 @@ describe("StreamDetailsContent loading skeleton", () => { // Skeleton should be gone expect(screen.queryByRole("status")).not.toBeInTheDocument(); - // Stream-specific content should be visible (the header and receipt rows - // both reference the stream id, so assert on all matches). - expect(screen.getAllByText(/stream #42/i).length).toBeGreaterThan(0); // Stream-specific content should be visible expect(screen.getAllByText(/stream #42/i).length).toBeGreaterThanOrEqual(1); }); diff --git a/frontend/src/components/dashboard/dashboard-view.tsx b/frontend/src/components/dashboard/dashboard-view.tsx index a9603605..6daa3c50 100644 --- a/frontend/src/components/dashboard/dashboard-view.tsx +++ b/frontend/src/components/dashboard/dashboard-view.tsx @@ -911,9 +911,6 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { incomingStreams={filteredIncoming} onWithdraw={handleIncomingWithdraw} withdrawingStreamId={withdrawingIncomingStreamId} - onBatchClaimSuccess={async () => { - await refetchSnapshot(); - }} onOpenBatchClaim={() => setShowBatchClaim(true)} onBatchClaimSuccess={() => { void refetchSnapshot(); diff --git a/frontend/src/components/stream-creation/ScheduleStep.tsx b/frontend/src/components/stream-creation/ScheduleStep.tsx index 1fec3703..ddd2a860 100644 --- a/frontend/src/components/stream-creation/ScheduleStep.tsx +++ b/frontend/src/components/stream-creation/ScheduleStep.tsx @@ -9,10 +9,7 @@ type ScheduleFormData = Pick< "duration" | "durationUnit" | "descriptionTag" | "memo" >; -// The props mirror the shared form hook, so this step edits the same model the -// wizard validates and submits rather than a local copy of it. interface ScheduleStepProps { - formData: StreamFormData; formData: ScheduleFormData; errors: StreamFormErrors; onUpdate: (data: Partial) => void; @@ -22,8 +19,6 @@ const MAX_MEMO_BYTES = 28; export function ScheduleStep({ formData, errors, onUpdate }: ScheduleStepProps) { const memo = formData.memo || ""; - // Derived during render instead of mirrored into state from an effect. - const memoByteCount = useMemo(() => new TextEncoder().encode(memo).length, [memo]); // UTF-8 byte length, derived during render rather than stored in state. const memoByteCount = useMemo( () => new TextEncoder().encode(memo).length, @@ -58,7 +53,6 @@ export function ScheduleStep({ formData, errors, onUpdate }: ScheduleStepProps)