UNPKG

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