From faa5cf99df8ab71ee9b99d8a734a7b3bdfc7d923 Mon Sep 17 00:00:00 2001 From: Sweet-Kid Date: Mon, 31 Aug 2026 08:19:16 +0100 Subject: [PATCH 1/6] fix(#544): replace asTimestamp < 1e12 heuristic with digit-count heuristic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old threshold (value < 1e12 ⇒ seconds) misclassified pre-2001 millisecond timestamps and post-33658 second timestamps. Use digit count instead: 10 digits ⇒ seconds, 13 ⇒ ms. --- src/dashboard/transaction-history.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/dashboard/transaction-history.ts b/src/dashboard/transaction-history.ts index e999cf7..1ddfc10 100644 --- a/src/dashboard/transaction-history.ts +++ b/src/dashboard/transaction-history.ts @@ -213,7 +213,21 @@ function toIso8601(value: string): string { function asTimestamp(value: unknown): number { if (typeof value === 'number' && Number.isFinite(value)) { // Indexers emit seconds for `createdAt`; normalise to milliseconds. - return value < 1e12 ? Math.trunc(value) * 1000 : Math.trunc(value); + // Use digit count to disambiguate: 10 digits ⇒ seconds, 13 digits ⇒ ms. + // This avoids the pre-2001 ms misclassification of the old < 1e12 check. + const abs = Math.abs(value); + if (abs >= 1e9 && abs < 1e10) { + // 10 digits: seconds epoch (1970–2033 range) + return Math.trunc(value) * 1000; + } + if (abs >= 1e12 && abs < 1e13) { + // 13 digits: milliseconds epoch + return Math.trunc(value); + } + // Ambiguous digit count (e.g. 11-12 digits): fall through to existing + // callers — they already handle numeric timestamps in a context-dependent + // way, and the ambiguous band is small relative to the pre-2001 breakage. + return Math.trunc(value); } if (typeof value === 'string' && value.trim() !== '') { const trimmed = value.trim(); From 806b91bf68cd7a7b56b3283c5ae1794dc970c03d Mon Sep 17 00:00:00 2001 From: Sweet-Kid Date: Mon, 31 Aug 2026 08:20:11 +0100 Subject: [PATCH 2/6] fix(#543): make sequence optional in event type interfaces Events without a topics[2] sequence slot will carry undefined for sequence, matching the updated dispatchEvent return behavior. --- src/types/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/types/index.ts b/src/types/index.ts index 2cb1641..8b29a3d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -118,12 +118,12 @@ export interface GovernorConfig { // ── Events ────────────────────────────────────────────────────────────────── -export interface WithdrawEvent { amount: bigint; recipient: string; totalWithdrawn: bigint; remaining: bigint; sequence: bigint; } -export interface CancelEvent { refundAmount: bigint; withdrawnSoFar: bigint; sender: string; sequence: bigint; } -export interface PauseEvent { pausedAt: number; withdrawable: bigint; sender: string; sequence: bigint; } -export interface ResumeEvent { resumedAt: number; sender: string; sequence: bigint; } -export interface TopUpEvent { amount: bigint; newBalance: bigint; sender: string; sequence: bigint; } -export interface ClawbackEvent { amount: bigint; sender: string; sequence: bigint; } +export interface WithdrawEvent { amount: bigint; recipient: string; totalWithdrawn: bigint; remaining: bigint; sequence?: bigint; } +export interface CancelEvent { refundAmount: bigint; withdrawnSoFar: bigint; sender: string; sequence?: bigint; } +export interface PauseEvent { pausedAt: number; withdrawable: bigint; sender: string; sequence?: bigint; } +export interface ResumeEvent { resumedAt: number; sender: string; sequence?: bigint; } +export interface TopUpEvent { amount: bigint; newBalance: bigint; sender: string; sequence?: bigint; } +export interface ClawbackEvent { amount: bigint; sender: string; sequence?: bigint; } /** A gap detected in the per-contract event sequence — see `DataKey::EventSequence` in contracts/stream/src/events.rs. */ export interface EventGap { From a5d853df56b8a3f8aa25bd46af65e22a1c38210f Mon Sep 17 00:00:00 2001 From: Sweet-Kid Date: Mon, 31 Aug 2026 08:25:17 +0100 Subject: [PATCH 3/6] fix(#543): return undefined for events missing the sequence topic dispatchEvent previously returned 0n for events without topics[2], which caused spurious onGap calls and reset lastSequence to 0n. Now returns undefined so the poll loop's sequence !== undefined guard works correctly. --- src/events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/events.ts b/src/events.ts index 38de97b..a396026 100644 --- a/src/events.ts +++ b/src/events.ts @@ -223,7 +223,7 @@ export function dispatchEvent( const topicName = topics[0]?.sym()?.toString() ?? ''; const actor = addressField(topics[1]); - const sequence = topics[2] ? scValToU64(topics[2]) : 0n; + const sequence = topics[2] ? scValToU64(topics[2]) : undefined; switch (topicName) { case TOPIC.WITHDRAWN: { From 059876a92aa61105d4fc934f57531f0df88556f4 Mon Sep 17 00:00:00 2001 From: Sweet-Kid Date: Mon, 31 Aug 2026 08:25:24 +0100 Subject: [PATCH 4/6] fix(#542): validate networkPassphrase before signing Transaction objects KeypairWalletAdapter.signTransaction previously ignored opts.networkPassphrase when given a Transaction object, allowing a testnet-signed transaction to be validly signed for mainnet. Now asserts tx.networkPassphrase matches the caller's opts.networkPassphrase before signing (when both are defined). --- src/adapters/keypair.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/adapters/keypair.ts b/src/adapters/keypair.ts index b108630..5b60c70 100644 --- a/src/adapters/keypair.ts +++ b/src/adapters/keypair.ts @@ -28,6 +28,17 @@ export class KeypairWalletAdapter implements WalletAdapter { return parsedTx.toXDR(); } + if ( + _opts?.networkPassphrase && + tx.networkPassphrase !== undefined && + tx.networkPassphrase !== _opts.networkPassphrase + ) { + throw new Error( + `networkPassphrase mismatch: the transaction was built for "${tx.networkPassphrase}" ` + + `but opts.networkPassphrase is "${_opts.networkPassphrase}".`, + ); + } + tx.sign(this.keypair); return tx; } From a27474cc07083b488ea109166e02e19387d4c244 Mon Sep 17 00:00:00 2001 From: Sweet-Kid Date: Mon, 31 Aug 2026 08:25:31 +0100 Subject: [PATCH 5/6] fix(#546): delegate progress calculation to streamProgress/normalizeProgress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Module49.processSingleItem reimplemented progress logic (returning 0.5 for open-ended streams) instead of using the shared utils.streamProgress and normalizeProgress. This duplicated Module48's pre-#433 bug and diverged from the canonical NaN→0.5 mapping. Now delegates to the shared functions like Module48. --- src/module49.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/module49.ts b/src/module49.ts index 87f568a..44b422c 100644 --- a/src/module49.ts +++ b/src/module49.ts @@ -1,5 +1,5 @@ import type { StreamInfo } from './types/index.js'; -import { withdrawableLocal } from './utils.js'; +import { withdrawableLocal, streamProgress, normalizeProgress } from './utils.js'; import { LruMemoCache } from './lru-memo-cache.js'; export interface Module49Config { @@ -102,17 +102,7 @@ export class Module49 { this.cacheMisses++; const withdrawable = withdrawableLocal(item.stream, nowSec); - let progress = 0; - const { startTime, endTime } = item.stream; - if (nowSec >= startTime) { - if (endTime === 0) { - progress = 0.5; // open-ended active - } else if (nowSec >= endTime) { - progress = 1.0; - } else { - progress = (nowSec - startTime) / (endTime - startTime); - } - } + const progress = normalizeProgress(streamProgress(item.stream, nowSec)); const computedAt = nowSec; From 4ab846135385a5a31c1d4a25389e3be41c8a27f5 Mon Sep 17 00:00:00 2001 From: Sweet-Kid Date: Mon, 31 Aug 2026 08:29:06 +0100 Subject: [PATCH 6/6] test(#543): update events-subscribe test to expect undefined for missing sequence The clawback event in the test has only 2 topics (no topics[2] sequence slot), so sequence is now undefined instead of 0n. --- src/tests/events-subscribe.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/events-subscribe.test.ts b/src/tests/events-subscribe.test.ts index 9d3770f..65a6b1d 100644 --- a/src/tests/events-subscribe.test.ts +++ b/src/tests/events-subscribe.test.ts @@ -166,7 +166,7 @@ describe('subscribeToStream', () => { onClawback: (e) => { received = e; }, }); await vi.waitFor(() => expect(received).toBeDefined()); - expect(received).toEqual({ sender, amount: 5_000n, sequence: 0n }); + expect(received).toEqual({ sender, amount: 5_000n, sequence: undefined }); sub.unsubscribe(); });