├── tslint.json ├── .gitignore ├── .editorconfig ├── tsconfig.json ├── webpack.config.js ├── CHANGELOG.md ├── src ├── cosmos-ledger.d.ts └── cosmos-ledger.ts ├── README.md ├── package.json ├── .circleci └── config.yml └── test └── test.ts /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-standard", 4 | "tslint-config-prettier" 5 | ] 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | lib 9 | compiled 10 | .awcache 11 | .rpt2_cache 12 | docs 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | #root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = 100 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es5", 5 | "module": "es2015", 6 | "lib": [ 7 | "es2015", 8 | "es2016", 9 | "es2017", 10 | "dom" 11 | ], 12 | "strict": true, 13 | "sourceMap": true, 14 | "declaration": true, 15 | "allowSyntheticDefaultImports": true, 16 | "experimentalDecorators": true, 17 | "emitDecoratorMetadata": true, 18 | "declarationDir": "lib/types", 19 | "outDir": "lib", 20 | "typeRoots": [ 21 | "node_modules/@types" 22 | ], 23 | "noImplicitAny": false 24 | }, 25 | "include": [ 26 | "src" 27 | ] 28 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | devtool: "cheap-source-map", 3 | entry: ['./src/cosmos-ledger.ts'], 4 | output: { 5 | path: __dirname + '/lib', 6 | filename: 'cosmos-ledger.js', 7 | library: 'cosmos-ledger', 8 | libraryTarget: 'umd', 9 | umdNamedDefine: true, 10 | }, 11 | resolve: { 12 | extensions: ['.tsx', '.ts', '.js'] 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | loader: 'babel-loader', 18 | test: /\.js$/, 19 | exclude: /node_modules/, 20 | query: { 21 | presets: ['@babel/preset-env'] 22 | } 23 | }, 24 | { 25 | test: /\.tsx?$/, 26 | use: 'ts-loader', 27 | exclude: /node_modules/ 28 | } 29 | ] 30 | } 31 | } 32 | module.exports = config; -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | 9 | 10 | ## [0.0.5] - 2019-06-12 11 | 12 | ### Fixed 13 | 14 | - Fixed address confirmation @faboweb 15 | 16 | ## [0.0.4] - 2019-06-12 17 | 18 | ### Fixed 19 | 20 | - Removed webusb due to lack of support @faboweb 21 | 22 | ## [0.0.3] - 2019-06-05 23 | 24 | ### Repository 25 | 26 | - Simplified and unified repository (using webpack) @faboweb 27 | - Migrated to cosmos ledger lib v2 @faboweb 28 | 29 | ## [0.0.2] - 2019-05-24 30 | 31 | ### Added 32 | 33 | - Extracted the Ledger Cosmos App wrapper from Lunie into this repo @faboweb -------------------------------------------------------------------------------- /src/cosmos-ledger.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ledger-cosmos-js' { 2 | interface CommunicationMethod {} 3 | 4 | export class App { 5 | constructor(communicationMethod: CommunicationMethod) 6 | 7 | getVersion: () => { 8 | major: Number 9 | minor: Number 10 | patch: Number 11 | test_mode: Boolean 12 | error_message: string 13 | device_locked: Boolean 14 | } 15 | publicKey: ( 16 | hdPath: Array 17 | ) => { 18 | compressed_pk: Buffer 19 | error_message: string 20 | } 21 | getAddressAndPubKey: ( 22 | bech32Prefix: string, 23 | hdPath: Array 24 | ) => { 25 | compressed_pk: Buffer 26 | address: string 27 | error_message: string 28 | } 29 | appInfo: () => { 30 | appName: string 31 | error_message: string 32 | } 33 | sign: ( 34 | hdPath: Array, 35 | signMessage: string 36 | ) => { 37 | signature: Buffer 38 | error_message: string 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cosmos Ledger App wrapper 2 | 3 | This library helps interfacing with Cosmos Ledger App. It provides a developer friendly interface and user friendly error messages. 4 | 5 | ## THANK YOU 6 | 7 | This library is based on `ledger-cosmos-js` by Juan Leni who implemente the Cosmos Ledger App. Thank you Juan! 8 | 9 | ## Install 10 | 11 | ```bash 12 | yarn add @lunie/cosmos-ledger 13 | ``` 14 | 15 | ## Usage 16 | 17 | ### Sign using the Ledger 18 | 19 | ```js 20 | import Ledger from "@lunie/cosmos-ledger" 21 | 22 | const signMessage = ... message to sign, generate messages with "@lunie/cosmos-js" 23 | 24 | const ledger = await Ledger().connect() 25 | 26 | const signature = await ledger.sign(signMessage) 27 | ``` 28 | 29 | ### Using with cosmos-js 30 | 31 | ```js 32 | import Ledger from "@lunie/cosmos-ledger" 33 | import Cosmos from "@lunie/cosmos-js" 34 | 35 | const privateKey = Buffer.from(...) 36 | const publicKey = Buffer.from(...) 37 | 38 | // init cosmos sender 39 | const cosmos = Cosmos(STARGATE_URL, ADDRESS) 40 | 41 | // create message 42 | const msg = cosmos 43 | .MsgSend({toAddress: 'cosmos1abcd09876', amounts: [{ denom: 'stake', amount: 10 }}) 44 | 45 | // create a signer from this local js signer library 46 | const ledgerSigner = async (signMessage) => { 47 | const ledger = await Ledger().connect() 48 | const publicKey = await ledger.getPubKey() 49 | const signature = await ledger.sign(signMessage) 50 | 51 | return { 52 | signature, 53 | publicKey 54 | } 55 | } 56 | 57 | // send the transaction 58 | const { included }= await msg.send({ gas: 200000 }, ledgerSigner) 59 | 60 | // await tx to be included in a block 61 | await included() 62 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lunie/cosmos-ledger", 3 | "version": "0.0.5", 4 | "description": "provide simple Ledger tooling for the Cosmos Ledger App with user friendly errors", 5 | "keywords": [ 6 | "cosmos", 7 | "cosmos.network", 8 | "cosmos wallet", 9 | "cosmos signer", 10 | "ledger", 11 | "cosmos javascript", 12 | "cosmos sdk", 13 | "cosmos-sdk" 14 | ], 15 | "main": "lib/cosmos-ledger.js", 16 | "typings": "lib/types/cosmos-ledger.d.ts", 17 | "files": [ 18 | "lib" 19 | ], 20 | "author": "Fabian Weber ", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/luniehq/cosmos-ledger.git" 24 | }, 25 | "license": "MIT", 26 | "engines": { 27 | "node": ">=6.0.0" 28 | }, 29 | "scripts": { 30 | "lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'", 31 | "prebuild": "rimraf lib", 32 | "build": "webpack", 33 | "test": "jest --coverage", 34 | "test:watch": "jest --coverage --watch", 35 | "test:prod": "npm run lint && npm run test -- --no-cache", 36 | "report-coverage": "cat ./coverage/lcov.info | coveralls", 37 | "prepublishOnly": "npm run build", 38 | "precommit": "lint-staged", 39 | "log": "simsala log", 40 | "release": "git checkout develop & git pull & git push origin develop:release" 41 | }, 42 | "lint-staged": { 43 | "{src,test}/**/*.ts": [ 44 | "prettier --write", 45 | "git add" 46 | ] 47 | }, 48 | "jest": { 49 | "transform": { 50 | ".(ts|tsx)": "ts-jest" 51 | }, 52 | "testEnvironment": "node", 53 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 54 | "moduleFileExtensions": [ 55 | "ts", 56 | "tsx", 57 | "js" 58 | ], 59 | "coveragePathIgnorePatterns": [ 60 | "/node_modules/", 61 | "/test/" 62 | ], 63 | "coverageThreshold": { 64 | "global": { 65 | "branches": 0, 66 | "functions": 95, 67 | "lines": 95, 68 | "statements": 95 69 | } 70 | }, 71 | "collectCoverageFrom": [ 72 | "src/*.{js,ts}" 73 | ] 74 | }, 75 | "prettier": { 76 | "semi": false, 77 | "singleQuote": true 78 | }, 79 | "devDependencies": { 80 | "@types/jest": "^23.3.2", 81 | "@types/node": "^10.11.0", 82 | "@types/secp256k1": "^3.5.0", 83 | "@types/semver": "^6.0.0", 84 | "coveralls": "^3.0.2", 85 | "cross-env": "^5.2.0", 86 | "husky": "^1.0.1", 87 | "jest": "^23.6.0", 88 | "jest-config": "^23.6.0", 89 | "lint-staged": "^8.0.0", 90 | "lodash.camelcase": "^4.3.0", 91 | "prettier": "^1.14.3", 92 | "rimraf": "^2.6.2", 93 | "ts-jest": "^23.10.2", 94 | "ts-loader": "^6.0.2", 95 | "ts-node": "^7.0.1", 96 | "tslint": "^5.11.0", 97 | "tslint-config-prettier": "^1.15.0", 98 | "tslint-config-standard": "^8.0.1", 99 | "typedoc": "^0.12.0", 100 | "typescript": "^3.0.3", 101 | "webpack": "^4.32.2", 102 | "webpack-cli": "^3.3.2" 103 | }, 104 | "dependencies": { 105 | "@ledgerhq/hw-transport-u2f": "^4.61.0", 106 | "@ledgerhq/hw-transport-webusb": "^4.61.0", 107 | "@lunie/cosmos-keys": "^0.0.9", 108 | "ledger-cosmos-js": "^2.0.2", 109 | "secp256k1": "^3.7.0", 110 | "semver": "^6.1.0" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | # reusable commands 4 | commands: 5 | yarn-install: 6 | description: "[YARN] update and install" 7 | steps: 8 | - restore_cache: 9 | keys: 10 | - v4-dependencies-root-{{ checksum "package.json" }} 11 | - v4-dependencies-root- 12 | 13 | - run: yarn install 14 | - save_cache: 15 | paths: 16 | - yarn.lock 17 | - node_modules 18 | key: v4-dependencies-root-{{ checksum "package.json" }} 19 | 20 | jobs: 21 | pendingUpdated: 22 | docker: 23 | - image: circleci/node:10.15.3 24 | steps: 25 | - checkout 26 | - run: yarn add simsala 27 | - run: node node_modules/simsala/src/cli.js check --pending-path ./changes 28 | 29 | lint: 30 | docker: 31 | - image: circleci/node:10.15.3 32 | steps: 33 | - checkout 34 | - yarn-install 35 | - run: yarn run lint 36 | 37 | testUnit: 38 | docker: 39 | - image: circleci/node:10.15.3 40 | steps: 41 | - checkout 42 | - yarn-install 43 | - run: 44 | name: Setup Code Climate test-reporter 45 | command: | 46 | # download test reporter as a static binary 47 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 48 | chmod +x ./cc-test-reporter 49 | - run: 50 | name: Test 51 | command: | 52 | # notify Code Climate of a pending test report using `before-build` 53 | ./cc-test-reporter before-build 54 | yarn run test 55 | # upload test report to Code Climate 56 | ./cc-test-reporter format-coverage -t lcov ./test/unit/coverage/lcov.info 57 | ./cc-test-reporter upload-coverage 58 | no_output_timeout: 120 59 | 60 | security: 61 | docker: 62 | - image: circleci/node:10.15.3 63 | steps: 64 | - checkout 65 | - run: 66 | name: Audit 67 | command: | 68 | set +e 69 | 70 | SUMMARY="$(yarn audit | grep Severity)" 71 | VULNERABILITIES=".*(High|Critical).*" 72 | 73 | if [[ $SUMMARY =~ $VULNERABILITIES ]]; then 74 | echo "Unsafe dependencies found: $SUMMARY" >&2 75 | exit 1 76 | fi 77 | echo "Your dependencies are secure enough: $SUMMARY" 78 | exit 0 79 | 80 | # Create release. 81 | release: 82 | docker: 83 | - image: circleci/node:10.15.3 84 | steps: 85 | - checkout 86 | - run: | 87 | yarn add simsala 88 | git config user.email "bot@lunie.io" 89 | git config user.name "Lunie Bot" 90 | node node_modules/simsala/src/cli.js release-candidate --semver prerelease --owner luniehq --repository cosmos-js --token $GIT_BOT_TOKEN 91 | 92 | # Push merges to master immediatly back to develop to stay in sync 93 | mergeBack: 94 | docker: 95 | - image: circleci/node:10.15.3 96 | steps: 97 | - checkout 98 | - run: 99 | command: | 100 | git remote add bot https://${GIT_BOT_TOKEN}@github.com/luniehq/lunie.git 101 | git checkout develop 102 | git pull 103 | git merge origin/master 104 | git push 105 | 106 | 107 | workflows: 108 | version: 2 109 | build-and-deploy: 110 | jobs: 111 | # Static checks before 112 | - pendingUpdated: 113 | filters: 114 | branches: 115 | ignore: 116 | - release 117 | - master 118 | 119 | - security: 120 | filters: 121 | branches: 122 | ignore: release 123 | 124 | - lint: 125 | filters: 126 | branches: 127 | ignore: release 128 | 129 | - testUnit: 130 | filters: 131 | branches: 132 | ignore: release 133 | releaseManually: 134 | jobs: 135 | - release: 136 | filters: 137 | branches: 138 | only: 139 | - release 140 | mergeBack: 141 | jobs: 142 | - mergeBack: 143 | filters: 144 | branches: 145 | only: master 146 | -------------------------------------------------------------------------------- /src/cosmos-ledger.ts: -------------------------------------------------------------------------------- 1 | import App from 'ledger-cosmos-js' 2 | import { getCosmosAddress } from '@lunie/cosmos-keys' 3 | import { signatureImport } from 'secp256k1' 4 | import TransportU2F from '@ledgerhq/hw-transport-u2f' 5 | const semver = require('semver') 6 | 7 | const INTERACTION_TIMEOUT = 120 // seconds to wait for user action on Ledger, currently is always limited to 60 8 | const REQUIRED_COSMOS_APP_VERSION = '1.5.0' 9 | //const REQUIRED_LEDGER_FIRMWARE = "1.1.1" 10 | 11 | declare global { 12 | interface Window { 13 | chrome: any 14 | opr: any 15 | } 16 | } 17 | 18 | /* 19 | HD wallet derivation path (BIP44) 20 | DerivationPath{44, 118, account, 0, index} 21 | */ 22 | const HDPATH = [44, 118, 0, 0, 0] 23 | const BECH32PREFIX = `cosmos` 24 | 25 | export default class Ledger { 26 | private readonly testModeAllowed: Boolean 27 | private cosmosApp: any 28 | 29 | constructor({ testModeAllowed = false }: { testModeAllowed: Boolean }) { 30 | this.testModeAllowed = testModeAllowed 31 | } 32 | 33 | // quickly test connection and compatibility with the Ledger device throwing away the connection 34 | async testDevice() { 35 | // poll device with low timeout to check if the device is connected 36 | const secondsTimeout = 3 // a lower value always timeouts 37 | await this.connect(secondsTimeout) 38 | this.cosmosApp = null 39 | 40 | return this 41 | } 42 | 43 | // check if the connection we established with the Ledger device is working 44 | private async isSendingData() { 45 | // check if the device is connected or on screensaver mode 46 | const response = await this.cosmosApp.publicKey(HDPATH) 47 | this.checkLedgerErrors(response, { 48 | timeoutMessag: 'Could not find a connected and unlocked Ledger device' 49 | }) 50 | } 51 | 52 | // check if the Ledger device is ready to receive signing requests 53 | private async isReady() { 54 | // check if the version is supported 55 | const version = await this.getCosmosAppVersion() 56 | 57 | if (!semver.gte(version, REQUIRED_COSMOS_APP_VERSION)) { 58 | const msg = `Outdated version: Please update Ledger Cosmos App to the latest version.` 59 | throw new Error(msg) 60 | } 61 | 62 | // throws if not open 63 | await this.isCosmosAppOpen() 64 | } 65 | 66 | // connects to the device and checks for compatibility 67 | // the timeout is the time the user has to react to requests on the Ledger device 68 | // set a low timeout to only check the connection without preparing the connection for user input 69 | async connect(timeout = INTERACTION_TIMEOUT) { 70 | // assume well connection if connected once 71 | if (this.cosmosApp) return this 72 | 73 | let transport = await TransportU2F.create(timeout * 1000) 74 | 75 | const cosmosLedgerApp = new App(transport) 76 | 77 | this.cosmosApp = cosmosLedgerApp 78 | 79 | await this.isSendingData() 80 | await this.isReady() 81 | 82 | return this 83 | } 84 | 85 | // returns the cosmos app version as a string like "1.1.0" 86 | async getCosmosAppVersion() { 87 | await this.connect() 88 | 89 | const response = await this.cosmosApp.getVersion() 90 | this.checkLedgerErrors(response) 91 | const { major, minor, patch, test_mode } = response 92 | checkAppMode(this.testModeAllowed, test_mode) 93 | const version = versionString({ major, minor, patch }) 94 | 95 | return version 96 | } 97 | 98 | // checks if the cosmos app is open 99 | // to be used for a nicer UX 100 | async isCosmosAppOpen() { 101 | await this.connect() 102 | 103 | const response = await this.cosmosApp.appInfo() 104 | this.checkLedgerErrors(response) 105 | const { appName } = response 106 | 107 | if (appName.toLowerCase() !== `cosmos`) { 108 | throw new Error(`Close ${appName} and open the Cosmos app`) 109 | } 110 | } 111 | 112 | // returns the public key from the Ledger device as a Buffer 113 | async getPubKey() { 114 | await this.connect() 115 | 116 | const response = await this.cosmosApp.publicKey(HDPATH) 117 | this.checkLedgerErrors(response) 118 | return response.compressed_pk 119 | } 120 | 121 | // returns the cosmos address from the Ledger as a string 122 | async getCosmosAddress() { 123 | await this.connect() 124 | 125 | const pubKey = await this.getPubKey() 126 | return getCosmosAddress(pubKey) 127 | } 128 | 129 | // triggers a confirmation request of the cosmos address on the Ledger device 130 | async confirmLedgerAddress() { 131 | await this.connect() 132 | const cosmosAppVersion = await this.getCosmosAppVersion() 133 | 134 | if (semver.lt(cosmosAppVersion, REQUIRED_COSMOS_APP_VERSION)) { 135 | // we can't check the address on an old cosmos app 136 | return 137 | } 138 | 139 | const response = await this.cosmosApp.getAddressAndPubKey(HDPATH, BECH32PREFIX) 140 | this.checkLedgerErrors(response, { 141 | rejectionMessage: 'Displayed address was rejected' 142 | }) 143 | } 144 | 145 | // create a signature for any message 146 | // in Cosmos this should be a serialized StdSignMsg 147 | // this is ideally generated by the @lunie/cosmos-js library 148 | async sign(signMessage: string) { 149 | await this.connect() 150 | 151 | const response = await this.cosmosApp.sign(HDPATH, signMessage) 152 | this.checkLedgerErrors(response) 153 | // we have to parse the signature from Ledger as it's in DER format 154 | const parsedSignature = signatureImport(response.signature) 155 | return parsedSignature 156 | } 157 | 158 | // parse Ledger errors in a more user friendly format 159 | /* istanbul ignore next: maps a bunch of errors */ 160 | private checkLedgerErrors( 161 | { error_message, device_locked }: { error_message: string; device_locked: Boolean }, 162 | { 163 | timeoutMessag = 'Connection timed out. Please try again.', 164 | rejectionMessage = 'User rejected the transaction' 165 | } = {} 166 | ) { 167 | if (device_locked) { 168 | throw new Error(`Ledger's screensaver mode is on`) 169 | } 170 | switch (error_message) { 171 | case `U2F: Timeout`: 172 | throw new Error(timeoutMessag) 173 | case `Cosmos app does not seem to be open`: 174 | throw new Error(`Cosmos app is not open`) 175 | case `Command not allowed`: 176 | throw new Error(`Transaction rejected`) 177 | case `Transaction rejected`: 178 | throw new Error(rejectionMessage) 179 | case `Unknown Status Code: 26628`: 180 | throw new Error(`Ledger's screensaver mode is on`) 181 | case `Instruction not supported`: 182 | throw new Error( 183 | `Your Cosmos Ledger App is not up to date. ` + 184 | `Please update to version ${REQUIRED_COSMOS_APP_VERSION}.` 185 | ) 186 | case `No errors`: 187 | // do nothing 188 | break 189 | default: 190 | throw new Error(error_message) 191 | } 192 | } 193 | } 194 | 195 | // stiched version string from Ledger app version object 196 | function versionString({ major, minor, patch }: { major: Number; minor: Number; patch: Number }) { 197 | return `${major}.${minor}.${patch}` 198 | } 199 | 200 | // wrapper to throw if app is in testmode but it is not allowed to be in testmode 201 | export const checkAppMode = (testModeAllowed: Boolean, testMode: Boolean) => { 202 | if (testMode && !testModeAllowed) { 203 | throw new Error( 204 | `DANGER: The Cosmos Ledger app is in test mode and shouldn't be used on mainnet!` 205 | ) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | import Ledger from '../src/cosmos-ledger' 2 | 3 | jest.mock('secp256k1', () => ({ 4 | signatureImport: () => Buffer.from('1234') 5 | })) 6 | 7 | const config = { 8 | testModeAllowed: false 9 | } 10 | 11 | describe(`Ledger`, () => { 12 | let ledger: Ledger 13 | beforeEach(() => { 14 | ledger = new Ledger(config) 15 | }) 16 | 17 | it('testDevice', async () => { 18 | const self = { 19 | connect: jest.fn() 20 | } 21 | await ledger.testDevice.call(self) 22 | expect(self.connect).toHaveBeenCalledWith(3) 23 | }) 24 | 25 | describe('connect', () => { 26 | it('connects', async () => { 27 | jest.resetModules() 28 | jest.doMock('ledger-cosmos-js', () => ({ 29 | App: class MockApp { 30 | publicKey() { 31 | return { 32 | error_message: 'No errors' 33 | } 34 | } 35 | get_version() { 36 | return { 37 | major: '1', 38 | minor: '5', 39 | patch: '0', 40 | test_mode: false, 41 | error_message: 'No errors' 42 | } 43 | } 44 | appInfo() { 45 | return { 46 | appName: 'Cosmos', 47 | error_message: 'No errors' 48 | } 49 | } 50 | }, 51 | comm_u2f: { 52 | create_async: () => ({}) 53 | } 54 | })) 55 | const Ledger = require('../src/ledger').default 56 | ledger = new Ledger(config) 57 | await ledger.connect() 58 | }) 59 | 60 | it('uses existing connection', async () => { 61 | const self = { 62 | isSendingData: jest.fn(), 63 | isReady: jest.fn(), 64 | cosmosApp: {} 65 | } 66 | await ledger.connect.call(self) 67 | expect(self.cosmosApp).toBeDefined() 68 | expect(self.isSendingData).not.toHaveBeenCalled() 69 | expect(self.isReady).not.toHaveBeenCalled() 70 | }) 71 | 72 | it("fails if can't get data from device", async () => { 73 | jest.resetModules() 74 | jest.doMock('ledger-cosmos-js', () => ({ 75 | App: class MockApp { 76 | publicKey() { 77 | return { 78 | error_message: 'BIG ERROR' 79 | } 80 | } 81 | }, 82 | comm_u2f: { 83 | create_async: () => ({}) 84 | } 85 | })) 86 | const Ledger = require('../src/ledger').default 87 | ledger = new Ledger(config) 88 | 89 | await expect(ledger.connect()).rejects.toThrow('BIG ERROR') 90 | }) 91 | 92 | it('fails if Cosmos App is outdated', async () => { 93 | jest.resetModules() 94 | jest.doMock('ledger-cosmos-js', () => ({ 95 | App: class MockApp { 96 | publicKey() { 97 | return { 98 | error_message: 'No errors' 99 | } 100 | } 101 | get_version() { 102 | return { 103 | major: '1', 104 | minor: '0', 105 | patch: '0', 106 | test_mode: false, 107 | error_message: 'No errors' 108 | } 109 | } 110 | }, 111 | comm_u2f: { 112 | create_async: () => ({}) 113 | } 114 | })) 115 | const Ledger = require('../src/ledger').default 116 | ledger = new Ledger(config) 117 | 118 | await expect(ledger.connect()).rejects.toThrow( 119 | 'Outdated version: Please update Ledger Cosmos App to the latest version.' 120 | ) 121 | }) 122 | 123 | it('fails if Ledger device is locked', async () => { 124 | jest.resetModules() 125 | jest.doMock('ledger-cosmos-js', () => ({ 126 | App: class MockApp { 127 | publicKey() { 128 | return { 129 | error_message: 'No errors', 130 | device_locked: true 131 | } 132 | } 133 | }, 134 | comm_u2f: { 135 | create_async: () => ({}) 136 | } 137 | })) 138 | const Ledger = require('../src/ledger').default 139 | ledger = new Ledger(config) 140 | 141 | await expect(ledger.connect()).rejects.toThrow('') 142 | }) 143 | }) 144 | 145 | describe('getCosmosAppVersion', () => { 146 | it('new version', async () => { 147 | const self = { 148 | connect: jest.fn(), 149 | cosmosApp: { 150 | get_version: () => ({ 151 | major: '1', 152 | minor: '5', 153 | patch: '0', 154 | test_mode: false 155 | }) 156 | }, 157 | checkLedgerErrors: jest.fn() 158 | } 159 | const res = await ledger.getCosmosAppVersion.call(self) 160 | expect(self.connect).toHaveBeenCalled() 161 | expect(self.checkLedgerErrors).toHaveBeenCalled() 162 | expect(res).toBe('1.5.0') 163 | }) 164 | 165 | it('old version', async () => { 166 | const self = { 167 | connect: jest.fn(), 168 | cosmosApp: { 169 | get_version: () => ({ 170 | major: '1', 171 | minor: '1', 172 | patch: '0', 173 | test_mode: false, 174 | error_message: 'No errors' 175 | }) 176 | }, 177 | checkLedgerErrors: jest.fn() 178 | } 179 | expect(await ledger.getCosmosAppVersion.call(self)).toBe('1.1.0') 180 | expect(self.connect).toHaveBeenCalled() 181 | expect(self.checkLedgerErrors).toHaveBeenCalled() 182 | }) 183 | 184 | it('test mode', async () => { 185 | const self = { 186 | connect: jest.fn(), 187 | cosmosApp: { 188 | get_version: () => ({ 189 | major: '1', 190 | minor: '5', 191 | patch: '0', 192 | test_mode: true, 193 | error_message: 'No errors' 194 | }) 195 | }, 196 | checkLedgerErrors: jest.fn() 197 | } 198 | await expect(ledger.getCosmosAppVersion.call(self)).rejects.toThrow() 199 | expect(self.connect).toHaveBeenCalled() 200 | expect(self.checkLedgerErrors).toHaveBeenCalled() 201 | }) 202 | }) 203 | 204 | describe('isCosmosAppOpen', () => { 205 | it('success', async () => { 206 | const self = { 207 | connect: jest.fn(), 208 | cosmosApp: { 209 | appInfo: () => ({ 210 | appName: 'Cosmos', 211 | error_message: 'No errors' 212 | }) 213 | }, 214 | checkLedgerErrors: jest.fn() 215 | } 216 | await ledger.isCosmosAppOpen.call(self) 217 | expect(self.connect).toHaveBeenCalled() 218 | expect(self.checkLedgerErrors).toHaveBeenCalled() 219 | }) 220 | 221 | it('failure', async () => { 222 | const self = { 223 | connect: jest.fn(), 224 | cosmosApp: { 225 | appInfo: () => ({ 226 | appName: 'Ethereum', 227 | error_message: 'No errors' 228 | }) 229 | }, 230 | checkLedgerErrors: jest.fn() 231 | } 232 | await expect(ledger.isCosmosAppOpen.call(self)).rejects.toThrow() 233 | expect(self.connect).toHaveBeenCalled() 234 | expect(self.checkLedgerErrors).toHaveBeenCalled() 235 | }) 236 | }) 237 | 238 | it('getCosmosAddress', async () => { 239 | const self = { 240 | connect: jest.fn(), 241 | getPubKey: jest.fn(() => Buffer.from('1234')) 242 | } 243 | const res = await ledger.getCosmosAddress.call(self) 244 | expect(self.connect).toHaveBeenCalled() 245 | expect(res).toBe('cosmos1l4aqmqyen0kawmy6pq5q27qhl3synfg8uqcsa5') 246 | }) 247 | 248 | it('getPubKey', async () => { 249 | const self = { 250 | connect: jest.fn(), 251 | checkLedgerErrors: jest.fn(), 252 | cosmosApp: { 253 | publicKey: () => ({ 254 | compressed_pk: Buffer.from('1234'), 255 | error_message: 'No errors' 256 | }) 257 | } 258 | } 259 | const res = await ledger.getPubKey.call(self) 260 | expect(self.connect).toHaveBeenCalled() 261 | expect(self.checkLedgerErrors).toHaveBeenCalled() 262 | expect(res instanceof Buffer).toBe(true) 263 | }) 264 | 265 | describe('confirmLedgerAddress', () => { 266 | it('new version', async () => { 267 | const self = { 268 | checkLedgerErrors: jest.fn(), 269 | connect: jest.fn(), 270 | getCosmosAppVersion: () => '1.5.0', 271 | cosmosApp: { 272 | getAddressAndPubKey: jest.fn(() => ({ 273 | error_message: 'No errors' 274 | })) 275 | } 276 | } 277 | await ledger.confirmLedgerAddress.call(self) 278 | expect(self.connect).toHaveBeenCalled() 279 | expect(self.checkLedgerErrors).toHaveBeenCalled() 280 | expect(self.cosmosApp.getAddressAndPubKey).toHaveBeenCalled() 281 | }) 282 | 283 | it('old version', async () => { 284 | const self = { 285 | checkLedgerErrors: jest.fn(), 286 | connect: jest.fn(), 287 | getCosmosAppVersion: () => '1.1.0', 288 | cosmosApp: { 289 | getAddressAndPubKey: jest.fn(() => ({ 290 | error_message: 'No errors' 291 | })) 292 | } 293 | } 294 | await ledger.confirmLedgerAddress.call(self) 295 | expect(self.connect).toHaveBeenCalled() 296 | expect(self.checkLedgerErrors).not.toHaveBeenCalled() 297 | expect(self.cosmosApp.getAddressAndPubKey).not.toHaveBeenCalled() 298 | }) 299 | }) 300 | 301 | it('sign', async () => { 302 | const self = { 303 | checkLedgerErrors: jest.fn(), 304 | connect: jest.fn(), 305 | getCosmosAppVersion: () => '1.1.0', 306 | cosmosApp: { 307 | sign: jest.fn(() => ({ 308 | signature: Buffer.from('1234'), // needs to be a DER signature, but the upstream library is mockeed here 309 | error_message: 'No errors' 310 | })) 311 | } 312 | } 313 | const res = await ledger.sign.call(self, 'message') 314 | expect(self.connect).toHaveBeenCalled() 315 | expect(self.checkLedgerErrors).toHaveBeenCalled() 316 | expect(self.cosmosApp.sign).toHaveBeenCalledWith(expect.any(Array), 'message') 317 | expect(res instanceof Buffer).toBe(true) 318 | }) 319 | }) 320 | --------------------------------------------------------------------------------