@ledgerhq/hw-app-eth 6.28.2 → 6.29.0

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 (106) hide show
  1. package/.turbo/turbo-build.log +5 -2
  2. package/CHANGELOG.md +13 -0
  3. package/README.md +58 -16
  4. package/jest.config.ts +6 -0
  5. package/lib/Eth.d.ts +37 -0
  6. package/lib/Eth.d.ts.map +1 -1
  7. package/lib/Eth.js +66 -72
  8. package/lib/Eth.js.map +1 -1
  9. package/lib/modules/EIP712/EIP712.types.d.ts +44 -0
  10. package/lib/modules/EIP712/EIP712.types.d.ts.map +1 -0
  11. package/lib/modules/EIP712/EIP712.types.js +3 -0
  12. package/lib/modules/EIP712/EIP712.types.js.map +1 -0
  13. package/lib/modules/EIP712/EIP712.utils.d.ts +65 -0
  14. package/lib/modules/EIP712/EIP712.utils.d.ts.map +1 -0
  15. package/lib/modules/EIP712/EIP712.utils.js +217 -0
  16. package/lib/modules/EIP712/EIP712.utils.js.map +1 -0
  17. package/lib/modules/EIP712/index.d.ts +60 -0
  18. package/lib/modules/EIP712/index.d.ts.map +1 -0
  19. package/lib/modules/EIP712/index.js +554 -0
  20. package/lib/modules/EIP712/index.js.map +1 -0
  21. package/lib/utils.d.ts +15 -1
  22. package/lib/utils.d.ts.map +1 -1
  23. package/lib/utils.js +43 -3
  24. package/lib/utils.js.map +1 -1
  25. package/lib-es/Eth.d.ts +37 -0
  26. package/lib-es/Eth.d.ts.map +1 -1
  27. package/lib-es/Eth.js +42 -48
  28. package/lib-es/Eth.js.map +1 -1
  29. package/lib-es/modules/EIP712/EIP712.types.d.ts +44 -0
  30. package/lib-es/modules/EIP712/EIP712.types.d.ts.map +1 -0
  31. package/lib-es/modules/EIP712/EIP712.types.js +2 -0
  32. package/lib-es/modules/EIP712/EIP712.types.js.map +1 -0
  33. package/lib-es/modules/EIP712/EIP712.utils.d.ts +65 -0
  34. package/lib-es/modules/EIP712/EIP712.utils.d.ts.map +1 -0
  35. package/lib-es/modules/EIP712/EIP712.utils.js +211 -0
  36. package/lib-es/modules/EIP712/EIP712.utils.js.map +1 -0
  37. package/lib-es/modules/EIP712/index.d.ts +60 -0
  38. package/lib-es/modules/EIP712/index.d.ts.map +1 -0
  39. package/lib-es/modules/EIP712/index.js +549 -0
  40. package/lib-es/modules/EIP712/index.js.map +1 -0
  41. package/lib-es/services/ledger/contracts.d.ts +0 -0
  42. package/lib-es/services/ledger/contracts.d.ts.map +0 -0
  43. package/lib-es/services/ledger/contracts.js +0 -0
  44. package/lib-es/services/ledger/contracts.js.map +0 -0
  45. package/lib-es/services/ledger/erc20.d.ts +0 -0
  46. package/lib-es/services/ledger/erc20.d.ts.map +0 -0
  47. package/lib-es/services/ledger/erc20.js +0 -0
  48. package/lib-es/services/ledger/erc20.js.map +0 -0
  49. package/lib-es/services/ledger/index.d.ts +0 -0
  50. package/lib-es/services/ledger/index.d.ts.map +0 -0
  51. package/lib-es/services/ledger/index.js +0 -0
  52. package/lib-es/services/ledger/index.js.map +0 -0
  53. package/lib-es/services/ledger/loadConfig.d.ts +0 -0
  54. package/lib-es/services/ledger/loadConfig.d.ts.map +0 -0
  55. package/lib-es/services/ledger/loadConfig.js +0 -0
  56. package/lib-es/services/ledger/loadConfig.js.map +0 -0
  57. package/lib-es/services/ledger/nfts.d.ts +0 -0
  58. package/lib-es/services/ledger/nfts.d.ts.map +0 -0
  59. package/lib-es/services/ledger/nfts.js +0 -0
  60. package/lib-es/services/ledger/nfts.js.map +0 -0
  61. package/lib-es/services/types.d.ts +0 -0
  62. package/lib-es/services/types.d.ts.map +0 -0
  63. package/lib-es/services/types.js +0 -0
  64. package/lib-es/services/types.js.map +0 -0
  65. package/lib-es/utils.d.ts +15 -1
  66. package/lib-es/utils.d.ts.map +1 -1
  67. package/lib-es/utils.js +38 -2
  68. package/lib-es/utils.js.map +1 -1
  69. package/package.json +13 -8
  70. package/src/Eth.ts +59 -56
  71. package/src/modules/EIP712/EIP712.types.ts +54 -0
  72. package/src/modules/EIP712/EIP712.utils.ts +251 -0
  73. package/src/modules/EIP712/index.ts +409 -0
  74. package/src/utils.ts +42 -2
  75. package/tests/EIP712.unit.test.ts +760 -0
  76. package/tests/sample-messages/0.apdus +58 -0
  77. package/tests/sample-messages/0.json +44 -0
  78. package/tests/sample-messages/1.apdus +66 -0
  79. package/tests/sample-messages/1.json +50 -0
  80. package/tests/sample-messages/10.apdus +30 -0
  81. package/tests/sample-messages/10.json +23 -0
  82. package/tests/sample-messages/2.apdus +126 -0
  83. package/tests/sample-messages/2.json +153 -0
  84. package/tests/sample-messages/3.apdus +42 -0
  85. package/tests/sample-messages/3.json +31 -0
  86. package/tests/sample-messages/4.apdus +84 -0
  87. package/tests/sample-messages/4.json +110 -0
  88. package/tests/sample-messages/5.apdus +112 -0
  89. package/tests/sample-messages/5.json +92 -0
  90. package/tests/sample-messages/6.apdus +94 -0
  91. package/tests/sample-messages/6.json +78 -0
  92. package/tests/sample-messages/7.apdus +70 -0
  93. package/tests/sample-messages/7.json +55 -0
  94. package/tests/sample-messages/8.apdus +68 -0
  95. package/tests/sample-messages/8.json +50 -0
  96. package/tests/sample-messages/9.apdus +68 -0
  97. package/tests/sample-messages/9.json +50 -0
  98. package/LICENSE +0 -202
  99. package/lib-es/contracts.d.ts +0 -17
  100. package/lib-es/contracts.d.ts.map +0 -1
  101. package/lib-es/contracts.js +0 -103
  102. package/lib-es/contracts.js.map +0 -1
  103. package/lib-es/erc20.d.ts +0 -22
  104. package/lib-es/erc20.d.ts.map +0 -1
  105. package/lib-es/erc20.js +0 -64
  106. package/lib-es/erc20.js.map +0 -1
@@ -0,0 +1,409 @@
1
+ import Transport from "@ledgerhq/hw-transport";
2
+ import {
3
+ EIP712Message,
4
+ EIP712MessageTypes,
5
+ EIP712MessageTypesEntry,
6
+ StructDefData,
7
+ StructImplemData,
8
+ } from "./EIP712.types";
9
+ import { hexBuffer, intAsHexBytes, splitPath } from "../../utils";
10
+ import {
11
+ destructTypeFromString,
12
+ EIP712_TYPE_ENCODERS,
13
+ EIP712_TYPE_PROPERTIES,
14
+ makeTypeEntryStructBuffer,
15
+ } from "./EIP712.utils";
16
+
17
+ /**
18
+ * @ignore for the README
19
+ *
20
+ * Factory to create the recursive function that will pass on each
21
+ * field level and APDUs to describe its structure implementation
22
+ *
23
+ * @param {Eth["EIP712SendStructImplem"]} EIP712SendStructImplem
24
+ * @param {EIP712MessageTypes} types
25
+ * @returns {void}
26
+ */
27
+ const makeRecursiveFieldStructImplem = (
28
+ transport: Transport,
29
+ types: EIP712MessageTypes
30
+ ): ((
31
+ destructedType: ReturnType<typeof destructTypeFromString>,
32
+ data: unknown
33
+ ) => void) => {
34
+ const typesMap = {} as Record<string, Record<string, string>>;
35
+ for (const type in types) {
36
+ typesMap[type] = types[type]?.reduce(
37
+ (acc, curr) => ({ ...acc, [curr.name]: curr.type }),
38
+ {}
39
+ );
40
+ }
41
+
42
+ // This recursion will call itself to handle each level of each field
43
+ // in order to send APDUs for each of them
44
+ const recursiveFieldStructImplem = async (
45
+ destructedType: ReturnType<typeof destructTypeFromString>,
46
+ data
47
+ ) => {
48
+ const [typeDescription, arrSizes] = destructedType;
49
+ const [currSize, ...restSizes] = arrSizes;
50
+ const isCustomType =
51
+ !EIP712_TYPE_PROPERTIES[typeDescription?.name?.toUpperCase() || ""];
52
+
53
+ if (Array.isArray(data) && typeof currSize !== "undefined") {
54
+ await EIP712SendStructImplem(transport, {
55
+ structType: "array",
56
+ value: data.length,
57
+ });
58
+ for (const entry of data) {
59
+ await recursiveFieldStructImplem([typeDescription, restSizes], entry);
60
+ }
61
+ } else if (isCustomType) {
62
+ for (const [fieldName, fieldValue] of Object.entries(
63
+ data as EIP712Message["message"]
64
+ )) {
65
+ const fieldType = typesMap?.[typeDescription?.name || ""][fieldName];
66
+
67
+ if (fieldType) {
68
+ await recursiveFieldStructImplem(
69
+ destructTypeFromString(fieldType),
70
+ fieldValue
71
+ );
72
+ }
73
+ }
74
+ } else {
75
+ await EIP712SendStructImplem(transport, {
76
+ structType: "field",
77
+ value: {
78
+ data,
79
+ type: typeDescription?.name || "",
80
+ sizeInBits: typeDescription?.bits,
81
+ },
82
+ });
83
+ }
84
+ };
85
+
86
+ return recursiveFieldStructImplem;
87
+ };
88
+
89
+ /**
90
+ * @ignore for the README
91
+ *
92
+ * This method is used to send the message definition with all its types.
93
+ * This method should be used before the EIP712SendStructImplem one
94
+ *
95
+ * @param {String} structType
96
+ * @param {String|Buffer} value
97
+ * @returns {Promise<void>}
98
+ */
99
+ const EIP712SendStructDef = (
100
+ transport: Transport,
101
+ structDef: StructDefData
102
+ ): Promise<Buffer> => {
103
+ enum APDU_FIELDS {
104
+ CLA = 0xe0,
105
+ INS = 0x1a,
106
+ P1_complete = 0x00,
107
+ P1_partial = 0x01,
108
+ P2_name = 0x00,
109
+ P2_field = 0xff,
110
+ }
111
+
112
+ const { structType, value } = structDef;
113
+ const data =
114
+ structType === "name" && typeof value === "string"
115
+ ? Buffer.from(value, "utf-8")
116
+ : (value as Buffer);
117
+
118
+ return transport.send(
119
+ APDU_FIELDS.CLA,
120
+ APDU_FIELDS.INS,
121
+ APDU_FIELDS.P1_complete,
122
+ structType === "name" ? APDU_FIELDS.P2_name : APDU_FIELDS.P2_field,
123
+ data
124
+ );
125
+ };
126
+
127
+ /**
128
+ * @ignore for the README
129
+ *
130
+ * This method provides a trusted new display name to use for the upcoming field.
131
+ * This method should be used after the EIP712SendStructDef one.
132
+ *
133
+ * If the method describes an empty name (length of 0), the upcoming field will be taken
134
+ * into account but won’t be shown on the device.
135
+ *
136
+ * The signature is computed on :
137
+ * json key length || json key || display name length || display name
138
+ *
139
+ * signed by the following secp256k1 public key:
140
+ * 0482bbf2f34f367b2e5bc21847b6566f21f0976b22d3388a9a5e446ac62d25cf725b62a2555b2dd464a4da0ab2f4d506820543af1d242470b1b1a969a27578f353
141
+ *
142
+ * @param {String} structType "root" | "array" | "field"
143
+ * @param {string | number | StructFieldData} value
144
+ * @returns {Promise<Buffer | void>}
145
+ */
146
+ const EIP712SendStructImplem = async (
147
+ transport: Transport,
148
+ structImplem: StructImplemData
149
+ ): Promise<Buffer | void> => {
150
+ enum APDU_FIELDS {
151
+ CLA = 0xe0,
152
+ INS = 0x1c,
153
+ P1_complete = 0x00,
154
+ P1_partial = 0x01,
155
+ P2_root = 0x00,
156
+ P2_array = 0x0f,
157
+ P2_field = 0xff,
158
+ }
159
+
160
+ const { structType, value } = structImplem;
161
+
162
+ if (structType === "root") {
163
+ return transport.send(
164
+ APDU_FIELDS.CLA,
165
+ APDU_FIELDS.INS,
166
+ APDU_FIELDS.P1_complete,
167
+ APDU_FIELDS.P2_root,
168
+ Buffer.from(value, "utf-8")
169
+ );
170
+ }
171
+
172
+ if (structType === "array") {
173
+ return transport.send(
174
+ APDU_FIELDS.CLA,
175
+ APDU_FIELDS.INS,
176
+ APDU_FIELDS.P1_complete,
177
+ APDU_FIELDS.P2_array,
178
+ Buffer.from(intAsHexBytes(value, 1), "hex")
179
+ );
180
+ }
181
+
182
+ if (structType === "field") {
183
+ const { data: rawData, type, sizeInBits } = value;
184
+ const encodedData: Buffer | null = EIP712_TYPE_ENCODERS[
185
+ type.toUpperCase()
186
+ ]?.(rawData, sizeInBits);
187
+
188
+ if (encodedData) {
189
+ // const dataLengthPer16Bits = (encodedData.length & 0xff00) >> 8;
190
+ const dataLengthPer16Bits = Math.floor(encodedData.length / 256);
191
+ // const dataLengthModulo16Bits = encodedData.length & 0xff;
192
+ const dataLengthModulo16Bits = encodedData.length % 256;
193
+
194
+ const data = Buffer.concat([
195
+ Buffer.from(intAsHexBytes(dataLengthPer16Bits, 1), "hex"),
196
+ Buffer.from(intAsHexBytes(dataLengthModulo16Bits, 1), "hex"),
197
+ encodedData,
198
+ ]);
199
+
200
+ const bufferSlices = new Array(Math.ceil(data.length / 256))
201
+ .fill(null)
202
+ .map((_, i) => data.slice(i * 255, (i + 1) * 255));
203
+
204
+ for (const bufferSlice of bufferSlices) {
205
+ await transport.send(
206
+ APDU_FIELDS.CLA,
207
+ APDU_FIELDS.INS,
208
+ bufferSlice !== bufferSlices[bufferSlices.length - 1]
209
+ ? APDU_FIELDS.P1_partial
210
+ : APDU_FIELDS.P1_complete,
211
+ APDU_FIELDS.P2_field,
212
+ bufferSlice
213
+ );
214
+ }
215
+ }
216
+ }
217
+
218
+ return Promise.resolve();
219
+ };
220
+
221
+ /**
222
+ * @ignore for the README
223
+ *
224
+ * Sign an EIP-721 formatted message following the specification here:
225
+ * https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.asc#sign-eth-eip-712
226
+ * @example
227
+ eth.signEIP721Message("44'/60'/0'/0/0", {
228
+ domain: {
229
+ chainId: 69,
230
+ name: "Da Domain",
231
+ verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
232
+ version: "1"
233
+ },
234
+ types: {
235
+ "EIP712Domain": [
236
+ { name: "name", type: "string" },
237
+ { name: "version", type: "string" },
238
+ { name: "chainId", type: "uint256" },
239
+ { name: "verifyingContract", type: "address" }
240
+ ],
241
+ "Test": [
242
+ { name: "contents", type: "string" }
243
+ ]
244
+ },
245
+ primaryType: "Test",
246
+ message: {contents: "Hello, Bob!"},
247
+ })
248
+ *
249
+ * @param {String} path derivationPath
250
+ * @param {Object} jsonMessage message to sign
251
+ * @param {Boolean} fullImplem use the legacy implementation
252
+ * @returns {Promise}
253
+ */
254
+ export const signEIP712Message = async (
255
+ transport: Transport,
256
+ path: string,
257
+ jsonMessage: EIP712Message,
258
+ fullImplem = false
259
+ ): Promise<{
260
+ v: number;
261
+ s: string;
262
+ r: string;
263
+ }> => {
264
+ enum APDU_FIELDS {
265
+ CLA = 0xe0,
266
+ INS = 0x0c,
267
+ P1 = 0x00,
268
+ P2_v0 = 0x00,
269
+ P2_full = 0x01,
270
+ }
271
+ const { primaryType, types, domain, message } = jsonMessage;
272
+
273
+ const typeEntries = Object.entries(types) as [
274
+ keyof EIP712MessageTypes,
275
+ EIP712MessageTypesEntry[]
276
+ ][];
277
+ // Looping on all types entries and fields to send structures' definitions
278
+ for (const [typeName, entries] of typeEntries) {
279
+ await EIP712SendStructDef(transport, {
280
+ structType: "name",
281
+ value: typeName as string,
282
+ });
283
+
284
+ for (const { name, type } of entries) {
285
+ const typeEntryBuffer = makeTypeEntryStructBuffer({ name, type });
286
+ await EIP712SendStructDef(transport, {
287
+ structType: "field",
288
+ value: typeEntryBuffer,
289
+ });
290
+ }
291
+ }
292
+
293
+ // Create the recursion that should pass on each entry
294
+ // of the domain fields and primaryType fields
295
+ const recursiveFieldStructImplem = makeRecursiveFieldStructImplem(
296
+ transport,
297
+ types
298
+ );
299
+
300
+ // Looping on all domain type entries and fields to send
301
+ // structures' implementations
302
+ const domainName = "EIP712Domain";
303
+ await EIP712SendStructImplem(transport, {
304
+ structType: "root",
305
+ value: domainName,
306
+ });
307
+ const domainTypeFields = types[domainName];
308
+ for (const { name, type } of domainTypeFields) {
309
+ const domainFieldValue = domain[name];
310
+ await recursiveFieldStructImplem(
311
+ destructTypeFromString(type as string),
312
+ domainFieldValue
313
+ );
314
+ }
315
+
316
+ // Looping on all primaryType type entries and fields to send
317
+ // structures' implementations
318
+ await EIP712SendStructImplem(transport, {
319
+ structType: "root",
320
+ value: primaryType,
321
+ });
322
+ const primaryTypeFields = types[primaryType];
323
+ for (const { name, type } of primaryTypeFields) {
324
+ const primaryTypeValue = message[name];
325
+ await recursiveFieldStructImplem(
326
+ destructTypeFromString(type as string),
327
+ primaryTypeValue
328
+ );
329
+ }
330
+
331
+ // Sending the final signature.
332
+ const paths = splitPath(path);
333
+ const signatureBuffer = Buffer.alloc(1 + paths.length * 4);
334
+ signatureBuffer[0] = paths.length;
335
+ paths.forEach((element, index) => {
336
+ signatureBuffer.writeUInt32BE(element, 1 + 4 * index);
337
+ });
338
+
339
+ return transport
340
+ .send(
341
+ APDU_FIELDS.CLA,
342
+ APDU_FIELDS.INS,
343
+ APDU_FIELDS.P1,
344
+ fullImplem ? APDU_FIELDS.P2_v0 : APDU_FIELDS.P2_full,
345
+ signatureBuffer
346
+ )
347
+ .then((response) => {
348
+ const v = response[0];
349
+ const r = response.slice(1, 1 + 32).toString("hex");
350
+ const s = response.slice(1 + 32, 1 + 32 + 32).toString("hex");
351
+
352
+ return {
353
+ v,
354
+ r,
355
+ s,
356
+ };
357
+ });
358
+ };
359
+
360
+ /**
361
+ * @ignore for the README
362
+ * Sign a prepared message following web3.eth.signTypedData specification. The host computes the domain separator and hashStruct(message)
363
+ * @example
364
+ eth.signEIP712HashedMessage("44'/60'/0'/0/0", Buffer.from("0101010101010101010101010101010101010101010101010101010101010101").toString("hex"), Buffer.from("0202020202020202020202020202020202020202020202020202020202020202").toString("hex")).then(result => {
365
+ var v = result['v'] - 27;
366
+ v = v.toString(16);
367
+ if (v.length < 2) {
368
+ v = "0" + v;
369
+ }
370
+ console.log("Signature 0x" + result['r'] + result['s'] + v);
371
+ })
372
+ */
373
+ export const signEIP712HashedMessage = (
374
+ transport: Transport,
375
+ path: string,
376
+ domainSeparatorHex: string,
377
+ hashStructMessageHex: string
378
+ ): Promise<{
379
+ v: number;
380
+ s: string;
381
+ r: string;
382
+ }> => {
383
+ const domainSeparator = hexBuffer(domainSeparatorHex);
384
+ const hashStruct = hexBuffer(hashStructMessageHex);
385
+ const paths = splitPath(path);
386
+ const buffer = Buffer.alloc(1 + paths.length * 4 + 32 + 32, 0);
387
+ let offset = 0;
388
+ buffer[0] = paths.length;
389
+ paths.forEach((element, index) => {
390
+ buffer.writeUInt32BE(element, 1 + 4 * index);
391
+ });
392
+ offset = 1 + 4 * paths.length;
393
+ domainSeparator.copy(buffer, offset);
394
+ offset += 32;
395
+ hashStruct.copy(buffer, offset);
396
+
397
+ return transport.send(0xe0, 0x0c, 0x00, 0x00, buffer).then((response) => {
398
+ const v = response[0];
399
+ const r = response.slice(1, 1 + 32).toString("hex");
400
+ const s = response.slice(1 + 32, 1 + 32 + 32).toString("hex");
401
+ return {
402
+ v,
403
+ r,
404
+ s,
405
+ };
406
+ });
407
+ };
408
+
409
+ export { EIP712Message } from "./EIP712.types";
package/src/utils.ts CHANGED
@@ -1,7 +1,34 @@
1
1
  import { encode, decode } from "@ethersproject/rlp";
2
2
  import { BigNumber } from "bignumber.js";
3
3
 
4
- export function decodeTxInfo(rawTx: Buffer) {
4
+ export function splitPath(path: string): number[] {
5
+ const result: number[] = [];
6
+ const components = path.split("/");
7
+ components.forEach((element) => {
8
+ let number = parseInt(element, 10);
9
+ if (isNaN(number)) {
10
+ return; // FIXME shouldn't it throws instead?
11
+ }
12
+ if (element.length > 1 && element[element.length - 1] === "'") {
13
+ number += 0x80000000;
14
+ }
15
+ result.push(number);
16
+ });
17
+ return result;
18
+ }
19
+
20
+ export function hexBuffer(str: string): Buffer {
21
+ return Buffer.from(str.startsWith("0x") ? str.slice(2) : str, "hex");
22
+ }
23
+
24
+ export function maybeHexBuffer(
25
+ str: string | null | undefined
26
+ ): Buffer | null | undefined {
27
+ if (!str) return null;
28
+ return hexBuffer(str);
29
+ }
30
+
31
+ export const decodeTxInfo = (rawTx: Buffer) => {
5
32
  const VALID_TYPES = [1, 2];
6
33
  const txType = VALID_TYPES.includes(rawTx[0]) ? rawTx[0] : null;
7
34
  const rlpData = txType === null ? rawTx : rawTx.slice(1);
@@ -74,4 +101,17 @@ export function decodeTxInfo(rawTx: Buffer) {
74
101
  chainIdTruncated,
75
102
  vrsOffset,
76
103
  };
77
- }
104
+ };
105
+
106
+ /**
107
+ * @ignore for the README
108
+ *
109
+ * Helper to convert an integer as a hexadecimal string with the right amount of digits
110
+ * to respect the number of bytes given as parameter
111
+ *
112
+ * @param int Integer
113
+ * @param bytes Number of bytes it should be represented as (1 byte = 2 caraters)
114
+ * @returns The given integer as an hexa string padded with the right number of 0
115
+ */
116
+ export const intAsHexBytes = (int: number, bytes: number): string =>
117
+ int.toString(16).padStart(2 * bytes, "0");