Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@ jobs:

- name: Lint
run: yarn lint

# Unit tests dropped in commit 58272ddd (CircleCI → GHA migration).
# Restored here so PRs catch regressions in the 49-test jest suite.
# Integration tests (yarn test:integration) still need a kkemu
# sidecar — that's a follow-up, tracked in a separate PR/issue.
- name: Unit tests
run: yarn test --runInBand --coverage=false
18 changes: 18 additions & 0 deletions packages/hdwallet-core/src/solana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,26 @@ export interface SolanaWalletInfo extends HDWalletInfo {
solanaNextAccountPath(msg: SolanaAccountPath): SolanaAccountPath | undefined;
}

// ── Off-chain message signing (domain-separated envelope) ────────────

export interface SolanaSignOffchainMessage {
addressNList: BIP32Path;
/** Off-chain message spec version. Only 0 is currently defined. */
version?: number;
/** 0 = restricted ASCII, 1 = UTF-8 limited (max 1212 bytes). Format 2 is not supported on KeepKey. */
messageFormat?: number;
message: Uint8Array | string;
showDisplay?: boolean;
}

export interface SolanaOffchainMessageSignature {
publicKey: Uint8Array | string;
signature: Uint8Array | string;
}

export interface SolanaWallet extends SolanaWalletInfo, HDWallet {
readonly _supportsSolana: boolean;
solanaGetAddress(msg: SolanaGetAddress): Promise<string | null>;
solanaSignTx(msg: SolanaSignTx): Promise<SolanaSignedTx | null>;
solanaSignOffchainMessage(msg: SolanaSignOffchainMessage): Promise<SolanaOffchainMessageSignature | null>;
}
14 changes: 14 additions & 0 deletions packages/hdwallet-core/src/ton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,22 @@ export interface TonWalletInfo extends HDWalletInfo {
tonNextAccountPath(msg: TonAccountPath): TonAccountPath | undefined;
}

// ── Bare Ed25519 message signing (AdvancedMode-gated firmware-side) ──

export interface TonSignMessage {
addressNList: BIP32Path;
message: Uint8Array | string;
showDisplay?: boolean;
}

export interface TonMessageSignature {
publicKey: Uint8Array | string;
signature: Uint8Array | string;
}

export interface TonWallet extends TonWalletInfo, HDWallet {
readonly _supportsTon: boolean;
tonGetAddress(msg: TonGetAddress): Promise<string | null>;
tonSignTx(msg: TonSignTx): Promise<TonSignedTx | null>;
tonSignMessage(msg: TonSignMessage): Promise<TonMessageSignature | null>;
}
37 changes: 37 additions & 0 deletions packages/hdwallet-core/src/tron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,45 @@ export interface TronWalletInfo extends HDWalletInfo {
tronNextAccountPath(msg: TronAccountPath): TronAccountPath | undefined;
}

// ── TIP-191 personal_sign ─────────────────────────────────────────────

export interface TronSignMessage {
addressNList: BIP32Path;
message: Uint8Array | string;
showDisplay?: boolean;
}

export interface TronMessageSignature {
address: string;
signature: Uint8Array | string;
}

export interface TronVerifyMessage {
address: string;
signature: Uint8Array | string;
message: Uint8Array | string;
}

// ── TIP-712 typed-data hash mode ──────────────────────────────────────

export interface TronSignTypedHash {
addressNList: BIP32Path;
/** 32-byte domainSeparator hash (host-precomputed per TIP-712 spec) */
domainSeparatorHash: Uint8Array | string;
/** 32-byte message hash; omit for primaryType=EIP712Domain */
messageHash?: Uint8Array | string;
}

export interface TronTypedDataSignature {
address: string;
signature: Uint8Array | string;
}

export interface TronWallet extends TronWalletInfo, HDWallet {
readonly _supportsTron: boolean;
tronGetAddress(msg: TronGetAddress): Promise<string | null>;
tronSignTx(msg: TronSignTx): Promise<TronSignedTx | null>;
tronSignMessage(msg: TronSignMessage): Promise<TronMessageSignature | null>;
tronVerifyMessage(msg: TronVerifyMessage): Promise<boolean>;
tronSignTypedHash(msg: TronSignTypedHash): Promise<TronTypedDataSignature | null>;
}
8 changes: 2 additions & 6 deletions packages/hdwallet-keepkey/src/keepkey-initialize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ describe("KeepKeyHDWallet.initialize() version-field validation", () => {
},
});
const wallet = new KeepKeyHDWallet(transport);
await expect(wallet.initialize()).rejects.toThrow(
/KeepKey Initialize returned Features without firmware version/
);
await expect(wallet.initialize()).rejects.toThrow(/KeepKey Initialize returned Features without firmware version/);
});

it("resolves successfully when all version fields are present", async () => {
Expand Down Expand Up @@ -82,8 +80,6 @@ describe("KeepKeyHDWallet.initialize() version-field validation", () => {
err = e;
}
expect(err).toBeDefined();
expect(String(err.message ?? err)).not.toMatch(
/Invalid Version: vundefined\.undefined\.undefined/
);
expect(String(err.message ?? err)).not.toMatch(/Invalid Version: vundefined\.undefined\.undefined/);
});
});
24 changes: 22 additions & 2 deletions packages/hdwallet-keepkey/src/keepkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1200,8 +1200,8 @@ export class KeepKeyHDWallet implements core.HDWallet, core.BTCWallet, core.ETHW
) {
throw new Error(
`KeepKey Initialize returned Features without firmware version ` +
`(major=${out.majorVersion}, minor=${out.minorVersion}, patch=${out.patchVersion}). ` +
`Device may be in bootloader mode, mid-update, or returned an unexpected message type.`
`(major=${out.majorVersion}, minor=${out.minorVersion}, patch=${out.patchVersion}). ` +
`Device may be in bootloader mode, mid-update, or returned an unexpected message type.`
);
}

Expand Down Expand Up @@ -1489,6 +1489,10 @@ export class KeepKeyHDWallet implements core.HDWallet, core.BTCWallet, core.ETHW
return Solana.solanaSignTx(this.transport, msg);
}

public solanaSignOffchainMessage(msg: core.SolanaSignOffchainMessage): Promise<core.SolanaOffchainMessageSignature> {
return Solana.solanaSignOffchainMessage(this.transport, msg);
}

public solanaSignMessage(msg: {
addressNList: number[];
message: Uint8Array | string;
Expand All @@ -1513,6 +1517,18 @@ export class KeepKeyHDWallet implements core.HDWallet, core.BTCWallet, core.ETHW
return Tron.tronSignTx(this.transport, msg);
}

public tronSignMessage(msg: core.TronSignMessage): Promise<core.TronMessageSignature> {
return Tron.tronSignMessage(this.transport, msg);
}

public tronVerifyMessage(msg: core.TronVerifyMessage): Promise<boolean> {
return Tron.tronVerifyMessage(this.transport, msg);
}

public tronSignTypedHash(msg: core.TronSignTypedHash): Promise<core.TronTypedDataSignature> {
return Tron.tronSignTypedHash(this.transport, msg);
}

public tronNextAccountPath(msg: core.TronAccountPath): core.TronAccountPath | undefined {
return this.info.tronNextAccountPath(msg);
}
Expand All @@ -1529,6 +1545,10 @@ export class KeepKeyHDWallet implements core.HDWallet, core.BTCWallet, core.ETHW
return Ton.tonSignTx(this.transport, msg);
}

public tonSignMessage(msg: core.TonSignMessage): Promise<core.TonMessageSignature> {
return Ton.tonSignMessage(this.transport, msg);
}

public tonNextAccountPath(msg: core.TonAccountPath): core.TonAccountPath | undefined {
return this.info.tonNextAccountPath(msg);
}
Expand Down
162 changes: 162 additions & 0 deletions packages/hdwallet-keepkey/src/solana-offchain.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* Unit tests for Solana off-chain message signing protobuf shims and wrapper.
*
* Off-chain spec envelope (constructed firmware-side):
* "\xff" || "solana offchain" || version:u8 || format:u8 || length:u16 LE || message
*/
import * as jspb from "google-protobuf";

import { SolanaOffchainMessageSignature, SolanaSignOffchainMessage, solanaSignOffchainMessage } from "./solana";
import { messageNameRegistry, messageTypeRegistry } from "./typeRegistry";

const MESSAGETYPE_SOLANASIGNOFFCHAINMESSAGE = 756;
const MESSAGETYPE_SOLANAOFFCHAINMESSAGESIGNATURE = 757;

const ALLALLALL_SOLANA_PATH = [0x80000000 + 44, 0x80000000 + 501, 0x80000000, 0x80000000];

function makeMockTransport(callImpl: jest.Mock) {
return {
debugLink: false,
call: callImpl,
lockDuring: <T>(fn: () => Promise<T>) => fn(),
} as any;
}

describe("Solana SignOffchainMessage protobuf registration", () => {
it("registers SolanaSignOffchainMessage (756)", () => {
expect(messageTypeRegistry[MESSAGETYPE_SOLANASIGNOFFCHAINMESSAGE]).toBeDefined();
expect(messageNameRegistry[MESSAGETYPE_SOLANASIGNOFFCHAINMESSAGE]).toBe("SolanaSignOffchainMessage");
});
it("registers SolanaOffchainMessageSignature (757)", () => {
expect(messageTypeRegistry[MESSAGETYPE_SOLANAOFFCHAINMESSAGESIGNATURE]).toBeDefined();
expect(messageNameRegistry[MESSAGETYPE_SOLANAOFFCHAINMESSAGESIGNATURE]).toBe("SolanaOffchainMessageSignature");
});
});

describe("Solana SignOffchainMessage jspb round-trip", () => {
it("SolanaSignOffchainMessage: all fields", () => {
const m = new SolanaSignOffchainMessage();
m.setAddressNList(ALLALLALL_SOLANA_PATH);
m.setVersion(0);
m.setMessageFormat(0);
m.setMessage(new Uint8Array([0x68, 0x69])); // "hi"
m.setShowDisplay(true);

const bytes = m.serializeBinary();
const decoded = new SolanaSignOffchainMessage();
SolanaSignOffchainMessage.deserializeBinaryFromReader(decoded, new jspb.BinaryReader(bytes));

expect(decoded.getAddressNList()).toEqual(ALLALLALL_SOLANA_PATH);
expect(decoded.getCoinName()).toBe("Solana");
expect(decoded.getVersion()).toBe(0);
expect(decoded.getMessageFormat()).toBe(0);
expect(Array.from(decoded.getMessage() as Uint8Array)).toEqual([0x68, 0x69]);
expect(decoded.getShowDisplay()).toBe(true);
});

it("SolanaSignOffchainMessage: format=1 UTF-8 limited at boundary (1212 bytes)", () => {
const m = new SolanaSignOffchainMessage();
m.setAddressNList(ALLALLALL_SOLANA_PATH);
m.setMessageFormat(1);
m.setMessage(new Uint8Array(1212).fill(0x41)); // "A"*1212

const bytes = m.serializeBinary();
const decoded = new SolanaSignOffchainMessage();
SolanaSignOffchainMessage.deserializeBinaryFromReader(decoded, new jspb.BinaryReader(bytes));

expect(decoded.getMessageFormat()).toBe(1);
expect((decoded.getMessage() as Uint8Array).length).toBe(1212);
});

it("SolanaOffchainMessageSignature: public_key (32) + signature (64)", () => {
const pk = new Uint8Array(32).fill(0x33);
const sig = new Uint8Array(64).fill(0x55);
const m = new SolanaOffchainMessageSignature();
m.setPublicKey(pk);
m.setSignature(sig);

const bytes = m.serializeBinary();
const decoded = new SolanaOffchainMessageSignature();
SolanaOffchainMessageSignature.deserializeBinaryFromReader(decoded, new jspb.BinaryReader(bytes));

expect((decoded.getPublicKey() as Uint8Array).length).toBe(32);
expect((decoded.getSignature() as Uint8Array).length).toBe(64);
expect((decoded.getPublicKey() as Uint8Array)[0]).toBe(0x33);
expect((decoded.getSignature() as Uint8Array)[0]).toBe(0x55);
});
});

describe("solanaSignOffchainMessage wrapper", () => {
it("encodes string message as UTF-8 bytes", async () => {
const transport = makeMockTransport(
jest.fn().mockImplementation((mtype: number, msg: any) => {
expect(mtype).toBe(MESSAGETYPE_SOLANASIGNOFFCHAINMESSAGE);
// Wrapper should have UTF-8 encoded "hi"
expect(Array.from(msg.getMessage() as Uint8Array)).toEqual([0x68, 0x69]);

const respProto = new SolanaOffchainMessageSignature();
respProto.setPublicKey(new Uint8Array(32));
respProto.setSignature(new Uint8Array(64));
return Promise.resolve({
message_enum: MESSAGETYPE_SOLANAOFFCHAINMESSAGESIGNATURE,
message_type: "SolanaOffchainMessageSignature",
proto: respProto,
});
})
);

await solanaSignOffchainMessage(transport, {
addressNList: ALLALLALL_SOLANA_PATH,
message: "hi",
});
});

it("forwards version + messageFormat parameters", async () => {
const transport = makeMockTransport(
jest.fn().mockImplementation((_mtype: number, msg: any) => {
expect(msg.getVersion()).toBe(0);
expect(msg.getMessageFormat()).toBe(1);
const respProto = new SolanaOffchainMessageSignature();
respProto.setPublicKey(new Uint8Array(32));
respProto.setSignature(new Uint8Array(64));
return Promise.resolve({
message_enum: MESSAGETYPE_SOLANAOFFCHAINMESSAGESIGNATURE,
message_type: "SolanaOffchainMessageSignature",
proto: respProto,
});
})
);

await solanaSignOffchainMessage(transport, {
addressNList: ALLALLALL_SOLANA_PATH,
version: 0,
messageFormat: 1,
message: "test",
});
});

it("returns publicKey + signature on success", async () => {
const transport = makeMockTransport(
jest.fn().mockImplementation(() => {
const respProto = new SolanaOffchainMessageSignature();
respProto.setPublicKey(new Uint8Array(32).fill(0xa1));
respProto.setSignature(new Uint8Array(64).fill(0xb2));
return Promise.resolve({
message_enum: MESSAGETYPE_SOLANAOFFCHAINMESSAGESIGNATURE,
message_type: "SolanaOffchainMessageSignature",
proto: respProto,
});
})
);

const result = await solanaSignOffchainMessage(transport, {
addressNList: ALLALLALL_SOLANA_PATH,
message: "verify-binding",
});

expect((result.publicKey as Uint8Array).length).toBe(32);
expect((result.publicKey as Uint8Array)[0]).toBe(0xa1);
expect((result.signature as Uint8Array).length).toBe(64);
expect((result.signature as Uint8Array)[0]).toBe(0xb2);
});
});
Loading
Loading