├── .babelrc ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package.json ├── src ├── LedgerAPI.ts ├── LedgerUtils.ts ├── SignatureProvider.ts ├── index.ts └── modules.d.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-proposal-class-properties"] 4 | } -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ## Change Description 5 | 6 | 7 | 8 | ## API Changes 9 | - [ ] API Changes 10 | 11 | 12 | 13 | 14 | ## Documentation Additions 15 | - [ ] Documentation Additions 16 | 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #VS_Code 2 | /.vscode 3 | 4 | # dependencies 5 | /node_modules 6 | 7 | # testing 8 | /coverage 9 | 10 | # production 11 | /build 12 | /dist 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | *.orig 26 | package-lock.json 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Exclude all files by default 2 | * 3 | 4 | # Include distribution bundle but exclude test files if they are built into dist 5 | !dist/** 6 | *.test.* 7 | 8 | # Include documentation and version information in bundle 9 | !CONTRIBUTING.md -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '10.0.0' 5 | stages: 6 | - test 7 | jobs: 8 | include: 9 | - stage: test 10 | name: "Lint" 11 | script: 12 | - yarn lint 13 | notifications: 14 | webhooks: 15 | secure: "BOaQ7yPpNGz4zHn13kmtE9xvjD0zORVQvNS5vxVzUBiWx6Z9d95fOqm0FT0tI7toPZphGVis0oS/9Gou2PlRF2ih5q86q8roqHMaoRJrSM3gNa8O0ykimkIU857vE8WPYugSLcVVJq+FnK2jrOkh6aFKdyN0hw4kHFErKcTkNe3VfchVhsCO0k++V2jVtmlo6cUtEn8GgaurgHUjz3jyPjHTrIAev0tYlT3Gi1iEn+ymDe2+MeGdHU/i5V3aYt5R3jMVOMkUofAsTll2yX7LAwdibFayKibv2Zc6CxxxYWox9TqCjPB6x+vIEfs8/KkV7dtBKFybw4NGmgCClNLBdQfwgnYeAc9TFDafhTbnIZhdye2WO8WxYFL4RGxS3zWhY37UXQO9spgQq+66YBqIuk4wr5Iu4Ir6bCfe7hgjz337jkJzqiSV7wxb+ETq2Vd1tbv3U8iqyxJiDp3YOoCTk4a//uM6VcXkJbsG8UTqA6QqiKvtdNWz1JJkw5AKCi1EHwlUMxIY6pQt7dGmFA8tWSvqt/Ro9fusal0g/2oV1ty8wfyjVGMwaUP0XCvTJ8D9yrCZboBwc/qz40iryaJbIT+fLy6PSAs7GHQtpS6qnrCzmPKyG+SEIo2J8MTLS2ug4kTDzgvFc/XpEviY6n2CycMkd/Te5n93NroHcc6mhEs=" 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to eosjs-ledger-signature-provider 2 | 3 | Interested in contributing? That's awesome! Here are some guidelines to get started quickly and easily: 4 | 5 | - [Reporting An Issue](#reporting-an-issue) 6 | - [Bug Reports](#bug-reports) 7 | - [Feature Requests](#feature-requests) 8 | - [Change Requests](#change-requests) 9 | - [Working on eosjs-ledger-signature-provider](#working-on-eosjs-ledger-signature-provider) 10 | - [Feature Branches](#feature-branches) 11 | - [Submitting Pull Requests](#submitting-pull-requests) 12 | - [Testing and Quality Assurance](#testing-and-quality-assurance) 13 | - [Conduct](#conduct) 14 | - [Contributor License & Acknowledgments](#contributor-license--acknowledgments) 15 | - [References](#references) 16 | 17 | ## Reporting An Issue 18 | 19 | If you're about to raise an issue because you think you've found a problem with eosjs-ledger-signature-provider, or you'd like to make a request for a new feature in the codebase, or any other reason… please read this first. 20 | 21 | The GitHub issue tracker is the preferred channel for [bug reports](#bug-reports), [feature requests](#feature-requests), and [submitting pull requests](#submitting-pull-requests), but please respect the following restrictions: 22 | 23 | * Please **search for existing issues**. Help us keep duplicate issues to a minimum by checking to see if someone has already reported your problem or requested your idea. 24 | 25 | * Please **be civil**. Keep the discussion on topic and respect the opinions of others. See also our [Contributor Code of Conduct](#conduct). 26 | 27 | ### Bug Reports 28 | 29 | A bug is a _demonstrable problem_ that is caused by the code in the repository. Good bug reports are extremely helpful - thank you! 30 | 31 | Guidelines for bug reports: 32 | 33 | 1. **Use the GitHub issue search** — check if the issue has already been 34 | reported. 35 | 36 | 1. **Check if the issue has been fixed** — look for [closed issues in the 37 | current milestone](https://github.com/EOSIO/eosjs-ledger-signature-provider/issues?q=is%3Aissue+is%3Aclosed) or try to reproduce it 38 | using the latest `develop` branch. 39 | 40 | A good bug report shouldn't leave others needing to chase you up for more information. Be sure to include the details of your environment and relevant tests that demonstrate the failure. 41 | 42 | [Report a bug](https://github.com/EOSIO/eosjs-ledger-signature-provider/issues/new?title=Bug%3A) 43 | 44 | ### Feature Requests 45 | 46 | Feature requests are welcome. Before you submit one be sure to have: 47 | 48 | 1. **Use the GitHub search** and check the feature hasn't already been requested. 49 | 1. Take a moment to think about whether your idea fits with the scope and aims of the project. 50 | 1. Remember, it's up to *you* to make a strong case to convince the project's leaders of the merits of this feature. Please provide as much detail and context as possible, this means explaining the use case and why it is likely to be common. 51 | 52 | ### Change Requests 53 | 54 | Change requests cover both architectural and functional changes to how eosjs-ledger-signature-provider works. If you have an idea for a new or different dependency, a refactor, or an improvement to a feature, etc - please be sure to: 55 | 56 | 1. **Use the GitHub search** and check someone else didn't get there first 57 | 1. Take a moment to think about the best way to make a case for, and explain what you're thinking. Are you sure this shouldn't really be 58 | a [bug report](#bug-reports) or a [feature request](#feature-requests)? Is it really one idea or is it many? What's the context? What problem are you solving? Why is what you are suggesting better than what's already there? 59 | 60 | ## Working on eosjs-ledger-signature-provider 61 | 62 | Code contributions are welcome and encouraged! If you are looking for a good place to start, check out the [good first issue](https://github.com/EOSIO/eosjs-ledger-signature-provider/labels/good%20first%20issue) label in GitHub issues. 63 | 64 | Also, please follow these guidelines when submitting code: 65 | 66 | ### Feature Branches 67 | 68 | To get it out of the way: 69 | 70 | - **[develop](https://github.com/EOSIO/eosjs-ledger-signature-provider/tree/develop)** is the development branch. All work on the next release happens here so you should generally branch off `develop`. Do **NOT** use this branch for a production site. 71 | - **[master](https://github.com/EOSIO/eosjs-ledger-signature-provider)** contains the latest release of eosjs-ledger-signature-provider. This branch may be used in production. Do **NOT** use this branch to work on eosjs-ledger-signature-provider's source. 72 | 73 | ### Submitting Pull Requests 74 | 75 | Pull requests are awesome. If you're looking to raise a PR for something which doesn't have an open issue, please think carefully about [raising an issue](#reporting-an-issue) which your PR can close, especially if you're fixing a bug. This makes it more likely that there will be enough information available for your PR to be properly tested and merged. 76 | 77 | ### Testing and Quality Assurance 78 | 79 | Never underestimate just how useful quality assurance is. If you're looking to get involved with the code base and don't know where to start, checking out and testing a pull request is one of the most useful things you could do. 80 | 81 | Essentially, [check out the latest develop branch](#working-on-eosjs-ledger-signature-provider), take it for a spin, and if you find anything odd, please follow the [bug report guidelines](#bug-reports) and let us know! 82 | 83 | ## Conduct 84 | 85 | While contributing, please be respectful and constructive, so that participation in our project is a positive experience for everyone. 86 | 87 | Examples of behavior that contributes to creating a positive environment include: 88 | - Using welcoming and inclusive language 89 | - Being respectful of differing viewpoints and experiences 90 | - Gracefully accepting constructive criticism 91 | - Focusing on what is best for the community 92 | - Showing empathy towards other community members 93 | 94 | Examples of unacceptable behavior include: 95 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 96 | - Trolling, insulting/derogatory comments, and personal or political attacks 97 | - Public or private harassment 98 | - Publishing others’ private information, such as a physical or electronic address, without explicit permission 99 | - Other conduct which could reasonably be considered inappropriate in a professional setting 100 | 101 | 102 | 103 | ## Contributor License & Acknowledgments 104 | 105 | Whenever you make a contribution to this project, you license your contribution under the same terms as set out in LICENSE, and you represent and warrant that you have the right to license your contribution under those terms. Whenever you make a contribution to this project, you also certify in the terms of the Developer’s Certificate of Origin set out below: 106 | 107 | ``` 108 | Developer Certificate of Origin 109 | Version 1.1 110 | 111 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 112 | 1 Letterman Drive 113 | Suite D4700 114 | San Francisco, CA, 94129 115 | 116 | Everyone is permitted to copy and distribute verbatim copies of this 117 | license document, but changing it is not allowed. 118 | 119 | 120 | Developer's Certificate of Origin 1.1 121 | 122 | By making a contribution to this project, I certify that: 123 | 124 | (a) The contribution was created in whole or in part by me and I 125 | have the right to submit it under the open source license 126 | indicated in the file; or 127 | 128 | (b) The contribution is based upon previous work that, to the best 129 | of my knowledge, is covered under an appropriate open source 130 | license and I have the right under that license to submit that 131 | work with modifications, whether created in whole or in part 132 | by me, under the same open source license (unless I am 133 | permitted to submit under a different license), as indicated 134 | in the file; or 135 | 136 | (c) The contribution was provided directly to me by some other 137 | person who certified (a), (b) or (c) and I have not modified 138 | it. 139 | 140 | (d) I understand and agree that this project and the contribution 141 | are public and that a record of the contribution (including all 142 | personal information I submit with it, including my sign-off) is 143 | maintained indefinitely and may be redistributed consistent with 144 | this project or the open source license(s) involved. 145 | ``` 146 | 147 | ## References 148 | 149 | * Overall CONTRIB adapted from https://github.com/mathjax/MathJax/blob/master/CONTRIBUTING.md 150 | * Conduct section adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 151 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2019 block.one and its contributors. All rights reserved. 2 | 3 | The MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EOSJS Signature Provider for Ledger 2 | 3 | ## Overview 4 | A SignatureProvider for communicating with [eosjs](https://github.com/EOSIO/eosjs) from a Ledger device. 5 | 6 | When plugged into `eosjs`, this signature provider enables applications to route signing requests to a Ledger device. Full instructions for `eosjs` can be found [here](https://github.com/EOSIO/eosjs). 7 | 8 | ![EOSIO Labs](https://img.shields.io/badge/EOSIO-Labs-5cb3ff.svg) 9 | 10 | # About EOSIO Labs 11 | 12 | EOSIO Labs repositories are experimental. Developers in the community are encouraged to use EOSIO Labs repositories as the basis for code and concepts to incorporate into their applications. Community members are also welcome to contribute and further develop these repositories. Since these repositories are not supported by Block.one, we may not provide responses to issue reports, pull requests, updates to functionality, or other requests from the community, and we encourage the community to take responsibility for these. 13 | 14 | ## Installation 15 | 16 | ```bash 17 | # Using yarn 18 | yarn add eosjs-ledger-signature-provider 19 | ``` 20 | 21 | ## Usage 22 | 23 | #### Example 24 | ```javascript 25 | const { SignatureProvider } from 'eosjs-ledger-signature-provider' 26 | 27 | const signatureProvider = new SignatureProvider() 28 | 29 | signatureProvider.getAvailableKeys() 30 | .then((result) => console.info('Keys: ', result)) 31 | .catch((error) => console.info('Error: ', error)) 32 | 33 | const chainId = '000000000' 34 | const serializedTransaction = {} // A transaction as a Uint8Array. View `serializeTransaction` in https://github.com/EOSIO/eosjs/blob/develop/src/eosjs-api.ts 35 | 36 | signatureProvider.sign({ chainId, serializedTransaction }) 37 | .then((result) => console.info('TransactionId: ', result)) 38 | .catch((error) => console.info('Error: ', error)) 39 | ``` 40 | 41 | #### Example with eosjs 42 | ```javascript 43 | const { Api, JsonRpc } from 'eosjs' 44 | const { SignatureProvider } from 'eosjs-ledger-signature-provider' 45 | 46 | const rpcEndpoint = 'https://localhost:3000' 47 | const signatureProvider = new SignatureProvider() 48 | const rpc = new JsonRpc(rpcEndpoint) 49 | const api = new Api({ signatureProvider, rpc }) 50 | 51 | // eosjs will call both `getAvailableKeys` and `sign` from the SignatureProvider 52 | api.transact(...) 53 | .then((result) => console.info('TransactionId: ', result)) 54 | .catch((error) => console.info('Error: ', error)) 55 | ``` 56 | 57 | ## Contribution 58 | Check out the [Contributing](./CONTRIBUTING.md) guide 59 | 60 | ## License 61 | [MIT licensed](./LICENSE) 62 | 63 | ## Important 64 | 65 | See LICENSE for copyright and license terms. Block.one makes its contribution on a voluntary basis as a member of the EOSIO community and is not responsible for ensuring the overall performance of the software or any related applications. We make no representation, warranty, guarantee or undertaking in respect of the software or any related documentation, whether expressed or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall we be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or documentation or the use or other dealings in the software or documentation. Any test results or performance figures are indicative and will not reflect performance under all conditions. Any reference to any third party or third-party product, service or other resource is not an endorsement or recommendation by Block.one. We are not responsible, and disclaim any and all responsibility and liability, for your use of or reliance on any of these resources. Third-party resources may be updated, changed or terminated at any time, so the information here may be out of date or inaccurate. Any person using or offering this software in connection with providing software, goods or services to third parties shall advise such third parties of these license terms, disclaimers and exclusions of liability. Block.one, EOSIO, EOSIO Labs, EOS, the heptahedron and associated logos are trademarks of Block.one. 66 | 67 | Wallets and related components are complex software that require the highest levels of security. If incorrectly built or used, they may compromise users’ private keys and digital assets. Wallet applications and related components should undergo thorough security evaluations before being used. Only experienced developers should work with this software. 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eosjs-ledger-signature-provider", 3 | "version": "0.1.2", 4 | "description": "A Signature provider for communicating with eosjs from a Ledger device.", 5 | "author": { 6 | "name": "block.one", 7 | "url": "https://www.block.one" 8 | }, 9 | "contributors": [ 10 | "Randy Torres", 11 | "Brandon Fancher" 12 | ], 13 | "license": "MIT", 14 | "main": "dist/index.js", 15 | "scripts": { 16 | "lint": "tslint -c tslint.json src/*.ts", 17 | "build": "yarn clean & tsc -p ./tsconfig.json", 18 | "clean": "rm -rf dist", 19 | "coverage": "jest --coverage", 20 | "test": "jest", 21 | "prepublish": "yarn build", 22 | "docs": "typedoc --excludePrivate --excludeProtected --out ./docs --theme markdown" 23 | }, 24 | "dependencies": { 25 | "@babel/core": "7.6.0", 26 | "@babel/preset-env": "7.2.3", 27 | "@babel/preset-react": "7.0.0", 28 | "@ledgerhq/hw-transport": "4.48.0", 29 | "@ledgerhq/hw-transport-u2f": "4.48.0", 30 | "asn1-ber": "1.0.9", 31 | "bip32-path": "0.4.2", 32 | "buffer": "5.2.1", 33 | "eosjs": "20.0.0", 34 | "eosjs-ecc": "4.0.4" 35 | }, 36 | "devDependencies": { 37 | "@babel/cli": "^7.6.0", 38 | "@babel/plugin-proposal-class-properties": "^7.3.0", 39 | "@blockone/tslint-config-blockone": "^4.0.0", 40 | "@types/node": "^11.9.4", 41 | "babel-polyfill": "^6.26.0", 42 | "jest": "^24.1.0", 43 | "ts-jest": "^23.10.4", 44 | "tslib": "^1.9.3", 45 | "tslint": "^5.11.0", 46 | "tslint-eslint-rules": "^5.4.0", 47 | "typedoc": "^0.13.0", 48 | "typedoc-plugin-markdown": "^1.1.19", 49 | "typescript": "^3.2.2" 50 | }, 51 | "jest": { 52 | "moduleFileExtensions": [ 53 | "ts", 54 | "tsx", 55 | "js" 56 | ], 57 | "transform": { 58 | "^.+\\.(tsx?)$": "ts-jest" 59 | }, 60 | "globals": { 61 | "ts-jest": { 62 | "tsConfigFile": "tsconfig.json" 63 | } 64 | }, 65 | "testRegex": "(/src/.*(\\.|/)(test|spec))\\.(ts?|tsx?)$" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/LedgerAPI.ts: -------------------------------------------------------------------------------- 1 | import bippath from 'bip32-path' 2 | import Buff from 'buffer/' 3 | 4 | import { 5 | convertSignatures, 6 | GET_LEDGER_PATHS, 7 | iteratePromises, 8 | LEDGER_CODES, 9 | serialize, 10 | } from './LedgerUtils' 11 | 12 | export interface LedgerAPIInterface { 13 | transport: any 14 | addressIndex: number 15 | scrambleKey: string 16 | } 17 | 18 | /** 19 | * Ledger API 20 | * @param transport Ledger transport method 21 | */ 22 | export class LedgerAPI implements LedgerAPIInterface { 23 | public transport: any = null 24 | public addressIndex: number = 0 25 | public scrambleKey: string = 'e0s' 26 | 27 | constructor(transport: any) { 28 | this.transport = transport 29 | 30 | try { 31 | transport.decorateAppAPIMethods( 32 | this, 33 | ['getPublicKey', 'signTransaction', 'getAppConfiguration'], 34 | this.scrambleKey 35 | ) 36 | } catch (error) { 37 | console.error(error) 38 | throw error 39 | } 40 | } 41 | 42 | public getAppConfiguration = () => ( 43 | this.transport.send( 44 | LEDGER_CODES.CLA, 45 | LEDGER_CODES.INS_GET_APP_CONFIGURATION, 46 | LEDGER_CODES.P1_NON_CONFIRM, 47 | LEDGER_CODES.P1_NON_CONFIRM, 48 | ) 49 | .then(() => true) 50 | .catch((err: any) => { throw Error(err) }) 51 | ) 52 | 53 | /** 54 | * @returns [keys] An array of public keys 55 | */ 56 | public getPublicKey(requestPermission: boolean = true): Promise { 57 | return new Promise((resolve, reject) => { 58 | setTimeout(() => { 59 | const path = GET_LEDGER_PATHS(this.addressIndex) 60 | const paths = bippath.fromString(path).toPathArray() 61 | const buffer = Buff.Buffer.alloc(1 + paths.length * 4) 62 | buffer[0] = paths.length 63 | paths.forEach((element: number, index: number) => { 64 | buffer.writeUInt32BE(element, 1 + 4 * index) 65 | }) 66 | 67 | return this.transport 68 | .send( 69 | LEDGER_CODES.CLA, 70 | LEDGER_CODES.INS_GET_PUBLIC_KEY, 71 | requestPermission ? LEDGER_CODES.P1_CONFIRM : LEDGER_CODES.P1_NON_CONFIRM, 72 | LEDGER_CODES.P1_NON_CONFIRM, 73 | buffer 74 | ) 75 | .then((response: any) => { 76 | const publicKeyLength = response[0] 77 | const addressLength = response[1 + publicKeyLength] 78 | 79 | resolve(response 80 | .slice( 81 | 1 + publicKeyLength + 1, 82 | 1 + publicKeyLength + 1 + addressLength 83 | ) 84 | .toString('ascii')) 85 | }).catch((err: Error) => { 86 | reject(err) 87 | }) 88 | }, 1) 89 | }) 90 | } 91 | 92 | /** 93 | * @returns A Signed eos transaction 94 | */ 95 | public async signTransaction( 96 | { chainId, serializedTransaction }: { chainId: string, serializedTransaction: Uint8Array } 97 | ) { 98 | const path = GET_LEDGER_PATHS(this.addressIndex) 99 | const paths = bippath.fromString(path).toPathArray() 100 | let offset = 0 101 | let transactionBuffer 102 | 103 | try { 104 | transactionBuffer = serialize(chainId, serializedTransaction).toString('hex') 105 | } catch (error) { 106 | console.error(error) 107 | throw new Error('Unable to deserialize transaction') 108 | } 109 | 110 | const rawTx = Buff.Buffer.from(transactionBuffer, 'hex') 111 | const toSend = [] 112 | let response: any 113 | while (offset !== rawTx.length) { 114 | const maxChunkSize = offset === 0 ? 150 - 1 - paths.length * 4 : 150 115 | const chunkSize = 116 | offset + maxChunkSize > rawTx.length 117 | ? rawTx.length - offset 118 | : maxChunkSize 119 | const buffer = Buff.Buffer.alloc( 120 | offset === 0 ? 1 + paths.length * 4 + chunkSize : chunkSize 121 | ) 122 | if (offset === 0) { 123 | buffer[0] = paths.length 124 | paths.forEach((element: number, index: number) => { 125 | buffer.writeUInt32BE(element, 1 + 4 * index) 126 | }) 127 | rawTx.copy(buffer, 1 + 4 * paths.length, offset, offset + chunkSize) 128 | } else { 129 | rawTx.copy(buffer, 0, offset, offset + chunkSize) 130 | } 131 | toSend.push(buffer) 132 | offset += chunkSize 133 | } 134 | 135 | return iteratePromises(toSend, (data: any[], i: number) => 136 | this.transport 137 | .send( 138 | LEDGER_CODES.CLA, 139 | LEDGER_CODES.INS_SIGN, 140 | i === 0 ? LEDGER_CODES.P1_FIRST : LEDGER_CODES.P1_MORE, 141 | LEDGER_CODES.P1_NON_CONFIRM, 142 | data, 143 | ) 144 | .then((apduResponse: any) => { 145 | response = apduResponse 146 | return response 147 | }) 148 | ).then(() => { 149 | const v = response.slice(0, 1).toString('hex') 150 | const r = response.slice(1, 1 + 32).toString('hex') 151 | const s = response.slice(1 + 32, 1 + 32 + 32).toString('hex') 152 | return convertSignatures(v + r + s) 153 | }).catch((error) => { 154 | console.error(error) 155 | throw error 156 | }) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/LedgerUtils.ts: -------------------------------------------------------------------------------- 1 | import Transport from '@ledgerhq/hw-transport-u2f' 2 | import { Api, JsonRpc, Serialize } from 'eosjs' 3 | import { JsSignatureProvider } from 'eosjs/dist/eosjs-jssig' 4 | 5 | import ecc from 'eosjs-ecc' 6 | 7 | import asn1 from 'asn1-ber' 8 | import Buff from 'buffer/' 9 | 10 | declare const TextDecoder: any 11 | declare const TextEncoder: any 12 | 13 | export enum LEDGER_CODES { 14 | CLA = 0xD4, 15 | INS_GET_PUBLIC_KEY = 0x02, 16 | INS_SIGN = 0x04, 17 | INS_GET_APP_CONFIGURATION = 0x06, 18 | P1_CONFIRM = 0x01, 19 | P1_NON_CONFIRM = 0x00, 20 | P1_FIRST = 0x00, 21 | P1_MORE = 0x80, 22 | } 23 | 24 | export const GET_LEDGER_PATHS = (index = 0) => `44'/194'/0'/0/${index}` 25 | 26 | let cachedTransport: any 27 | export async function getTransport() { 28 | if (cachedTransport) { 29 | return cachedTransport 30 | } 31 | cachedTransport = await Transport.create() 32 | return cachedTransport 33 | } 34 | 35 | export const convertSignatures = (sigs: string[]): string[] => { 36 | if (!Array.isArray(sigs)) { 37 | sigs = [sigs] 38 | } 39 | 40 | sigs = [].concat.apply([], sigs) 41 | 42 | for (let i = 0; i < sigs.length; i++) { 43 | const sig = sigs[i] 44 | if (typeof sig === 'string' && sig.length === 130) { 45 | sigs[i] = ecc.Signature.from(sig).toString() 46 | } 47 | } 48 | 49 | return sigs 50 | } 51 | 52 | export const iteratePromises = (arr: any[], callback: (x: any[], i: number) => Promise) => { 53 | const iterate = (index: number, array: any[], result: any): any => { 54 | if (index >= array.length) { 55 | return result 56 | } 57 | return callback(array[index], index) 58 | .then((res) => { 59 | result.push(res) 60 | return iterate(index + 1, array, result) 61 | }) 62 | } 63 | 64 | return Promise.resolve().then(() => iterate(0, arr, [])) 65 | } 66 | 67 | export const serialize = (chainId: string, serializedTransaction: Uint8Array) => { 68 | const api = new Api({ rpc: new JsonRpc(''), signatureProvider: new JsSignatureProvider([]) }) 69 | const transaction = api.deserializeTransaction(serializedTransaction) 70 | const writer = new asn1.BerWriter() 71 | 72 | encode(writer, createNewBuffer(api, 'checksum256', chainId)) 73 | encode(writer, createNewBuffer(api, 'time_point_sec', transaction.expiration)) 74 | encode(writer, createNewBuffer(api, 'uint16', transaction.ref_block_num)) 75 | encode(writer, createNewBuffer(api, 'uint32', transaction.ref_block_prefix)) 76 | encode(writer, createNewBuffer(api, 'varuint32', 0)) // max_net_usage_words 77 | encode(writer, createNewBuffer(api, 'uint8', transaction.max_cpu_usage_ms)) 78 | encode(writer, createNewBuffer(api, 'varuint32', transaction.delay_sec)) 79 | 80 | encode(writer, createNewBuffer(api, 'uint8', 0)) // ctx_free_actions_size 81 | 82 | encode(writer, createNewBuffer(api, 'uint8', transaction.actions.length)) 83 | for (const action of transaction.actions) { 84 | encode(writer, createNewBuffer(api, 'name', action.account)) 85 | encode(writer, createNewBuffer(api, 'name', action.name)) 86 | encode(writer, createNewBuffer(api, 'uint8', action.authorization.length)) 87 | 88 | for (const authorization of action.authorization) { 89 | encode(writer, createNewBuffer(api, 'name', authorization.actor)) 90 | encode(writer, createNewBuffer(api, 'name', authorization.permission)) 91 | } 92 | 93 | const actionData = Buff.Buffer.from(action.data, 'hex') 94 | encode(writer, createNewBuffer(api, 'uint8', actionData.length)) 95 | 96 | const actionDataBuffer = new Serialize.SerialBuffer({ 97 | textDecoder: new TextDecoder(), 98 | textEncoder: new TextEncoder(), 99 | }) 100 | actionDataBuffer.pushArray(actionData) 101 | encode(writer, actionDataBuffer.asUint8Array()) 102 | } 103 | 104 | encode(writer, createNewBuffer(api, 'uint8', 0)) // transaction_extensions 105 | encode(writer, createNewBuffer(api, 'checksum256', Buff.Buffer.alloc(32, 0).toString('hex'))) // ctx_free_data 106 | 107 | return writer.buffer 108 | } 109 | 110 | const createNewBuffer = (api: Api, type: string, data: any) => { 111 | const buffer = new Serialize.SerialBuffer({ textDecoder: new TextDecoder(), textEncoder: new TextEncoder() }) 112 | 113 | api.serialize(buffer, type, data) 114 | return buffer.asUint8Array() 115 | } 116 | 117 | const encode = (writer: any , buffer: Uint8Array) => { 118 | writer.writeBuffer(Buff.Buffer.from(buffer), asn1.Ber.OctetString) 119 | } 120 | -------------------------------------------------------------------------------- /src/SignatureProvider.ts: -------------------------------------------------------------------------------- 1 | import { Api } from 'eosjs' 2 | import { LedgerAPI } from './LedgerAPI' 3 | import { getTransport } from './LedgerUtils' 4 | 5 | interface SignatureProviderInterface { 6 | eosjsApi: Api 7 | ledgerApi: LedgerAPI 8 | cachedKeys: string[] 9 | } 10 | 11 | export class SignatureProvider implements SignatureProviderInterface { 12 | public eosjsApi: Api = null 13 | public ledgerApi: LedgerAPI = null 14 | public cachedKeys: string[] = [] 15 | 16 | public async getLedgerApi() { 17 | if (this.ledgerApi) { 18 | return this.ledgerApi 19 | } 20 | 21 | const transport = await getTransport() 22 | this.ledgerApi = new LedgerAPI(transport) 23 | return this.ledgerApi 24 | } 25 | 26 | /** Public keys associated with the private keys that the `SignatureProvider` holds */ 27 | public async getAvailableKeys(requestPermission?: boolean) { 28 | if (this.cachedKeys.length) { 29 | return this.cachedKeys 30 | } 31 | 32 | try { 33 | const api = await this.getLedgerApi() 34 | const key = await api.getPublicKey(requestPermission) 35 | this.cachedKeys = [key] 36 | return this.cachedKeys 37 | } catch (error) { 38 | throw error 39 | } 40 | } 41 | 42 | /** Sign a transaction */ 43 | public async sign({ chainId, serializedTransaction }: { chainId: string, serializedTransaction: Uint8Array }) { 44 | try { 45 | const api = await this.getLedgerApi() 46 | const signatures = await api.signTransaction({ chainId, serializedTransaction }) 47 | return { signatures, serializedTransaction } 48 | } catch (error) { 49 | throw error 50 | } 51 | } 52 | 53 | protected getCachedKeys(): string[] { 54 | return this.cachedKeys 55 | } 56 | 57 | protected setCachedKeys(keys: string[]): void { 58 | this.cachedKeys = keys 59 | } 60 | 61 | public clearCachedKeys(): void { 62 | this.cachedKeys = null 63 | } 64 | 65 | public cleanUp(): void { 66 | return 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SignatureProvider' 2 | -------------------------------------------------------------------------------- /src/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'bip32-path' 2 | declare module '@ledgerhq/hw-transport-u2f' 3 | declare module 'eosjs-ecc' 4 | declare module 'asn1-ber' 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "CommonJs", 5 | "outDir": "dist", 6 | "alwaysStrict": true, 7 | "sourceMap": true, 8 | "noImplicitAny": true, 9 | "moduleResolution": "node", 10 | "declaration": true, 11 | "downlevelIteration": true, 12 | "esModuleInterop": true, 13 | "skipLibCheck": true, 14 | "jsx": "react", 15 | "types": [], 16 | "lib": ["es2015", "dom"] 17 | }, 18 | "include": [ 19 | "src/*.ts", 20 | "src/*.js" 21 | ], 22 | "exclude": [ 23 | "src/*.test.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended", 5 | "tslint-eslint-rules", 6 | "@blockone/tslint-config-blockone/tslint-config" 7 | ], 8 | "jsRules": {}, 9 | "rules": {}, 10 | "rulesDirectory": [] 11 | } 12 | --------------------------------------------------------------------------------