diff --git a/.gitignore b/.gitignore index 57a61e9..b1ccdbd 100644 --- a/.gitignore +++ b/.gitignore @@ -173,4 +173,5 @@ coverage/ # Vitest coverage.data -coverage/ \ No newline at end of file +coverage/package-lock.json +package-lock.json diff --git a/examples/node/package.json b/examples/node/package.json new file mode 100644 index 0000000..d860859 --- /dev/null +++ b/examples/node/package.json @@ -0,0 +1,13 @@ +{ + "name": "node", + "version": "1.0.0", + "description": "This directory contains Node.js examples demonstrating the Lumera SDK capabilities.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs" +} diff --git a/examples/node/upload.ts b/examples/node/upload.ts index 5452128..9210a2c 100644 --- a/examples/node/upload.ts +++ b/examples/node/upload.ts @@ -1,234 +1,123 @@ /** - * Advanced Upload Example: Direct Message Composition - * - * This example demonstrates how to use the Telescope-generated message composers - * directly for more granular control over the upload workflow: - * - * 1. Build file metadata manually - * 2. Create messages using generated MessageComposer - * 3. Simulate, sign, and broadcast transactions - * 4. Complete the action with finalization - * + * Cascade Upload Example: LumeraClient Facade + * + * This example demonstrates the high-level upload workflow using the + * LumeraClient facade, which handles RaptorQ encoding, supernode + * communication, and on-chain action registration automatically: + * + * 1. Initialize wallets (Direct + Amino for ADR-036 signing) + * 2. Create a LumeraClient with testnet preset + * 3. Upload a file via the Cascade uploader + * * Prerequisites: - * - Set the MNEMONIC environment variable with a valid 24-word mnemonic + * - Set the MNEMONIC environment variable with a valid mnemonic * - Ensure the account has sufficient balance for transaction fees - * - Have a file to upload (e.g., example.bin) - * + * * Usage: * MNEMONIC="your mnemonic here" npx tsx examples/node/upload.ts */ -import { makeBlockchainClient } from "../../src/blockchain/client"; +import { createLumeraClient } from "../../src"; import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; -import { lumera } from "../../src/codegen"; -import { calculateCascadeFee } from "../../src/blockchain/messages"; -import fs from "node:fs"; -import crypto from "node:crypto"; +import { Secp256k1HdWallet, makeSignDoc as makeAminoSignDoc } from "@cosmjs/amino"; async function main() { console.log("=".repeat(60)); - console.log("Lumera SDK - Direct Message Composition Example"); + console.log("Lumera SDK - Cascade Upload Example"); console.log("=".repeat(60)); // ============================================================================ - // STEP 1: Initialize wallet and blockchain client + // STEP 1: Initialize wallets // ============================================================================ - console.log("\n[Step 1] Setting up wallet and blockchain client..."); - + console.log("\n[Step 1] Setting up wallets..."); + if (!process.env.MNEMONIC) { throw new Error("Set MNEMONIC environment variable"); } - const wallet = await DirectSecp256k1HdWallet.fromMnemonic(process.env.MNEMONIC, { - prefix: "lumera" + const directWallet = await DirectSecp256k1HdWallet.fromMnemonic(process.env.MNEMONIC, { + prefix: "lumera", }); - const [account] = await wallet.getAccounts(); - console.log(`✓ Using address: ${account.address}`); - - const chain = await makeBlockchainClient({ - rpcUrl: "https://rpc.testnet.lumera.io", - lcdUrl: "https://lcd.testnet.lumera.io", - chainId: "lumera-testnet-2", - signer: wallet, - address: account.address, - gasPrice: "0.025ulume" + const aminoWallet = await Secp256k1HdWallet.fromMnemonic(process.env.MNEMONIC, { + prefix: "lumera", }); - - console.log(`✓ Connected to chain: ${await chain.getChainId()}`); - - // ============================================================================ - // STEP 2: Query blockchain parameters - // ============================================================================ - console.log("\n[Step 2] Querying action module parameters..."); - - const params = await chain.Action.getParams(); - console.log(` rq_ids_max: ${params.rq_ids_max}`); - console.log(` rq_ids_ic: ${params.rq_ids_ic}`); - console.log(` fee_base: ${params.fee_base}`); - console.log(` fee_per_kb: ${params.fee_per_kb}`); - - // ============================================================================ - // STEP 3: Prepare file and metadata - // ============================================================================ - console.log("\n[Step 3] Preparing file metadata..."); - - // For this example, create a sample file if it doesn't exist - const filePath = "./example.bin"; - let fileData: Buffer; - - if (fs.existsSync(filePath)) { - fileData = fs.readFileSync(filePath); - console.log(`✓ Loaded existing file: ${filePath} (${fileData.length} bytes)`); - } else { - // Create a sample file - fileData = crypto.randomBytes(1024); // 1KB sample file - fs.writeFileSync(filePath, fileData); - console.log(`✓ Created sample file: ${filePath} (${fileData.length} bytes)`); - } - - // Calculate file hash using SHA-256 - const fileHash = crypto.createHash("sha256").update(fileData).digest("hex"); - console.log(` File hash: ${fileHash}`); - - // Generate action ID - const actionId = `example-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`; - console.log(` Action ID: ${actionId}`); - - // Build metadata for Cascade action - const metadata = { - data_hash: fileHash, - file_name: "example.bin", - rq_ids_ic: params.rq_ids_ic, - signatures: "", // Would be populated with RaptorQ signatures in real use - public: false, + const [account] = await directWallet.getAccounts(); + console.log(` Address: ${account.address}`); + + // Combine direct + amino wallets and add signArbitrary (ADR-036) for Cascade + const signer = { + getAccounts: () => directWallet.getAccounts(), + signDirect: (addr: string, doc: any) => directWallet.signDirect(addr, doc), + signAmino: (addr: string, doc: any) => aminoWallet.signAmino(addr, doc), + async signArbitrary(_chainId: string, signerAddress: string, data: string) { + const signDoc = makeAminoSignDoc( + [ + { + type: "sign/MsgSignData", + value: { + signer: signerAddress, + data: Buffer.from(data).toString("base64"), + }, + }, + ], + { gas: "0", amount: [] }, + "", // ADR-036 requires empty chain_id + "", + 0, + 0 + ); + const { signature } = await aminoWallet.signAmino(signerAddress, signDoc); + return { + signed: data, + signature: signature.signature, + pub_key: signature.pub_key, + }; + }, }; - // Calculate price based on file size - const price = calculateCascadeFee(fileData.length, params.fee_base, params.fee_per_kb); - console.log(` Calculated fee: ${price} ulume`); - - // Set expiration time (24 hours from now) - const expirationTime = Math.floor(Date.now() / 1000 + 86400).toString(); + console.log(" Wallets ready"); // ============================================================================ - // STEP 4: Build and broadcast MsgRequestAction using generated composer + // STEP 2: Create LumeraClient // ============================================================================ - console.log("\n[Step 4] Building MsgRequestAction with generated composer..."); - - // Use the Telescope-generated message composer - const msgRequestAction = lumera.action.v1.MessageComposer.withTypeUrl.requestAction({ - creator: account.address, - actionType: "cascade", - metadata: JSON.stringify(metadata), - price, - expirationTime, - }); + console.log("\n[Step 2] Creating LumeraClient..."); - console.log(`✓ Message created with typeUrl: ${msgRequestAction.typeUrl}`); - console.log(` Action type: ${msgRequestAction.value.actionType}`); - console.log(` Price: ${msgRequestAction.value.price}`); - - // Simulate transaction to estimate gas - console.log("\n Simulating transaction..."); - const gasEstimate = await chain.Tx.simulate(account.address, [msgRequestAction]); - console.log(` Estimated gas: ${gasEstimate}`); - - // Broadcast the transaction - console.log("\n Broadcasting transaction..."); - const result = await chain.Tx.signAndBroadcast( - account.address, - [msgRequestAction], - { amount: [{ denom: "ulume", amount: "10000" }], gas: gasEstimate.toString() }, - "Request Cascade action" - ); - - if (result.code !== 0) { - console.error(`✗ Transaction failed: ${result.rawLog}`); - process.exit(1); - } + const client = await createLumeraClient({ + preset: "testnet", + signer: signer as any, + address: account.address, + gasPrice: "0.025ulume", + }); - console.log(`✓ Transaction successful!`); - console.log(` TX Hash: ${result.transactionHash}`); - console.log(` Block Height: ${result.height}`); + console.log(" Connected to testnet"); // ============================================================================ - // STEP 5: Build and broadcast MsgFinalizeAction + // STEP 3: Upload a file via Cascade // ============================================================================ - console.log("\n[Step 5] Building MsgFinalizeAction with generated composer..."); + console.log("\n[Step 3] Uploading file via Cascade..."); - // In a real scenario, you would: - // 1. Upload the file data to supernodes - // 2. Get the RaptorQ IDs from supernodes - // 3. Include those in the finalize metadata + const file = new TextEncoder().encode("Hello, Lumera!"); + // Expiration must be at least 86400s from now; add buffer to avoid race with block time + const expirationTime = String(Math.floor(Date.now() / 1000) + 86400 + 600); - const finalizeMetadata = { - ...metadata, - rq_ids_max: params.rq_ids_max, - rq_ids: ["id1", "id2", "id3"], // Would be real RaptorQ IDs - }; + console.log(` File size: ${file.length} bytes`); + console.log(` Expiration: ${expirationTime}`); - // Use the Telescope-generated message composer - const msgFinalizeAction = lumera.action.v1.MessageComposer.withTypeUrl.finalizeAction({ - creator: account.address, - actionId, - actionType: "cascade", - metadata: JSON.stringify(finalizeMetadata), + const result = await client.Cascade.uploader.uploadFile(file, { + fileName: "hello.txt", + isPublic: true, + expirationTime, + taskOptions: { pollInterval: 2000, timeout: 300000 }, }); - console.log(`✓ Message created with typeUrl: ${msgFinalizeAction.typeUrl}`); - console.log(` Action ID: ${msgFinalizeAction.value.actionId}`); - - // Simulate and broadcast finalize transaction - console.log("\n Simulating finalize transaction..."); - const finalizeGas = await chain.Tx.simulate(account.address, [msgFinalizeAction]); - console.log(` Estimated gas: ${finalizeGas}`); - - console.log("\n Broadcasting finalize transaction..."); - const finalizeResult = await chain.Tx.signAndBroadcast( - account.address, - [msgFinalizeAction], - { amount: [{ denom: "ulume", amount: "10000" }], gas: finalizeGas.toString() }, - "Finalize Cascade action" - ); - - if (finalizeResult.code !== 0) { - console.error(`✗ Finalize transaction failed: ${finalizeResult.rawLog}`); - process.exit(1); - } - - console.log(`✓ Finalize transaction successful!`); - console.log(` TX Hash: ${finalizeResult.transactionHash}`); - console.log(` Block Height: ${finalizeResult.height}`); - - // ============================================================================ - // STEP 6: Verify action on blockchain - // ============================================================================ - console.log("\n[Step 6] Verifying action on blockchain..."); - - try { - const action = await chain.Action.getAction(actionId); - console.log(`✓ Action found on chain:`); - console.log(` Action ID: ${action.actionId}`); - console.log(` Action Type: ${action.actionType}`); - console.log(` Creator: ${action.creator}`); - console.log(` State: ${action.state}`); - } catch (error) { - console.log(` Note: Action query may fail if not yet indexed`); - } - console.log("\n" + "=".repeat(60)); - console.log("SUCCESS! Demonstrated direct message composition workflow"); + console.log("Upload complete!"); console.log("=".repeat(60)); - console.log("\nKey takeaways:"); - console.log(" - Used lumera.action.v1.MessageComposer.withTypeUrl for messages"); - console.log(" - Demonstrated both RequestAction and FinalizeAction"); - console.log(" - Showed gas estimation and transaction broadcasting"); - console.log(" - This is the low-level approach for advanced use cases"); - console.log("\nFor simpler workflows, use the LumeraClient facade instead!"); + console.log(result); } main().catch((error) => { - console.error("\n✗ Fatal error:"); + console.error("\nFatal error:"); console.error(error); process.exit(1); -}); \ No newline at end of file +}); diff --git a/package.json b/package.json index 88646fc..fcf7a5b 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,13 @@ "LICENSE" ], "type": "module", - "main": "dist/cjs/index.cjs", + "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "types": "dist/types/index.d.ts", "exports": { ".": { "import": "./dist/esm/index.js", - "require": "./dist/cjs/index.cjs", + "require": "./dist/cjs/index.js", "types": "./dist/types/index.d.ts" }, "./compat/blake3": { @@ -37,7 +37,7 @@ "clean": "rm -rf dist", "build:esm": "tsc -p tsconfig.json", "build:cjs": "tsc -p tsconfig.cjs.json", - "build": "npm run clean && npm run build:esm && npm run build:cjs", + "build": "npm run clean && npm run build:esm && npm run build:cjs && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json", "gen:snapi": "openapi-typescript docs/snapi-swagger.json -o src/types/snapi.gen.ts", "lint": "eslint 'src/**/*.{ts,tsx}'", "test": "vitest run", diff --git a/src/wasm/raptorq-proxy.ts b/src/wasm/raptorq-proxy.ts index 5e8d254..73a80c2 100644 --- a/src/wasm/raptorq-proxy.ts +++ b/src/wasm/raptorq-proxy.ts @@ -6,19 +6,39 @@ * interface for RaptorQ operations. */ -import init, { - RaptorQSession, - writeFileChunk, - readFileChunk, - getFileSize, - createDirAll, - dirExists, - syncDirExists, - flushFile -} from "rq-library-wasm"; -import wasmUrl from 'rq-library-wasm/rq_library_bg.wasm?url'; import type { Layout } from "./types.js"; +// Lazy-load rq-library-wasm to avoid eager evaluation of browser-only code (window) +let _rqMod: typeof import("rq-library-wasm") | null = null; +async function getRqModule() { + if (!_rqMod) { + // Shim window for Node.js — rq-library-wasm's browser_fs_mem.js expects it + if (typeof globalThis.window === "undefined") { + (globalThis as any).window = globalThis; + } + _rqMod = await import("rq-library-wasm"); + } + return _rqMod; +} + +const isNode = typeof process !== "undefined" && !!process.versions?.node; + +async function getWasmSource(): Promise { + if (isNode) { + // Node.js: read the .wasm file from disk as a buffer + const { readFileSync } = await import("node:fs"); + const { createRequire } = await import("node:module"); + // __filename exists in CJS; for ESM, anchor on cwd — Node resolves up to node_modules either way + const req = createRequire(typeof __filename !== "undefined" ? __filename : process.cwd() + "/"); + const wasmPath = req.resolve("rq-library-wasm/rq_library_bg.wasm"); + return readFileSync(wasmPath); + } else { + // Browser: use the Vite ?url import + const mod = await import("rq-library-wasm/rq_library_bg.wasm?url"); + return mod.default; + } +} + /** * Proxy to the rq-library-wasm package for RaptorQ operations. * @@ -119,7 +139,9 @@ export class RaptorQProxy { try { // Pass the wasmUrl to the init function for browser environments // The init() function will use the URL to fetch the WASM module - await init(wasmUrl); + const wasmSource = await getWasmSource(); + const rq = await getRqModule(); + await rq.default(wasmSource); this.initialized = true; } catch (error) { this.initPromise = null; @@ -145,10 +167,11 @@ export class RaptorQProxy { redundancyFactor: number = RaptorQProxy.DEFAULT_REDUNDANCY_FACTOR, maxMemoryMb: bigint = RaptorQProxy.DEFAULT_MAX_MEMORY_MB, concurrencyLimit: bigint = RaptorQProxy.DEFAULT_CONCURRENCY_LIMIT - ): Promise { + ): Promise>["RaptorQSession"]>> { await this.ensureInitialized(); - - return new RaptorQSession( + const rq = await getRqModule(); + + return new rq.RaptorQSession( symbolSize, redundancyFactor, maxMemoryMb, @@ -188,20 +211,22 @@ export class RaptorQProxy { const layoutPath = `/temp_layout_${timestamp}.json`; try { + const rq = await getRqModule(); + // Step 1: Write file bytes to in-memory FS // IMPORTANT: File must be in FS before creating session! - await writeFileChunk(inputPath, 0, fileBytes); - + await rq.writeFileChunk(inputPath, 0, fileBytes); + // Step 2: Create a session for metadata generation const session = await this.createSession(); - + // Step 3: Call create_metadata to generate the layout // block_size = 0 means auto-calculate const metadata = await session.create_metadata(inputPath, layoutPath, 0); - + // Step 4: Read the layout file from in-memory FS - const layoutSize = getFileSize(layoutPath); - const layoutBytes = await readFileChunk(layoutPath, 0, layoutSize); + const layoutSize = rq.getFileSize(layoutPath); + const layoutBytes = await rq.readFileChunk(layoutPath, 0, layoutSize); // Step 5: Clean up session.free(); @@ -245,7 +270,8 @@ export class RaptorQProxy { public async getVersion(): Promise { await this.ensureInitialized(); - return RaptorQSession.version(); + const rq = await getRqModule(); + return rq.RaptorQSession.version(); } /** @@ -270,16 +296,34 @@ export function parseLayoutFile(layoutBytes: Uint8Array): Layout { return JSON.parse(text) as Layout; } -// Re-export types and classes for convenience -export type { RaptorQSession }; +// Re-export types for convenience +export type { RaptorQSession } from "rq-library-wasm"; + +// Re-export filesystem utilities — preserves original sync/async signatures. +// Requires RaptorQProxy.initialize() to have been called first. +function assertInitialized() { + if (!_rqMod) throw new Error("WASM module not initialized. Call RaptorQProxy.initialize() first."); + return _rqMod; +} -// Re-export filesystem utilities for advanced use cases -export { - writeFileChunk, - readFileChunk, - getFileSize, - createDirAll, - dirExists, - syncDirExists, - flushFile -}; \ No newline at end of file +export function writeFileChunk(...args: Parameters) { + return assertInitialized().writeFileChunk(...args); +} +export function readFileChunk(...args: Parameters) { + return assertInitialized().readFileChunk(...args); +} +export function getFileSize(...args: Parameters) { + return assertInitialized().getFileSize(...args); +} +export function createDirAll(...args: Parameters) { + return assertInitialized().createDirAll(...args); +} +export function dirExists(...args: Parameters) { + return assertInitialized().dirExists(...args); +} +export function syncDirExists(...args: Parameters) { + return assertInitialized().syncDirExists(...args); +} +export function flushFile(...args: Parameters) { + return assertInitialized().flushFile(...args); +} \ No newline at end of file diff --git a/tests/wasm/raptorq-proxy.spec.ts b/tests/wasm/raptorq-proxy.spec.ts index 4c29f12..809351f 100644 --- a/tests/wasm/raptorq-proxy.spec.ts +++ b/tests/wasm/raptorq-proxy.spec.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; // Create mocks for the rq-library-wasm package const sessionMock = { get_recommended_block_size: vi.fn().mockReturnValue(1024 * 1024), // 1MB - create_metadata: vi.fn().mockResolvedValue({ success: true }), + create_metadata: vi.fn(), free: vi.fn(), }; @@ -14,7 +14,7 @@ RaptorQSessionMock.version = vi.fn().mockReturnValue("1.0.0"); // Mock filesystem functions const mockFileSystem = new Map(); -const writeFileChunkMock = vi.fn().mockImplementation(async (path: string, offset: number, data: Uint8Array) => { +const writeFileChunkMock = vi.fn().mockImplementation(async (path: string, _offset: number, data: Uint8Array) => { mockFileSystem.set(path, data); }); @@ -43,7 +43,18 @@ vi.mock("rq-library-wasm", () => ({ flushFile: vi.fn(), })); -// Mock the WASM URL import +// Mock Node.js modules used by getWasmSource +vi.mock("node:fs", () => ({ + readFileSync: vi.fn().mockReturnValue(new Uint8Array([0, 1, 2])), +})); + +vi.mock("node:module", () => ({ + createRequire: vi.fn(() => ({ + resolve: vi.fn().mockReturnValue("/mocked/path/to/rq_library_bg.wasm"), + })), +})); + +// Mock the WASM URL import (browser path) vi.mock("rq-library-wasm/rq_library_bg.wasm?url", () => ({ default: "/mocked/path/to/wasm", })); @@ -58,33 +69,46 @@ describe("RaptorQProxy", () => { vi.clearAllMocks(); vi.resetModules(); mockFileSystem.clear(); - + // Reset mock implementations initMock.mockResolvedValue(undefined); sessionMock.get_recommended_block_size.mockReturnValue(1024 * 1024); - sessionMock.create_metadata.mockResolvedValue({ success: true }); - - // Mock layout file creation - writeFileChunkMock.mockImplementation(async (path: string, offset: number, data: Uint8Array) => { + + writeFileChunkMock.mockImplementation(async (path: string, _offset: number, data: Uint8Array) => { mockFileSystem.set(path, data); - // If this is a layout file being written by create_metadata, simulate it - if (path.includes('layout')) { - const layout = { - transfer_length: 100, - symbol_size: 65535, - num_source_blocks: 1, - num_sub_blocks: 1, - symbol_alignment: 8, - source_blocks: [{ - source_symbols: 2, - sub_symbols: 1, - sub_symbol_size: 8 - }] - }; - mockFileSystem.set(path, new TextEncoder().encode(JSON.stringify(layout))); - } }); - + + readFileChunkMock.mockImplementation(async (path: string, offset: number, length: number) => { + const data = mockFileSystem.get(path); + if (!data) throw new Error(`File not found: ${path}`); + return data.slice(offset, offset + length); + }); + + getFileSizeMock.mockImplementation((path: string) => { + const data = mockFileSystem.get(path); + if (!data) throw new Error(`File not found: ${path}`); + return data.length; + }); + + // Mock create_metadata to write the layout file into the mock filesystem + sessionMock.create_metadata.mockImplementation(async (inputPath: string, layoutPath: string, _blockSize: number) => { + const inputData = mockFileSystem.get(inputPath); + const layout = { + transfer_length: inputData?.length ?? 0, + symbol_size: 65535, + num_source_blocks: 1, + num_sub_blocks: 1, + symbol_alignment: 8, + source_blocks: [{ + source_symbols: 2, + sub_symbols: 1, + sub_symbol_size: 8 + }] + }; + mockFileSystem.set(layoutPath, new TextEncoder().encode(JSON.stringify(layout))); + return { success: true }; + }); + // Re-setup the version static method after clearAllMocks RaptorQSessionMock.version = vi.fn().mockReturnValue("1.0.0"); }); @@ -94,33 +118,32 @@ describe("RaptorQProxy", () => { RaptorQProxy.resetInstance(); const proxy = RaptorQProxy.getInstance(); - + // Create two layouts const result1 = await proxy.createSingleBlockLayout(new Uint8Array(100)); const result2 = await proxy.createSingleBlockLayout(new Uint8Array(200)); // Verify init was only called once expect(initMock).toHaveBeenCalledTimes(1); - expect(initMock).toHaveBeenCalledWith("/mocked/path/to/wasm"); - + // Verify sessions were created twice (once per layout) expect(RaptorQSessionMock).toHaveBeenCalledTimes(2); - expect(RaptorQSessionMock).toHaveBeenCalledWith(65535, 10, 1024n, 4n); - + expect(RaptorQSessionMock).toHaveBeenCalledWith(65535, 6, 4096n, 1n); + // Verify session methods were called - expect(sessionMock.get_recommended_block_size).toHaveBeenCalledTimes(2); + expect(sessionMock.create_metadata).toHaveBeenCalledTimes(2); expect(sessionMock.free).toHaveBeenCalledTimes(2); - + // Verify results are valid layout JSON expect(result1).toBeInstanceOf(Uint8Array); expect(result2).toBeInstanceOf(Uint8Array); - + const layout1 = JSON.parse(new TextDecoder().decode(result1)); const layout2 = JSON.parse(new TextDecoder().decode(result2)); - + expect(layout1).toHaveProperty("transfer_length", 100); expect(layout2).toHaveProperty("transfer_length", 200); - + expect(proxy.isInitialized()).toBe(true); }); @@ -139,12 +162,12 @@ describe("RaptorQProxy", () => { // Verify init was only called once despite concurrent calls expect(initMock).toHaveBeenCalledTimes(1); - + // Verify sessions were created three times expect(RaptorQSessionMock).toHaveBeenCalledTimes(3); - + // Verify session methods were called for each layout - expect(sessionMock.get_recommended_block_size).toHaveBeenCalledTimes(3); + expect(sessionMock.create_metadata).toHaveBeenCalledTimes(3); expect(sessionMock.free).toHaveBeenCalledTimes(3); }); @@ -172,14 +195,14 @@ describe("RaptorQProxy", () => { // Second attempt should succeed const result = await proxy.createSingleBlockLayout(new Uint8Array(200)); - + expect(result).toBeInstanceOf(Uint8Array); const layout = JSON.parse(new TextDecoder().decode(result)); expect(layout).toHaveProperty("transfer_length", 200); // Verify init was called twice (once failed, once succeeded) expect(initMock).toHaveBeenCalledTimes(2); - + // Verify session was only created once (after successful init) expect(RaptorQSessionMock).toHaveBeenCalledTimes(1); expect(sessionMock.free).toHaveBeenCalledTimes(1); @@ -193,7 +216,7 @@ describe("RaptorQProxy", () => { const session = await proxy.createSession(); expect(initMock).toHaveBeenCalledTimes(1); - expect(RaptorQSessionMock).toHaveBeenCalledWith(65535, 10, 1024n, 4n); + expect(RaptorQSessionMock).toHaveBeenCalledWith(65535, 6, 4096n, 1n); expect(session).toBe(sessionMock); }); @@ -246,11 +269,11 @@ describe("RaptorQProxy", () => { RaptorQProxy.resetInstance(); const proxy = RaptorQProxy.getInstance(); - + expect(proxy.isInitialized()).toBe(false); - + await proxy.initialize(); - + expect(proxy.isInitialized()).toBe(true); }); -}); \ No newline at end of file +});