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; } diff --git a/src/dashboard/transaction-history.ts b/src/dashboard/transaction-history.ts index 991b943..e27ef9c 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(); diff --git a/src/events.ts b/src/events.ts index c69e501..b54af9c 100644 --- a/src/events.ts +++ b/src/events.ts @@ -243,7 +243,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: { diff --git a/src/module49.ts b/src/module49.ts index a115a19..e1fcb3a 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,19 +102,9 @@ export class Module49 { } this.cacheMisses++; -this.totalProcessed++; -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 withdrawable = withdrawableLocal(item.stream, nowSec); + + const progress = normalizeProgress(streamProgress(item.stream, nowSec)); const computedAt = nowSec; 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(); }); diff --git a/src/types/index.ts b/src/types/index.ts index 6b461d0..b2c32b3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -132,12 +132,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; } /** Published once when the factory deploys a new DripStream (`created` topic). */ export interface CreatedEvent {