Merge branch 'input-paths' into 'master'

Add input paths for a signature request

See merge request riftenlabs/lib/wizardconnect!6
This commit is contained in:
Dagur Valberg Johannsson 2026-03-16 14:29:38 +00:00
commit 6d4d8139f0
7 changed files with 255 additions and 7 deletions

View file

@ -133,6 +133,7 @@ const request: SignTransactionRequest = {
userPrompt: "Confirm swap",
broadcast: true,
},
inputPaths: [["receive", 0], ["defi", 5]], // one per sourceOutput
};
try {

View file

@ -229,6 +229,7 @@ interface SignTransactionRequest {
action: "sign_transaction_request";
transaction: WcSignTransactionRequest; // from @bch-wc2/interfaces
sequence: number;
inputPaths: [PathName, number][]; // [pathName, addressIndex] per sourceOutput
time: number;
}
```
@ -241,6 +242,12 @@ to match responses to requests.
(for signing), version, locktime, and an optional `userPrompt` string shown to the user in the
wallet UI.
`inputPaths` is a parallel array to `transaction.sourceOutputs` (and to `transaction.transaction.inputs`).
Each entry is a `[PathName, number]` tuple identifying the HD derivation path name and address index
that the dapp used to derive the locking script for that input. This allows the wallet to sign each
input without scanning or guessing which key was used. The array must have the same length as
`sourceOutputs`.
### sign_transaction_response
```typescript

12
package-lock.json generated
View file

@ -2418,9 +2418,9 @@
}
},
"node_modules/flatted": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz",
"integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==",
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
"integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",
"dev": true,
"license": "ISC"
},
@ -4100,7 +4100,7 @@
},
"packages/core": {
"name": "@wizardconnect/core",
"version": "0.1.0",
"version": "0.1.2",
"dependencies": {
"@bch-wc2/interfaces": "^0.0.8",
"@bitauth/libauth": "^3.1.0-next.2",
@ -4117,7 +4117,7 @@
},
"packages/dapp": {
"name": "@wizardconnect/dapp",
"version": "0.1.0",
"version": "0.1.2",
"dependencies": {
"@wizardconnect/core": "*",
"eventemitter3": "^5.0.1"
@ -4149,7 +4149,7 @@
},
"packages/wallet": {
"name": "@wizardconnect/wallet",
"version": "0.1.0",
"version": "0.1.2",
"dependencies": {
"@bitauth/libauth": "^3.1.0-next.2",
"@wizardconnect/core": "*",

View file

@ -0,0 +1,69 @@
// Copyright (C) 2026 Whiterun LLC,
// This software is licensed under the GNU Lesser General Public License (LGPL), version 3.0 or later.
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/lgpl-3.0.html
import { describe, it, expect } from "vitest";
import { isSignTransactionRequest, RelayMsgAction } from "./hdwalletv1.js";
describe("isSignTransactionRequest", () => {
const valid = {
action: RelayMsgAction.SignTransactionRequest,
transaction: { transaction: {}, sourceOutputs: [] },
sequence: 1,
inputPaths: [
["receive", 0],
["change", 3],
],
time: 1000,
};
it("accepts valid message with inputPaths", () => {
expect(isSignTransactionRequest(valid)).toBe(true);
});
it("accepts empty inputPaths", () => {
expect(isSignTransactionRequest({ ...valid, inputPaths: [] })).toBe(true);
});
it("rejects missing inputPaths", () => {
const noInputPaths = { ...valid } as Record<string, unknown>;
delete noInputPaths.inputPaths;
expect(isSignTransactionRequest(noInputPaths)).toBe(false);
});
it("rejects non-array inputPaths", () => {
expect(isSignTransactionRequest({ ...valid, inputPaths: "bad" })).toBe(
false,
);
});
it("rejects tuple with wrong types", () => {
expect(
isSignTransactionRequest({ ...valid, inputPaths: [[0, "receive"]] }),
).toBe(false);
});
it("rejects tuple with wrong length", () => {
expect(
isSignTransactionRequest({ ...valid, inputPaths: [["receive"]] }),
).toBe(false);
});
it("rejects missing action", () => {
const noAction = { ...valid } as Record<string, unknown>;
delete noAction.action;
expect(isSignTransactionRequest(noAction)).toBe(false);
});
it("rejects missing transaction", () => {
const noTx = { ...valid } as Record<string, unknown>;
delete noTx.transaction;
expect(isSignTransactionRequest(noTx)).toBeFalsy();
});
it("rejects missing sequence", () => {
const noSeq = { ...valid } as Record<string, unknown>;
delete noSeq.sequence;
expect(isSignTransactionRequest(noSeq)).toBe(false);
});
});

View file

@ -114,6 +114,7 @@ export interface SignTransactionRequest extends ProtocolMessage {
action: RelayMsgAction.SignTransactionRequest;
transaction: WcSignTransactionRequest;
sequence: number;
inputPaths: [PathName, number][]; // [pathName, addressIndex] per sourceOutput
}
export interface SignTransactionResponse extends ProtocolMessage {
@ -155,7 +156,15 @@ export function isSignTransactionRequest(
msg.action === RelayMsgAction.SignTransactionRequest &&
msg.transaction &&
typeof msg.transaction === "object" &&
typeof msg.sequence === "number"
typeof msg.sequence === "number" &&
Array.isArray(msg.inputPaths) &&
msg.inputPaths.every(
(p: any) =>
Array.isArray(p) &&
p.length === 2 &&
typeof p[0] === "string" &&
typeof p[1] === "number",
)
);
}

View file

@ -86,6 +86,7 @@ async function sendSignRequest(
userPrompt: "Test sign request from wiz-test CLI",
broadcast: false,
},
inputPaths: [],
time: Math.floor(Date.now() / 1000),
};

View file

@ -0,0 +1,161 @@
// Copyright (C) 2026 Whiterun LLC,
// This software is licensed under the GNU Lesser General Public License (LGPL), version 3.0 or later.
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/lgpl-3.0.html
/**
* sign_transaction_request tests verifies that inputPaths sent by the dapp
* arrive intact on the wallet side via the pendingSignRequest event.
*/
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import {
initiateDappRelay,
RelayMsgAction,
PROTOCOL_NAME,
type RelayUpdatePayload,
type RelayClient,
type DappReadyMessage,
type WalletReadyMessage,
type ProtocolMessage,
type SignTransactionRequest,
} from "@wizardconnect/core";
import {
WalletConnectionManager,
type PendingSignRequest,
} from "@wizardconnect/wallet";
import { makeTestAdapter, waitFor } from "./helpers.js";
const TEST_RELAY_URL =
process.env.TEST_RELAY_URL ?? "wss://relay.cauldron.quest:443";
describe("WalletConnectionManager — sign_transaction_request with inputPaths", () => {
let dappCleanup: () => void;
let dappClient: RelayClient | null = null;
let manager: WalletConnectionManager;
let connectionId: string;
let walletReadyReceived = false;
const pendingRequests: PendingSignRequest[] = [];
beforeAll(async () => {
// ---- Dapp setup ----
const dappRelay = initiateDappRelay(
(payload: RelayUpdatePayload) => {
if (payload.client && !dappClient) dappClient = payload.client;
},
{ explicitRelayUrls: [TEST_RELAY_URL] },
);
dappCleanup = dappRelay.cleanup;
dappRelay.events.on("keyexchangecomplete", async () => {
await new Promise((r) => setTimeout(r, 50));
while (!dappClient!.isKeyExchangeComplete()) {
await new Promise((r) => setTimeout(r, 50));
}
dappClient!.on("message", (msg: ProtocolMessage) => {
if (msg.action === RelayMsgAction.WalletReady) {
walletReadyReceived = true;
const wr = msg as WalletReadyMessage;
if (!wr.dapp_discovered) {
const reply: DappReadyMessage = {
action: RelayMsgAction.DappReady,
supported_protocols: [PROTOCOL_NAME],
wallet_discovered: true,
time: Math.floor(Date.now() / 1000),
};
dappClient!.relay(reply).catch(() => {});
}
}
});
const initMsg: DappReadyMessage = {
action: RelayMsgAction.DappReady,
supported_protocols: [PROTOCOL_NAME],
wallet_discovered: false,
time: Math.floor(Date.now() / 1000),
};
await dappClient!.relay(initMsg);
});
// ---- Wallet side ----
const adapter = makeTestAdapter();
manager = new WalletConnectionManager(adapter);
manager.on("pendingSignRequest", (req: PendingSignRequest) => {
pendingRequests.push(req);
});
connectionId = manager.connect(dappRelay.uri);
await waitFor(() => walletReadyReceived, {
timeoutMs: 15000,
what: "wallet_ready received on dapp side",
});
// Give time for reactive dapp_ready to be processed
await new Promise((r) => setTimeout(r, 300));
}, 30000);
afterAll(() => {
dappCleanup?.();
manager?.disconnectAll();
});
it("wallet receives inputPaths from dapp sign request", async () => {
const signMsg: SignTransactionRequest = {
action: RelayMsgAction.SignTransactionRequest,
sequence: 1,
transaction: {
transaction: { inputs: [], outputs: [], version: 2, locktime: 0 },
sourceOutputs: [],
userPrompt: "test",
broadcast: false,
},
inputPaths: [
["receive", 0],
["defi", 5],
["change", 2],
],
time: Math.floor(Date.now() / 1000),
};
await dappClient!.relay(signMsg);
await waitFor(() => pendingRequests.length > 0, {
timeoutMs: 5000,
what: "pendingSignRequest event on wallet",
});
expect(pendingRequests[0].connectionId).toBe(connectionId);
expect(pendingRequests[0].request.inputPaths).toEqual([
["receive", 0],
["defi", 5],
["change", 2],
]);
}, 15000);
it("wallet receives empty inputPaths for zero-input transaction", async () => {
const signMsg: SignTransactionRequest = {
action: RelayMsgAction.SignTransactionRequest,
sequence: 2,
transaction: {
transaction: { inputs: [], outputs: [], version: 2, locktime: 0 },
sourceOutputs: [],
broadcast: false,
},
inputPaths: [],
time: Math.floor(Date.now() / 1000),
};
await dappClient!.relay(signMsg);
await waitFor(() => pendingRequests.length >= 2, {
timeoutMs: 5000,
what: "second pendingSignRequest event on wallet",
});
expect(pendingRequests[1].request.inputPaths).toEqual([]);
}, 15000);
});