From f872a6415e7d2e49465619845048b76583d8a198 Mon Sep 17 00:00:00 2001 From: p4uld4vid016-code Date: Mon, 31 Aug 2026 02:23:53 +0000 Subject: [PATCH] fix(builder): encode ScVals with ABI-correct integer types (#497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit paramToScVal() forced every integer number to i64 and every bigint to i128, so u64 contract parameters — create_stream's start_time/end_time and stream IDs — arrived with the wrong ScVal type and were rejected contract-side. Untyped positive integers now encode as u64 (negatives as i64), explicit ScVal type hints are supported, already-encoded xdr.ScVals pass through untouched, and BatchOperation.types supplies per-field type information for the params map. ConduitBatcher.execute()'s create_stream path now builds ABI-exact positional args (i128 amounts, u64 times, bool clawback) and honors startTime/endTime/clawbackEnabled. Also removes a duplicate `let consecutiveFailures` in src/events.ts that made the module a SyntaxError, breaking the build and every event-subscription test. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- CHANGELOG.md | 4 + README.md | 2 +- docs/api.md | 6 +- src/batch-tx.ts | 56 ++++++-- src/builder.ts | 32 +++-- src/events.ts | 1 - src/index.ts | 1 + src/tests/batch-real-xdr.test.ts | 51 +++++++- src/tests/builder-param-encoding.test.ts | 157 +++++++++++++++++++++++ src/tests/events-subscribe.test.ts | 2 +- 10 files changed, 281 insertions(+), 31 deletions(-) create mode 100644 src/tests/builder-param-encoding.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 47ef228..d840f18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes are documented here. Format based on [Keep a Changelog](http - `StreamsModule.transferRecipient()` — wraps the contract's `transfer_recipient()` so the current recipient can reassign the recipient role (previously only a prose TODO) (#454) - `NonceManager` (the bigint-based, queue-with-cancellation implementation) is now exported from the package entry point, along with its `NonceLock`/`NonceManagerOptions` types. Previously neither `NonceManager` implementation was reachable outside SDK source (#483). - `subscribeToStream()` / `client.streams.subscribe()` now accept `maxBackoffMs` and `maxConsecutiveFailures` options controlling the new exponential-backoff-with-cutoff polling behaviour (#485). +- `paramToScVal(value, type?)` accepts an explicit ScVal type hint (`'u64'`, `'i128'`, ...) and passes already-encoded `xdr.ScVal`s through untouched; `BatchOperation.types` supplies per-field type information for the `params` map (e.g. `{ streamId: 'u64', amount: 'i128' }`) so a u64 stream ID or an i128 amount encodes with the exact width the contract expects instead of the default inference (#497). ### Performance - `FactoryModule.streamAddress()` now caches resolved stream→contract-address lookups in-memory, since the mapping is fixed at stream creation and never changes. Eliminates redundant RPC round trips on every `StreamsModule` read/write operation (`get`, `withdraw`, `cancel`, `pause`, `resume`, `topUp`, `clawback`) and on each page of `list()`, which previously re-resolved the same address for every stream on every call. @@ -27,6 +28,7 @@ All notable changes are documented here. Format based on [Keep a Changelog](http ### Changed - `StreamsModule` now routes all signer-selection logic through its private `_signer()` helper instead of touching `config.signer` directly, removing dead code (#446). - CAIP-2→network mapping consolidated into a single exported `CAIP2_TO_NETWORK` constant shared by `ConduitClient`'s wallet network check and `WalletConnectAdapter`'s chain validation, so the two can never disagree (#445). +- **Breaking (encoded ScVal types):** `paramToScVal()` no longer forces every integer `number` to `i64` and every `bigint` to `i128`. Untyped positive integers now encode as `u64` and negatives as `i64` — matching the contract's u64 ABI fields (`create_stream`'s `start_time`/`end_time`, stream IDs), which previously arrived with the wrong ScVal type and were rejected contract-side (#497). ### Removed - Removed orphaned `RoomManager` (`src/room-manager.js`) and `src/server.js` WebSocket server, along with unused `dotenv` production dependency (#442). @@ -47,6 +49,8 @@ All notable changes are documented here. Format based on [Keep a Changelog](http - Documented `StreamBuilder.startTime()`/`endTime()`/`clawbackEnabled()`/`toContractArgs()`/`toBatchOperation()` in `docs/api.md`, and added a note under `ConduitBatcher` clarifying that `execute()` alone cannot build a real `create_stream` invocation (#435). ### Fixed +- `ConduitBatcher.execute()`'s `create_stream` path now builds ABI-exact positional args — `deposit_amount`/`rate_per_sec` as `i128`, `start_time`/`end_time` as `u64` (honoring `startTime`/`endTime`/`clawbackEnabled` when present) — instead of feeding raw values through the old blanket i64/i128 encoding (#497). +- Removed a duplicate `let consecutiveFailures = 0;` declaration in `src/events.ts` that made the module a `SyntaxError` at load time, breaking `npm run typecheck`, the `npm ci` build, and every event-subscription test. - `subscribeToStream()` (`client.streams.subscribe()`) now seeds `startLedger` from `server.getLatestLedger()` before its first `getEvents()` call. Previously the first poll omitted `startLedger` entirely (it started at `0` and was only included once `> 0`), so Soroban RPC's `getEvents` rejected every call, the rejection was swallowed, and the subscription never delivered a single event (#484). - `subscribeToStream()` now backs off exponentially (`pollInterval * 2^(consecutiveFailures - 1)`, capped at the new `maxBackoffMs`) after consecutive polling failures instead of retrying at a fixed interval forever, and stops polling once `maxConsecutiveFailures` consecutive failures have occurred instead of spinning against a permanently-broken RPC endpoint indefinitely (#485). - `Module36` and `Module48` no longer each maintain their own copy of the "open-ended stream progress (`NaN`) → `0.5`" normalization. It's now a single shared `normalizeProgress()` in `src/utils.ts`, closing the gap where the two modules' progress calculations could in principle drift out of sync at the edges (#482). diff --git a/README.md b/README.md index a739fc8..7062e83 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ The `StreamBuilder` class exposes the following chainable methods: ### Batching Streams -You can bundle multiple stream operations together and compile them using `ConduitBatcher`. `execute()` alone only knows how to turn arbitrary objects into a generic map argument — to invoke the real `create_stream` contract, build each stream's `BatchOperation` with `toBatchOperation()` and submit through `executeAsync()`: +You can bundle multiple stream operations together and compile them using `ConduitBatcher`. `execute()` turns each item into the ABI-exact positional `create_stream` args when the default method is used (amount/rate encoded as `i128`, `startTime`/`endTime` as `u64`), falling back to a generic sorted-map argument for other methods. To invoke the real `create_stream` contract with full control (including validated, defaulted `start_time`/`end_time`/`clawback_enabled`), build each stream's `BatchOperation` with `toBatchOperation()` and submit through `executeAsync()`: ```typescript import { StreamBuilder, ConduitBatcher } from '@conduit-protocol/sdk'; diff --git a/docs/api.md b/docs/api.md index bf06a09..df05759 100644 --- a/docs/api.md +++ b/docs/api.md @@ -619,7 +619,9 @@ const result = await new StreamBuilder() A utility class to bundle multiple stream operations with mandatory client-side validation. `execute`/`executeAsync` are **instance methods** — instantiate with `new ConduitBatcher()` first (see [`examples/fluent-builder.ts`](../examples/fluent-builder.ts)). -> **Building real `create_stream` calls:** `execute()` takes plain `Record[]` and, with no `args`, encodes each item as a single sorted map keyed by whatever properties it happens to have — it has no knowledge of any contract's ABI. Passing raw `StreamBuilder.build()` output to it therefore does **not** produce a valid `create_stream` invocation (wrong key casing, `amount` encoded as `i64` instead of `i128`, and no `start_time`/`end_time`/`clawback_enabled` at all). To actually invoke `create_stream`, build a `BatchOperation` with `StreamBuilder.toBatchOperation()` (which supplies the correct positional, ABI-typed `args`) and pass it to `executeAsync()`. +> **Building real `create_stream` calls:** `execute()` takes plain `Record[]`. With the default `create_stream` method it builds the exact positional, ABI-typed args (`deposit_amount`/`rate_per_sec` as `i128`, `start_time`/`end_time` as `u64`, honoring `startTime`/`endTime`/`clawbackEnabled` when present); for other methods it encodes each item as a single sorted map keyed by whatever properties it happens to have. `execute()` does no ABI validation (a missing `ratePerSecond` silently becomes `0`, for example), so for a fully validated `create_stream` invocation build a `BatchOperation` with `StreamBuilder.toBatchOperation()` (which supplies the correct positional, ABI-typed `args`) and pass it to `executeAsync()`. +> +> **Integer encoding:** `paramToScVal()` no longer forces every integer `number` to `i64` and every `bigint` to `i128`. Untyped positive integers now encode as `u64` (matching the contract's `start_time`/`end_time`/stream-ID types) and negatives as `i64`; pass an explicit type (`paramToScVal(value, 'i128')`) or a per-field `BatchOperation.types` hint (e.g. `{ streamId: 'u64', amount: 'i128' }`) to force a specific width, and already-encoded `xdr.ScVal`s pass through untouched (#497). #### Methods @@ -649,7 +651,7 @@ if (!result.success) { } ``` -* `executeAsync(operations: BatchOperation[], signalOrOptions?: AbortSignal | BatchExecuteAsyncOptions): Promise` - Asynchronously execute a batch with abort signal / options support. `BatchOperation.args`, when present, is used verbatim as the contract's positional arguments (see `StreamBuilder.toBatchOperation()`). +* `executeAsync(operations: BatchOperation[], signalOrOptions?: AbortSignal | BatchExecuteAsyncOptions): Promise` - Asynchronously execute a batch with abort signal / options support. `BatchOperation.args`, when present, is used verbatim as the contract's positional arguments (see `StreamBuilder.toBatchOperation()`). `BatchOperation.types` supplies per-field ScVal type hints for the `params` map (e.g. `{ method: 'withdraw', params: { streamId: 1n }, types: { streamId: 'u64' } }`) so u64 stream IDs and i128 amounts encode with the correct width instead of the default inference (#497). **Throws:** `Error` if batcher is destroyed. diff --git a/src/batch-tx.ts b/src/batch-tx.ts index e70a671..df09712 100644 --- a/src/batch-tx.ts +++ b/src/batch-tx.ts @@ -143,14 +143,33 @@ export function validateContext(context: BatchTransactionContext): string[] { return errors; } +/** + * ScVal type names accepted by {@link paramToScVal} (and per-field `types` on + * batch operations) to force a specific encoding instead of the default + * inference. + */ +export type ScValType = + | 'u32' | 'i32' | 'u64' | 'i64' | 'u128' | 'i128' | 'u256' | 'i256' + | 'string' | 'symbol' | 'address' | 'bool' | 'bytes'; + /** * Convert a single parameter to an ScVal. * - * Strings that are valid Stellar addresses become `Address` values rather than - * string values — passing a G- or C-address as a plain string is a common way - * to build a transaction the contract then rejects. + * - Already-encoded `xdr.ScVal` values pass through untouched — their type is + * already explicit, so re-encoding would destroy it. + * - An explicit `type` hint wins over every heuristic (see + * {@link BatchOperation.types} for per-field hints in the params map). + * - Strings that are valid Stellar addresses become `Address` values rather + * than string values — passing a G- or C-address as a plain string is a + * common way to build a transaction the contract then rejects. + * - Everything else falls back to the SDK's natural encoding: positive + * integers become `u64` and negatives `i64`. This is what the contract + * expects for the u64-heavy ABI fields such as `create_stream`'s + * `start_time`/`end_time` and stream IDs; previously every integer `number` + * was forced to `i64` and every `bigint` to `i128`, producing the wrong + * ScVal type and a contract-side type error (see #497). */ -export function paramToScVal(value: unknown): xdr.ScVal { +export function paramToScVal(value: unknown, type?: ScValType): xdr.ScVal { // Values with no ScVal representation (symbols, functions, undefined) map to // void rather than throwing — validation has already accepted the payload, so // a stray non-serialisable field must not take the whole batch down. @@ -164,15 +183,20 @@ export function paramToScVal(value: unknown): xdr.ScVal { if (value === null) { return xdr.ScVal.scvVoid(); } - if (typeof value === 'string' && (StrKey.isValidEd25519PublicKey(value) || StrKey.isValidContract(value))) { - return new Address(value).toScVal(); + if (value instanceof xdr.ScVal) { + return value; } - if (typeof value === 'bigint') { - return nativeToScVal(value, { type: 'i128' }); + + // An explicit type hint means the caller knows the contract ABI — trust it + // over any heuristic below (e.g. `{ streamId: 'u64' }`, `{ amount: 'i128' }`). + if (type !== undefined) { + return nativeToScVal(value, { type }); } - if (typeof value === 'number' && Number.isInteger(value)) { - return nativeToScVal(value, { type: 'i64' }); + + if (typeof value === 'string' && (StrKey.isValidEd25519PublicKey(value) || StrKey.isValidContract(value))) { + return new Address(value).toScVal(); } + return nativeToScVal(value); } @@ -181,14 +205,18 @@ export function paramToScVal(value: unknown): xdr.ScVal { * * `args` wins when present, so a caller who knows the contract ABI controls the * positional arguments exactly. Otherwise `params` is passed as a single map - * argument, keyed by field name. + * argument, keyed by field name, with `types` supplying per-field ScVal type + * information when the default inference would pick the wrong type (e.g. a + * u64 stream ID, which untyped would encode as a string after bigint + * serialisation, or an i128 amount). */ export function operationToScVals(operation: { params?: Record | undefined; + types?: Record | undefined; args?: unknown[] | undefined; }): xdr.ScVal[] { if (Array.isArray(operation.args)) { - return operation.args.map(paramToScVal); + return operation.args.map(arg => paramToScVal(arg)); } const params = operation.params ?? {}; @@ -203,7 +231,7 @@ export function operationToScVals(operation: { .map(([key, value]) => new xdr.ScMapEntry({ key: nativeToScVal(key, { type: 'symbol' }), - val: paramToScVal(value), + val: paramToScVal(value, operation.types?.[key]), }), ), ), @@ -213,6 +241,8 @@ export function operationToScVals(operation: { interface BuildableOperation { method: string; params?: Record | undefined; + /** Per-field ScVal type hints for `params` map entries (see #497). */ + types?: Record | undefined; args?: unknown[] | undefined; } diff --git a/src/builder.ts b/src/builder.ts index 2241d87..b7845b8 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -4,9 +4,10 @@ import { boolToScVal } from './soroban.js'; import { buildBatchTransactions, buildBatchTransactionsSync, + paramToScVal, validateContext, } from './batch-tx.js'; -import type { BatchTransactionContext, BuiltBatchTransaction } from './batch-tx.js'; +import type { BatchTransactionContext, BuiltBatchTransaction, ScValType } from './batch-tx.js'; export interface SubmitOptions { maxRetries?: number; @@ -440,6 +441,13 @@ export interface BatchOperation { * single map argument. */ args?: unknown[]; + /** + * Per-field ScVal type hints for the `params` map, keyed by field name + * (e.g. `{ streamId: 'u64' }` for a u64 stream ID, `{ amount: 'i128' }` + * for an i128 amount). Without a hint the default inference applies — + * positive integers encode as `u64`, negatives as `i64` (see #497). + */ + types?: Record; } export interface BatchExecuteOptions { @@ -657,15 +665,21 @@ export class ConduitBatcher { const method = options.method ?? 'create_stream'; const operations = sanitized.map(params => { if (method === 'create_stream') { + // ABI-exact create_stream args: (sender, recipient, token, + // deposit_amount: i128, rate_per_sec: i128, start_time: u64, + // end_time: u64, clawback_enabled: bool). Previously these raw values + // were run through paramToScVal's blanket i64/i128 encoding, so + // start_time/end_time arrived as i64 and amounts as the wrong width; + // each arg is now typed explicitly (see #497). const args = [ - params.sender, - params.recipient, - params.token, - params.amount, - params.ratePerSecond, - 0, // start_time - 0, // end_time - false, // clawback + paramToScVal(params.sender), + paramToScVal(params.recipient), + paramToScVal(params.token), + paramToScVal(params.amount, 'i128'), + paramToScVal(params.ratePerSecond, 'i128'), + paramToScVal(params.startTime ?? 0, 'u64'), + paramToScVal(params.endTime ?? 0, 'u64'), + boolToScVal(params.clawbackEnabled === true), ]; return { method, args }; } diff --git a/src/events.ts b/src/events.ts index 2eb3f4b..38de97b 100644 --- a/src/events.ts +++ b/src/events.ts @@ -91,7 +91,6 @@ export function subscribeToStream( let consecutiveFailures = 0; let stopped = false; let timer: ReturnType | undefined; - let consecutiveFailures = 0; // Last per-contract event sequence seen (topics[2]), for gap detection // across a poll or reconnect — see contracts/stream/src/events.rs. let lastSequence: bigint | undefined; diff --git a/src/index.ts b/src/index.ts index 475ef00..a1ce3a0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ export { export type { BatchTransactionContext, BuiltBatchTransaction, + ScValType, } from './batch-tx.js'; export { GraphQLIndexer } from './indexer.js'; export { KeypairSigner } from './signer.js'; diff --git a/src/tests/batch-real-xdr.test.ts b/src/tests/batch-real-xdr.test.ts index 212d540..7c0091a 100644 --- a/src/tests/batch-real-xdr.test.ts +++ b/src/tests/batch-real-xdr.test.ts @@ -9,7 +9,7 @@ */ import { describe, it, expect, beforeEach } from 'vitest'; -import { Keypair, Networks, Transaction, TransactionBuilder, xdr } from '@stellar/stellar-sdk'; +import { Keypair, Networks, Transaction, TransactionBuilder, nativeToScVal, xdr } from '@stellar/stellar-sdk'; import { ConduitBatcher } from '../builder.js'; import { BatchBuildError, @@ -255,8 +255,29 @@ describe('ConduitBatcher builds real XDR', () => { expect(paramToScVal(CONTRACT_ID).switch().name).toBe('scvAddress'); }); - it('encodes bigints as i128', () => { - expect(paramToScVal(42n).switch().name).toBe('scvI128'); + it('encodes positive integers as u64 and negatives as i64 — not a blanket i128/i64', () => { + // Regression for #497: paramToScVal used to force every bigint to i128 + // and every integer number to i64, which produced the wrong ScVal type + // for u64 contract parameters (start_time/end_time, stream IDs). + expect(paramToScVal(42).switch().name).toBe('scvU64'); + expect(paramToScVal(42n).switch().name).toBe('scvU64'); + expect(paramToScVal(0).switch().name).toBe('scvU64'); + expect(paramToScVal(-42).switch().name).toBe('scvI64'); + expect(paramToScVal(-42n).switch().name).toBe('scvI64'); + }); + + it('honors an explicit type hint over the default inference', () => { + expect(paramToScVal(42n, 'i128').switch().name).toBe('scvI128'); + expect(paramToScVal(42, 'i64').switch().name).toBe('scvI64'); + expect(paramToScVal(42, 'u64').switch().name).toBe('scvU64'); + expect(paramToScVal('hello', 'symbol').switch().name).toBe('scvSymbol'); + expect(paramToScVal(SOURCE, 'string').switch().name).toBe('scvString'); + }); + + it('passes already-encoded ScVals through untouched', () => { + const scVal = nativeToScVal(7n, { type: 'u64' }); + expect(paramToScVal(scVal)).toBe(scVal); + expect(paramToScVal(scVal).switch().name).toBe('scvU64'); }); it('maps values with no ScVal representation to void instead of throwing', () => { @@ -268,10 +289,17 @@ describe('ConduitBatcher builds real XDR', () => { it('passes positional args verbatim when supplied', () => { const args = operationToScVals({ args: [1n, SOURCE], params: { ignored: true } }); expect(args).toHaveLength(2); - expect(args[0]!.switch().name).toBe('scvI128'); + expect(args[0]!.switch().name).toBe('scvU64'); expect(args[1]!.switch().name).toBe('scvAddress'); }); + it('passes pre-encoded ScVals in positional args through unchanged', () => { + const scVal = nativeToScVal(1n, { type: 'u64' }); + const args = operationToScVals({ args: [scVal] }); + expect(args[0]).toBe(scVal); + expect(args[0]!.switch().name).toBe('scvU64'); + }); + it('passes params as a single sorted map when no args are given', () => { const args = operationToScVals({ params: { zeta: 1, alpha: 2 } }); @@ -282,6 +310,21 @@ describe('ConduitBatcher builds real XDR', () => { expect(keys).toEqual(['alpha', 'zeta']); }); + it('applies per-field type hints to params map values', () => { + // A u64 stream ID passed through the params path must stay u64 — the + // #497 regression this fixes. + const args = operationToScVals({ + params: { streamId: 1n, amount: 100 }, + types: { streamId: 'u64', amount: 'i128' }, + }); + + expect(args).toHaveLength(1); + const map = args[0]!.map()!; + const byKey = new Map(map.map(e => [e.key().sym().toString(), e.val()])); + expect(byKey.get('streamId')!.switch().name).toBe('scvU64'); + expect(byKey.get('amount')!.switch().name).toBe('scvI128'); + }); + it('sends no arguments for an operation with empty params', () => { expect(operationToScVals({ params: {} })).toEqual([]); }); diff --git a/src/tests/builder-param-encoding.test.ts b/src/tests/builder-param-encoding.test.ts new file mode 100644 index 0000000..9ad865f --- /dev/null +++ b/src/tests/builder-param-encoding.test.ts @@ -0,0 +1,157 @@ +/** + * Regression tests for #497: `paramToScVal` used to force every integer + * `number` to `i64` and every `bigint` to `i128`, so u64 contract parameters + * (`create_stream`'s `start_time`/`end_time`, stream IDs) encoded with the + * wrong ScVal type and were rejected contract-side. + * + * These tests assert the end-to-end ScVal types that actually land in the + * built transaction XDR: + * - `execute()`'s `create_stream` path builds ABI-exact positional args + * (i128 amounts, u64 times, bool clawback). + * - `executeAsync()`'s params map honors per-field `types` hints (u64 IDs). + * - Pre-encoded `xdr.ScVal`s in `args` pass through untouched. + */ + +import { describe, it, expect } from 'vitest'; +import { Networks, Transaction, TransactionBuilder, nativeToScVal, scValToNative, xdr } from '@stellar/stellar-sdk'; +import { ConduitBatcher } from '../builder.js'; + +const CONTRACT_ID = 'CAAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQC526'; +const SOURCE = 'GAAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQDZ7H'; +const RECIPIENT = 'GABAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEJXA'; + +const CONTEXT = { + contractId: CONTRACT_ID, + sourceAccount: SOURCE, + network: 'testnet' as const, + sequence: '1', +}; + +/** Decode the invoke-contract args back out of a built transaction. */ +function decodeArgs(envelope: string): xdr.ScVal[] { + const tx = TransactionBuilder.fromXDR(envelope, Networks.TESTNET); + if (!(tx instanceof Transaction)) throw new Error('Expected a plain transaction'); + const op = tx.operations[0] as { type: string; func: import('@stellar/stellar-sdk').xdr.HostFunction }; + expect(op.type).toBe('invokeHostFunction'); + return op.func.invokeContract().args(); +} + +describe('ConduitBatcher params-path ScVal encoding (#497)', () => { + it('execute() builds ABI-exact create_stream args: i128 amounts, u64 times', () => { + const result = new ConduitBatcher().execute( + [ + { + token: CONTRACT_ID, + sender: SOURCE, + recipient: RECIPIENT, + amount: 1000, + ratePerSecond: 10, + }, + ], + { context: CONTEXT }, + ); + + expect(result.success).toBe(true); + const args = decodeArgs(result.xdr); + expect(args).toHaveLength(8); + expect(args.map(a => a.switch().name)).toEqual([ + 'scvAddress', // sender + 'scvAddress', // recipient + 'scvAddress', // token + 'scvI128', // deposit_amount + 'scvI128', // rate_per_sec + 'scvU64', // start_time + 'scvU64', // end_time + 'scvBool', // clawback_enabled + ]); + expect(scValToNative(args[3]!)).toBe(1000n); + expect(scValToNative(args[4]!)).toBe(10n); + }); + + it('execute() honors startTime/endTime/clawbackEnabled from the stream object', () => { + const start = Math.floor(Date.now() / 1000); + const end = start + 3600; + + const result = new ConduitBatcher().execute( + [ + { + token: CONTRACT_ID, + sender: SOURCE, + recipient: RECIPIENT, + amount: 1000, + ratePerSecond: 10, + startTime: start, + endTime: end, + clawbackEnabled: true, + }, + ], + { context: CONTEXT }, + ); + + expect(result.success).toBe(true); + const args = decodeArgs(result.xdr); + expect(args[5]!.switch().name).toBe('scvU64'); + expect(args[6]!.switch().name).toBe('scvU64'); + expect(Number(scValToNative(args[5]!))).toBe(start); + expect(Number(scValToNative(args[6]!))).toBe(end); + expect(scValToNative(args[7]!)).toBe(true); + }); + + it('execute() keeps encoding a bigint amount as i128 after serialisation', () => { + const result = new ConduitBatcher().execute( + [ + { + token: CONTRACT_ID, + sender: SOURCE, + recipient: RECIPIENT, + amount: 9007199254740993n, + ratePerSecond: 10n, + }, + ], + { context: CONTEXT }, + ); + + expect(result.success).toBe(true); + const args = decodeArgs(result.xdr); + expect(args[3]!.switch().name).toBe('scvI128'); + expect(scValToNative(args[3]!)).toBe(9007199254740993n); + }); + + it('executeAsync() params map encodes a u64 stream ID with a per-field type hint', async () => { + const result = await new ConduitBatcher().executeAsync( + [ + { + method: 'withdraw', + params: { streamId: 1n }, + types: { streamId: 'u64' }, + }, + ], + { context: CONTEXT }, + ); + + expect(result.success).toBe(true); + const args = decodeArgs(result.xdr); + expect(args).toHaveLength(1); + expect(args[0]!.switch().name).toBe('scvMap'); + const entry = args[0]!.map()![0]!; + expect(entry.key().sym().toString()).toBe('streamId'); + expect(entry.val().switch().name).toBe('scvU64'); + expect(scValToNative(entry.val())).toBe(1n); + }); + + it('executeAsync() passes pre-encoded u64 ScVals in args through unchanged', async () => { + const streamIdScVal = nativeToScVal(42n, { type: 'u64' }); + const result = await new ConduitBatcher().executeAsync( + [{ method: 'withdraw', params: {}, args: [streamIdScVal] }], + { context: CONTEXT }, + ); + + expect(result.success).toBe(true); + const args = decodeArgs(result.xdr); + expect(args).toHaveLength(1); + // The ScVal survives the encode→XDR→decode round-trip with its u64 type + // intact (instance identity is not preserved through serialisation). + expect(args[0]!.switch().name).toBe('scvU64'); + expect(scValToNative(args[0]!)).toBe(42n); + }); +}); diff --git a/src/tests/events-subscribe.test.ts b/src/tests/events-subscribe.test.ts index 3a88859..9d3770f 100644 --- a/src/tests/events-subscribe.test.ts +++ b/src/tests/events-subscribe.test.ts @@ -64,7 +64,7 @@ describe('subscribeToStream', () => { const sub = subscribeToStream('http://localhost:8000', 'CSTREAM', {}); await vi.waitFor(() => expect(mockGetEvents).toHaveBeenCalled()); expect(mockGetLatestLedger).toHaveBeenCalledTimes(1); - expect(mockGetEvents.mock.calls[0]?.[0]).toHaveProperty('startLedger', 100); + expect(mockGetEvents.mock.calls[0]?.[0]).toHaveProperty('startLedger', 4242); sub.unsubscribe(); });