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:
commit
6d4d8139f0
7 changed files with 255 additions and 7 deletions
|
|
@ -133,6 +133,7 @@ const request: SignTransactionRequest = {
|
|||
userPrompt: "Confirm swap",
|
||||
broadcast: true,
|
||||
},
|
||||
inputPaths: [["receive", 0], ["defi", 5]], // one per sourceOutput
|
||||
};
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -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
12
package-lock.json
generated
|
|
@ -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": "*",
|
||||
|
|
|
|||
69
packages/core/src/protocols/hdwalletv1.test.ts
Normal file
69
packages/core/src/protocols/hdwalletv1.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ async function sendSignRequest(
|
|||
userPrompt: "Test sign request from wiz-test CLI",
|
||||
broadcast: false,
|
||||
},
|
||||
inputPaths: [],
|
||||
time: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
|
||||
|
|
|
|||
161
packages/wallet/src/integration/sign-request.test.ts
Normal file
161
packages/wallet/src/integration/sign-request.test.ts
Normal 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);
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue