Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions src/builder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { StrKey, Address, nativeToScVal } from '@stellar/stellar-sdk';
import { bigintSafeStringify } from './utils.js';
import { bigintSafeStringify, toStroops } from './utils.js';
import { boolToScVal } from './soroban.js';
import {
buildBatchTransactions,
Expand Down Expand Up @@ -574,11 +574,24 @@ function validatePayload(streams: unknown): string[] {
}
}

// Validate amount field — must be a positive finite number (where present)
// Validate amount field — must be a bigint or a decimal string parseable by toStroops
if (obj.amount !== undefined && obj.amount !== null) {
const amount = Number(obj.amount);
if (!Number.isFinite(amount) || amount <= 0) {
errors.push(`Batch item at index ${i}: amount must be a positive finite number, got "${obj.amount}"`);
const amt = obj.amount;
if (typeof amt === 'bigint') {
if (amt <= 0n) {
errors.push(`Batch item at index ${i}: amount must be a positive bigint, got ${amt.toString()}`);
}
} else if (typeof amt === 'string') {
try {
const stroops = toStroops(amt);
if (stroops <= 0n) {
errors.push(`Batch item at index ${i}: amount must be a positive value, got "${amt}"`);
}
} catch {
errors.push(`Batch item at index ${i}: amount must be a valid decimal string (e.g. "1000" or "1.5"), got "${amt}"`);
}
} else {
errors.push(`Batch item at index ${i}: amount must be a bigint or decimal string, got ${typeof amt}`);
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ export function clampListLimit(limit: number): number {
return Math.min(truncated, MAX_LIST_LIMIT);
}

/**
* Clamp a caller-supplied pagination `offset` to a non-negative integer
* within the valid u32 range (`[0, 2^32 - 1]`). Non-finite or negative
* input returns 0 rather than producing an invalid u32 conversion.
*/
export function clampOffset(offset: number): number {
if (!Number.isFinite(offset)) return 0;
return Math.min(Math.max(Math.trunc(offset), 0), 0xFFFFFFFF);
}

/**
* Bit-flags packed into the on-chain `StreamInfo.flags` (`u32`). `paused`,
* `cancelled` and `clawback_enabled` are NOT individual struct fields -- they
Expand Down
6 changes: 3 additions & 3 deletions src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { nativeToScVal, xdr, Address } from '@stellar/stellar-sdk';
import type { ConduitConfig } from './types/index.js';
import type { WalletAdapter } from './adapters/types.js';
import { KeypairWalletAdapter } from './adapters/keypair.js';
import { ZERO_ADDR, DEFAULT_LIST_LIMIT, clampListLimit } from './constants.js';
import { ZERO_ADDR, DEFAULT_LIST_LIMIT, clampListLimit, clampOffset } from './constants.js';
import {
buildContractCallTx,
simulateReadOnly,
Expand Down Expand Up @@ -197,7 +197,7 @@ export class FactoryModule {
this.factoryId, 'streams_by_sender',
[
new Address(address).toScVal(),
nativeToScVal(offset, { type: 'u32' }),
nativeToScVal(clampOffset(offset), { type: 'u32' }),
nativeToScVal(clampListLimit(limit), { type: 'u32' }),
],
);
Expand All @@ -218,7 +218,7 @@ export class FactoryModule {
this.factoryId, 'streams_by_recipient',
[
new Address(address).toScVal(),
nativeToScVal(offset, { type: 'u32' }),
nativeToScVal(clampOffset(offset), { type: 'u32' }),
nativeToScVal(clampListLimit(limit), { type: 'u32' }),
],
);
Expand Down
11 changes: 9 additions & 2 deletions src/relayer/WebSocketRelayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface WebSocketRelayerOptions {
maxPendingMessages?: number;
maxReconnectAttempts?: number;
reconnectDelayMs?: number;
maxReconnectDelayMs?: number;
onStateChange?: StateChangeHandler;
}

Expand All @@ -37,6 +38,7 @@ export class WebSocketRelayer {
private reconnectExhausted = false;
private maxReconnectAttempts: number;
private reconnectDelayMs: number;
private maxReconnectDelayMs: number;
private pendingMessages: WebSocketMessage[] = [];
private maxPendingMessages: number;
private stateTransition: RelayerStateTransition = 'disconnected';
Expand All @@ -45,6 +47,7 @@ export class WebSocketRelayer {
this.url = url;
this.maxReconnectAttempts = options?.maxReconnectAttempts ?? 5;
this.reconnectDelayMs = options?.reconnectDelayMs ?? 1000;
this.maxReconnectDelayMs = options?.maxReconnectDelayMs ?? 30_000;
this.maxPendingMessages = options?.maxPendingMessages ?? 1000;
if (options?.onStateChange) {
this.stateChangeHandlers.add(options.onStateChange);
Expand Down Expand Up @@ -304,8 +307,12 @@ export class WebSocketRelayer {

this.reconnectAttempts++;
this.emitStateChange('reconnecting');
await new Promise((r) => setTimeout(r, this.reconnectDelayMs * this.reconnectAttempts));
if (this.isDestroyed || !this.reconnectEnabled) return;
const delay = Math.min(
this.maxReconnectDelayMs,
this.reconnectDelayMs * 2 ** (this.reconnectAttempts - 1),
) * (0.5 + Math.random() * 0.5);
await new Promise((r) => setTimeout(r, delay));
if (this.isDestroyed) return;

await this.establishConnection();
} catch {
Expand Down
6 changes: 6 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export function fromStroops(stroops: bigint, decimals = 7): string {
* @param decimals Token decimal places (default 7 for Stellar assets)
*/
export function calculateRate(depositAmount: string, durationSecs: number, decimals = 7): bigint {
if (!Number.isInteger(durationSecs) || durationSecs <= 0) {
throw new Error(`durationSecs must be a positive integer, got ${durationSecs}`);
}
const stroops = toStroops(depositAmount, decimals);
const divisor = BigInt(durationSecs);
if (divisor === 0n) return 0n;
Expand All @@ -80,6 +83,9 @@ export function calculateYield(
durationSecs = 31_536_000,
decimals = 7,
): string {
if (!Number.isInteger(durationSecs) || durationSecs <= 0) {
throw new Error(`durationSecs must be a positive integer, got ${durationSecs}`);
}
const totalStroops = ratePerSecond * BigInt(durationSecs);
return fromStroops(totalStroops, decimals);
}
Expand Down