@ledgerhq/hw-app-eth 6.26.1 → 6.27.1

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.
@@ -0,0 +1,1120 @@
1
+ /********************************************************************************
2
+ * Ledger Node JS API
3
+ * (c) 2016-2017 Ledger
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ ********************************************************************************/
17
+ //@flow
18
+
19
+ // FIXME drop:
20
+ import { splitPath, foreach } from "./utils";
21
+ import { log } from "@ledgerhq/logs";
22
+ import { EthAppPleaseEnableContractData } from "@ledgerhq/errors";
23
+ import type Transport from "@ledgerhq/hw-transport";
24
+ import { BigNumber } from "bignumber.js";
25
+ import { ethers } from "ethers";
26
+ import { byContractAddress } from "./erc20";
27
+ import { getInfosForContractMethod } from "./contracts";
28
+
29
+ export type StarkQuantizationType =
30
+ | "eth"
31
+ | "erc20"
32
+ | "erc721"
33
+ | "erc20mintable"
34
+ | "erc721mintable";
35
+
36
+ const starkQuantizationTypeMap = {
37
+ eth: 1,
38
+ erc20: 2,
39
+ erc721: 3,
40
+ erc20mintable: 4,
41
+ erc721mintable: 5,
42
+ };
43
+
44
+ function hexBuffer(str: string): Buffer {
45
+ return Buffer.from(str.startsWith("0x") ? str.slice(2) : str, "hex");
46
+ }
47
+
48
+ function maybeHexBuffer(str: ?string): ?Buffer {
49
+ if (!str) return null;
50
+ return hexBuffer(str);
51
+ }
52
+
53
+ const remapTransactionRelatedErrors = (e) => {
54
+ if (e && e.statusCode === 0x6a80) {
55
+ return new EthAppPleaseEnableContractData(
56
+ "Please enable Contract data on the Ethereum app Settings"
57
+ );
58
+ }
59
+ return e;
60
+ };
61
+
62
+ /**
63
+ * Ethereum API
64
+ *
65
+ * @example
66
+ * import Eth from "@ledgerhq/hw-app-eth";
67
+ * const eth = new Eth(transport)
68
+ */
69
+ export default class Eth {
70
+ transport: Transport<*>;
71
+
72
+ constructor(transport: Transport<*>, scrambleKey: string = "w0w") {
73
+ this.transport = transport;
74
+ transport.decorateAppAPIMethods(
75
+ this,
76
+ [
77
+ "getAddress",
78
+ "provideERC20TokenInformation",
79
+ "signTransaction",
80
+ "signPersonalMessage",
81
+ "getAppConfiguration",
82
+ "signEIP712HashedMessage",
83
+ "starkGetPublicKey",
84
+ "starkSignOrder",
85
+ "starkSignOrder_v2",
86
+ "starkSignTransfer",
87
+ "starkSignTransfer_v2",
88
+ "starkProvideQuantum",
89
+ "starkProvideQuantum_v2",
90
+ "starkUnsafeSign",
91
+ "eth2GetPublicKey",
92
+ "eth2SetWithdrawalIndex",
93
+ "setExternalPlugin",
94
+ ],
95
+ scrambleKey
96
+ );
97
+ }
98
+
99
+ /**
100
+ * get Ethereum address for a given BIP 32 path.
101
+ * @param path a path in BIP 32 format
102
+ * @option boolDisplay optionally enable or not the display
103
+ * @option boolChaincode optionally enable or not the chaincode request
104
+ * @return an object with a publicKey, address and (optionally) chainCode
105
+ * @example
106
+ * eth.getAddress("44'/60'/0'/0/0").then(o => o.address)
107
+ */
108
+ getAddress(
109
+ path: string,
110
+ boolDisplay?: boolean,
111
+ boolChaincode?: boolean
112
+ ): Promise<{
113
+ publicKey: string,
114
+ address: string,
115
+ chainCode?: string,
116
+ }> {
117
+ let paths = splitPath(path);
118
+ let buffer = Buffer.alloc(1 + paths.length * 4);
119
+ buffer[0] = paths.length;
120
+ paths.forEach((element, index) => {
121
+ buffer.writeUInt32BE(element, 1 + 4 * index);
122
+ });
123
+ return this.transport
124
+ .send(
125
+ 0xe0,
126
+ 0x02,
127
+ boolDisplay ? 0x01 : 0x00,
128
+ boolChaincode ? 0x01 : 0x00,
129
+ buffer
130
+ )
131
+ .then((response) => {
132
+ let result = {};
133
+ let publicKeyLength = response[0];
134
+ let addressLength = response[1 + publicKeyLength];
135
+ result.publicKey = response
136
+ .slice(1, 1 + publicKeyLength)
137
+ .toString("hex");
138
+ result.address =
139
+ "0x" +
140
+ response
141
+ .slice(
142
+ 1 + publicKeyLength + 1,
143
+ 1 + publicKeyLength + 1 + addressLength
144
+ )
145
+ .toString("ascii");
146
+ if (boolChaincode) {
147
+ result.chainCode = response
148
+ .slice(
149
+ 1 + publicKeyLength + 1 + addressLength,
150
+ 1 + publicKeyLength + 1 + addressLength + 32
151
+ )
152
+ .toString("hex");
153
+ }
154
+ return result;
155
+ });
156
+ }
157
+
158
+ /**
159
+ * This commands provides a trusted description of an ERC 20 token
160
+ * to associate a contract address with a ticker and number of decimals.
161
+ *
162
+ * It shall be run immediately before performing a transaction involving a contract
163
+ * calling this contract address to display the proper token information to the user if necessary.
164
+ *
165
+ * @param {*} info: a blob from "erc20.js" utilities that contains all token information.
166
+ *
167
+ * @example
168
+ * import { byContractAddress } from "@ledgerhq/hw-app-eth/erc20"
169
+ * const zrxInfo = byContractAddress("0xe41d2489571d322189246dafa5ebde1f4699f498")
170
+ * if (zrxInfo) await appEth.provideERC20TokenInformation(zrxInfo)
171
+ * const signed = await appEth.signTransaction(path, rawTxHex)
172
+ */
173
+ provideERC20TokenInformation({ data }: { data: Buffer }): Promise<boolean> {
174
+ return provideERC20TokenInformation(this.transport, data);
175
+ }
176
+
177
+ /**
178
+ * You can sign a transaction and retrieve v, r, s given the raw transaction and the BIP 32 path of the account to sign
179
+ * @example
180
+ eth.signTransaction("44'/60'/0'/0/0", "e8018504e3b292008252089428ee52a8f3d6e5d15f8b131996950d7f296c7952872bd72a2487400080").then(result => ...)
181
+ */
182
+ async signTransaction(
183
+ path: string,
184
+ rawTxHex: string
185
+ ): Promise<{
186
+ s: string,
187
+ v: string,
188
+ r: string,
189
+ }> {
190
+ let paths = splitPath(path);
191
+ let offset = 0;
192
+ let rawTx = Buffer.from(rawTxHex, "hex");
193
+ let toSend = [];
194
+ let response;
195
+ // Check if the TX is encoded following EIP 155
196
+ let rlpTx = ethers.utils.RLP.decode("0x" + rawTxHex).map((hex) =>
197
+ Buffer.from(hex.slice(2), "hex")
198
+ );
199
+
200
+ let rlpOffset = 0;
201
+ let chainIdPrefix = "";
202
+ if (rlpTx.length > 6) {
203
+ let rlpVrs = Buffer.from(
204
+ ethers.utils.RLP.encode(rlpTx.slice(-3)).slice(2),
205
+ "hex"
206
+ );
207
+
208
+ rlpOffset = rawTx.length - (rlpVrs.length - 1);
209
+ const chainIdSrc = rlpTx[6];
210
+ const chainIdBuf = Buffer.alloc(4);
211
+ chainIdSrc.copy(chainIdBuf, 4 - chainIdSrc.length);
212
+ chainIdPrefix = (chainIdBuf.readUInt32BE(0) * 2 + 35)
213
+ .toString(16)
214
+ .slice(0, -2); // Drop the low byte, that comes from the ledger.
215
+ if (chainIdPrefix.length % 2 === 1) {
216
+ chainIdPrefix = "0" + chainIdPrefix;
217
+ }
218
+ }
219
+ while (offset !== rawTx.length) {
220
+ let maxChunkSize = offset === 0 ? 150 - 1 - paths.length * 4 : 150;
221
+ let chunkSize =
222
+ offset + maxChunkSize > rawTx.length
223
+ ? rawTx.length - offset
224
+ : maxChunkSize;
225
+ if (rlpOffset != 0 && offset + chunkSize == rlpOffset) {
226
+ // Make sure that the chunk doesn't end right on the EIP 155 marker if set
227
+ chunkSize--;
228
+ }
229
+ let buffer = Buffer.alloc(
230
+ offset === 0 ? 1 + paths.length * 4 + chunkSize : chunkSize
231
+ );
232
+ if (offset === 0) {
233
+ buffer[0] = paths.length;
234
+ paths.forEach((element, index) => {
235
+ buffer.writeUInt32BE(element, 1 + 4 * index);
236
+ });
237
+ rawTx.copy(buffer, 1 + 4 * paths.length, offset, offset + chunkSize);
238
+ } else {
239
+ rawTx.copy(buffer, 0, offset, offset + chunkSize);
240
+ }
241
+ toSend.push(buffer);
242
+ offset += chunkSize;
243
+ }
244
+
245
+ rlpTx = ethers.utils.RLP.decode("0x" + rawTxHex);
246
+
247
+ const decodedTx = {
248
+ data: rlpTx[5],
249
+ to: rlpTx[3],
250
+ };
251
+ const provideForContract = async (address) => {
252
+ const erc20Info = byContractAddress(address);
253
+ if (erc20Info) {
254
+ log(
255
+ "ethereum",
256
+ "loading erc20token info for " +
257
+ erc20Info.contractAddress +
258
+ " (" +
259
+ erc20Info.ticker +
260
+ ")"
261
+ );
262
+ await provideERC20TokenInformation(this.transport, erc20Info.data);
263
+ }
264
+ };
265
+
266
+ if (decodedTx.data.length >= 10) {
267
+ const selector = decodedTx.data.substring(0, 10);
268
+ const infos = getInfosForContractMethod(decodedTx.to, selector);
269
+
270
+ if (infos) {
271
+ let { plugin, payload, signature, erc20OfInterest, abi } = infos;
272
+
273
+ if (plugin) {
274
+ log("ethereum", "loading plugin for " + selector);
275
+ await setExternalPlugin(this.transport, payload, signature);
276
+ }
277
+
278
+ if (erc20OfInterest && erc20OfInterest.length && abi) {
279
+ const contract = new ethers.utils.Interface(abi);
280
+ const args = contract.parseTransaction(decodedTx).args;
281
+
282
+ for (path of erc20OfInterest) {
283
+ const address = path.split(".").reduce((value, seg) => {
284
+ if (seg === "-1" && Array.isArray(value)) {
285
+ return value[value.length - 1];
286
+ }
287
+ return value[seg];
288
+ }, args);
289
+ await provideForContract(address);
290
+ }
291
+ }
292
+ } else {
293
+ log("ethereum", "no infos for selector " + selector);
294
+ }
295
+
296
+ await provideForContract(decodedTx.to);
297
+ }
298
+
299
+ return foreach(toSend, (data, i) =>
300
+ this.transport
301
+ .send(0xe0, 0x04, i === 0 ? 0x00 : 0x80, 0x00, data)
302
+ .then((apduResponse) => {
303
+ response = apduResponse;
304
+ })
305
+ ).then(
306
+ () => {
307
+ const v = chainIdPrefix + response.slice(0, 1).toString("hex");
308
+ const r = response.slice(1, 1 + 32).toString("hex");
309
+ const s = response.slice(1 + 32, 1 + 32 + 32).toString("hex");
310
+ return { v, r, s };
311
+ },
312
+ (e) => {
313
+ throw remapTransactionRelatedErrors(e);
314
+ }
315
+ );
316
+ }
317
+
318
+ /**
319
+ */
320
+ getAppConfiguration(): Promise<{
321
+ arbitraryDataEnabled: number,
322
+ erc20ProvisioningNecessary: number,
323
+ starkEnabled: number,
324
+ starkv2Supported: number,
325
+ version: string,
326
+ }> {
327
+ return this.transport.send(0xe0, 0x06, 0x00, 0x00).then((response) => {
328
+ let result = {};
329
+ result.arbitraryDataEnabled = response[0] & 0x01;
330
+ result.erc20ProvisioningNecessary = response[0] & 0x02;
331
+ result.starkEnabled = response[0] & 0x04;
332
+ result.starkv2Supported = response[0] & 0x08;
333
+ result.version = "" + response[1] + "." + response[2] + "." + response[3];
334
+ return result;
335
+ });
336
+ }
337
+
338
+ /**
339
+ * You can sign a message according to eth_sign RPC call and retrieve v, r, s given the message and the BIP 32 path of the account to sign.
340
+ * @example
341
+ eth.signPersonalMessage("44'/60'/0'/0/0", Buffer.from("test").toString("hex")).then(result => {
342
+ var v = result['v'] - 27;
343
+ v = v.toString(16);
344
+ if (v.length < 2) {
345
+ v = "0" + v;
346
+ }
347
+ console.log("Signature 0x" + result['r'] + result['s'] + v);
348
+ })
349
+ */
350
+ signPersonalMessage(
351
+ path: string,
352
+ messageHex: string
353
+ ): Promise<{
354
+ v: number,
355
+ s: string,
356
+ r: string,
357
+ }> {
358
+ let paths = splitPath(path);
359
+ let offset = 0;
360
+ let message = Buffer.from(messageHex, "hex");
361
+ let toSend = [];
362
+ let response;
363
+ while (offset !== message.length) {
364
+ let maxChunkSize = offset === 0 ? 150 - 1 - paths.length * 4 - 4 : 150;
365
+ let chunkSize =
366
+ offset + maxChunkSize > message.length
367
+ ? message.length - offset
368
+ : maxChunkSize;
369
+ let buffer = Buffer.alloc(
370
+ offset === 0 ? 1 + paths.length * 4 + 4 + chunkSize : chunkSize
371
+ );
372
+ if (offset === 0) {
373
+ buffer[0] = paths.length;
374
+ paths.forEach((element, index) => {
375
+ buffer.writeUInt32BE(element, 1 + 4 * index);
376
+ });
377
+ buffer.writeUInt32BE(message.length, 1 + 4 * paths.length);
378
+ message.copy(
379
+ buffer,
380
+ 1 + 4 * paths.length + 4,
381
+ offset,
382
+ offset + chunkSize
383
+ );
384
+ } else {
385
+ message.copy(buffer, 0, offset, offset + chunkSize);
386
+ }
387
+ toSend.push(buffer);
388
+ offset += chunkSize;
389
+ }
390
+ return foreach(toSend, (data, i) =>
391
+ this.transport
392
+ .send(0xe0, 0x08, i === 0 ? 0x00 : 0x80, 0x00, data)
393
+ .then((apduResponse) => {
394
+ response = apduResponse;
395
+ })
396
+ ).then(() => {
397
+ const v = response[0];
398
+ const r = response.slice(1, 1 + 32).toString("hex");
399
+ const s = response.slice(1 + 32, 1 + 32 + 32).toString("hex");
400
+ return { v, r, s };
401
+ });
402
+ }
403
+
404
+ /**
405
+ * Sign a prepared message following web3.eth.signTypedData specification. The host computes the domain separator and hashStruct(message)
406
+ * @example
407
+ eth.signEIP712HashedMessage("44'/60'/0'/0/0", Buffer.from("0101010101010101010101010101010101010101010101010101010101010101").toString("hex"), Buffer.from("0202020202020202020202020202020202020202020202020202020202020202").toString("hex")).then(result => {
408
+ var v = result['v'] - 27;
409
+ v = v.toString(16);
410
+ if (v.length < 2) {
411
+ v = "0" + v;
412
+ }
413
+ console.log("Signature 0x" + result['r'] + result['s'] + v);
414
+ })
415
+ */
416
+ signEIP712HashedMessage(
417
+ path: string,
418
+ domainSeparatorHex: string,
419
+ hashStructMessageHex: string
420
+ ): Promise<{
421
+ v: number,
422
+ s: string,
423
+ r: string,
424
+ }> {
425
+ const domainSeparator = hexBuffer(domainSeparatorHex);
426
+ const hashStruct = hexBuffer(hashStructMessageHex);
427
+ let paths = splitPath(path);
428
+ let buffer = Buffer.alloc(1 + paths.length * 4 + 32 + 32, 0);
429
+ let offset = 0;
430
+ buffer[0] = paths.length;
431
+ paths.forEach((element, index) => {
432
+ buffer.writeUInt32BE(element, 1 + 4 * index);
433
+ });
434
+ offset = 1 + 4 * paths.length;
435
+ domainSeparator.copy(buffer, offset);
436
+ offset += 32;
437
+ hashStruct.copy(buffer, offset);
438
+ return this.transport
439
+ .send(0xe0, 0x0c, 0x00, 0x00, buffer)
440
+ .then((response) => {
441
+ const v = response[0];
442
+ const r = response.slice(1, 1 + 32).toString("hex");
443
+ const s = response.slice(1 + 32, 1 + 32 + 32).toString("hex");
444
+ return { v, r, s };
445
+ });
446
+ }
447
+
448
+ /**
449
+ * get Stark public key for a given BIP 32 path.
450
+ * @param path a path in BIP 32 format
451
+ * @option boolDisplay optionally enable or not the display
452
+ * @return the Stark public key
453
+ */
454
+ starkGetPublicKey(path: string, boolDisplay?: boolean): Promise<Buffer> {
455
+ let paths = splitPath(path);
456
+ let buffer = Buffer.alloc(1 + paths.length * 4);
457
+ buffer[0] = paths.length;
458
+ paths.forEach((element, index) => {
459
+ buffer.writeUInt32BE(element, 1 + 4 * index);
460
+ });
461
+ return this.transport
462
+ .send(0xf0, 0x02, boolDisplay ? 0x01 : 0x00, 0x00, buffer)
463
+ .then((response) => {
464
+ return response.slice(0, response.length - 2);
465
+ });
466
+ }
467
+
468
+ /**
469
+ * sign a Stark order
470
+ * @param path a path in BIP 32 format
471
+ * @option sourceTokenAddress contract address of the source token (not present for ETH)
472
+ * @param sourceQuantization quantization used for the source token
473
+ * @option destinationTokenAddress contract address of the destination token (not present for ETH)
474
+ * @param destinationQuantization quantization used for the destination token
475
+ * @param sourceVault ID of the source vault
476
+ * @param destinationVault ID of the destination vault
477
+ * @param amountSell amount to sell
478
+ * @param amountBuy amount to buy
479
+ * @param nonce transaction nonce
480
+ * @param timestamp transaction validity timestamp
481
+ * @return the signature
482
+ */
483
+ starkSignOrder(
484
+ path: string,
485
+ sourceTokenAddress?: string,
486
+ sourceQuantization: BigNumber,
487
+ destinationTokenAddress?: string,
488
+ destinationQuantization: BigNumber,
489
+ sourceVault: number,
490
+ destinationVault: number,
491
+ amountSell: BigNumber,
492
+ amountBuy: BigNumber,
493
+ nonce: number,
494
+ timestamp: number
495
+ ): Promise<Buffer> {
496
+ const sourceTokenAddressHex = maybeHexBuffer(sourceTokenAddress);
497
+ const destinationTokenAddressHex = maybeHexBuffer(destinationTokenAddress);
498
+ let paths = splitPath(path);
499
+ let buffer = Buffer.alloc(
500
+ 1 + paths.length * 4 + 20 + 32 + 20 + 32 + 4 + 4 + 8 + 8 + 4 + 4,
501
+ 0
502
+ );
503
+ let offset = 0;
504
+ buffer[0] = paths.length;
505
+ paths.forEach((element, index) => {
506
+ buffer.writeUInt32BE(element, 1 + 4 * index);
507
+ });
508
+ offset = 1 + 4 * paths.length;
509
+ if (sourceTokenAddressHex) {
510
+ sourceTokenAddressHex.copy(buffer, offset);
511
+ }
512
+ offset += 20;
513
+ Buffer.from(sourceQuantization.toString(16).padStart(64, "0"), "hex").copy(
514
+ buffer,
515
+ offset
516
+ );
517
+ offset += 32;
518
+ if (destinationTokenAddressHex) {
519
+ destinationTokenAddressHex.copy(buffer, offset);
520
+ }
521
+ offset += 20;
522
+ Buffer.from(
523
+ destinationQuantization.toString(16).padStart(64, "0"),
524
+ "hex"
525
+ ).copy(buffer, offset);
526
+ offset += 32;
527
+ buffer.writeUInt32BE(sourceVault, offset);
528
+ offset += 4;
529
+ buffer.writeUInt32BE(destinationVault, offset);
530
+ offset += 4;
531
+ Buffer.from(amountSell.toString(16).padStart(16, "0"), "hex").copy(
532
+ buffer,
533
+ offset
534
+ );
535
+ offset += 8;
536
+ Buffer.from(amountBuy.toString(16).padStart(16, "0"), "hex").copy(
537
+ buffer,
538
+ offset
539
+ );
540
+ offset += 8;
541
+ buffer.writeUInt32BE(nonce, offset);
542
+ offset += 4;
543
+ buffer.writeUInt32BE(timestamp, offset);
544
+ return this.transport
545
+ .send(0xf0, 0x04, 0x01, 0x00, buffer)
546
+ .then((response) => {
547
+ const r = response.slice(1, 1 + 32).toString("hex");
548
+ const s = response.slice(1 + 32, 1 + 32 + 32).toString("hex");
549
+ return { r, s };
550
+ });
551
+ }
552
+
553
+ /**
554
+ * sign a Stark order using the Starkex V2 protocol
555
+ * @param path a path in BIP 32 format
556
+ * @option sourceTokenAddress contract address of the source token (not present for ETH)
557
+ * @param sourceQuantizationType quantization type used for the source token
558
+ * @option sourceQuantization quantization used for the source token (not present for erc 721 or mintable erc 721)
559
+ * @option sourceMintableBlobOrTokenId mintable blob (mintable erc 20 / mintable erc 721) or token id (erc 721) associated to the source token
560
+ * @option destinationTokenAddress contract address of the destination token (not present for ETH)
561
+ * @param destinationQuantizationType quantization type used for the destination token
562
+ * @option destinationQuantization quantization used for the destination token (not present for erc 721 or mintable erc 721)
563
+ * @option destinationMintableBlobOrTokenId mintable blob (mintable erc 20 / mintable erc 721) or token id (erc 721) associated to the destination token
564
+ * @param sourceVault ID of the source vault
565
+ * @param destinationVault ID of the destination vault
566
+ * @param amountSell amount to sell
567
+ * @param amountBuy amount to buy
568
+ * @param nonce transaction nonce
569
+ * @param timestamp transaction validity timestamp
570
+ * @return the signature
571
+ */
572
+ starkSignOrder_v2(
573
+ path: string,
574
+ sourceTokenAddress?: string,
575
+ sourceQuantizationType: StarkQuantizationType,
576
+ sourceQuantization?: BigNumber,
577
+ sourceMintableBlobOrTokenId?: BigNumber,
578
+ destinationTokenAddress?: string,
579
+ destinationQuantizationType: StarkQuantizationType,
580
+ destinationQuantization?: BigNumber,
581
+ destinationMintableBlobOrTokenId?: BigNumber,
582
+ sourceVault: number,
583
+ destinationVault: number,
584
+ amountSell: BigNumber,
585
+ amountBuy: BigNumber,
586
+ nonce: number,
587
+ timestamp: number
588
+ ): Promise<Buffer> {
589
+ const sourceTokenAddressHex = maybeHexBuffer(sourceTokenAddress);
590
+ const destinationTokenAddressHex = maybeHexBuffer(destinationTokenAddress);
591
+ if (!(sourceQuantizationType in starkQuantizationTypeMap)) {
592
+ throw new Error(
593
+ "eth.starkSignOrderv2 invalid source quantization type=" +
594
+ sourceQuantizationType
595
+ );
596
+ }
597
+ if (!(destinationQuantizationType in starkQuantizationTypeMap)) {
598
+ throw new Error(
599
+ "eth.starkSignOrderv2 invalid destination quantization type=" +
600
+ destinationQuantizationType
601
+ );
602
+ }
603
+ let paths = splitPath(path);
604
+ let buffer = Buffer.alloc(
605
+ 1 +
606
+ paths.length * 4 +
607
+ 1 +
608
+ 20 +
609
+ 32 +
610
+ 32 +
611
+ 1 +
612
+ 20 +
613
+ 32 +
614
+ 32 +
615
+ 4 +
616
+ 4 +
617
+ 8 +
618
+ 8 +
619
+ 4 +
620
+ 4,
621
+ 0
622
+ );
623
+ let offset = 0;
624
+ buffer[0] = paths.length;
625
+ paths.forEach((element, index) => {
626
+ buffer.writeUInt32BE(element, 1 + 4 * index);
627
+ });
628
+ offset = 1 + 4 * paths.length;
629
+ buffer[offset] = starkQuantizationTypeMap[sourceQuantizationType];
630
+ offset++;
631
+ if (sourceTokenAddressHex) {
632
+ sourceTokenAddressHex.copy(buffer, offset);
633
+ }
634
+ offset += 20;
635
+ if (sourceQuantization) {
636
+ Buffer.from(
637
+ sourceQuantization.toString(16).padStart(64, "0"),
638
+ "hex"
639
+ ).copy(buffer, offset);
640
+ }
641
+ offset += 32;
642
+ if (sourceMintableBlobOrTokenId) {
643
+ Buffer.from(
644
+ sourceMintableBlobOrTokenId.toString(16).padStart(64, "0"),
645
+ "hex"
646
+ ).copy(buffer, offset);
647
+ }
648
+ offset += 32;
649
+ buffer[offset] = starkQuantizationTypeMap[destinationQuantizationType];
650
+ offset++;
651
+ if (destinationTokenAddressHex) {
652
+ destinationTokenAddressHex.copy(buffer, offset);
653
+ }
654
+ offset += 20;
655
+ if (destinationQuantization) {
656
+ Buffer.from(
657
+ destinationQuantization.toString(16).padStart(64, "0"),
658
+ "hex"
659
+ ).copy(buffer, offset);
660
+ }
661
+ offset += 32;
662
+ if (destinationMintableBlobOrTokenId) {
663
+ Buffer.from(
664
+ destinationMintableBlobOrTokenId.toString(16).padStart(64, "0"),
665
+ "hex"
666
+ ).copy(buffer, offset);
667
+ }
668
+ offset += 32;
669
+ buffer.writeUInt32BE(sourceVault, offset);
670
+ offset += 4;
671
+ buffer.writeUInt32BE(destinationVault, offset);
672
+ offset += 4;
673
+ Buffer.from(amountSell.toString(16).padStart(16, "0"), "hex").copy(
674
+ buffer,
675
+ offset
676
+ );
677
+ offset += 8;
678
+ Buffer.from(amountBuy.toString(16).padStart(16, "0"), "hex").copy(
679
+ buffer,
680
+ offset
681
+ );
682
+ offset += 8;
683
+ buffer.writeUInt32BE(nonce, offset);
684
+ offset += 4;
685
+ buffer.writeUInt32BE(timestamp, offset);
686
+ return this.transport
687
+ .send(0xf0, 0x04, 0x03, 0x00, buffer)
688
+ .then((response) => {
689
+ const r = response.slice(1, 1 + 32).toString("hex");
690
+ const s = response.slice(1 + 32, 1 + 32 + 32).toString("hex");
691
+ return { r, s };
692
+ });
693
+ }
694
+
695
+ /**
696
+ * sign a Stark transfer
697
+ * @param path a path in BIP 32 format
698
+ * @option transferTokenAddress contract address of the token to be transferred (not present for ETH)
699
+ * @param transferQuantization quantization used for the token to be transferred
700
+ * @param targetPublicKey target Stark public key
701
+ * @param sourceVault ID of the source vault
702
+ * @param destinationVault ID of the destination vault
703
+ * @param amountTransfer amount to transfer
704
+ * @param nonce transaction nonce
705
+ * @param timestamp transaction validity timestamp
706
+ * @return the signature
707
+ */
708
+ starkSignTransfer(
709
+ path: string,
710
+ transferTokenAddress?: string,
711
+ transferQuantization: BigNumber,
712
+ targetPublicKey: string,
713
+ sourceVault: number,
714
+ destinationVault: number,
715
+ amountTransfer: BigNumber,
716
+ nonce: number,
717
+ timestamp: number
718
+ ): Promise<Buffer> {
719
+ const transferTokenAddressHex = maybeHexBuffer(transferTokenAddress);
720
+ const targetPublicKeyHex = hexBuffer(targetPublicKey);
721
+ let paths = splitPath(path);
722
+ let buffer = Buffer.alloc(
723
+ 1 + paths.length * 4 + 20 + 32 + 32 + 4 + 4 + 8 + 4 + 4,
724
+ 0
725
+ );
726
+ let offset = 0;
727
+ buffer[0] = paths.length;
728
+ paths.forEach((element, index) => {
729
+ buffer.writeUInt32BE(element, 1 + 4 * index);
730
+ });
731
+ offset = 1 + 4 * paths.length;
732
+ if (transferTokenAddressHex) {
733
+ transferTokenAddressHex.copy(buffer, offset);
734
+ }
735
+ offset += 20;
736
+ Buffer.from(
737
+ transferQuantization.toString(16).padStart(64, "0"),
738
+ "hex"
739
+ ).copy(buffer, offset);
740
+ offset += 32;
741
+ targetPublicKeyHex.copy(buffer, offset);
742
+ offset += 32;
743
+ buffer.writeUInt32BE(sourceVault, offset);
744
+ offset += 4;
745
+ buffer.writeUInt32BE(destinationVault, offset);
746
+ offset += 4;
747
+ Buffer.from(amountTransfer.toString(16).padStart(16, "0"), "hex").copy(
748
+ buffer,
749
+ offset
750
+ );
751
+ offset += 8;
752
+ buffer.writeUInt32BE(nonce, offset);
753
+ offset += 4;
754
+ buffer.writeUInt32BE(timestamp, offset);
755
+ return this.transport
756
+ .send(0xf0, 0x04, 0x02, 0x00, buffer)
757
+ .then((response) => {
758
+ const r = response.slice(1, 1 + 32).toString("hex");
759
+ const s = response.slice(1 + 32, 1 + 32 + 32).toString("hex");
760
+ return { r, s };
761
+ });
762
+ }
763
+
764
+ /**
765
+ * sign a Stark transfer or conditional transfer using the Starkex V2 protocol
766
+ * @param path a path in BIP 32 format
767
+ * @option transferTokenAddress contract address of the token to be transferred (not present for ETH)
768
+ * @param transferQuantizationType quantization type used for the token to be transferred
769
+ * @option transferQuantization quantization used for the token to be transferred (not present for erc 721 or mintable erc 721)
770
+ * @option transferMintableBlobOrTokenId mintable blob (mintable erc 20 / mintable erc 721) or token id (erc 721) associated to the token to be transferred
771
+ * @param targetPublicKey target Stark public key
772
+ * @param sourceVault ID of the source vault
773
+ * @param destinationVault ID of the destination vault
774
+ * @param amountTransfer amount to transfer
775
+ * @param nonce transaction nonce
776
+ * @param timestamp transaction validity timestamp
777
+ * @option conditionalTransferAddress onchain address of the condition for a conditional transfer
778
+ * @option conditionalTransferFact fact associated to the condition for a conditional transfer
779
+ * @return the signature
780
+ */
781
+ starkSignTransfer_v2(
782
+ path: string,
783
+ transferTokenAddress?: string,
784
+ transferQuantizationType: StarkQuantizationType,
785
+ transferQuantization?: BigNumber,
786
+ transferMintableBlobOrTokenId?: BigNumber,
787
+ targetPublicKey: string,
788
+ sourceVault: number,
789
+ destinationVault: number,
790
+ amountTransfer: BigNumber,
791
+ nonce: number,
792
+ timestamp: number,
793
+ conditionalTransferAddress?: string,
794
+ conditionalTransferFact?: BigNumber
795
+ ): Promise<Buffer> {
796
+ const transferTokenAddressHex = maybeHexBuffer(transferTokenAddress);
797
+ const targetPublicKeyHex = hexBuffer(targetPublicKey);
798
+ const conditionalTransferAddressHex = maybeHexBuffer(
799
+ conditionalTransferAddress
800
+ );
801
+ if (!(transferQuantizationType in starkQuantizationTypeMap)) {
802
+ throw new Error(
803
+ "eth.starkSignTransferv2 invalid quantization type=" +
804
+ transferQuantizationType
805
+ );
806
+ }
807
+ let paths = splitPath(path);
808
+ let buffer = Buffer.alloc(
809
+ 1 +
810
+ paths.length * 4 +
811
+ 1 +
812
+ 20 +
813
+ 32 +
814
+ 32 +
815
+ 32 +
816
+ 4 +
817
+ 4 +
818
+ 8 +
819
+ 4 +
820
+ 4 +
821
+ (conditionalTransferAddressHex ? 32 + 20 : 0),
822
+ 0
823
+ );
824
+ let offset = 0;
825
+ buffer[0] = paths.length;
826
+ paths.forEach((element, index) => {
827
+ buffer.writeUInt32BE(element, 1 + 4 * index);
828
+ });
829
+ offset = 1 + 4 * paths.length;
830
+ buffer[offset] = starkQuantizationTypeMap[transferQuantizationType];
831
+ offset++;
832
+ if (transferTokenAddressHex) {
833
+ transferTokenAddressHex.copy(buffer, offset);
834
+ }
835
+ offset += 20;
836
+ if (transferQuantization) {
837
+ Buffer.from(
838
+ transferQuantization.toString(16).padStart(64, "0"),
839
+ "hex"
840
+ ).copy(buffer, offset);
841
+ }
842
+ offset += 32;
843
+ if (transferMintableBlobOrTokenId) {
844
+ Buffer.from(
845
+ transferMintableBlobOrTokenId.toString(16).padStart(64, "0"),
846
+ "hex"
847
+ ).copy(buffer, offset);
848
+ }
849
+ offset += 32;
850
+ targetPublicKeyHex.copy(buffer, offset);
851
+ offset += 32;
852
+ buffer.writeUInt32BE(sourceVault, offset);
853
+ offset += 4;
854
+ buffer.writeUInt32BE(destinationVault, offset);
855
+ offset += 4;
856
+ Buffer.from(amountTransfer.toString(16).padStart(16, "0"), "hex").copy(
857
+ buffer,
858
+ offset
859
+ );
860
+ offset += 8;
861
+ buffer.writeUInt32BE(nonce, offset);
862
+ offset += 4;
863
+ buffer.writeUInt32BE(timestamp, offset);
864
+ if (conditionalTransferAddressHex && conditionalTransferFact) {
865
+ offset += 4;
866
+ Buffer.from(
867
+ conditionalTransferFact.toString(16).padStart(64, "0"),
868
+ "hex"
869
+ ).copy(buffer, offset);
870
+ offset += 32;
871
+ conditionalTransferAddressHex.copy(buffer, offset);
872
+ }
873
+ return this.transport
874
+ .send(
875
+ 0xf0,
876
+ 0x04,
877
+ conditionalTransferAddressHex ? 0x05 : 0x04,
878
+ 0x00,
879
+ buffer
880
+ )
881
+ .then((response) => {
882
+ const r = response.slice(1, 1 + 32).toString("hex");
883
+ const s = response.slice(1 + 32, 1 + 32 + 32).toString("hex");
884
+ return { r, s };
885
+ });
886
+ }
887
+
888
+ /**
889
+ * provide quantization information before singing a deposit or withdrawal Stark powered contract call
890
+ *
891
+ * It shall be run following a provideERC20TokenInformation call for the given contract
892
+ *
893
+ * @param operationContract contract address of the token to be transferred (not present for ETH)
894
+ * @param operationQuantization quantization used for the token to be transferred
895
+ */
896
+ starkProvideQuantum(
897
+ operationContract?: string,
898
+ operationQuantization: BigNumber
899
+ ): Promise<boolean> {
900
+ const operationContractHex = maybeHexBuffer(operationContract);
901
+ let buffer = Buffer.alloc(20 + 32, 0);
902
+ if (operationContractHex) {
903
+ operationContractHex.copy(buffer, 0);
904
+ }
905
+ Buffer.from(
906
+ operationQuantization.toString(16).padStart(64, "0"),
907
+ "hex"
908
+ ).copy(buffer, 20);
909
+ return this.transport.send(0xf0, 0x08, 0x00, 0x00, buffer).then(
910
+ () => true,
911
+ (e) => {
912
+ if (e && e.statusCode === 0x6d00) {
913
+ // this case happen for ETH application versions not supporting Stark extensions
914
+ return false;
915
+ }
916
+ throw e;
917
+ }
918
+ );
919
+ }
920
+
921
+ /**
922
+ * provide quantization information before singing a deposit or withdrawal Stark powered contract call using the Starkex V2 protocol
923
+ *
924
+ * It shall be run following a provideERC20TokenInformation call for the given contract
925
+ *
926
+ * @param operationContract contract address of the token to be transferred (not present for ETH)
927
+ * @param operationQuantizationType quantization type of the token to be transferred
928
+ * @option operationQuantization quantization used for the token to be transferred (not present for erc 721 or mintable erc 721)
929
+ * @option operationMintableBlobOrTokenId mintable blob (mintable erc 20 / mintable erc 721) or token id (erc 721) of the token to be transferred
930
+ */
931
+ starkProvideQuantum_v2(
932
+ operationContract?: string,
933
+ operationQuantizationType: StarkQuantizationType,
934
+ operationQuantization?: BigNumber,
935
+ operationMintableBlobOrTokenId?: BigNumber
936
+ ): Promise<boolean> {
937
+ const operationContractHex = maybeHexBuffer(operationContract);
938
+ if (!(operationQuantizationType in starkQuantizationTypeMap)) {
939
+ throw new Error(
940
+ "eth.starkProvideQuantumV2 invalid quantization type=" +
941
+ operationQuantizationType
942
+ );
943
+ }
944
+ let buffer = Buffer.alloc(20 + 32 + 32, 0);
945
+ let offset = 0;
946
+ if (operationContractHex) {
947
+ operationContractHex.copy(buffer, offset);
948
+ }
949
+ offset += 20;
950
+ if (operationQuantization) {
951
+ Buffer.from(
952
+ operationQuantization.toString(16).padStart(64, "0"),
953
+ "hex"
954
+ ).copy(buffer, offset);
955
+ }
956
+ offset += 32;
957
+ if (operationMintableBlobOrTokenId) {
958
+ Buffer.from(
959
+ operationMintableBlobOrTokenId.toString(16).padStart(64, "0"),
960
+ "hex"
961
+ ).copy(buffer, offset);
962
+ }
963
+ return this.transport
964
+ .send(
965
+ 0xf0,
966
+ 0x08,
967
+ starkQuantizationTypeMap[operationQuantizationType],
968
+ 0x00,
969
+ buffer
970
+ )
971
+ .then(
972
+ () => true,
973
+ (e) => {
974
+ if (e && e.statusCode === 0x6d00) {
975
+ // this case happen for ETH application versions not supporting Stark extensions
976
+ return false;
977
+ }
978
+ throw e;
979
+ }
980
+ );
981
+ }
982
+
983
+ /**
984
+ * sign the given hash over the Stark curve
985
+ * It is intended for speed of execution in case an unknown Stark model is pushed and should be avoided as much as possible.
986
+ * @param path a path in BIP 32 format
987
+ * @param hash hexadecimal hash to sign
988
+ * @return the signature
989
+ */
990
+ starkUnsafeSign(path: string, hash: string): Promise<Buffer> {
991
+ const hashHex = hexBuffer(hash);
992
+ let paths = splitPath(path);
993
+ let buffer = Buffer.alloc(1 + paths.length * 4 + 32);
994
+ let offset = 0;
995
+ buffer[0] = paths.length;
996
+ paths.forEach((element, index) => {
997
+ buffer.writeUInt32BE(element, 1 + 4 * index);
998
+ });
999
+ offset = 1 + 4 * paths.length;
1000
+ hashHex.copy(buffer, offset);
1001
+ return this.transport
1002
+ .send(0xf0, 0x0a, 0x00, 0x00, buffer)
1003
+ .then((response) => {
1004
+ const r = response.slice(1, 1 + 32).toString("hex");
1005
+ const s = response.slice(1 + 32, 1 + 32 + 32).toString("hex");
1006
+ return { r, s };
1007
+ });
1008
+ }
1009
+
1010
+ /**
1011
+ * get an Ethereum 2 BLS-12 381 public key for a given BIP 32 path.
1012
+ * @param path a path in BIP 32 format
1013
+ * @option boolDisplay optionally enable or not the display
1014
+ * @return an object with a publicKey
1015
+ * @example
1016
+ * eth.eth2GetPublicKey("12381/3600/0/0").then(o => o.publicKey)
1017
+ */
1018
+ eth2GetPublicKey(
1019
+ path: string,
1020
+ boolDisplay?: boolean
1021
+ ): Promise<{
1022
+ publicKey: string,
1023
+ }> {
1024
+ let paths = splitPath(path);
1025
+ let buffer = Buffer.alloc(1 + paths.length * 4);
1026
+ buffer[0] = paths.length;
1027
+ paths.forEach((element, index) => {
1028
+ buffer.writeUInt32BE(element, 1 + 4 * index);
1029
+ });
1030
+ return this.transport
1031
+ .send(0xe0, 0x0e, boolDisplay ? 0x01 : 0x00, 0x00, buffer)
1032
+ .then((response) => {
1033
+ let result = {};
1034
+ result.publicKey = response.slice(0, -2).toString("hex");
1035
+ return result;
1036
+ });
1037
+ }
1038
+
1039
+ /**
1040
+ * Set the index of a Withdrawal key used as withdrawal credentials in an ETH 2 deposit contract call signature
1041
+ *
1042
+ * It shall be run before the ETH 2 deposit transaction is signed. If not called, the index is set to 0
1043
+ *
1044
+ * @param withdrawalIndex index path in the EIP 2334 path m/12381/3600/withdrawalIndex/0
1045
+ * @return True if the method was executed successfully
1046
+ */
1047
+ eth2SetWithdrawalIndex(withdrawalIndex: number): Promise<boolean> {
1048
+ let buffer = Buffer.alloc(4, 0);
1049
+ buffer.writeUInt32BE(withdrawalIndex, 0);
1050
+ return this.transport.send(0xe0, 0x10, 0x00, 0x00, buffer).then(
1051
+ () => true,
1052
+ (e) => {
1053
+ if (e && e.statusCode === 0x6d00) {
1054
+ // this case happen for ETH application versions not supporting ETH 2
1055
+ return false;
1056
+ }
1057
+ throw e;
1058
+ }
1059
+ );
1060
+ }
1061
+
1062
+ /**
1063
+ * Set the name of the plugin that should be used to parse the next transaction
1064
+ *
1065
+ * @param pluginName string containing the name of the plugin, must have length between 1 and 30 bytes
1066
+ * @return True if the method was executed successfully
1067
+ */
1068
+ setExternalPlugin(
1069
+ pluginName: string,
1070
+ contractAddress: string,
1071
+ selector: string
1072
+ ): Promise<boolean> {
1073
+ return setExternalPlugin(this.transport, pluginName, selector);
1074
+ }
1075
+ }
1076
+
1077
+ // internal helpers
1078
+
1079
+ function provideERC20TokenInformation(
1080
+ transport: Transport<*>,
1081
+ data: Buffer
1082
+ ): Promise<boolean> {
1083
+ return transport.send(0xe0, 0x0a, 0x00, 0x00, data).then(
1084
+ () => true,
1085
+ (e) => {
1086
+ if (e && e.statusCode === 0x6d00) {
1087
+ // this case happen for older version of ETH app, since older app version had the ERC20 data hardcoded, it's fine to assume it worked.
1088
+ // we return a flag to know if the call was effective or not
1089
+ return false;
1090
+ }
1091
+ throw e;
1092
+ }
1093
+ );
1094
+ }
1095
+
1096
+ function setExternalPlugin(
1097
+ transport: Transport<*>,
1098
+ payload: string,
1099
+ signature: string
1100
+ ): Promise<boolean> {
1101
+ let payloadBuffer = Buffer.from(payload, "hex");
1102
+ let signatureBuffer = Buffer.from(signature, "hex");
1103
+ let buffer = Buffer.concat([payloadBuffer, signatureBuffer]);
1104
+ return transport.send(0xe0, 0x12, 0x00, 0x00, buffer).then(
1105
+ () => true,
1106
+ (e) => {
1107
+ if (e && e.statusCode === 0x6a80) {
1108
+ // this case happen when the plugin name is too short or too long
1109
+ return false;
1110
+ } else if (e && e.statusCode === 0x6984) {
1111
+ // this case happen when the plugin requested is not installed on the device
1112
+ return false;
1113
+ } else if (e && e.statusCode === 0x6d00) {
1114
+ // this case happen for older version of ETH app
1115
+ return false;
1116
+ }
1117
+ throw e;
1118
+ }
1119
+ );
1120
+ }