Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
566663c
fix(osmosis): forward supported token denominations
BitHighlander Jul 23, 2026
d221492
feat(solana): carry KKSOLSC1 instruction schemas on SolanaSignTx
BitHighlander Jul 27, 2026
fb05dda
feat(zcash): forward Ironwood PCZT metadata
BitHighlander Jul 30, 2026
5d9ab1d
fix(zcash): pin Ironwood client protocol
BitHighlander Jul 30, 2026
2d168a3
Merge branch 'feat/solana-clearsign-schema' into feat/clearsign-studi…
BitHighlander Jul 30, 2026
1e6f83b
feat(clearsign): integrate attestor client with Ironwood
BitHighlander Jul 30, 2026
920d0d4
fix(zcash): consume published Ironwood protocol
BitHighlander Jul 31, 2026
a4821a5
feat(keepkey): expose firmware-reported pre-image hash on EthSignTx r…
BitHighlander May 8, 2026
50ce5ca
fix(core): declare deviceSignedHash on ETHSignedTx so callers can rea…
BitHighlander May 8, 2026
8aba6e8
feat(ripple): pass memo field to device for THORChain swap routing
BitHighlander May 15, 2026
06f4a85
fix(nodewebusb): drop legacy PID 0x0001 from WebUSB getDevice filter
BitHighlander Jun 27, 2026
7af24f2
style(nodewebusb): format request filter
BitHighlander Jul 31, 2026
94ed6cd
style(ripple): format memo guard
BitHighlander Jul 31, 2026
bbc75e3
feat: clear-sign x402 payments
BitHighlander Jul 31, 2026
73884b5
fix: satisfy x402 CI lint gates
BitHighlander Jul 31, 2026
a24617b
fix: serialize structured EIP-712 signing
BitHighlander Jul 31, 2026
25310f2
Merge pull request #60 from keepkey/agent/x402-solana-metadata
BitHighlander Jul 31, 2026
5785ccf
chore: pin portable canonical device protocol
BitHighlander Jul 31, 2026
b6ad14d
Merge pull request #61 from keepkey/agent/pin-portable-device-protocol
BitHighlander Jul 31, 2026
0572619
feat(bitcoin): add KeepKey Taproot host support
BitHighlander Aug 3, 2026
db9233f
Merge pull request #62 from keepkey/agent/taproot-host-support
BitHighlander Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/hdwallet-core/src/bitcoin-taproot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { BTCInputScriptType, describeUTXOPath, taprootAccount } from "./bitcoin";

describe("Bitcoin Taproot paths", () => {
it("describes a BIP-86 account as Taproot", () => {
const account = taprootAccount("Bitcoin", 0, 7);
expect(account).toEqual({
coin: "Bitcoin",
scriptType: BTCInputScriptType.SpendTaproot,
addressNList: [0x80000000 + 86, 0x80000000, 0x80000000 + 7],
});
expect(describeUTXOPath(account.addressNList, "Bitcoin", BTCInputScriptType.SpendTaproot)).toMatchObject({
coin: "Bitcoin",
accountIdx: 7,
wholeAccount: true,
isKnown: true,
scriptType: BTCInputScriptType.SpendTaproot,
verbose: "Bitcoin Account #7 (Taproot)",
});
});

it("does not describe BIP-86 with another script type", () => {
const path = [0x80000000 + 86, 0x80000000, 0x80000000];
expect(describeUTXOPath(path, "Bitcoin", BTCInputScriptType.SpendWitness).isKnown).toBe(false);
});
});
25 changes: 22 additions & 3 deletions packages/hdwallet-core/src/bitcoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ type BTCSignTxInputNativeBase = BTCSignTxInputBase & {
};

type BTCSignTxInputNativeSegwitBase = BTCSignTxInputNativeBase & {
scriptType: BTCInputScriptType.SpendWitness | BTCInputScriptType.SpendP2SHWitness;
scriptType: BTCInputScriptType.SpendWitness | BTCInputScriptType.SpendP2SHWitness | BTCInputScriptType.SpendTaproot;
};

type BTCSignTxInputNativeSegwitWithHex = BTCSignTxInputNativeSegwitBase & {
Expand Down Expand Up @@ -106,7 +106,11 @@ type BTCSignTxInputKKBase = BTCSignTxInputBase & {
};

type BTCSignTxInputKKSegwit = BTCSignTxInputKKBase & {
scriptType: BTCInputScriptType.SpendWitness | BTCInputScriptType.SpendP2SHWitness | BTCInputScriptType.External;
scriptType:
| BTCInputScriptType.SpendWitness
| BTCInputScriptType.SpendP2SHWitness
| BTCInputScriptType.SpendTaproot
| BTCInputScriptType.External;
hex?: string;
};

Expand Down Expand Up @@ -231,6 +235,7 @@ export enum BTCInputScriptType {
External = "external",
SpendWitness = "p2wpkh",
SpendP2SHWitness = "p2sh-p2wpkh",
SpendTaproot = "p2tr",
}

export enum BTCOutputScriptType {
Expand All @@ -239,6 +244,7 @@ export enum BTCOutputScriptType {
Bech32 = "bech32",
PayToWitness = "p2wpkh",
PayToP2SHWitness = "p2sh-p2wpkh",
PayToTaproot = "p2tr", // device-derived change only
}

export enum BTCOutputAddressType {
Expand Down Expand Up @@ -362,12 +368,16 @@ export function describeUTXOPath(path: BIP32Path, coin: Coin, scriptType: BTCInp

const purpose = path[0] & 0x7fffffff;

if (![44, 49, 84].includes(purpose)) return unknown;
if (![44, 49, 84, 86].includes(purpose)) return unknown;

if (purpose === 44 && scriptType !== BTCInputScriptType.SpendAddress) return unknown;

if (purpose === 49 && scriptType !== BTCInputScriptType.SpendP2SHWitness) return unknown;

if (purpose === 84 && scriptType !== BTCInputScriptType.SpendWitness) return unknown;

if (purpose === 86 && scriptType !== BTCInputScriptType.SpendTaproot) return unknown;

const wholeAccount = path.length === 3;

const script = (
Expand All @@ -376,6 +386,7 @@ export function describeUTXOPath(path: BIP32Path, coin: Coin, scriptType: BTCInp
[BTCInputScriptType.SpendP2SHWitness]: [],
[BTCInputScriptType.SpendWitness]: ["Segwit"],
[BTCInputScriptType.Bech32]: ["Segwit Native"],
[BTCInputScriptType.SpendTaproot]: ["Taproot"],
} as Partial<Record<BTCInputScriptType, string[]>>
)[scriptType];

Expand Down Expand Up @@ -471,3 +482,11 @@ export function segwitNativeAccount(coin: Coin, slip44: number, accountIdx: numb
addressNList: [0x80000000 + 84, 0x80000000 + slip44, 0x80000000 + accountIdx],
};
}

export function taprootAccount(coin: Coin, slip44: number, accountIdx: number): BTCAccountPath {
return {
coin,
scriptType: BTCInputScriptType.SpendTaproot,
addressNList: [0x80000000 + 86, 0x80000000 + slip44, 0x80000000 + accountIdx],
};
}
4 changes: 4 additions & 0 deletions packages/hdwallet-core/src/ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ export interface ETHSignedTx {
s: string;
/** big-endian hex, prefixed with '0x' */
serialized: string;
/** KeepKey-only: keccak256 pre-image the firmware actually signed (32-byte hex).
* Optional — older firmware doesn't populate it. Useful for diagnostics where
* the caller needs to verify which bytes the device hashed. */
deviceSignedHash?: string;
}

export interface ETHSignMessage {
Expand Down
35 changes: 35 additions & 0 deletions packages/hdwallet-core/src/solana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,44 @@ export interface SolanaAddress {
address: string;
}

export interface SolanaTokenInfo {
/** 32-byte SPL mint, encoded as bytes, hex, base64, or base58. */
mint: Uint8Array | string;
symbol?: string;
decimals?: number;
signature?: Uint8Array | string;
signerKeyId?: number;
}

export interface SolanaSignTx {
addressNList: BIP32Path;
rawTx: Uint8Array | string;
/** Optional token definitions used by firmware display policy. */
tokenInfo?: SolanaTokenInfo[];
/**
* Candidate owners for signed SPL token destinations (for example x402
* payTo). Firmware displays one only after deriving and matching its ATA.
*/
tokenRecipientOwners?: Array<Uint8Array | string>;
/** One-request opaque-signing authorization; does not mutate AdvancedMode. */
allowBlindSigning?: boolean;
/** Transaction-bound, signer-attested KKSOLSW1 swap descriptor. */
swapMetadata?: {
payload: Uint8Array | string;
signature: Uint8Array | string;
signerKeyId: number;
};
/**
* Signer-attested KKSOLSC1 instruction schema. Unlike swapMetadata this is
* NOT bound to one transaction: it describes how to read a program's
* instruction, so a single signature is reused for every transaction to
* that program and the device decodes values from the bytes it signs.
*/
schema?: {
payload: Uint8Array | string;
signature: Uint8Array | string;
signerKeyId: number;
};
}

export interface SolanaSignedTx {
Expand Down
10 changes: 6 additions & 4 deletions packages/hdwallet-keepkey-nodewebusb/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ export const NodeWebUSBAdapterDelegate = {
return devices.filter((x) => x.vendorId === VENDOR_ID && [WEBUSB_PRODUCT_ID, HID_PRODUCT_ID].includes(x.productId));
},
async getDevice(serialNumber?: string): Promise<Device> {
// Only match the WebUSB PID (0x0002). The TransportDelegate ctor rejects any
// other PID with FirmwareUpdateRequired, so matching legacy 0x0001 here just
// produced a doomed pair attempt + a misleading "Firmware 6.1.0 required"
// before the caller's HID fallback. Old (PID 0x0001) devices now skip WebUSB
// cleanly and pair over HID. (getDevices() still lists 0x0001 for detection.)
const out = await webusb.requestDevice({
filters: [
{ vendorId: VENDOR_ID, productId: WEBUSB_PRODUCT_ID, serialNumber },
{ vendorId: VENDOR_ID, productId: HID_PRODUCT_ID, serialNumber },
],
filters: [{ vendorId: VENDOR_ID, productId: WEBUSB_PRODUCT_ID, serialNumber }],
});
if (out.serialNumber === undefined) throw new Error("expected serial number");
return out as Device;
Expand Down
2 changes: 1 addition & 1 deletion packages/hdwallet-keepkey/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"dependencies": {
"@ethereumjs/common": "^2.4.0",
"@ethereumjs/tx": "^3.3.0",
"@keepkey/device-protocol": "npm:@bithighlander/device-protocol@7.16.0",
"@keepkey/device-protocol": "https://github.com/keepkey/device-protocol.git#674777f6d4dd16e2b8c4c2df10608976375ee879",
"@keepkey/hdwallet-core": "1.53.16",
"@keepkey/proto-tx-builder": "^0.9.1",
"@shapeshiftoss/bitcoinjs-lib": "5.2.0-shapeshift.2",
Expand Down
24 changes: 19 additions & 5 deletions packages/hdwallet-keepkey/src/bitcoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const supportedCoins = [
];

const segwitCoins = ["Bitcoin", "Testnet", "BitcoinGold", "Litecoin"];
const taprootCoins = ["Bitcoin", "Testnet"];

function legacyAccount(coin: core.Coin, slip44: number, accountIdx: number): core.BTCAccountPath {
return {
Expand All @@ -51,6 +52,14 @@ function segwitNativeAccount(coin: core.Coin, slip44: number, accountIdx: number
};
}

function taprootAccount(coin: core.Coin, slip44: number, accountIdx: number): core.BTCAccountPath {
return {
coin,
scriptType: core.BTCInputScriptType.SpendTaproot,
addressNList: [0x80000000 + 86, 0x80000000 + slip44, 0x80000000 + accountIdx],
};
}

function packVarint(n: number): string {
if (n < 253) return n.toString(16).padStart(2, "0");
else if (n < 0xffff) return "FD" + n.toString(16).padStart(4, "0");
Expand Down Expand Up @@ -120,6 +129,7 @@ function prepareSignTx(
if (
inputTx.scriptType === core.BTCInputScriptType.SpendP2SHWitness ||
inputTx.scriptType === core.BTCInputScriptType.SpendWitness ||
inputTx.scriptType === core.BTCInputScriptType.SpendTaproot ||
inputTx.scriptType === core.BTCInputScriptType.External
)
return;
Expand Down Expand Up @@ -248,6 +258,7 @@ export async function btcSupportsScriptType(coin: core.Coin, scriptType?: core.B
if (!supportedCoins.includes(coin)) return false;
if (!segwitCoins.includes(coin) && scriptType === core.BTCInputScriptType.SpendP2SHWitness) return false;
if (!segwitCoins.includes(coin) && scriptType === core.BTCInputScriptType.SpendWitness) return false;
if (!taprootCoins.includes(coin) && scriptType === core.BTCInputScriptType.SpendTaproot) return false;
return true;
}

Expand Down Expand Up @@ -546,6 +557,7 @@ export function btcGetAccountPaths(msg: core.BTCGetAccountPaths): Array<core.BTC
const bip44 = legacyAccount(msg.coin, slip44, msg.accountIdx);
const bip49 = segwitAccount(msg.coin, slip44, msg.accountIdx);
const bip84 = segwitNativeAccount(msg.coin, slip44, msg.accountIdx);
const bip86 = taprootAccount(msg.coin, slip44, msg.accountIdx);

// For BTC Forks
const btcLegacy = legacyAccount(msg.coin, core.slip44ByCoin("Bitcoin"), msg.accountIdx);
Expand All @@ -558,12 +570,12 @@ export function btcGetAccountPaths(msg: core.BTCGetAccountPaths): Array<core.BTC
let paths: Array<core.BTCAccountPath> =
(
{
Bitcoin: [bip44, bip49, bip84],
Bitcoin: [bip44, bip49, bip84, bip86],
Litecoin: [bip44, bip49, bip84],
Dash: [bip44],
DigiByte: [bip44, bip49, bip84],
Dogecoin: [bip44],
Testnet: [bip44, bip49, bip84],
Testnet: [bip44, bip49, bip84, bip86],
BitcoinCash: [bip44, btcLegacy],
BitcoinSV: [bip44, bchLegacy, btcLegacy],
BitcoinGold: [bip44, bip49, bip84, btcLegacy, btcSegwit, btcSegwitNative],
Expand All @@ -581,7 +593,7 @@ export function btcGetAccountPaths(msg: core.BTCGetAccountPaths): Array<core.BTC
export function btcIsSameAccount(msg: Array<core.BTCAccountPath>): boolean {
if (msg.length < 1) return false;

if (msg.length > 3) return false;
if (msg.length > 4) return false;

const account0 = msg[0];
if (account0.addressNList.length != 3) return false;
Expand All @@ -592,6 +604,7 @@ export function btcIsSameAccount(msg: Array<core.BTCAccountPath>): boolean {
[core.BTCInputScriptType.SpendAddress]: 0x80000000 + 44,
[core.BTCInputScriptType.SpendP2SHWitness]: 0x80000000 + 49,
[core.BTCInputScriptType.SpendWitness]: 0x80000000 + 84,
[core.BTCInputScriptType.SpendTaproot]: 0x80000000 + 86,
} as Partial<Record<core.BTCInputScriptType, number>>;
if (purposeForScriptType[account0.scriptType] !== purpose) return false;

Expand All @@ -604,12 +617,13 @@ export function btcIsSameAccount(msg: Array<core.BTCAccountPath>): boolean {
if (idx < 0x80000000) return false;

// Accounts must have the same SLIP44 and Account Idx, but may have differing
// purpose fields (so long as they're BIP44/BIP49/BIP84)
// purpose fields (so long as they're BIP44/BIP49/BIP84/BIP86)
if (
msg.find((path) => {
if (path.addressNList.length != 3) return true;

if (![0x80000000 + 44, 0x80000000 + 49, 0x80000000 + 84].includes(path.addressNList[0])) return true;
if (![0x80000000 + 44, 0x80000000 + 49, 0x80000000 + 84, 0x80000000 + 86].includes(path.addressNList[0]))
return true;

if (purposeForScriptType[path.scriptType] !== path.addressNList[0]) return true;

Expand Down
51 changes: 51 additions & 0 deletions packages/hdwallet-keepkey/src/clearsign.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Regression coverage for ClearSign attestor message decoding.
*
* Transport.fromMessageBuffer always calls deserializeBinaryFromReader on the
* constructor stored in messageTypeRegistry. Keep the attestor entries on the
* canonical generated classes so public-key and signature responses cannot be
* replaced by incomplete hand-written shims.
*/
import "./clearsign";

import * as jspb from "google-protobuf";

import { messageTypeRegistry } from "./typeRegistry";

const ATTESTOR_TYPES = [
[1700, "ClearsignAttestorGetPublicKey"],
[1701, "ClearsignAttestorPublicKey"],
[1702, "ClearsignAttestorSign"],
[1703, "ClearsignAttestorSignature"],
] as const;

describe("ClearSign attestor protobuf transport", () => {
it.each(ATTESTOR_TYPES)("registers message type %i (%s) with reader decoding", (typeId) => {
const registeredType = messageTypeRegistry[typeId] as any;
expect(registeredType).toBeDefined();
expect(typeof registeredType.deserializeBinaryFromReader).toBe("function");
});

it("decodes the public-key response through the transport registry path", () => {
const publicKey = new Uint8Array(33).fill(0x02);
const writer = new jspb.BinaryWriter();
writer.writeBytes(1, publicKey);
const MType = messageTypeRegistry[1701] as any;
const decoded = MType.deserializeBinaryFromReader(new MType(), new jspb.BinaryReader(writer.getResultBuffer()));

expect(Array.from(decoded.getPublicKey_asU8())).toEqual(Array.from(publicKey));
});

it("decodes the signature response through the transport registry path", () => {
const signature = new Uint8Array(64).fill(0x5a);
const publicKey = new Uint8Array(33).fill(0x03);
const writer = new jspb.BinaryWriter();
writer.writeBytes(1, signature);
writer.writeBytes(2, publicKey);
const MType = messageTypeRegistry[1703] as any;
const decoded = MType.deserializeBinaryFromReader(new MType(), new jspb.BinaryReader(writer.getResultBuffer()));

expect(Array.from(decoded.getSignature_asU8())).toEqual(Array.from(signature));
expect(Array.from(decoded.getPublicKey_asU8())).toEqual(Array.from(publicKey));
});
});
Loading
Loading