Lint - also gonna leave this branch and probably drop it. Dagur has a design plan.
This commit is contained in:
parent
cdd357cb08
commit
52d710dbb5
6 changed files with 179 additions and 39 deletions
|
|
@ -4,7 +4,10 @@
|
|||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { ChunkAssembler, CHUNK_TIMEOUT_MS } from "./chunk-assembler.js";
|
||||
import { type ChunkRelayMessage, RelayMsgAction } from "./protocols/hdwalletv1.js";
|
||||
import {
|
||||
type ChunkRelayMessage,
|
||||
RelayMsgAction,
|
||||
} from "./protocols/hdwalletv1.js";
|
||||
|
||||
function makeChunk(
|
||||
chunk_id: string,
|
||||
|
|
@ -22,12 +25,23 @@ function makeChunk(
|
|||
};
|
||||
}
|
||||
|
||||
function splitMessage(message: object, chunkSize: number, chunk_id: string): ChunkRelayMessage[] {
|
||||
function splitMessage(
|
||||
message: object,
|
||||
chunkSize: number,
|
||||
chunk_id: string,
|
||||
): ChunkRelayMessage[] {
|
||||
const json = JSON.stringify(message);
|
||||
const chunks: ChunkRelayMessage[] = [];
|
||||
const total = Math.ceil(json.length / chunkSize);
|
||||
for (let i = 0; i < total; i++) {
|
||||
chunks.push(makeChunk(chunk_id, i, total, json.slice(i * chunkSize, (i + 1) * chunkSize)));
|
||||
chunks.push(
|
||||
makeChunk(
|
||||
chunk_id,
|
||||
i,
|
||||
total,
|
||||
json.slice(i * chunkSize, (i + 1) * chunkSize),
|
||||
),
|
||||
);
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
|
@ -46,7 +60,11 @@ describe("ChunkAssembler", () => {
|
|||
|
||||
describe("single-fragment messages", () => {
|
||||
it("reassembles a single-fragment message immediately", () => {
|
||||
const original = { action: RelayMsgAction.SignCancel, sequence: 1, time: 1000 };
|
||||
const original = {
|
||||
action: RelayMsgAction.SignCancel,
|
||||
sequence: 1,
|
||||
time: 1000,
|
||||
};
|
||||
const [chunk] = splitMessage(original, 10000, "id1");
|
||||
|
||||
const result = assembler.addChunk(chunk);
|
||||
|
|
@ -55,7 +73,11 @@ describe("ChunkAssembler", () => {
|
|||
});
|
||||
|
||||
it("clears the pending buffer after assembly", () => {
|
||||
const original = { action: RelayMsgAction.SignCancel, sequence: 1, time: 1000 };
|
||||
const original = {
|
||||
action: RelayMsgAction.SignCancel,
|
||||
sequence: 1,
|
||||
time: 1000,
|
||||
};
|
||||
const [chunk] = splitMessage(original, 10000, "id1");
|
||||
|
||||
assembler.addChunk(chunk);
|
||||
|
|
@ -66,7 +88,11 @@ describe("ChunkAssembler", () => {
|
|||
|
||||
describe("multi-fragment messages", () => {
|
||||
it("returns null for each fragment until the last", () => {
|
||||
const original = { action: RelayMsgAction.SignCancel, sequence: 7, time: 1000 };
|
||||
const original = {
|
||||
action: RelayMsgAction.SignCancel,
|
||||
sequence: 7,
|
||||
time: 1000,
|
||||
};
|
||||
const chunks = splitMessage(original, 5, "id2");
|
||||
expect(chunks.length).toBeGreaterThan(1);
|
||||
|
||||
|
|
@ -76,7 +102,11 @@ describe("ChunkAssembler", () => {
|
|||
});
|
||||
|
||||
it("returns the reassembled message on the last fragment", () => {
|
||||
const original = { action: RelayMsgAction.SignCancel, sequence: 7, time: 1000 };
|
||||
const original = {
|
||||
action: RelayMsgAction.SignCancel,
|
||||
sequence: 7,
|
||||
time: 1000,
|
||||
};
|
||||
const chunks = splitMessage(original, 5, "id3");
|
||||
|
||||
let result = null;
|
||||
|
|
@ -88,7 +118,11 @@ describe("ChunkAssembler", () => {
|
|||
});
|
||||
|
||||
it("reassembles correctly when fragments arrive out of order", () => {
|
||||
const original = { action: RelayMsgAction.SignCancel, sequence: 42, time: 2000 };
|
||||
const original = {
|
||||
action: RelayMsgAction.SignCancel,
|
||||
sequence: 42,
|
||||
time: 2000,
|
||||
};
|
||||
const chunks = splitMessage(original, 3, "id4");
|
||||
expect(chunks.length).toBeGreaterThan(1);
|
||||
|
||||
|
|
@ -146,8 +180,16 @@ describe("ChunkAssembler", () => {
|
|||
|
||||
describe("independent chunk_ids", () => {
|
||||
it("tracks multiple concurrent assemblies independently", () => {
|
||||
const msgA = { action: RelayMsgAction.SignCancel, sequence: 1, time: 1000 };
|
||||
const msgB = { action: RelayMsgAction.SignCancel, sequence: 2, time: 2000 };
|
||||
const msgA = {
|
||||
action: RelayMsgAction.SignCancel,
|
||||
sequence: 1,
|
||||
time: 1000,
|
||||
};
|
||||
const msgB = {
|
||||
action: RelayMsgAction.SignCancel,
|
||||
sequence: 2,
|
||||
time: 2000,
|
||||
};
|
||||
const chunksA = splitMessage(msgA, 5, "idA");
|
||||
const chunksB = splitMessage(msgB, 5, "idB");
|
||||
|
||||
|
|
@ -165,8 +207,16 @@ describe("ChunkAssembler", () => {
|
|||
});
|
||||
|
||||
it("completing one assembly does not affect pending assemblies", () => {
|
||||
const msgA = { action: RelayMsgAction.SignCancel, sequence: 1, time: 1000 };
|
||||
const msgB = { action: RelayMsgAction.SignCancel, sequence: 2, time: 2000 };
|
||||
const msgA = {
|
||||
action: RelayMsgAction.SignCancel,
|
||||
sequence: 1,
|
||||
time: 1000,
|
||||
};
|
||||
const msgB = {
|
||||
action: RelayMsgAction.SignCancel,
|
||||
sequence: 2,
|
||||
time: 2000,
|
||||
};
|
||||
const [chunkA] = splitMessage(msgA, 10000, "idA");
|
||||
const chunksB = splitMessage(msgB, 5, "idB");
|
||||
|
||||
|
|
@ -224,7 +274,11 @@ describe("ChunkAssembler", () => {
|
|||
|
||||
describe("timeout cleanup", () => {
|
||||
it("discards incomplete assembly after timeout", () => {
|
||||
const msgA = { action: RelayMsgAction.SignCancel, sequence: 1, time: 1000 };
|
||||
const msgA = {
|
||||
action: RelayMsgAction.SignCancel,
|
||||
sequence: 1,
|
||||
time: 1000,
|
||||
};
|
||||
const chunks = splitMessage(msgA, 5, "idTimeout");
|
||||
expect(chunks.length).toBeGreaterThan(1);
|
||||
|
||||
|
|
@ -240,7 +294,11 @@ describe("ChunkAssembler", () => {
|
|||
});
|
||||
|
||||
it("does not timeout before CHUNK_TIMEOUT_MS", () => {
|
||||
const msgA = { action: RelayMsgAction.SignCancel, sequence: 1, time: 1000 };
|
||||
const msgA = {
|
||||
action: RelayMsgAction.SignCancel,
|
||||
sequence: 1,
|
||||
time: 1000,
|
||||
};
|
||||
const chunks = splitMessage(msgA, 5, "idTimeout2");
|
||||
|
||||
for (let i = 0; i < chunks.length - 1; i++) {
|
||||
|
|
@ -253,7 +311,11 @@ describe("ChunkAssembler", () => {
|
|||
});
|
||||
|
||||
it("clears timeout when assembly completes before timeout", () => {
|
||||
const msgA = { action: RelayMsgAction.SignCancel, sequence: 1, time: 1000 };
|
||||
const msgA = {
|
||||
action: RelayMsgAction.SignCancel,
|
||||
sequence: 1,
|
||||
time: 1000,
|
||||
};
|
||||
const chunks = splitMessage(msgA, 5, "idTimeout3");
|
||||
|
||||
for (const chunk of chunks) {
|
||||
|
|
@ -266,7 +328,11 @@ describe("ChunkAssembler", () => {
|
|||
});
|
||||
|
||||
it("after timeout, late arriving fragments start a fresh assembly", () => {
|
||||
const original = { action: RelayMsgAction.SignCancel, sequence: 1, time: 1000 };
|
||||
const original = {
|
||||
action: RelayMsgAction.SignCancel,
|
||||
sequence: 1,
|
||||
time: 1000,
|
||||
};
|
||||
const chunks = splitMessage(original, 5, "idReuse");
|
||||
|
||||
// Send first fragment, let it time out
|
||||
|
|
@ -285,7 +351,11 @@ describe("ChunkAssembler", () => {
|
|||
|
||||
describe("duplicate fragment index", () => {
|
||||
it("last write wins for duplicate index — still assembles correctly if content is the same", () => {
|
||||
const original = { action: RelayMsgAction.SignCancel, sequence: 1, time: 1000 };
|
||||
const original = {
|
||||
action: RelayMsgAction.SignCancel,
|
||||
sequence: 1,
|
||||
time: 1000,
|
||||
};
|
||||
const chunks = splitMessage(original, 5, "idDup");
|
||||
expect(chunks.length).toBeGreaterThan(1);
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,10 @@ export class ChunkAssembler {
|
|||
const { chunk_id, chunk_index, chunk_total, chunk_data } = chunk;
|
||||
|
||||
if (chunk_total < 1) {
|
||||
logError(Scope.Relay, `Invalid chunk_total ${chunk_total} for chunk_id ${chunk_id}`);
|
||||
logError(
|
||||
Scope.Relay,
|
||||
`Invalid chunk_total ${chunk_total} for chunk_id ${chunk_id}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -81,14 +84,20 @@ export class ChunkAssembler {
|
|||
return this.pending.size;
|
||||
}
|
||||
|
||||
private assemble(chunk_id: string, assembly: PendingAssembly): ProtocolMessage | null {
|
||||
private assemble(
|
||||
chunk_id: string,
|
||||
assembly: PendingAssembly,
|
||||
): ProtocolMessage | null {
|
||||
this.discard(chunk_id);
|
||||
|
||||
const parts: string[] = [];
|
||||
for (let i = 0; i < assembly.total; i++) {
|
||||
const part = assembly.fragments.get(i);
|
||||
if (part === undefined) {
|
||||
logError(Scope.Relay, `Missing fragment index ${i} for chunk_id ${chunk_id}`);
|
||||
logError(
|
||||
Scope.Relay,
|
||||
`Missing fragment index ${i} for chunk_id ${chunk_id}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
parts.push(part);
|
||||
|
|
@ -99,12 +108,18 @@ export class ChunkAssembler {
|
|||
try {
|
||||
parsed = JSON.parse(json);
|
||||
} catch {
|
||||
logError(Scope.Relay, `Failed to parse assembled message for chunk_id ${chunk_id}`);
|
||||
logError(
|
||||
Scope.Relay,
|
||||
`Failed to parse assembled message for chunk_id ${chunk_id}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isProtocolMessage(parsed)) {
|
||||
logError(Scope.Relay, `Assembled message for chunk_id ${chunk_id} is not a ProtocolMessage`);
|
||||
logError(
|
||||
Scope.Relay,
|
||||
`Assembled message for chunk_id ${chunk_id} is not a ProtocolMessage`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -259,7 +259,10 @@ export class RelayClient extends EventEmitter {
|
|||
}
|
||||
|
||||
const messageJson = JSON.stringify(message);
|
||||
if (messageJson.length > MAX_SAFE_MESSAGE_SIZE && this.peerSupportsChunking) {
|
||||
if (
|
||||
messageJson.length > MAX_SAFE_MESSAGE_SIZE &&
|
||||
this.peerSupportsChunking
|
||||
) {
|
||||
return this.relayChunked(message.action, messageJson);
|
||||
}
|
||||
|
||||
|
|
@ -270,12 +273,18 @@ export class RelayClient extends EventEmitter {
|
|||
return this.publishMessage(message);
|
||||
}
|
||||
|
||||
private async relayChunked(originalAction: string, messageJson: string): Promise<void> {
|
||||
private async relayChunked(
|
||||
originalAction: string,
|
||||
messageJson: string,
|
||||
): Promise<void> {
|
||||
const chunkId = Math.random().toString(36).slice(2, 14);
|
||||
const chunkTotal = Math.ceil(messageJson.length / CHUNK_SIZE);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
debug(Scope.Relay, `Chunking ${originalAction} into ${chunkTotal} fragments (${messageJson.length} chars)`);
|
||||
debug(
|
||||
Scope.Relay,
|
||||
`Chunking ${originalAction} into ${chunkTotal} fragments (${messageJson.length} chars)`,
|
||||
);
|
||||
|
||||
for (let i = 0; i < chunkTotal; i++) {
|
||||
const chunk: ChunkRelayMessage = {
|
||||
|
|
@ -457,7 +466,10 @@ export class RelayClient extends EventEmitter {
|
|||
if (isChunkRelayMessage(message)) {
|
||||
const assembled = this.chunkAssembler.addChunk(message);
|
||||
if (assembled !== null) {
|
||||
this.netlog("recv", `${assembled.action} [reassembled from ${message.chunk_total} chunks]`);
|
||||
this.netlog(
|
||||
"recv",
|
||||
`${assembled.action} [reassembled from ${message.chunk_total} chunks]`,
|
||||
);
|
||||
this.emit("message", assembled);
|
||||
}
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -474,7 +474,9 @@ export class DappConnectionManager extends EventEmitter<DappConnectionManagerEve
|
|||
}
|
||||
|
||||
// Enable chunked sends if the wallet advertises the transport extension
|
||||
this.conn?.setPeerSupportsChunking(!!sessionData.extensions?.[EXTENSION_CHUNKED_MESSAGES]);
|
||||
this.conn?.setPeerSupportsChunking(
|
||||
!!sessionData.extensions?.[EXTENSION_CHUNKED_MESSAGES],
|
||||
);
|
||||
|
||||
// Store raw paths for getSessionPaths() and xpub nodes for derivation
|
||||
this.sessionPaths = [...sessionData.paths];
|
||||
|
|
|
|||
|
|
@ -124,8 +124,21 @@ async function sendLargeSignRequest(
|
|||
contractName: "CauldronV4",
|
||||
constructorInputs: [{ name: "ownerPkh", type: "bytes20" }],
|
||||
abi: [
|
||||
{ name: "trade", inputs: [{ name: "isAdd", type: "bool" }, { name: "tokenAmount", type: "uint64" }, { name: "satoshiAmount", type: "uint64" }] },
|
||||
{ name: "withdraw", inputs: [{ name: "sig", type: "datasig" }, { name: "pk", type: "pubkey" }] },
|
||||
{
|
||||
name: "trade",
|
||||
inputs: [
|
||||
{ name: "isAdd", type: "bool" },
|
||||
{ name: "tokenAmount", type: "uint64" },
|
||||
{ name: "satoshiAmount", type: "uint64" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "withdraw",
|
||||
inputs: [
|
||||
{ name: "sig", type: "datasig" },
|
||||
{ name: "pk", type: "pubkey" },
|
||||
],
|
||||
},
|
||||
],
|
||||
bytecode: "d".repeat(500),
|
||||
source: "// CASL source\n" + "e".repeat(800),
|
||||
|
|
@ -163,12 +176,18 @@ async function sendLargeSignRequest(
|
|||
|
||||
console.log(
|
||||
chalk.yellow(`→ sign_transaction_request (LARGE)`) +
|
||||
chalk.dim(` seq=${sequence} ${json.length} chars → ${chunkCount} chunk${chunkCount !== 1 ? "s" : ""}`),
|
||||
chalk.dim(
|
||||
` seq=${sequence} ${json.length} chars → ${chunkCount} chunk${chunkCount !== 1 ? "s" : ""}`,
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
await (client as any).relay(msg);
|
||||
console.log(chalk.green(` ✓ sent — if wallet receives seq=${sequence} with all ${sourceOutputs.length} sourceOutputs, chunking works`));
|
||||
console.log(
|
||||
chalk.green(
|
||||
` ✓ sent — if wallet receives seq=${sequence} with all ${sourceOutputs.length} sourceOutputs, chunking works`,
|
||||
),
|
||||
);
|
||||
} catch (err: any) {
|
||||
console.error(chalk.red(" ✗ send error:"), err.message);
|
||||
}
|
||||
|
|
@ -197,7 +216,8 @@ function handleMessage(
|
|||
| Hdwalletv1Session
|
||||
| undefined;
|
||||
const pathsSummary = hdwv1?.paths?.map((p) => p.name).join(",") ?? "none";
|
||||
const supportsChunking = hdwv1?.extensions?.[EXTENSION_CHUNKED_MESSAGES] !== undefined;
|
||||
const supportsChunking =
|
||||
hdwv1?.extensions?.[EXTENSION_CHUNKED_MESSAGES] !== undefined;
|
||||
|
||||
if (supportsChunking) {
|
||||
client.setPeerSupportsChunking(true);
|
||||
|
|
@ -210,7 +230,9 @@ function handleMessage(
|
|||
` dapp_discovered=${msg.dapp_discovered}` +
|
||||
` protocols=[${msg.supported_protocols.join(",")}]` +
|
||||
` paths=[${pathsSummary}]` +
|
||||
(supportsChunking ? chalk.cyan(" chunking=✓") : chalk.dim(" chunking=✗")),
|
||||
(supportsChunking
|
||||
? chalk.cyan(" chunking=✓")
|
||||
: chalk.dim(" chunking=✗")),
|
||||
);
|
||||
|
||||
if (!msg.dapp_discovered) {
|
||||
|
|
@ -230,7 +252,9 @@ function handleMessage(
|
|||
|
||||
if (sendLargeSign && state.walletReady) {
|
||||
console.log(
|
||||
chalk.dim(" (--large-sign: scheduling large sign request in 500ms...)"),
|
||||
chalk.dim(
|
||||
" (--large-sign: scheduling large sign request in 500ms...)",
|
||||
),
|
||||
);
|
||||
setTimeout(() => {
|
||||
sendLargeSignRequest(client, state).catch(() => {});
|
||||
|
|
@ -279,9 +303,17 @@ export async function runDappMode(options: {
|
|||
console.log(chalk.bold("\nwiz-test dapp mode"));
|
||||
console.log(chalk.dim(`relay: ${options.relay}`));
|
||||
if (options.sign)
|
||||
console.log(chalk.yellow("--sign: will send dummy sign request after wallet is ready"));
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
"--sign: will send dummy sign request after wallet is ready",
|
||||
),
|
||||
);
|
||||
if (options.largeSign)
|
||||
console.log(chalk.yellow("--large-sign: will send 30-contract-input sign request (chunking smoke test)"));
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
"--large-sign: will send 30-contract-input sign request (chunking smoke test)",
|
||||
),
|
||||
);
|
||||
console.log();
|
||||
|
||||
let connected = false;
|
||||
|
|
@ -361,7 +393,13 @@ export async function runDappMode(options: {
|
|||
|
||||
// Register message handler
|
||||
client.on("message", (message: ProtocolMessage) => {
|
||||
handleMessage(message, client, state, options.sign ?? false, options.largeSign ?? false);
|
||||
handleMessage(
|
||||
message,
|
||||
client,
|
||||
state,
|
||||
options.sign ?? false,
|
||||
options.largeSign ?? false,
|
||||
);
|
||||
});
|
||||
|
||||
// Send initial dapp_ready
|
||||
|
|
|
|||
|
|
@ -120,11 +120,14 @@ export async function runWalletMode(options: {
|
|||
});
|
||||
|
||||
manager.on("pendingSignRequest", (request) => {
|
||||
const sourceCount = (request.request.transaction as any)?.sourceOutputs?.length ?? "?";
|
||||
const sourceCount =
|
||||
(request.request.transaction as any)?.sourceOutputs?.length ?? "?";
|
||||
const jsonSize = JSON.stringify(request.request).length;
|
||||
console.log(
|
||||
chalk.yellow("← sign_request") +
|
||||
chalk.dim(` conn=${request.connectionId} seq=${request.request.sequence}`) +
|
||||
chalk.dim(
|
||||
` conn=${request.connectionId} seq=${request.request.sequence}`,
|
||||
) +
|
||||
chalk.dim(` ${jsonSize} chars sourceOutputs=${sourceCount}`),
|
||||
);
|
||||
console.log(chalk.dim(" (auto-rejecting — test wallet does not sign)"));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue