@ukeyfe/hardware-transport 1.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.eslintrc +24 -0
  2. package/README.md +29 -0
  3. package/__tests__/build-receive.test.js +117 -0
  4. package/__tests__/decode-features.test.js +72 -0
  5. package/__tests__/encode-decode-basic.test.js +272 -0
  6. package/__tests__/encode-decode.test.js +532 -0
  7. package/__tests__/messages.test.js +86 -0
  8. package/dist/constants.d.ts +6 -0
  9. package/dist/constants.d.ts.map +1 -0
  10. package/dist/index.d.ts +5203 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +942 -0
  13. package/dist/serialization/index.d.ts +6 -0
  14. package/dist/serialization/index.d.ts.map +1 -0
  15. package/dist/serialization/protobuf/decode.d.ts +6 -0
  16. package/dist/serialization/protobuf/decode.d.ts.map +1 -0
  17. package/dist/serialization/protobuf/encode.d.ts +5 -0
  18. package/dist/serialization/protobuf/encode.d.ts.map +1 -0
  19. package/dist/serialization/protobuf/index.d.ts +4 -0
  20. package/dist/serialization/protobuf/index.d.ts.map +1 -0
  21. package/dist/serialization/protobuf/messages.d.ts +11 -0
  22. package/dist/serialization/protobuf/messages.d.ts.map +1 -0
  23. package/dist/serialization/protocol/decode.d.ts +11 -0
  24. package/dist/serialization/protocol/decode.d.ts.map +1 -0
  25. package/dist/serialization/protocol/encode.d.ts +11 -0
  26. package/dist/serialization/protocol/encode.d.ts.map +1 -0
  27. package/dist/serialization/protocol/index.d.ts +3 -0
  28. package/dist/serialization/protocol/index.d.ts.map +1 -0
  29. package/dist/serialization/receive.d.ts +8 -0
  30. package/dist/serialization/receive.d.ts.map +1 -0
  31. package/dist/serialization/send.d.ts +7 -0
  32. package/dist/serialization/send.d.ts.map +1 -0
  33. package/dist/types/index.d.ts +3 -0
  34. package/dist/types/index.d.ts.map +1 -0
  35. package/dist/types/messages.d.ts +3762 -0
  36. package/dist/types/messages.d.ts.map +1 -0
  37. package/dist/types/transport.d.ts +62 -0
  38. package/dist/types/transport.d.ts.map +1 -0
  39. package/dist/utils/highlevel-checks.d.ts +10 -0
  40. package/dist/utils/highlevel-checks.d.ts.map +1 -0
  41. package/dist/utils/logBlockCommand.d.ts +2 -0
  42. package/dist/utils/logBlockCommand.d.ts.map +1 -0
  43. package/dist/utils/protobuf.d.ts +2 -0
  44. package/dist/utils/protobuf.d.ts.map +1 -0
  45. package/jest.config.js +7 -0
  46. package/package.json +31 -0
  47. package/protocol.md +21 -0
  48. package/scripts/protobuf-build.sh +58 -0
  49. package/scripts/protobuf-patches/TxAck.js +44 -0
  50. package/scripts/protobuf-patches/TxInputType.js +49 -0
  51. package/scripts/protobuf-patches/TxOutputType.js +50 -0
  52. package/scripts/protobuf-patches/index.js +274 -0
  53. package/scripts/protobuf-types.js +283 -0
  54. package/src/constants.ts +8 -0
  55. package/src/index.ts +41 -0
  56. package/src/serialization/index.ts +8 -0
  57. package/src/serialization/protobuf/decode.ts +95 -0
  58. package/src/serialization/protobuf/encode.ts +79 -0
  59. package/src/serialization/protobuf/index.ts +3 -0
  60. package/src/serialization/protobuf/messages.ts +37 -0
  61. package/src/serialization/protocol/decode.ts +48 -0
  62. package/src/serialization/protocol/encode.ts +59 -0
  63. package/src/serialization/protocol/index.ts +2 -0
  64. package/src/serialization/receive.ts +18 -0
  65. package/src/serialization/send.ts +56 -0
  66. package/src/types/index.ts +2 -0
  67. package/src/types/messages.ts +4864 -0
  68. package/src/types/transport.ts +71 -0
  69. package/src/utils/highlevel-checks.ts +88 -0
  70. package/src/utils/logBlockCommand.ts +1 -0
  71. package/src/utils/protobuf.ts +24 -0
  72. package/tsconfig.json +11 -0
@@ -0,0 +1,95 @@
1
+ import { Type, Message, Field } from 'protobufjs/light';
2
+ import ByteBuffer from 'bytebuffer';
3
+ import { isPrimitiveField } from '../../utils/protobuf';
4
+
5
+ const transform = (field: Field, value: any) => {
6
+ // [compatibility]: optional undefined keys should be null. Example: Features.fw_major.
7
+ if (field?.optional && typeof value === 'undefined') {
8
+ return null;
9
+ }
10
+
11
+ if (field?.type === 'bytes') {
12
+ return ByteBuffer.wrap(value).toString('hex');
13
+ // return value.toString('hex');
14
+ }
15
+
16
+ // [compatibility]
17
+ // it is likely that we can remove this right away because trezor-connect tests don't ever trigger this condition
18
+ // we should probably make sure that trezor-connect treats following protobuf types as strings: int64, uint64, sint64, fixed64, sfixed64
19
+ if (field?.long) {
20
+ if (Number.isSafeInteger(value.toNumber())) {
21
+ // old trezor-link behavior https://github.com/trezor/trezor-link/blob/9c200cc5608976cff0542484525e98c753ba1888/src/lowlevel/protobuf/message_decoder.js#L80
22
+ return value.toNumber();
23
+ }
24
+ // otherwise return as string
25
+ return value.toString();
26
+ }
27
+
28
+ return value;
29
+ };
30
+
31
+ function messageToJSON(Message: Message<Record<string, unknown>>, fields: Type['fields']) {
32
+ // get rid of Message.prototype references
33
+ const { ...message } = Message;
34
+ const res: { [key: string]: any } = {};
35
+
36
+ Object.keys(fields).forEach(key => {
37
+ const field = fields[key];
38
+ // @ts-ignore
39
+ const value = message[key];
40
+
41
+ /* istanbul ignore else */
42
+ if (field.repeated) {
43
+ /* istanbul ignore else */
44
+ if (isPrimitiveField(field.type)) {
45
+ res[key] = value.map((v: any) => transform(field, v));
46
+ }
47
+ // [compatibility]: keep array enums as array of numbers.
48
+ else if ('valuesById' in field.resolvedType!) {
49
+ res[key] = value;
50
+ } else if ('fields' in field.resolvedType!) {
51
+ res[key] = value.map((v: any) => messageToJSON(v, (field.resolvedType as Type).fields));
52
+ } else {
53
+ throw new Error(`case not handled for repeated key: ${key}`);
54
+ }
55
+ } else if (isPrimitiveField(field.type)) {
56
+ res[key] = transform(field, value);
57
+ }
58
+ // enum type
59
+ else if ('valuesById' in field.resolvedType!) {
60
+ res[key] = field.resolvedType.valuesById[value];
61
+ }
62
+ // message type
63
+ else if (field.resolvedType!.fields) {
64
+ res[key] = messageToJSON(value, field.resolvedType!.fields);
65
+ } else {
66
+ throw new Error(`case not handled: ${key}`);
67
+ }
68
+ });
69
+
70
+ return res;
71
+ }
72
+
73
+ export const decode = (Message: Type, data: ByteBuffer) => {
74
+ const buff = data.toBuffer();
75
+ const a = new Uint8Array(buff);
76
+
77
+ let decoded;
78
+ try {
79
+ decoded = Message.decode(a);
80
+ } catch (error) {
81
+ // Fix a battery_level problem with the device
82
+ if (a.length > 1 && a[a.length - 1] === 0xff) {
83
+ a[a.length - 1] = 0x04;
84
+ decoded = Message.decode(a);
85
+ } else {
86
+ throw error;
87
+ }
88
+ }
89
+
90
+ // [compatibility]: in the end it should be possible to get rid of messageToJSON method and call
91
+ // Message.toObject(decoded) to return result as plain javascript object. This method should be able to do
92
+ // all required conversions (for example bytes to hex) but we can't use it at the moment for compatibility reasons
93
+ // for example difference between enum decoding when [enum] vs enum
94
+ return messageToJSON(decoded, decoded.$type.fields);
95
+ };
@@ -0,0 +1,79 @@
1
+ import { Type } from 'protobufjs/light';
2
+ import { Buffer } from 'buffer';
3
+ import ByteBuffer from 'bytebuffer';
4
+
5
+ import { isPrimitiveField } from '../../utils/protobuf';
6
+
7
+ const transform = (fieldType: string, value: any) => {
8
+ if (fieldType === 'bytes') {
9
+ // special edge case
10
+ // for example MultisigRedeemScriptType might have field signatures ['', '', ''] (check in TrezorConnect signTransactionMultisig test fixtures).
11
+ // trezor needs to receive such field as signatures: [b'', b'', b'']. If we transfer this to empty buffer with protobufjs, this will be decoded by
12
+ // trezor as signatures: [] (empty array)
13
+ if (typeof value === 'string' && !value) return value;
14
+
15
+ // normal flow
16
+ return Buffer.from(value, 'hex');
17
+ }
18
+ if (typeof value === 'number' && !Number.isSafeInteger(value)) {
19
+ throw new RangeError('field value is not within safe integer range');
20
+ }
21
+ return value;
22
+ };
23
+
24
+ export function patch(Message: Type, payload: any) {
25
+ const patched: any = {};
26
+
27
+ if (!Message.fields) {
28
+ return patched;
29
+ }
30
+
31
+ Object.keys(Message.fields).forEach(key => {
32
+ const field = Message.fields[key];
33
+ const value = payload[key];
34
+
35
+ // no value for this field
36
+ if (typeof value === 'undefined') {
37
+ return;
38
+ }
39
+ // primitive type
40
+ if (field && isPrimitiveField(field.type)) {
41
+ if (field.repeated) {
42
+ patched[key] = value.map((v: any) => transform(field.type, v));
43
+ } else {
44
+ patched[key] = transform(field.type, value);
45
+ }
46
+ return;
47
+ }
48
+ // repeated
49
+ if (field.repeated) {
50
+ const RefMessage = Message.lookupTypeOrEnum(field.type);
51
+ patched[key] = value.map((v: any) => patch(RefMessage, v));
52
+ }
53
+ // message type
54
+ else if (typeof value === 'object' && value !== null) {
55
+ const RefMessage = Message.lookupType(field.type);
56
+ patched[key] = patch(RefMessage, value);
57
+ }
58
+ // enum type
59
+ else if (typeof value === 'number') {
60
+ const RefMessage = Message.lookupEnum(field.type);
61
+ patched[key] = RefMessage.values[value];
62
+ } else {
63
+ patched[key] = value;
64
+ }
65
+ });
66
+
67
+ return patched;
68
+ }
69
+
70
+ export const encode = (Message: Type, data: Record<string, unknown>) => {
71
+ const payload = patch(Message, data);
72
+ const message = Message.fromObject(payload);
73
+ // Encode a message to an Uint8Array (browser) or Buffer (node)
74
+ const buffer = Message.encode(message).finish();
75
+ const bytebuffer = new ByteBuffer(buffer.byteLength);
76
+ bytebuffer.append(buffer);
77
+ bytebuffer.reset();
78
+ return bytebuffer;
79
+ };
@@ -0,0 +1,3 @@
1
+ export * from './decode';
2
+ export * from './encode';
3
+ export * from './messages';
@@ -0,0 +1,37 @@
1
+ import * as protobuf from 'protobufjs/light';
2
+
3
+ export function parseConfigure(data: protobuf.INamespace) {
4
+ // @ts-ignore [compatiblity]: connect is sending stringified json
5
+ if (typeof data === 'string') {
6
+ return protobuf.Root.fromJSON(JSON.parse(data));
7
+ }
8
+ return protobuf.Root.fromJSON(data);
9
+ }
10
+
11
+ export const createMessageFromName = (messages: protobuf.Root, name: string) => {
12
+ const Message = messages.lookupType(name);
13
+ const MessageType = messages.lookupEnum('MessageType');
14
+ let messageType = MessageType.values[`MessageType_${name}`];
15
+
16
+ if (!messageType && Message.options) {
17
+ messageType = Message.options['(wire_type)'];
18
+ }
19
+
20
+ return {
21
+ Message,
22
+ messageType,
23
+ };
24
+ };
25
+
26
+ export const createMessageFromType = (messages: protobuf.Root, typeId: number) => {
27
+ const MessageType = messages.lookupEnum('MessageType');
28
+
29
+ const messageName = MessageType.valuesById[typeId].replace('MessageType_', '');
30
+
31
+ const Message = messages.lookupType(messageName);
32
+
33
+ return {
34
+ Message,
35
+ messageName,
36
+ };
37
+ };
@@ -0,0 +1,48 @@
1
+ import ByteBuffer from 'bytebuffer';
2
+ import { MESSAGE_HEADER_BYTE } from '../../constants';
3
+
4
+ /**
5
+ * Reads meta information from buffer
6
+ */
7
+ const readHeader = (buffer: ByteBuffer) => {
8
+ const typeId = buffer.readUint16();
9
+ const length = buffer.readUint32();
10
+
11
+ return { typeId, length };
12
+ };
13
+
14
+ /**
15
+ * Reads meta information from chunked buffer
16
+ */
17
+ const readHeaderChunked = (buffer: ByteBuffer) => {
18
+ const sharp1 = buffer.readByte();
19
+ const sharp2 = buffer.readByte();
20
+ const typeId = buffer.readUint16();
21
+ const length = buffer.readUint32();
22
+
23
+ return { sharp1, sharp2, typeId, length };
24
+ };
25
+
26
+ export const decode = (byteBuffer: ByteBuffer) => {
27
+ const { typeId } = readHeader(byteBuffer);
28
+
29
+ return {
30
+ typeId,
31
+ buffer: byteBuffer,
32
+ };
33
+ };
34
+
35
+ // Parses first raw input that comes from Trezor and returns some information about the whole message.
36
+ // [compatibility]: accept Buffer just like decode does. But this would require changes in lower levels
37
+ export const decodeChunked = (bytes: ArrayBuffer) => {
38
+ // convert to ByteBuffer so it's easier to read
39
+ const byteBuffer = ByteBuffer.wrap(bytes, undefined, undefined, true);
40
+
41
+ const { sharp1, sharp2, typeId, length } = readHeaderChunked(byteBuffer);
42
+
43
+ if (sharp1 !== MESSAGE_HEADER_BYTE || sharp2 !== MESSAGE_HEADER_BYTE) {
44
+ throw new Error("Didn't receive expected header signature.");
45
+ }
46
+
47
+ return { length, typeId, restBuffer: byteBuffer };
48
+ };
@@ -0,0 +1,59 @@
1
+ import ByteBuffer from 'bytebuffer';
2
+
3
+ import { HEADER_SIZE, MESSAGE_HEADER_BYTE, BUFFER_SIZE } from '../../constants';
4
+
5
+ type Options<Chunked> = {
6
+ chunked: Chunked;
7
+ addTrezorHeaders: boolean;
8
+ messageType: number;
9
+ };
10
+
11
+ function encode(data: ByteBuffer, options: Options<true>): Buffer[];
12
+ function encode(data: ByteBuffer, options: Options<false>): Buffer;
13
+ function encode(data: any, options: any): any {
14
+ const { addTrezorHeaders, chunked, messageType } = options;
15
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
16
+ const fullSize = (addTrezorHeaders ? HEADER_SIZE : HEADER_SIZE - 2) + data.limit;
17
+
18
+ const encodedByteBuffer = new ByteBuffer(fullSize);
19
+
20
+ if (addTrezorHeaders) {
21
+ // 2*1 byte
22
+ encodedByteBuffer.writeByte(MESSAGE_HEADER_BYTE);
23
+ encodedByteBuffer.writeByte(MESSAGE_HEADER_BYTE);
24
+ }
25
+
26
+ // 2 bytes
27
+ encodedByteBuffer.writeUint16(messageType);
28
+
29
+ // 4 bytes (so 8 in total)
30
+ encodedByteBuffer.writeUint32(data.limit);
31
+
32
+ // then put in the actual message
33
+ encodedByteBuffer.append(data.buffer);
34
+
35
+ encodedByteBuffer.reset();
36
+
37
+ if (chunked === false) {
38
+ return encodedByteBuffer;
39
+ }
40
+
41
+ const result: Buffer[] = [];
42
+ const size = BUFFER_SIZE;
43
+
44
+ // How many pieces will there actually be
45
+ const count = Math.floor((encodedByteBuffer.limit - 1) / size) + 1 || 1;
46
+
47
+ // slice and dice
48
+ for (let i = 0; i < count; i++) {
49
+ const start = i * size;
50
+ const end = Math.min((i + 1) * size, encodedByteBuffer.limit);
51
+ const slice = encodedByteBuffer.slice(start, end);
52
+ slice.compact();
53
+ result.push(slice.buffer);
54
+ }
55
+
56
+ return result;
57
+ }
58
+
59
+ export { encode };
@@ -0,0 +1,2 @@
1
+ export * from './decode';
2
+ export * from './encode';
@@ -0,0 +1,18 @@
1
+ import { Root } from 'protobufjs/light';
2
+ import ByteBuffer from 'bytebuffer';
3
+
4
+ import * as decodeProtobuf from './protobuf/decode';
5
+ import * as decodeProtocol from './protocol/decode';
6
+ import { createMessageFromType } from './protobuf/messages';
7
+
8
+ export function receiveOne(messages: Root, data: string) {
9
+ const bytebuffer = ByteBuffer.wrap(data, 'hex');
10
+
11
+ const { typeId, buffer } = decodeProtocol.decode(bytebuffer);
12
+ const { Message, messageName } = createMessageFromType(messages, typeId);
13
+ const message = decodeProtobuf.decode(Message, buffer);
14
+ return {
15
+ message,
16
+ type: messageName,
17
+ };
18
+ }
@@ -0,0 +1,56 @@
1
+ // Logic of sending data to trezor
2
+ //
3
+ // Logic of "call" is broken to two parts - sending and receiving
4
+ import { Root } from 'protobufjs/light';
5
+ import ByteBuffer from 'bytebuffer';
6
+ import { encode as encodeProtobuf } from './protobuf';
7
+ import { encode as encodeProtocol } from './protocol';
8
+ import { createMessageFromName } from './protobuf/messages';
9
+ import { BUFFER_SIZE, MESSAGE_TOP_CHAR } from '../constants';
10
+
11
+ // Sends message to device.
12
+ // Resolves if everything gets sent
13
+ export function buildOne(messages: Root, name: string, data: Record<string, unknown>) {
14
+ const { Message, messageType } = createMessageFromName(messages, name);
15
+
16
+ const buffer = encodeProtobuf(Message, data);
17
+ return encodeProtocol(buffer, {
18
+ addTrezorHeaders: false,
19
+ chunked: false,
20
+ messageType,
21
+ });
22
+ }
23
+
24
+ export const buildEncodeBuffers = (messages: Root, name: string, data: Record<string, unknown>) => {
25
+ const { Message, messageType } = createMessageFromName(messages, name);
26
+ const buffer = encodeProtobuf(Message, data);
27
+ return encodeProtocol(buffer, {
28
+ addTrezorHeaders: true,
29
+ chunked: true,
30
+ messageType,
31
+ });
32
+ };
33
+
34
+ export const buildBuffers = (messages: Root, name: string, data: Record<string, unknown>) => {
35
+ // const { Message, messageType } = createMessageFromName(messages, name);
36
+ // const buffer = encodeProtobuf(Message, data);
37
+ // const encodeBuffers = encodeProtocol(buffer, {
38
+ // addTrezorHeaders: true,
39
+ // chunked: true,
40
+ // messageType,
41
+ // });
42
+
43
+ const encodeBuffers = buildEncodeBuffers(messages, name, data);
44
+
45
+ const outBuffers: ByteBuffer[] = [];
46
+
47
+ for (const buf of encodeBuffers) {
48
+ const chunkBuffer = new ByteBuffer(BUFFER_SIZE + 1);
49
+ chunkBuffer.writeByte(MESSAGE_TOP_CHAR);
50
+ chunkBuffer.append(buf);
51
+ chunkBuffer.reset();
52
+ outBuffers.push(chunkBuffer);
53
+ }
54
+
55
+ return outBuffers;
56
+ };
@@ -0,0 +1,2 @@
1
+ export * from './transport';
2
+ export * as Messages from './messages';