├── .babelrc ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── package.json ├── src ├── @types │ ├── bn.js.d.ts │ ├── elliptic.d.ts │ ├── keccak.d.ts │ └── rlp.d.ts ├── Address.test.ts ├── Address.ts ├── HDKey.test.ts ├── HDKey.ts ├── Message.test.ts ├── Message.ts ├── Mnemonic.test.ts ├── Mnemonic.ts ├── Transaction.test.ts ├── Transaction.ts ├── denominations.ts ├── index.ts ├── util.test.ts ├── util.ts └── wordlist.en.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["latest-node", { "target": "current" }]] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # node.js 6 | # 7 | node_modules/ 8 | npm-debug.log 9 | yarn-error.log 10 | 11 | dist/ 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "amatiasq.sort-imports", 4 | "remimarsal.prettier-now" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "**/.git": true, 5 | "**/.DS_Store": true, 6 | "**/build/": true, 7 | "node_modules/": true 8 | }, 9 | "editor.formatOnSave": true, 10 | "emmet.excludeLanguages": [ 11 | "javascript", 12 | "javascriptreact", 13 | "typescript", 14 | "typescriptreact" 15 | ], 16 | "prettier.printWidth": 80, 17 | "prettier.tabWidth": 2, 18 | "prettier.useTabs": false, 19 | "prettier.singleQuote": true, 20 | "prettier.jsxSingleQuote": true, 21 | "prettier.trailingComma": "none", 22 | "prettier.bracketSpacing": false, 23 | "prettier.bracesSpacing": true, 24 | "prettier.breakProperty": false, 25 | "prettier.arrowParens": false, 26 | "prettier.arrayExpand": false, 27 | "prettier.flattenTernaries": false, 28 | "prettier.breakBeforeElse": false, 29 | "prettier.spaceBeforeFunctionParen": true, 30 | "prettier.alignObjectProperties": false, 31 | "prettier.semi": false, 32 | "prettier.jsxBracketSameLine": false, 33 | "prettier.noSpaceEmptyFn": false, 34 | "sortImports.languages": [ 35 | "javascript", 36 | "javascriptreact", 37 | "typescript", 38 | "typescriptreact" 39 | ], 40 | "typescript.tsdk": "node_modules/typescript/lib" 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2021 Peter Jihoon Kim 2 | Copyright (c) 2018-2021 Coinbase, Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cipher-ethereum 2 | =============== 3 | 4 | [![npm version](https://badge.fury.io/js/cipher-ethereum.svg)](https://www.npmjs.com/package/cipher-ethereum) 5 | [![Downloads](https://img.shields.io/npm/dm/cipher-ethereum.svg)](https://www.npmjs.com/package/cipher-ethereum) 6 | 7 | - - - 8 | MIT License 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cipher-ethereum", 3 | "version": "0.1.0", 4 | "description": "An Ethereum library used by Cipher Browser, a mobile Ethereum client", 5 | "keywords": [ 6 | "cipher", 7 | "cipherbrowser", 8 | "coinbase", 9 | "crypto", 10 | "cryptocurrency", 11 | "cypher", 12 | "eip55", 13 | "erc20", 14 | "eth", 15 | "ether", 16 | "ethereum", 17 | "etherium", 18 | "toshi", 19 | "typescript", 20 | "wallet" 21 | ], 22 | "main": "dist/index.js", 23 | "types": "dist/index.d.ts", 24 | "repository": "https://github.com/petejkim/cipher-ethereum.git", 25 | "author": "Peter Jihoon Kim", 26 | "license": "MIT", 27 | "scripts": { 28 | "test": "jest", 29 | "dist": "rm -rf dist && tsc && babel dist -d dist", 30 | "tsc": "tsc --noEmit --pretty", 31 | "lint": "tslint -p . 'src/**/*.ts{,x}'", 32 | "watch": "nodemon -e ts,tsx,js,json --watch src/ --exec 'yarn tsc && yarn lint'" 33 | }, 34 | "dependencies": { 35 | "bn.js": "^5.1.3", 36 | "bs58": "^4.0.1", 37 | "elliptic": "^6.5.3", 38 | "keccak": "^3.0.1", 39 | "rlp": "^2.2.6" 40 | }, 41 | "devDependencies": { 42 | "@types/bs58": "^4.0.1", 43 | "@types/jest": "^26.0.2", 44 | "@types/node": "^14.14.22", 45 | "babel-cli": "^6.24.1", 46 | "babel-preset-latest-node": "^1.0.0", 47 | "jest": "^26.6.3", 48 | "nodemon": "^2.0.7", 49 | "ts-jest": "^26.4.4", 50 | "tslint": "^6.1.3", 51 | "tslint-config-standard": "^9.0.0", 52 | "typescript": "^4.1.3" 53 | }, 54 | "engines": { 55 | "node": ">= 10.0.0" 56 | }, 57 | "jest": { 58 | "transform": { 59 | "^.+\\.tsx?$": "ts-jest" 60 | }, 61 | "testEnvironment": "node", 62 | "testPathIgnorePatterns": [ 63 | "/dist/", 64 | "/node_modules/" 65 | ], 66 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 67 | "moduleFileExtensions": [ 68 | "ts", 69 | "js", 70 | "json" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/@types/bn.js.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'bn.js' { 2 | import { Buffer } from 'buffer' 3 | 4 | type Endian = 'le' | 'be' 5 | 6 | type ArrayLike = { 7 | new (size: number): ArrayLike 8 | } 9 | 10 | export default class BN { 11 | constructor ( 12 | value: string | number | Buffer | BN, 13 | base?: number, 14 | endian?: Endian 15 | ) 16 | toArrayLike ( 17 | ArrayLike: { new (size: number): T }, 18 | endian?: Endian, 19 | length?: number 20 | ): T 21 | toArray (endian?: Endian, length?: number): number[] 22 | add (b: BN): BN 23 | mod (b: BN): BN 24 | cmp (b: BN): number 25 | toNumber (): number 26 | toString (base?: number, length?: number): string 27 | isZero (): boolean 28 | bitLength (): number 29 | toTwos (width: number): BN 30 | 31 | static isBN (num: any): boolean 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/@types/elliptic.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'elliptic' { 2 | import BN from 'bn.js' 3 | import { Buffer } from 'buffer' 4 | 5 | export class Point { 6 | x: BN 7 | y: BN 8 | mul (k: BN): Point 9 | add (p: Point): Point 10 | isInfinity (): boolean 11 | encode (encoding: 'hex', compact: boolean): string 12 | encode (encoding: null, compact: boolean): number[] 13 | encode (): number[] 14 | } 15 | 16 | export class Signature { 17 | r: BN 18 | s: BN 19 | recoveryParam: number | null 20 | } 21 | 22 | export class KeyPair { 23 | priv: Point 24 | pub: Point 25 | getPublic (): Point 26 | getPublic (encoding: 'hex'): string 27 | getPublic (compact: boolean, encoding: 'hex'): string 28 | sign ( 29 | msg: string, 30 | encoding: 'hex', 31 | options?: { canonical: boolean } 32 | ): Signature 33 | sign (msg: Buffer, options?: { canonical: boolean }): Signature 34 | } 35 | 36 | export class EC { 37 | n: BN 38 | g: Point 39 | constructor (curve: string) 40 | keyFromPrivate (priv: string, encoding: 'hex'): KeyPair 41 | keyFromPrivate (priv: Buffer): KeyPair 42 | keyFromPublic (pub: string, encoding: 'hex'): KeyPair 43 | keyFromPublic (pub: Buffer | BN): KeyPair 44 | recoverPubKey ( 45 | msg: Buffer, 46 | signature: { r: Buffer; s: Buffer }, 47 | recoveryParam: number 48 | ): Point 49 | } 50 | 51 | export { EC as ec } 52 | } 53 | -------------------------------------------------------------------------------- /src/@types/keccak.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'keccak/js' { 2 | class Keccak { 3 | update (data: Buffer): Keccak 4 | update (data: string, encoding: string): Keccak 5 | digest (): Buffer 6 | digest (encoding: string): string 7 | } 8 | 9 | function createKeccakHash ( 10 | algorithm: 11 | | 'keccak224' 12 | | 'keccak256' 13 | | 'keccak384' 14 | | 'keccak512' 15 | | 'sha3-224' 16 | | 'sha3-256' 17 | | 'sha3-384' 18 | | 'sha3-512' 19 | ): Keccak 20 | 21 | export default createKeccakHash 22 | } 23 | -------------------------------------------------------------------------------- /src/@types/rlp.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'rlp' { 2 | type Item = Buffer | string | number 3 | 4 | export function encode (input: Item | Item[]): Buffer 5 | } 6 | -------------------------------------------------------------------------------- /src/Address.test.ts: -------------------------------------------------------------------------------- 1 | import { Address } from './Address' 2 | 3 | describe('from', () => { 4 | test('derives an Ethereum address from a given compressed public key', () => { 5 | const testCases: { [key: string]: string } = { 6 | '028a8c59fa27d1e0f1643081ff80c3cf0392902acbf76ab0dc9c414b8d115b0ab3': 7 | '0xD11A13f484E2f2bD22d93c3C3131f61c05E876a9', 8 | '024ff6fdcb22e9f6a6e2efa88df7e97120883874a6c127b0decc01be7ebfde9289': 9 | '0xEA6695e4F122822C51B711D0f3d6CcaF1D9F5579', 10 | '0360176e6591e6782fc4efdc3d0bd26882ccbb42217c6c52cb28cd75e542b8849c': 11 | '0x0bFD0a556b97EDf81e2ACC5fAd6d642e338AbC58', 12 | '03c4252fcb1ef1298b213f7158c9d53030337bff3c91865754cd8e145132dc6d53': 13 | '0x000bF7ebE7f830F0a682FC4d2931c12716F7ba65', 14 | '037d267213eaf480b638a017657b97998c8d2be26ae236fc4301c6eed756e52ce4': 15 | '0x0000de016A766eA5dE351835912b92696225f916' 16 | } 17 | 18 | Object.keys(testCases).forEach(hex => { 19 | const publicKey = Buffer.from(hex, 'hex') 20 | const address = testCases[hex] 21 | const ea = Address.from(publicKey) 22 | expect(ea.address).toBe(address) 23 | expect(ea.rawAddress.toString('hex')).toBe(address.slice(2).toLowerCase()) 24 | }) 25 | }) 26 | 27 | test('derives an Ethereum address from a given uncompressed public key', () => { 28 | const testCases: { [key: string]: string } = { 29 | '048a8c59fa27d1e0f1643081ff80c3cf0392902acbf76ab0dc9c414b8d115b0ab3ab95ca5cc375db4bfa147cf4c1742b67ade817160bb3a776498e8e185cb06be2': 30 | '0xD11A13f484E2f2bD22d93c3C3131f61c05E876a9', 31 | '044ff6fdcb22e9f6a6e2efa88df7e97120883874a6c127b0decc01be7ebfde9289bdc3d2656abab51f96f87507b9159844e6e5b205c348ec28717bfcfb49fea6c4': 32 | '0xEA6695e4F122822C51B711D0f3d6CcaF1D9F5579', 33 | '0460176e6591e6782fc4efdc3d0bd26882ccbb42217c6c52cb28cd75e542b8849c955e8cc1e7a811ac89673c4658883a0255927d6f85168b8b6d941f6913a2892f': 34 | '0x0bFD0a556b97EDf81e2ACC5fAd6d642e338AbC58', 35 | '04c4252fcb1ef1298b213f7158c9d53030337bff3c91865754cd8e145132dc6d5327e8f1dbc307d7b9e880dab9af3cb48153c2636c5f768b5a3d4657017c7a4035': 36 | '0x000bF7ebE7f830F0a682FC4d2931c12716F7ba65', 37 | '047d267213eaf480b638a017657b97998c8d2be26ae236fc4301c6eed756e52ce4710a85bba8972095047f10615040da9984626fa394ea3230604222629a3c28af': 38 | '0x0000de016A766eA5dE351835912b92696225f916' 39 | } 40 | 41 | Object.keys(testCases).forEach(hex => { 42 | const publicKey = Buffer.from(hex, 'hex') 43 | const address = testCases[hex] 44 | const ea = Address.from(publicKey) 45 | expect(ea.address).toBe(address) 46 | expect(ea.rawAddress.toString('hex')).toBe(address.slice(2).toLowerCase()) 47 | }) 48 | }) 49 | }) 50 | 51 | describe('checksumAddress', () => { 52 | test('converts an address to a mixed-case checksum address', () => { 53 | const testCases: { [key: string]: string } = { 54 | '0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed': 55 | '0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed', 56 | '0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359': 57 | '0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359', 58 | '0xdbf03b407c01e7cd3cbea99509d93f8dddc8c6fb': 59 | '0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB', 60 | '0xd1220a0cf47c7b9be7a2e6ba89f429762e7b9adb': 61 | '0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb', 62 | '0XD1220A0CF47C7B9BE7A2E6BA89F429762E7B9ADB': 63 | '0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb', 64 | d1220a0cf47c7b9be7a2e6ba89f429762e7b9adb: 65 | '0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb', 66 | D1220A0CF47C7B9BE7A2E6BA89F429762E7B9ADB: 67 | '0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb' 68 | } 69 | 70 | Object.keys(testCases).forEach(address => { 71 | const checksumAddress = testCases[address] 72 | expect(Address.checksumAddress(address)).toBe(checksumAddress) 73 | }) 74 | }) 75 | 76 | test('throws an error if an invalid address is given', () => { 77 | expect(() => { 78 | Address.checksumAddress('0x5aaeb6053f3e94c9b9a09f33669435e7ef1beae') 79 | }).toThrow(/invalid/) 80 | expect(() => { 81 | Address.checksumAddress('0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaedf') 82 | }).toThrow(/invalid/) 83 | expect(() => { 84 | Address.checksumAddress('5aaeb6053f3e94c9b9a09f33669435e7ef1beae') 85 | }).toThrow(/invalid/) 86 | expect(() => { 87 | Address.checksumAddress('5aaeb6053f3e94c9b9a09f33669435e7ef1beaedf') 88 | }).toThrow(/invalid/) 89 | }) 90 | }) 91 | 92 | describe('isValid', () => { 93 | test('rejects addresses that are too short', () => { 94 | expect(Address.isValid('0x5aaeb6053f3e94c9b9a09f33669435e7ef1beae')).toBe( 95 | false 96 | ) 97 | expect(Address.isValid('5aaeb6053f3e94c9b9a09f33669435e7ef1beae')).toBe( 98 | false 99 | ) 100 | }) 101 | 102 | test('rejects addresses that are too long', () => { 103 | expect(Address.isValid('0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaedf')).toBe( 104 | false 105 | ) 106 | expect(Address.isValid('5aaeb6053f3e94c9b9a09f33669435e7ef1beaedf')).toBe( 107 | false 108 | ) 109 | }) 110 | 111 | test('rejects addresses that are mixed-case and has invalid checksum', () => { 112 | expect(Address.isValid('0x5Aaeb6053F3E94C9b9A09f33669435E7Ef1BeAed')).toBe( 113 | false 114 | ) 115 | expect(Address.isValid('0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5D359')).toBe( 116 | false 117 | ) 118 | expect(Address.isValid('5Aaeb6053F3E94C9b9A09f33669435E7Ef1BeAed')).toBe( 119 | false 120 | ) 121 | expect(Address.isValid('fB6916095ca1df60bB79Ce92cE3Ea74c37c5D359')).toBe( 122 | false 123 | ) 124 | }) 125 | 126 | test('rejects addresses with invalid characters', () => { 127 | expect(Address.isValid('0x5aaeb60!3f3e94c9b9a09f33669435e7ef1beaed')).toBe( 128 | false 129 | ) 130 | expect(Address.isValid('0xfb6916095ca1df60bb79ce92c$3ea74c37c5d359')).toBe( 131 | false 132 | ) 133 | expect(Address.isValid('5AAEB6053F3E94CYB9A09F33669435E7EF1BEAED')).toBe( 134 | false 135 | ) 136 | expect(Address.isValid('fb6916095ca1df60bb79ce92ce3ea74c37c5ggzz')).toBe( 137 | false 138 | ) 139 | }) 140 | 141 | test('accepts addresses with valid mixed-case checksum', () => { 142 | expect(Address.isValid('0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed')).toBe( 143 | true 144 | ) 145 | expect(Address.isValid('0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359')).toBe( 146 | true 147 | ) 148 | expect(Address.isValid('5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed')).toBe( 149 | true 150 | ) 151 | expect(Address.isValid('fB6916095ca1df60bB79Ce92cE3Ea74c37c5d359')).toBe( 152 | true 153 | ) 154 | }) 155 | 156 | test('accepts addresses that are not mixed-case', () => { 157 | expect(Address.isValid('0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed')).toBe( 158 | true 159 | ) 160 | expect(Address.isValid('0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359')).toBe( 161 | true 162 | ) 163 | expect(Address.isValid('0X5AAEB6053F3E94C9B9A09F33669435E7EF1BEAED')).toBe( 164 | true 165 | ) 166 | expect(Address.isValid('5aaeb6053f3e94c9b9a09f33669435e7ef1beaed')).toBe( 167 | true 168 | ) 169 | expect(Address.isValid('fb6916095ca1df60bb79ce92ce3ea74c37c5d359')).toBe( 170 | true 171 | ) 172 | expect(Address.isValid('5AAEB6053F3E94C9B9A09F33669435E7EF1BEAED')).toBe( 173 | true 174 | ) 175 | }) 176 | }) 177 | -------------------------------------------------------------------------------- /src/Address.ts: -------------------------------------------------------------------------------- 1 | import createKeccakHash from 'keccak/js' 2 | import { decompressPublicKey } from './util' 3 | 4 | export class Address { 5 | private _publicKey: Buffer 6 | private _rawAddress?: Buffer 7 | private _address?: string 8 | 9 | private constructor (publicKey: Buffer) { 10 | this._publicKey = decompressPublicKey(publicKey) 11 | } 12 | 13 | static from (publicKey: Buffer): Address { 14 | return new Address(publicKey) 15 | } 16 | 17 | static checksumAddress (address: string): string { 18 | const addrLowerCase = address.toLowerCase() 19 | if (!Address.isValid(addrLowerCase)) { 20 | throw new Error('invalid address') 21 | } 22 | const addr = addrLowerCase.startsWith('0x') 23 | ? addrLowerCase.slice(2) 24 | : addrLowerCase 25 | const hash = createKeccakHash('keccak256') 26 | .update(addr, 'ascii') 27 | .digest('hex') 28 | let newAddr: string = '0x' 29 | 30 | for (let i = 0; i < addr.length; i++) { 31 | if (hash[i] >= '8') { 32 | newAddr += addr[i].toUpperCase() 33 | } else { 34 | newAddr += addr[i] 35 | } 36 | } 37 | 38 | return newAddr 39 | } 40 | 41 | static isValid (address: string): boolean { 42 | const addr = address.match(/^0[xX]/) ? address.slice(2) : address 43 | if (addr.length !== 40) { 44 | return false 45 | } 46 | 47 | if (addr.match(/[0-9a-f]{40}/) || addr.match(/[0-9A-F]{40}/)) { 48 | return true 49 | } 50 | 51 | let checksumAddress: string 52 | try { 53 | checksumAddress = Address.checksumAddress(addr) 54 | } catch (_err) { 55 | return false 56 | } 57 | 58 | return addr === checksumAddress.slice(2) 59 | } 60 | 61 | get publicKey (): Buffer { 62 | return this._publicKey 63 | } 64 | 65 | get rawAddress (): Buffer { 66 | if (!this._rawAddress) { 67 | this._rawAddress = createKeccakHash('keccak256') 68 | .update(this._publicKey.slice(1)) 69 | .digest() 70 | .slice(-20) 71 | } 72 | return this._rawAddress 73 | } 74 | 75 | get address (): string { 76 | if (!this._address) { 77 | const rawAddress = this.rawAddress.toString('hex') 78 | this._address = Address.checksumAddress(rawAddress) 79 | } 80 | return this._address 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/HDKey.test.ts: -------------------------------------------------------------------------------- 1 | import { HDKey } from './HDKey' 2 | 3 | describe('parseMasterSeed', () => { 4 | test('initialize an instance of HDKey from master seed', () => { 5 | const seed = Buffer.from( 6 | 'b7df235f1e8addda6befbdf66f4df613474e8ff6041c7826e4df7fa68aa8c244a1d687eda050f97fc20fc2fcd8c09e19ef21d6c14f523639b033e9fc4e6375a6', 7 | 'hex' 8 | ) 9 | const hdkey = HDKey.parseMasterSeed(seed) 10 | expect(hdkey.privateKey && hdkey.privateKey.toString('hex')).toBe( 11 | 'ae4ae84fd731b25809815c22f5de48ef4b769484b4a2d2ae5c47f622fbda8e9f' 12 | ) 13 | expect(hdkey.publicKey.toString('hex')).toBe( 14 | '02f8205ad1bb6e9680bd920c9ae4ccd51a2a6f466b330bbe6792a831ae2c50a6d3' 15 | ) 16 | expect(hdkey.depth).toBe(0) 17 | expect(hdkey.index).toBe(0) 18 | expect(hdkey.parentFingerprint).toBe(null) 19 | }) 20 | }) 21 | 22 | describe('parseExtendedKey', () => { 23 | test('initialize an instance of HDKey from an extended private key', () => { 24 | const hdkey = HDKey.parseExtendedKey( 25 | 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi' 26 | ) 27 | 28 | expect(hdkey.depth).toBe(0) 29 | expect(hdkey.index).toBe(0) 30 | expect(hdkey.parentFingerprint).toBe(null) 31 | 32 | expect(hdkey.publicKey.toString('hex')).toBe( 33 | '0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2' 34 | ) 35 | expect(hdkey.privateKey && hdkey.privateKey.toString('hex')).toBe( 36 | 'e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35' 37 | ) 38 | 39 | expect(hdkey.extendedPublicKey).toBe( 40 | 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8' 41 | ) 42 | expect(hdkey.extendedPrivateKey).toBe( 43 | 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi' 44 | ) 45 | }) 46 | 47 | test('initialize an instance of HDKey from an extended public key', () => { 48 | const hdkey = HDKey.parseExtendedKey( 49 | 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8' 50 | ) 51 | 52 | expect(hdkey.depth).toBe(0) 53 | expect(hdkey.index).toBe(0) 54 | expect(hdkey.parentFingerprint).toBe(null) 55 | 56 | expect(hdkey.publicKey.toString('hex')).toBe( 57 | '0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2' 58 | ) 59 | expect(hdkey.privateKey).toBe(null) 60 | 61 | expect(hdkey.extendedPublicKey).toBe( 62 | 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8' 63 | ) 64 | expect(hdkey.extendedPrivateKey).toBe(null) 65 | }) 66 | 67 | test('initialize an instance of HDKey from a derived key', () => { 68 | const hdkey = HDKey.parseExtendedKey( 69 | 'xprv9uHRZZhbkedL7ChPMJeZbg3bP5LUqJsrpZ393GbdKnoNwZ4dVuA8mv9ZgAoi53z4Uq9EMgtVKFswXRjgiViKUbCnQ2K7uDVbKgCubQjqfDg' 70 | ) 71 | expect(hdkey.depth).toBe(1) 72 | expect(hdkey.index).toBe(2) 73 | expect( 74 | hdkey.parentFingerprint && hdkey.parentFingerprint.toString('hex') 75 | ).toBe('3442193e') 76 | 77 | expect(hdkey.publicKey.toString('hex')).toBe( 78 | '02fd648f85194d8cad102d63aa29bf86336ed148134eb521c59436500c15588fff' 79 | ) 80 | expect(hdkey.privateKey && hdkey.privateKey.toString('hex')).toBe( 81 | '271614f2ca446df6e17e3ea92dacc70a0b6360bf831648a42508e7918a71db8a' 82 | ) 83 | 84 | expect(hdkey.extendedPrivateKey).toBe( 85 | 'xprv9uHRZZhbkedL7ChPMJeZbg3bP5LUqJsrpZ393GbdKnoNwZ4dVuA8mv9ZgAoi53z4Uq9EMgtVKFswXRjgiViKUbCnQ2K7uDVbKgCubQjqfDg' 86 | ) 87 | expect(hdkey.extendedPublicKey).toBe( 88 | 'xpub68Gmy5EVb2BdKgmrTLBZxozKw7AyEmbiBmxjqf1Et8LMpMPn3SUPKiU3XTTrgkJzWbuF8h8E4Ah1m4bWsVqaPa3fzD6p7qEWrFTrgRR1iAe' 89 | ) 90 | }) 91 | }) 92 | 93 | describe('derive', () => { 94 | describe('test vector 1', () => { 95 | const seed = Buffer.from('000102030405060708090a0b0c0d0e0f', 'hex') 96 | const hdkey = HDKey.parseMasterSeed(seed) 97 | 98 | test('chain m', () => { 99 | expect(hdkey.extendedPublicKey).toBe( 100 | 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8' 101 | ) 102 | expect(hdkey.extendedPrivateKey).toBe( 103 | 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi' 104 | ) 105 | 106 | const derived = hdkey.derive('m') 107 | expect(hdkey.extendedPublicKey).toBe(derived.extendedPublicKey) 108 | expect(hdkey.extendedPrivateKey).toBe(derived.extendedPrivateKey) 109 | }) 110 | 111 | test("chain m/0'", () => { 112 | const child = hdkey.derive("m/0'") 113 | expect(child.extendedPublicKey).toBe( 114 | 'xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw' 115 | ) 116 | expect(child.extendedPrivateKey).toBe( 117 | 'xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7' 118 | ) 119 | }) 120 | 121 | test("chain m/0'/1", () => { 122 | const child = hdkey.derive("m/0'/1") 123 | expect(child.extendedPublicKey).toBe( 124 | 'xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ' 125 | ) 126 | expect(child.extendedPrivateKey).toBe( 127 | 'xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs' 128 | ) 129 | }) 130 | 131 | test("chain m/0'/1/2'", () => { 132 | const child = hdkey.derive("m/0'/1/2'") 133 | expect(child.extendedPublicKey).toBe( 134 | 'xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5' 135 | ) 136 | expect(child.extendedPrivateKey).toBe( 137 | 'xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM' 138 | ) 139 | }) 140 | 141 | test("chain m/1'/1/2'/2", () => { 142 | const child = hdkey.derive("m/0'/1/2'/2") 143 | expect(child.extendedPublicKey).toBe( 144 | 'xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV' 145 | ) 146 | expect(child.extendedPrivateKey).toBe( 147 | 'xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334' 148 | ) 149 | }) 150 | 151 | test("chain m/1'/1/2'/2/1000000000", () => { 152 | const child = hdkey.derive("m/0'/1/2'/2/1000000000") 153 | expect(child.extendedPublicKey).toBe( 154 | 'xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy' 155 | ) 156 | expect(child.extendedPrivateKey).toBe( 157 | 'xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76' 158 | ) 159 | }) 160 | 161 | test('derive key from a derived key', () => { 162 | const child = hdkey.derive("m/0'").derive('m/1') 163 | expect(child.extendedPublicKey).toBe( 164 | 'xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ' 165 | ) 166 | expect(child.extendedPrivateKey).toBe( 167 | 'xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs' 168 | ) 169 | }) 170 | 171 | test('public parent key -> hardened child key', () => { 172 | const parent = HDKey.parseExtendedKey(hdkey.extendedPublicKey) 173 | expect(() => { 174 | parent.derive("m/0'") 175 | }).toThrow(/hardened/) 176 | }) 177 | 178 | test('public parent key -> non-hardened child key', () => { 179 | const parent = HDKey.parseExtendedKey(hdkey.extendedPublicKey) 180 | const child = parent.derive('m/0') 181 | expect(child.extendedPublicKey).toBe( 182 | 'xpub68Gmy5EVb2BdFbj2LpWrk1M7obNuaPTpT5oh9QCCo5sRfqSHVYWex97WpDZzszdzHzxXDAzPLVSwybe4uPYkSk4G3gnrPqqkV9RyNzAcNJ1' 183 | ) 184 | expect(child.extendedPrivateKey).toBe(null) 185 | }) 186 | }) 187 | 188 | describe('test vector 2', () => { 189 | const seed = Buffer.from( 190 | 'fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542', 191 | 'hex' 192 | ) 193 | const hdkey = HDKey.parseMasterSeed(seed) 194 | 195 | test('chain m', () => { 196 | expect(hdkey.extendedPublicKey).toBe( 197 | 'xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB' 198 | ) 199 | expect(hdkey.extendedPrivateKey).toBe( 200 | 'xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U' 201 | ) 202 | }) 203 | 204 | test('chain m/0', () => { 205 | const child = hdkey.derive('m/0') 206 | expect(child.extendedPublicKey).toBe( 207 | 'xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH' 208 | ) 209 | expect(child.extendedPrivateKey).toBe( 210 | 'xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt' 211 | ) 212 | }) 213 | 214 | test("chain m/0/2147483647'", () => { 215 | const child = hdkey.derive("m/0/2147483647'") 216 | expect(child.extendedPublicKey).toBe( 217 | 'xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a' 218 | ) 219 | expect(child.extendedPrivateKey).toBe( 220 | 'xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9' 221 | ) 222 | }) 223 | 224 | test("chain m/0/2147483647'/1", () => { 225 | const child = hdkey.derive("m/0/2147483647'/1") 226 | expect(child.extendedPublicKey).toBe( 227 | 'xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon' 228 | ) 229 | expect(child.extendedPrivateKey).toBe( 230 | 'xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef' 231 | ) 232 | }) 233 | 234 | test("chain m/0/2147483647'/1/2147483646'", () => { 235 | const child = hdkey.derive("m/0/2147483647'/1/2147483646'") 236 | expect(child.extendedPublicKey).toBe( 237 | 'xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL' 238 | ) 239 | expect(child.extendedPrivateKey).toBe( 240 | 'xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc' 241 | ) 242 | }) 243 | 244 | test("chain m/0/2147483647'/1/2147483646'/2", () => { 245 | const child = hdkey.derive("m/0/2147483647'/1/2147483646'/2") 246 | expect(child.extendedPublicKey).toBe( 247 | 'xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt' 248 | ) 249 | expect(child.extendedPrivateKey).toBe( 250 | 'xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j' 251 | ) 252 | }) 253 | }) 254 | 255 | describe('test vector 3', () => { 256 | const seed = Buffer.from( 257 | '4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be', 258 | 'hex' 259 | ) 260 | const hdkey = HDKey.parseMasterSeed(seed) 261 | 262 | test('chain m', () => { 263 | expect(hdkey.extendedPublicKey).toBe( 264 | 'xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13' 265 | ) 266 | expect(hdkey.extendedPrivateKey).toBe( 267 | 'xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6' 268 | ) 269 | }) 270 | 271 | test("chain m/0'", () => { 272 | const child = hdkey.derive("m/0'") 273 | expect(child.extendedPublicKey).toBe( 274 | 'xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y' 275 | ) 276 | expect(child.extendedPrivateKey).toBe( 277 | 'xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L' 278 | ) 279 | }) 280 | }) 281 | }) 282 | -------------------------------------------------------------------------------- /src/HDKey.ts: -------------------------------------------------------------------------------- 1 | import * as bs58 from 'bs58' 2 | import * as crypto from 'crypto' 3 | 4 | import BN from 'bn.js' 5 | import { ec as EC } from 'elliptic' 6 | 7 | const HARDENED_KEY_OFFSET = 0x80000000 8 | const secp256k1 = new EC('secp256k1') 9 | 10 | export interface VersionBytes { 11 | bip32: { 12 | public: number 13 | private: number 14 | } 15 | public: number 16 | } 17 | 18 | export const HDKEY_VERSIONS: { [key: string]: VersionBytes } = { 19 | bitcoinMain: { 20 | bip32: { 21 | public: 0x0488b21e, 22 | private: 0x0488ade4 23 | }, 24 | public: 0 25 | }, 26 | bitcoinTest: { 27 | bip32: { 28 | public: 0x043587cf, 29 | private: 0x04358394 30 | }, 31 | public: 0 32 | } 33 | } 34 | export interface HDKeyConstructorOptions { 35 | chainCode: Buffer 36 | privateKey?: Buffer | null 37 | publicKey?: Buffer | null 38 | index?: number 39 | depth?: number 40 | parentFingerprint?: Buffer 41 | version?: VersionBytes 42 | } 43 | 44 | export class HDKey { 45 | private _version: VersionBytes 46 | private _privateKey?: Buffer 47 | private _publicKey: Buffer 48 | private _chainCode: Buffer 49 | private _index: number 50 | private _depth: number 51 | private _parentFingerprint?: Buffer 52 | private _keyIdentifier: Buffer 53 | 54 | constructor ({ 55 | privateKey, 56 | publicKey, 57 | chainCode, 58 | index, 59 | depth, 60 | parentFingerprint, 61 | version 62 | }: HDKeyConstructorOptions) { 63 | if (!privateKey && !publicKey) { 64 | throw new Error('either private key or public key must be provided') 65 | } 66 | if (privateKey) { 67 | this._privateKey = privateKey 68 | const ecdh = crypto.createECDH('secp256k1') 69 | if ((ecdh as any).curve && (ecdh as any).curve.keyFromPrivate) { 70 | // ECDH is not native, fallback to pure-JS elliptic lib 71 | this._publicKey = Buffer.from( 72 | secp256k1.keyFromPrivate(privateKey).getPublic(true, 'hex'), 73 | 'hex' 74 | ) 75 | } else { 76 | ecdh.setPrivateKey(privateKey) 77 | this._publicKey = Buffer.from( 78 | ecdh.getPublicKey('hex', 'compressed'), 79 | 'hex' 80 | ) 81 | } 82 | } else if (publicKey) { 83 | this._publicKey = publicKey 84 | } 85 | this._chainCode = chainCode 86 | this._depth = depth || 0 87 | this._index = index || 0 88 | this._parentFingerprint = parentFingerprint 89 | this._keyIdentifier = hash160(this._publicKey) 90 | this._version = version || HDKEY_VERSIONS.bitcoinMain 91 | } 92 | 93 | static parseMasterSeed (seed: Buffer, version?: VersionBytes): HDKey { 94 | const i = hmacSha512('Bitcoin seed', seed) 95 | const iL = i.slice(0, 32) 96 | const iR = i.slice(32) 97 | return new HDKey({ privateKey: iL, chainCode: iR, version }) 98 | } 99 | 100 | static parseExtendedKey ( 101 | key: string, 102 | version: VersionBytes = HDKEY_VERSIONS.bitcoinMain 103 | ): HDKey { 104 | // version_bytes[4] || depth[1] || parent_fingerprint[4] || index[4] || chain_code[32] || key_data[33] || checksum[4] 105 | const decoded = Buffer.from(bs58.decode(key)) 106 | if (decoded.length > 112) { 107 | throw new Error('invalid extended key') 108 | } 109 | 110 | const checksum = decoded.slice(-4) 111 | const buf = decoded.slice(0, -4) 112 | if (!sha256(sha256(buf)).slice(0, 4).equals(checksum)) { 113 | throw new Error('invalid checksum') 114 | } 115 | 116 | let o: number = 0 117 | const versionRead = buf.readUInt32BE(o) 118 | o += 4 119 | const depth = buf.readUInt8(o) 120 | o += 1 121 | let parentFingerprint: Buffer | undefined = buf.slice(o, (o += 4)) 122 | if (parentFingerprint.readUInt32BE(0) === 0) { 123 | parentFingerprint = undefined 124 | } 125 | const index = buf.readUInt32BE(o) 126 | o += 4 127 | const chainCode = buf.slice(o, (o += 32)) 128 | const keyData = buf.slice(o) 129 | const privateKey = keyData[0] === 0 ? keyData.slice(1) : undefined 130 | const publicKey = keyData[0] !== 0 ? keyData : undefined 131 | 132 | if ( 133 | (privateKey && versionRead !== version.bip32.private) || 134 | (publicKey && versionRead !== version.bip32.public) 135 | ) { 136 | throw new Error('invalid version bytes') 137 | } 138 | 139 | return new HDKey({ 140 | privateKey, 141 | publicKey, 142 | chainCode, 143 | index, 144 | depth, 145 | parentFingerprint, 146 | version 147 | }) 148 | } 149 | 150 | get privateKey (): Buffer | null { 151 | return this._privateKey || null 152 | } 153 | 154 | get publicKey (): Buffer { 155 | return this._publicKey 156 | } 157 | 158 | get chainCode (): Buffer { 159 | return this._chainCode 160 | } 161 | 162 | get depth (): number { 163 | return this._depth 164 | } 165 | 166 | get parentFingerprint (): Buffer | null { 167 | return this._parentFingerprint || null 168 | } 169 | 170 | get index (): number { 171 | return this._index 172 | } 173 | 174 | get keyIdentifier (): Buffer { 175 | return this._keyIdentifier 176 | } 177 | 178 | get fingerprint (): Buffer { 179 | return this._keyIdentifier.slice(0, 4) 180 | } 181 | 182 | get version (): VersionBytes { 183 | return this._version 184 | } 185 | 186 | get extendedPrivateKey (): string | null { 187 | return this._privateKey 188 | ? this.serialize(this._version.bip32.private, this._privateKey) 189 | : null 190 | } 191 | 192 | get extendedPublicKey (): string { 193 | return this.serialize(this._version.bip32.public, this._publicKey) 194 | } 195 | 196 | derive (chain: string): HDKey { 197 | const c = chain.toLowerCase() 198 | 199 | let childKey: HDKey = this 200 | c.split('/').forEach(path => { 201 | const p = path.trim() 202 | if (p === 'm' || p === "m'" || p === '') { 203 | return 204 | } 205 | const index = Number.parseInt(p, 10) 206 | if (Number.isNaN(index)) { 207 | throw new Error('invalid child key derivation chain') 208 | } 209 | const hardened = p.slice(-1) === "'" 210 | childKey = childKey.deriveChildKey(index, hardened) 211 | }) 212 | 213 | return childKey 214 | } 215 | 216 | private deriveChildKey (childIndex: number, hardened: boolean): HDKey { 217 | if (childIndex >= HARDENED_KEY_OFFSET) { 218 | throw new Error('invalid index') 219 | } 220 | if (!this.privateKey && !this.publicKey) { 221 | throw new Error('either private key or public key must be provided') 222 | } 223 | 224 | let index: number = childIndex 225 | const data: Buffer = Buffer.alloc(37) 226 | let o: number = 0 227 | if (hardened) { 228 | if (!this.privateKey) { 229 | throw new Error('cannot derive a hardened child key from a public key') 230 | } 231 | // 0x00 || ser256(kpar) || ser32(i) 232 | // 0x00[1] || parent_private_key[32] || child_index[4] 233 | index += HARDENED_KEY_OFFSET 234 | o += 1 235 | o += this.privateKey.copy(data, o) 236 | } else { 237 | // serP(point(kpar)) || ser32(i) 238 | // compressed_parent_public_key[33] || child_index[4] 239 | o += this.publicKey.copy(data, o) 240 | } 241 | o += data.writeUInt32BE(index, o) 242 | 243 | const i = hmacSha512(this.chainCode, data) 244 | const iL = new BN(i.slice(0, 32)) 245 | const iR = i.slice(32) 246 | 247 | // if parse256(IL) >= n, the resulting key is invalid; proceed with the next value for i 248 | if (iL.cmp(secp256k1.n) >= 0) { 249 | return this.deriveChildKey(childIndex + 1, hardened) 250 | } 251 | 252 | if (this.privateKey) { 253 | // ki is parse256(IL) + kpar (mod n) 254 | const childKey = iL.add(new BN(this.privateKey)).mod(secp256k1.n) 255 | 256 | // if ki = 0, the resulting key is invalid; proceed with the next value for i 257 | if (childKey.cmp(new BN(0)) === 0) { 258 | return this.deriveChildKey(childIndex + 1, hardened) 259 | } 260 | 261 | return new HDKey({ 262 | depth: this.depth + 1, 263 | privateKey: childKey.toArrayLike(Buffer, 'be', 32), 264 | chainCode: iR, 265 | parentFingerprint: this.fingerprint, 266 | index, 267 | version: this.version 268 | }) 269 | } else { 270 | // Ki is point(parse256(IL)) + Kpar = G * IL + Kpar 271 | const parentKey = secp256k1.keyFromPublic(this.publicKey).pub 272 | const childKey = secp256k1.g.mul(iL).add(parentKey) 273 | 274 | // if Ki is the point at infinity, the resulting key is invalid; proceed with the next value for i 275 | if (childKey.isInfinity()) { 276 | return this.deriveChildKey(childIndex + 1, false) 277 | } 278 | const compressedChildKey = Buffer.from(childKey.encode(null, true)) 279 | 280 | return new HDKey({ 281 | depth: this.depth + 1, 282 | publicKey: compressedChildKey, 283 | chainCode: iR, 284 | parentFingerprint: this.fingerprint, 285 | index, 286 | version: this.version 287 | }) 288 | } 289 | } 290 | 291 | private serialize (version: number, key: Buffer): string { 292 | // version_bytes[4] || depth[1] || parent_fingerprint[4] || index[4] || chain_code[32] || key_data[33] || checksum[4] 293 | const buf = Buffer.alloc(78) 294 | let o: number = buf.writeUInt32BE(version, 0) 295 | o = buf.writeUInt8(this.depth, o) 296 | o += this.parentFingerprint ? this.parentFingerprint.copy(buf, o) : 4 297 | o = buf.writeUInt32BE(this.index, o) 298 | o += this.chainCode.copy(buf, o) 299 | o += 33 - key.length 300 | key.copy(buf, o) 301 | const checksum = sha256(sha256(buf)).slice(0, 4) 302 | return bs58.encode(Buffer.concat([buf, checksum])) 303 | } 304 | } 305 | 306 | function hmacSha512 (key: Buffer | string, data: Buffer): Buffer { 307 | return crypto.createHmac('sha512', key).update(data).digest() 308 | } 309 | 310 | function sha256 (data: Buffer): Buffer { 311 | return crypto.createHash('sha256').update(data).digest() 312 | } 313 | 314 | function hash160 (data: Buffer): Buffer { 315 | const d = crypto.createHash('sha256').update(data).digest() 316 | return crypto.createHash('rmd160').update(d).digest() 317 | } 318 | -------------------------------------------------------------------------------- /src/Message.test.ts: -------------------------------------------------------------------------------- 1 | import { Message } from './Message' 2 | 3 | const privateKey = Buffer.from( 4 | '18aed7b31dea5e7d7e50c868b72efcb10e4e5b8060e9bb3cf30b6e2ca6b8471c', 5 | 'hex' 6 | ) // publicKey: 03c2cf95f0cce3e633427a7c26037ad3b028a91d6d7da52799adcaea18c13b9d7d 7 | 8 | const address = '0x3411cd4C838A3FEda31f0d24A958C801C4dB7d36' 9 | 10 | describe('hash', () => { 11 | test('works', () => { 12 | let message = new Message(Buffer.from('hello world', 'utf8')) 13 | expect(message.hash.toString('hex')).toBe( 14 | 'd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68' 15 | ) 16 | message = new Message(Buffer.from('deadbeefcafebabe0123456789', 'hex')) 17 | expect(message.hash.toString('hex')).toBe( 18 | 'aba82ee9ad6afdb9c75e1808cd351fc1e3a051079908d8106714e2a48c3e82a3' 19 | ) 20 | }) 21 | }) 22 | 23 | describe('signMessage', () => { 24 | test('signs a message', () => { 25 | let message = new Message(Buffer.from('hello world', 'utf8')) 26 | expect(message.sign(privateKey).toString('hex')).toBe( 27 | '8bdf11df0aac429a57fcb7595d3f43ff1cd8063a3f93e76594273d728e7b2fc229e9b08ea19fdded04a2d8776a8901dd493437eeb35ea6239d4da0884bf1b2ef1c' 28 | ) 29 | message = new Message(Buffer.from('deadbeefcafebabe0123456789', 'hex')) 30 | expect(message.sign(privateKey).toString('hex')).toBe( 31 | 'c7660c7905ecb6202b30aaf6884bba78739f0c01b19365674ee6a80367e0cb8858c1931958132c24101c3e9d0d408fd26bb0cb03b2904712ba8d94c33eab36d91c' 32 | ) 33 | }) 34 | }) 35 | 36 | describe('ecRecover', () => { 37 | test('returns the address associated with the private key used for signing', () => { 38 | let message = new Message(Buffer.from('hello world', 'utf8')) 39 | expect( 40 | message.ecRecover( 41 | Buffer.from( 42 | '8bdf11df0aac429a57fcb7595d3f43ff1cd8063a3f93e76594273d728e7b2fc229e9b08ea19fdded04a2d8776a8901dd493437eeb35ea6239d4da0884bf1b2ef1c', 43 | 'hex' 44 | ) 45 | ) 46 | ).toBe(address) 47 | 48 | message = new Message(Buffer.from('deadbeefcafebabe0123456789', 'hex')) 49 | expect( 50 | message.ecRecover( 51 | Buffer.from( 52 | 'c7660c7905ecb6202b30aaf6884bba78739f0c01b19365674ee6a80367e0cb8858c1931958132c24101c3e9d0d408fd26bb0cb03b2904712ba8d94c33eab36d91c', 53 | 'hex' 54 | ) 55 | ) 56 | ).toBe(address) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/Message.ts: -------------------------------------------------------------------------------- 1 | import { bnToBuffer, keccak256 } from './util' 2 | 3 | import { Address } from './Address' 4 | import { ec as EC } from 'elliptic' 5 | 6 | const secp256k1 = new EC('secp256k1') 7 | 8 | export class Message { 9 | private _message: Buffer 10 | private _hash: Buffer | null = null 11 | 12 | constructor (message: Buffer) { 13 | this._message = message 14 | } 15 | 16 | get message () { 17 | return this._message 18 | } 19 | 20 | get hash (): Buffer { 21 | if (this._hash) { 22 | return this._hash 23 | } 24 | const prefix = Buffer.from( 25 | `\x19Ethereum Signed Message:\n${this._message.length}`, 26 | 'utf8' 27 | ) 28 | const messageToSign = Buffer.concat([prefix, this._message]) 29 | this._hash = keccak256(messageToSign) 30 | return this._hash 31 | } 32 | 33 | sign (privateKey: Buffer): Buffer { 34 | const sig = secp256k1 35 | .keyFromPrivate(privateKey) 36 | .sign(this.hash, { canonical: true }) 37 | 38 | const sigBuf = Buffer.alloc(65) 39 | let offset = 0 40 | 41 | // copy r 42 | const r = bnToBuffer(sig.r) 43 | offset += 32 - r.length 44 | offset += r.copy(sigBuf, offset) 45 | 46 | // copy s 47 | const s = bnToBuffer(sig.s) 48 | offset += 32 - s.length 49 | offset += s.copy(sigBuf, offset) 50 | 51 | // copy v 52 | sigBuf.writeUInt8((sig.recoveryParam || 0) + 27, offset) 53 | return sigBuf 54 | } 55 | 56 | ecRecover (signature: Buffer): string { 57 | if (signature.length !== 65) { 58 | throw new Error('Invalid signature') 59 | } 60 | const recoveryParam = signature[signature.length - 1] - 27 61 | const sig = { 62 | r: signature.slice(0, 32), 63 | s: signature.slice(32, 64) 64 | } 65 | const point = secp256k1.recoverPubKey(this.hash, sig, recoveryParam) 66 | const pubKey = Buffer.from(point.encode('hex', true), 'hex') 67 | return Address.from(pubKey).address 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Mnemonic.test.ts: -------------------------------------------------------------------------------- 1 | import { Mnemonic } from './Mnemonic' 2 | 3 | describe('generate', () => { 4 | test('generates mnemonic phrase from entropy', () => { 5 | const testCases: { [key: string]: string } = { 6 | '4d3ef17b17a8a7ec7dfe3e112f7a61f6': 7 | 'essay wasp gain consider media wage wave sick bachelor knock observe undo', 8 | baa076aafddbeb78ea973289feb05383: 9 | 'rival admit primary wing salt round prevent town measure void belt almost', 10 | '50df9ecd8b1afc4f4afa49563b8b8cdc': 11 | 'express woman recall bike quit chicken cloud empty file sword tobacco rib' 12 | } 13 | 14 | Object.keys(testCases).forEach(hex => { 15 | const entropy = Buffer.from(hex, 'hex') 16 | const phrase = testCases[hex] 17 | const mnemonic = Mnemonic.generate(entropy) 18 | expect(mnemonic).not.toBe(null) 19 | expect(mnemonic!.entropy.toString('hex')).toBe(hex) 20 | expect(mnemonic!.phrase).toBe(phrase) 21 | expect(mnemonic!.words).toEqual(phrase.split(' ')) 22 | }) 23 | }) 24 | 25 | test('returns null when an entropy with an invalid length is passed', () => { 26 | const testCases: string[] = [ 27 | '4d3ef17b17a8a7ec7dfe3e112f7a61', 28 | 'baa076aafddbeb78ea973289feb053', 29 | '50df9ecd8b1afc4f4afa49563b' 30 | ] 31 | 32 | testCases.forEach(hex => { 33 | let entropy = Buffer.from(hex, 'hex') 34 | expect(Mnemonic.generate(entropy)).toBe(null) 35 | }) 36 | }) 37 | }) 38 | 39 | describe('parse', () => { 40 | test('parses mnemonic, verifies checksum, and decodes back to entropy', () => { 41 | const testCases: { [key: string]: string } = { 42 | 'essay wasp gain consider media wage wave sick bachelor knock observe undo': 43 | '4d3ef17b17a8a7ec7dfe3e112f7a61f6', 44 | 'rival admit primary wing salt round prevent town measure void belt almost': 45 | 'baa076aafddbeb78ea973289feb05383', 46 | 'express woman recall bike quit chicken cloud empty file sword tobacco rib': 47 | '50df9ecd8b1afc4f4afa49563b8b8cdc' 48 | } 49 | 50 | Object.keys(testCases).forEach(phrase => { 51 | const hex = testCases[phrase] 52 | const mnemonic = Mnemonic.parse(phrase) 53 | expect(mnemonic).not.toBe(null) 54 | expect(mnemonic!.entropy.toString('hex')).toBe(hex) 55 | expect(mnemonic!.phrase).toBe(phrase) 56 | expect(mnemonic!.words).toEqual(phrase.split(' ')) 57 | }) 58 | }) 59 | 60 | test('returns null when verification fails', () => { 61 | const testCases: string[] = [ 62 | 'essay wasp gain consider media wage wave sick bachelor knock observe', 63 | 'essay wasp gain consider media wage wave sick bachelor knock observe uncle', 64 | 'river admit primary wing salt round prevent town measure void belt almost', 65 | 'express woman recall biology quit chicken cloud empty file sword tobacco rib' 66 | ] 67 | 68 | testCases.forEach(phrase => { 69 | expect(Mnemonic.parse(phrase)).toBe(null) 70 | }) 71 | }) 72 | }) 73 | 74 | describe('toSeed', () => { 75 | test('returns a binary seed derived from a mnemonic phrase', () => { 76 | const testCases: { [key: string]: string } = { 77 | 'essay wasp gain consider media wage wave sick bachelor knock observe undo': 78 | 'b7df235f1e8addda6befbdf66f4df613474e8ff6041c7826e4df7fa68aa8c244a1d687eda050f97fc20fc2fcd8c09e19ef21d6c14f523639b033e9fc4e6375a6', 79 | 'rival admit primary wing salt round prevent town measure void belt almost': 80 | '267d6fe81adc779fd2e875d62739cc67690af67025bca8fc6b1f5f3228fb312a1a9b10dedbb3cffc62730438f5afc8725dac6ee11fcd319b98611863226a2957', 81 | 'express woman recall bike quit chicken cloud empty file sword tobacco rib': 82 | 'c35ecec5b1986cd3a1407bc3c829610eb5fc9497e59f31c151a5ce422c7ff7e68bdf3343343605a53db8e7376a932a74b3e08296c0b51476f3d288b750089d9d' 83 | } 84 | 85 | Object.keys(testCases).forEach(phrase => { 86 | const seed = testCases[phrase] 87 | const mnemonic = Mnemonic.parse(phrase) 88 | expect(mnemonic!.toSeed().toString('hex')).toBe(seed) 89 | }) 90 | }) 91 | }) 92 | 93 | describe('toSeedAsync', () => { 94 | test('returns a binary seed derived from a mnemonic phrase', async () => { 95 | const testCases: { [key: string]: string } = { 96 | 'essay wasp gain consider media wage wave sick bachelor knock observe undo': 97 | 'b7df235f1e8addda6befbdf66f4df613474e8ff6041c7826e4df7fa68aa8c244a1d687eda050f97fc20fc2fcd8c09e19ef21d6c14f523639b033e9fc4e6375a6', 98 | 'rival admit primary wing salt round prevent town measure void belt almost': 99 | '267d6fe81adc779fd2e875d62739cc67690af67025bca8fc6b1f5f3228fb312a1a9b10dedbb3cffc62730438f5afc8725dac6ee11fcd319b98611863226a2957', 100 | 'express woman recall bike quit chicken cloud empty file sword tobacco rib': 101 | 'c35ecec5b1986cd3a1407bc3c829610eb5fc9497e59f31c151a5ce422c7ff7e68bdf3343343605a53db8e7376a932a74b3e08296c0b51476f3d288b750089d9d' 102 | } 103 | 104 | const keys = Object.keys(testCases) 105 | for (let i = 0; i < keys.length; i++) { 106 | const phrase = keys[i] 107 | const seed = testCases[phrase] 108 | const mnemonic = Mnemonic.parse(phrase) 109 | const derivedSeed = await mnemonic!.toSeedAsync() 110 | expect(derivedSeed.toString('hex')).toBe(seed) 111 | } 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /src/Mnemonic.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto' 2 | 3 | import wordlist from './wordlist.en' 4 | 5 | export type Pbkdf2SyncFunction = ( 6 | password: string | Buffer, 7 | salt: string | Buffer, 8 | iterations: number, 9 | keylen: number, 10 | digest: string 11 | ) => Buffer 12 | 13 | export type Pbkdf2Function = ( 14 | password: string | Buffer, 15 | salt: string | Buffer, 16 | iterations: number, 17 | keylen: number, 18 | digest: string, 19 | callback: (err: Error | null, derivedKey: Buffer | null) => void 20 | ) => void 21 | 22 | export class Mnemonic { 23 | static pbkdf2Sync: Pbkdf2SyncFunction = crypto.pbkdf2Sync 24 | static pbkdf2: Pbkdf2Function = crypto.pbkdf2 25 | 26 | private _entropy: Buffer 27 | private _words: string[] 28 | private _phrase: string 29 | 30 | private constructor (entropy: Buffer, words: string[]) { 31 | this._entropy = entropy 32 | this._words = words 33 | } 34 | 35 | static generate (entropy: Buffer): Mnemonic | null { 36 | if (entropy.length % 4 !== 0) { 37 | return null 38 | } 39 | 40 | const ent = entropy.length * 8 41 | const cs = ent / 32 42 | 43 | const bits = flatten(Array.from(entropy).map(uint8ToBitArray)) 44 | const shasum = crypto.createHash('sha256').update(entropy).digest() 45 | const checksum = flatten(Array.from(shasum).map(uint8ToBitArray)).slice( 46 | 0, 47 | cs 48 | ) 49 | bits.push(...checksum) 50 | 51 | const words: string[] = [] 52 | for (let i = 0; i < bits.length / 11; i++) { 53 | const idx = elevenBitsToInt(bits.slice(i * 11, (i + 1) * 11)) 54 | words.push(wordlist[idx]) 55 | } 56 | 57 | return new Mnemonic(entropy, words) 58 | } 59 | 60 | static parse (phrase: string): Mnemonic | null { 61 | const words = phrase.normalize('NFKD').split(' ') 62 | if (words.length % 3 !== 0) return null 63 | 64 | const bitArrays: number[][] = [] 65 | for (let i = 0; i < words.length; i++) { 66 | const word = words[i] 67 | const idx = wordlist.indexOf(word) 68 | if (idx === -1) return null 69 | bitArrays.push(uint11ToBitArray(idx)) 70 | } 71 | 72 | const bits = flatten(bitArrays) 73 | const cs = bits.length / 33 74 | if (cs !== Math.floor(cs)) return null 75 | const checksum = bits.slice(-cs) 76 | bits.splice(-cs, cs) 77 | 78 | const entropy: number[] = [] 79 | for (let i = 0; i < bits.length / 8; i++) { 80 | entropy.push(eightBitsToInt(bits.slice(i * 8, (i + 1) * 8))) 81 | } 82 | const entropyBuf = Buffer.from(entropy) 83 | const shasum = crypto.createHash('sha256').update(entropyBuf).digest() 84 | const checksumFromSha = flatten( 85 | Array.from(shasum).map(uint8ToBitArray) 86 | ).slice(0, cs) 87 | 88 | if (!arraysEqual(checksumFromSha, checksum)) return null 89 | 90 | return new Mnemonic(entropyBuf, words) 91 | } 92 | 93 | get entropy (): Buffer { 94 | return this._entropy 95 | } 96 | 97 | get words (): string[] { 98 | return this._words 99 | } 100 | 101 | get phrase (): string { 102 | if (!this._phrase) { 103 | this._phrase = this._words.join(' ') 104 | } 105 | return this._phrase 106 | } 107 | 108 | toSeed (passphrase: string = ''): Buffer { 109 | const salt = `mnemonic${passphrase}` 110 | return Mnemonic.pbkdf2Sync( 111 | this.phrase.normalize('NFKD'), 112 | salt.normalize('NFKD'), 113 | 2048, 114 | 64, 115 | 'sha512' 116 | ) 117 | } 118 | 119 | toSeedAsync (passphrase: string = ''): Promise { 120 | const salt = `mnemonic${passphrase}` 121 | return new Promise((resolve, reject) => { 122 | Mnemonic.pbkdf2( 123 | this.phrase.normalize('NFKD'), 124 | salt.normalize('NFKD'), 125 | 2048, 126 | 64, 127 | 'sha512', 128 | (err, key) => { 129 | if (err) { 130 | reject(err) 131 | return 132 | } 133 | resolve(key!) 134 | } 135 | ) 136 | }) 137 | } 138 | } 139 | 140 | function flatten (input: T[][]): T[] { 141 | const arr: T[] = [] 142 | return arr.concat(...input) 143 | } 144 | 145 | function uint11ToBitArray (n: number): number[] { 146 | return [ 147 | Math.min(n & 1024, 1), 148 | Math.min(n & 512, 1), 149 | Math.min(n & 256, 1), 150 | Math.min(n & 128, 1), 151 | Math.min(n & 64, 1), 152 | Math.min(n & 32, 1), 153 | Math.min(n & 16, 1), 154 | Math.min(n & 8, 1), 155 | Math.min(n & 4, 1), 156 | Math.min(n & 2, 1), 157 | Math.min(n & 1, 1) 158 | ] 159 | } 160 | 161 | function uint8ToBitArray (n: number): number[] { 162 | return [ 163 | Math.min(n & 128, 1), 164 | Math.min(n & 64, 1), 165 | Math.min(n & 32, 1), 166 | Math.min(n & 16, 1), 167 | Math.min(n & 8, 1), 168 | Math.min(n & 4, 1), 169 | Math.min(n & 2, 1), 170 | Math.min(n & 1, 1) 171 | ] 172 | } 173 | 174 | function elevenBitsToInt (bits: number[]): number { 175 | return ( 176 | bits[0] * 1024 + 177 | bits[1] * 512 + 178 | bits[2] * 256 + 179 | bits[3] * 128 + 180 | bits[4] * 64 + 181 | bits[5] * 32 + 182 | bits[6] * 16 + 183 | bits[7] * 8 + 184 | bits[8] * 4 + 185 | bits[9] * 2 + 186 | bits[10] 187 | ) 188 | } 189 | 190 | function eightBitsToInt (bits: number[]): number { 191 | return ( 192 | bits[0] * 128 + 193 | bits[1] * 64 + 194 | bits[2] * 32 + 195 | bits[3] * 16 + 196 | bits[4] * 8 + 197 | bits[5] * 4 + 198 | bits[6] * 2 + 199 | bits[7] 200 | ) 201 | } 202 | 203 | function arraysEqual (a: Array, b: Array): boolean { 204 | if (a === b) return true 205 | if (a.length !== b.length) return false 206 | 207 | for (let i = 0; i < a.length; i++) { 208 | if (a[i] !== b[i]) return false 209 | } 210 | return true 211 | } 212 | -------------------------------------------------------------------------------- /src/Transaction.test.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js' 2 | import { Transaction, TransactionParams } from './Transaction' 3 | 4 | const privateKey = Buffer.from( 5 | '18aed7b31dea5e7d7e50c868b72efcb10e4e5b8060e9bb3cf30b6e2ca6b8471c', 6 | 'hex' 7 | ) 8 | 9 | const params: TransactionParams = { 10 | nonce: 27, 11 | gasPriceWei: new BN(20e9), 12 | gasLimit: new BN(21000), 13 | toAddress: '0xC589aC793Af309DB9690D819aBC9AAb37D169F6a', 14 | valueWei: new BN((1.5e18).toString()), 15 | data: '0xdeadbeef0cafebabe0123456789' 16 | } 17 | 18 | const paramsWithChainId: TransactionParams = { 19 | ...params, 20 | chainId: 1 21 | } 22 | 23 | describe('initialization', () => { 24 | test('allows some param fields to be optional', () => { 25 | let tx: Transaction | null = null 26 | expect(() => { 27 | tx = new Transaction({ 28 | nonce: 0, 29 | gasPriceWei: new BN(20e9), 30 | gasLimit: new BN(21000), 31 | valueWei: new BN((1.5e18).toString()) 32 | }) 33 | }).not.toThrow() 34 | expect(tx).not.toBe(null) 35 | }) 36 | }) 37 | 38 | describe('fields', () => { 39 | test('returns all fields of the transaction including v, r and s as a list of binary fields', () => { 40 | let tx = new Transaction(params) 41 | let hexFields = tx.fields.map(field => field.toString('hex')) 42 | 43 | // default v, r, and s 44 | expect(hexFields).toEqual([ 45 | '1b', // 0: nonce 46 | '04a817c800', // 1: gas price 47 | '5208', // 2: gas limit 48 | 'c589ac793af309db9690d819abc9aab37d169f6a', // 3: to 49 | '14d1120d7b160000', // 4: value 50 | '0deadbeef0cafebabe0123456789', // 5: data 51 | '1c', // 6: v = 28 52 | '', // 7: r = 0 53 | '' // 8: s = 0 54 | ]) 55 | 56 | tx = new Transaction({ 57 | ...params, 58 | toAddress: null, 59 | valueWei: new BN(0) 60 | }) 61 | hexFields = tx.fields.map(field => field.toString('hex')) 62 | expect(hexFields).toEqual([ 63 | '1b', // 0: nonce 64 | '04a817c800', // 1: gas price 65 | '5208', // 2: gas limit 66 | '', // 3: to 67 | '', // 4: value 68 | '0deadbeef0cafebabe0123456789', // 5: data 69 | '1c', // 6: v = 28 70 | '', // 7: r = 0 71 | '' // 8: s = 0 72 | ]) 73 | 74 | tx = new Transaction({ 75 | ...params, 76 | v: 37, 77 | r: '0xfe353f9175fcf4bb3e7b7fca1c1e40f7062db642102ca70db7348e7a7e42a046', 78 | s: '0x2ba7b98c5782fb4d85ca57557306633ae31663145a1ac0355ac3d5e84d87036b' 79 | }) 80 | hexFields = tx.fields.map(field => field.toString('hex')) 81 | 82 | expect(hexFields).toEqual([ 83 | '1b', // 0: nonce 84 | '04a817c800', // 1: gas price 85 | '5208', // 2: gas limit 86 | 'c589ac793af309db9690d819abc9aab37d169f6a', // 3: to 87 | '14d1120d7b160000', // 4: value 88 | '0deadbeef0cafebabe0123456789', // 5: data 89 | '25', // 6: v 90 | 'fe353f9175fcf4bb3e7b7fca1c1e40f7062db642102ca70db7348e7a7e42a046', // 7: r 91 | '2ba7b98c5782fb4d85ca57557306633ae31663145a1ac0355ac3d5e84d87036b' // 8: s 92 | ]) 93 | }) 94 | }) 95 | 96 | describe('fieldsForSigning', () => { 97 | test('returns the first 6 fields of the transaction as a list of binary fields', () => { 98 | const tx = new Transaction(params) 99 | const hexFields = tx.fieldsForSigning.map(field => field.toString('hex')) 100 | 101 | expect(hexFields).toEqual([ 102 | '1b', // 0: nonce 103 | '04a817c800', // 1: gas price 104 | '5208', // 2: gas limit 105 | 'c589ac793af309db9690d819abc9aab37d169f6a', // 3: to 106 | '14d1120d7b160000', // 4: value 107 | '0deadbeef0cafebabe0123456789' // 5: data 108 | ]) 109 | }) 110 | 111 | test('when chainId is set, return 3 additional fields (EIP155)', () => { 112 | const tx = new Transaction(paramsWithChainId) 113 | const hexFields = tx.fieldsForSigning.map(field => field.toString('hex')) 114 | 115 | expect(hexFields).toEqual([ 116 | '1b', // 0: nonce 117 | '04a817c800', // 1: gas price 118 | '5208', // 2: gas limit 119 | 'c589ac793af309db9690d819abc9aab37d169f6a', // 3: to 120 | '14d1120d7b160000', // 4: value 121 | '0deadbeef0cafebabe0123456789', // 5: data 122 | '01', // 6: v = chainId 123 | '', // 7: r = 0 124 | '' // s: s = 0 125 | ]) 126 | }) 127 | }) 128 | 129 | describe('hash', () => { 130 | test('returns keccak256 hash of the RLP representation of the transaction', () => { 131 | // before signing, so v = 28, r = 0, and s = 0 132 | let tx = new Transaction(params) 133 | expect(tx.hash.toString('hex')).toBe( 134 | '618a78020be294cd84983996ef9378dc2c65ad6e340cbde030bc031eff3a729f' 135 | ) 136 | 137 | // tx hash is the same because v, r, and s haven't been populated by signing 138 | tx = new Transaction(paramsWithChainId) 139 | expect(tx.hash.toString('hex')).toBe( 140 | '618a78020be294cd84983996ef9378dc2c65ad6e340cbde030bc031eff3a729f' 141 | ) 142 | }) 143 | }) 144 | 145 | describe('hashForSigning', () => { 146 | test('returns keccak256 hash of the RLP representation of fieldsForSigning', () => { 147 | let tx = new Transaction(params) 148 | expect(tx.hashForSigning.toString('hex')).toBe( 149 | '97ce0c356b5cd44a63c9357633b022745471c9994de29e15b9d1d21b16a93df1' 150 | ) 151 | 152 | tx = new Transaction(paramsWithChainId) 153 | expect(tx.hashForSigning.toString('hex')).toBe( 154 | 'a84951a3ffc1212a5770e0ceb921553075265d4a7d7a00361e7fd289c23733e6' 155 | ) 156 | }) 157 | }) 158 | 159 | describe('sign', () => { 160 | test('returns signed transaction in RLP format', () => { 161 | let tx = new Transaction(params) 162 | expect(tx.sign(privateKey).toString('hex')).toBe( 163 | 'f87a1b8504a817c80082520894c589ac793af309db9690d819abc9aab37d169f6a8814d1120d7b1600008e0deadbeef0cafebabe01234567891ba03cd26b08b246f23f74fceb2c063021955e691cf7d45fba443a2e504a4700dba5a0337b1f8dbf21ef35adf6e2a867d9c7bc836d1b79c8ab40c670385a2d0abca88c' 164 | ) 165 | // hash changes because v, r, and s have been populated by signing 166 | expect(tx.hash.toString('hex')).toBe( 167 | '60e9fc990234b033e8799c43fba36a296a8097f3a77b962d34f62c2b597882eb' 168 | ) 169 | 170 | // when chain ID is set, sign with v = chainId, r = 0, and s = 0 171 | tx = new Transaction(paramsWithChainId) 172 | expect(tx.sign(privateKey).toString('hex')).toBe( 173 | 'f87a1b8504a817c80082520894c589ac793af309db9690d819abc9aab37d169f6a8814d1120d7b1600008e0deadbeef0cafebabe012345678925a0fe353f9175fcf4bb3e7b7fca1c1e40f7062db642102ca70db7348e7a7e42a046a02ba7b98c5782fb4d85ca57557306633ae31663145a1ac0355ac3d5e84d87036b' 174 | ) 175 | // hash changes because v, r, and s have been populated by signing 176 | expect(tx.hash.toString('hex')).toBe( 177 | 'ae439162935e0e64bfb3f37aa06a0e895fdbd462cdcb00d9f83eb018bded5e97' 178 | ) 179 | }) 180 | }) 181 | -------------------------------------------------------------------------------- /src/Transaction.ts: -------------------------------------------------------------------------------- 1 | import * as rlp from 'rlp' 2 | import BN from 'bn.js' 3 | 4 | import { 5 | bnToBuffer, 6 | hexToBuffer, 7 | keccak256, 8 | numberToBuffer 9 | } from './util' 10 | 11 | import { ec as EC } from 'elliptic' 12 | 13 | const secp256k1 = new EC('secp256k1') 14 | 15 | export interface TransactionParams { 16 | nonce: number 17 | gasPriceWei: BN 18 | gasLimit: BN 19 | toAddress?: string | null 20 | valueWei: BN 21 | data?: string | null 22 | chainId?: number 23 | v?: number 24 | r?: string 25 | s?: string 26 | } 27 | 28 | export class Transaction { 29 | private nonce: Buffer 30 | private gasPriceWei: Buffer 31 | private gasLimit: Buffer 32 | private toAddress: Buffer 33 | private valueWei: Buffer // in wei 34 | private data: Buffer 35 | private chainId?: number 36 | private v: number 37 | private r: Buffer 38 | private s: Buffer 39 | 40 | constructor (params: TransactionParams) { 41 | this.nonce = numberToBuffer(params.nonce) 42 | this.gasPriceWei = bnToBuffer(params.gasPriceWei) 43 | this.gasLimit = bnToBuffer(params.gasLimit) 44 | this.toAddress = params.toAddress 45 | ? hexToBuffer(params.toAddress) 46 | : Buffer.alloc(0) 47 | this.valueWei = bnToBuffer(params.valueWei) 48 | this.data = params.data ? hexToBuffer(params.data) : Buffer.alloc(0) 49 | this.chainId = params.chainId ? params.chainId : undefined // disallow 0 50 | this.v = params.v || 28 51 | this.r = params.r ? hexToBuffer(params.r) : Buffer.alloc(0) 52 | this.s = params.s ? hexToBuffer(params.s) : Buffer.alloc(0) 53 | } 54 | 55 | get fields (): Buffer[] { 56 | return [ 57 | this.nonce, // 0: nonce 58 | this.gasPriceWei, // 1: gas price 59 | this.gasLimit, // 2: gas limit 60 | this.toAddress, // 3: to 61 | this.valueWei, // 4: value 62 | this.data, // 5: data 63 | numberToBuffer(this.v), // 6: v 64 | this.r, // 7: r 65 | this.s // 8: s 66 | ] 67 | } 68 | 69 | get rlp (): Buffer { 70 | return rlp.encode(this.fields) 71 | } 72 | 73 | get hash (): Buffer { 74 | return keccak256(this.rlp) 75 | } 76 | 77 | get fieldsForSigning (): Buffer[] { 78 | const fields = this.fields.slice(0, 6) 79 | return this.chainId 80 | ? fields.concat([ 81 | // EIP155 82 | numberToBuffer(this.chainId), // 6: v = chainID 83 | numberToBuffer(0), // 7: r = 0 84 | numberToBuffer(0) // 8: s = 0 85 | ]) 86 | : fields 87 | } 88 | 89 | get hashForSigning (): Buffer { 90 | return keccak256(rlp.encode(this.fieldsForSigning)) 91 | } 92 | 93 | sign (privateKey: Buffer): Buffer { 94 | const sig = secp256k1 95 | .keyFromPrivate(privateKey) 96 | .sign(this.hashForSigning, { canonical: true }) 97 | 98 | this.v = 99 | (sig.recoveryParam || 0) + 27 + (this.chainId ? this.chainId * 2 + 8 : 0) 100 | this.r = bnToBuffer(sig.r) 101 | this.s = bnToBuffer(sig.s) 102 | 103 | return this.rlp 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/denominations.ts: -------------------------------------------------------------------------------- 1 | export const denominations = { 2 | wei: 1, 3 | kwei: 1e3, 4 | Kwei: 1e3, 5 | babbage: 1e3, 6 | femtoether: 1e3, 7 | mwei: 1e6, 8 | Mwei: 1e6, 9 | lovelace: 1e6, 10 | picoether: 1e6, 11 | gwei: 1e9, 12 | Gwei: 1e9, 13 | shannon: 1e9, 14 | nanoether: 1e9, 15 | nano: 1e9, 16 | szabo: 1e12, 17 | microether: 1e12, 18 | micro: 1e12, 19 | finney: 1e15, 20 | milliether: 1e15, 21 | milli: 1e15, 22 | ether: 1e18, 23 | kether: 1e21, 24 | grand: 1e21, 25 | mether: 1e24, 26 | gether: 1e27, 27 | tether: 1e30 28 | } 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as util from './util' 2 | 3 | export * from './Address' 4 | export * from './HDKey' 5 | export * from './Message' 6 | export * from './Mnemonic' 7 | export * from './Transaction' 8 | 9 | export { denominations } from './denominations' 10 | export { util } 11 | -------------------------------------------------------------------------------- /src/util.test.ts: -------------------------------------------------------------------------------- 1 | import { decompressPublicKey } from './util' 2 | 3 | describe('decompressPublicKey', () => { 4 | test('does nothing and returns the same key if an already decompressed public key is given', () => { 5 | const testCases: string[] = [ 6 | '04bbcf65f8074fe2fba2b5a7ea44fcae88f7c12d0a51aa9c9193336739fee8167f72da911f6377971e1dcd4ff589f3ef84f51d4adde93b68498470b03399a8c451', 7 | '0406d900ae61f6f6a5fc73a1d0e7f6f6b0b22fcb31496a575ed5b07932b4cff0ee8479690287883c69f1f9e293b3397c9318ae13ec2144df29c8164d6cf935f5b6', 8 | '043ad5f4d0042a54769c390668bd120caa20ff193e54887342b1ccbe6a4df0e448c9e1517ddf27378ef0d75d964b21684d3f6a1d22d6d4c36898978afff9f55593', 9 | '04ce4bef9198f26a815f63e74c93b41d98d38c890d3bd19d65a55ede7efc5c8e5461caf6646800d4b44a38e39e81923158f10919d1aa791e151dc1f422a554c67e' 10 | ] 11 | 12 | testCases.forEach(hex => { 13 | const publicKey = Buffer.from(hex, 'hex') 14 | expect(decompressPublicKey(publicKey).toString('hex')).toBe(hex) 15 | }) 16 | }) 17 | 18 | test('returns an decompressed representation of a given comprsesed public key', () => { 19 | const testCases: { [key: string]: string } = { 20 | '03bbcf65f8074fe2fba2b5a7ea44fcae88f7c12d0a51aa9c9193336739fee8167f': 21 | '04bbcf65f8074fe2fba2b5a7ea44fcae88f7c12d0a51aa9c9193336739fee8167f72da911f6377971e1dcd4ff589f3ef84f51d4adde93b68498470b03399a8c451', 22 | '0206d900ae61f6f6a5fc73a1d0e7f6f6b0b22fcb31496a575ed5b07932b4cff0ee': 23 | '0406d900ae61f6f6a5fc73a1d0e7f6f6b0b22fcb31496a575ed5b07932b4cff0ee8479690287883c69f1f9e293b3397c9318ae13ec2144df29c8164d6cf935f5b6', 24 | '033ad5f4d0042a54769c390668bd120caa20ff193e54887342b1ccbe6a4df0e448': 25 | '043ad5f4d0042a54769c390668bd120caa20ff193e54887342b1ccbe6a4df0e448c9e1517ddf27378ef0d75d964b21684d3f6a1d22d6d4c36898978afff9f55593', 26 | '02ce4bef9198f26a815f63e74c93b41d98d38c890d3bd19d65a55ede7efc5c8e54': 27 | '04ce4bef9198f26a815f63e74c93b41d98d38c890d3bd19d65a55ede7efc5c8e5461caf6646800d4b44a38e39e81923158f10919d1aa791e151dc1f422a554c67e' 28 | } 29 | 30 | Object.keys(testCases).forEach(compressedHex => { 31 | const decompressedHex = testCases[compressedHex] 32 | const compressedKey = Buffer.from(compressedHex, 'hex') 33 | expect(decompressPublicKey(compressedKey).toString('hex')).toBe( 34 | decompressedHex 35 | ) 36 | }) 37 | }) 38 | 39 | test('throws an error if an invalid public key is given', () => { 40 | const testCases: string[] = [ 41 | '03bbcf65f8074fe2fba2b5a7ea44fcae88f7c12d0a51aa9c9193336739fee8167f72da911f6377971e1dcd4ff589f3ef84f51d4adde93b68498470b03399a8c451', 42 | '04ce4bef9198f26a815f63e74c93b41d98d38c890d3bd19d65a55ede7efc5c8e5461caf6646800d4b44a38e39e81923158f10919d1aa791e151dc1f422a554c67e00', 43 | '04ce4bef9198f26a815f63e74c93b41d98d38c890d3bd19d65a55ede7efc5c8e5461caf6646800d4b44a38e39e81923158f10919d1aa791e151dc1f422a554c6', 44 | '03bbcf65f8074fe2fba2b5a7ea44fcae88f7c12d0a51aa9c9193336739fee8167f0a', 45 | '01bbcf65f8074fe2fba2b5a7ea44fcae88f7c12d0a51aa9c9193336739fee8167f', 46 | '02ffd900ae61f6f6a5fc73a1d0e7f6f6b0b22fcb31496a575ed5b07932b4cff0ee', 47 | '0206d900ae61f6f6a5fc73a1d0e7f6f6b0b22fcb31496a575ed5b07932b4cff0' 48 | ] 49 | 50 | testCases.forEach(hex => { 51 | const publicKey = Buffer.from(hex, 'hex') 52 | expect(() => decompressPublicKey(publicKey)).toThrow(/invalid/) 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { ec as EC, KeyPair } from 'elliptic' 2 | 3 | import BN from 'bn.js' 4 | import createKeccakHash from 'keccak/js' 5 | 6 | const secp256k1 = new EC('secp256k1') 7 | 8 | export function numberToHex ( 9 | num: number, 10 | includePrefix: boolean = true 11 | ): string { 12 | const hex = new BN(num).toString(16) 13 | return includePrefix ? '0x' + hex : hex 14 | } 15 | 16 | export function bnToHex ( 17 | bn: BN, 18 | includePrefix: boolean = true 19 | ): string { 20 | const hex = bn.toString(16) 21 | return includePrefix ? '0x' + hex : hex 22 | } 23 | 24 | export function hexToEvenLengthHex ( 25 | hex: string, 26 | includePrefix: boolean = true 27 | ): string { 28 | let h = (hex.match(/^0x/i) ? hex.slice(2) : hex).toLowerCase() 29 | if (h.length % 2 === 1) { 30 | h = '0' + h 31 | } 32 | return includePrefix ? '0x' + h : h 33 | } 34 | 35 | export function hexToBuffer (hex: string): Buffer { 36 | if (hex.length === 0) { 37 | return Buffer.alloc(0) 38 | } 39 | return Buffer.from(hexToEvenLengthHex(hex, false), 'hex') 40 | } 41 | 42 | export function numberToBuffer (num: number): Buffer { 43 | if (num === 0) { 44 | return Buffer.alloc(0) 45 | } 46 | return hexToBuffer(numberToHex(num, false)) 47 | } 48 | 49 | export function bnToBuffer (bn: BN): Buffer { 50 | if (bn.isZero()) { 51 | return Buffer.alloc(0) 52 | } 53 | return hexToBuffer(bn.toString(16)) 54 | } 55 | 56 | export function keccak256 (data: Buffer | string): Buffer { 57 | const buf = data instanceof Buffer ? data : Buffer.from(data, 'utf8') 58 | return createKeccakHash('keccak256').update(buf).digest() 59 | } 60 | 61 | export function decompressPublicKey (publicKey: Buffer): Buffer { 62 | const length = publicKey.length 63 | const firstByte = publicKey[0] 64 | if ((length !== 33 && length !== 65) || firstByte < 2 || firstByte > 4) { 65 | throw new Error('invalid public key') 66 | } 67 | let key: KeyPair 68 | try { 69 | key = secp256k1.keyFromPublic(publicKey) 70 | } catch (_err) { 71 | throw new Error('invalid public key') 72 | } 73 | return Buffer.from(key.getPublic().encode()) 74 | } 75 | -------------------------------------------------------------------------------- /src/wordlist.en.ts: -------------------------------------------------------------------------------- 1 | const wordlist: string[] = [ 2 | 'abandon', 3 | 'ability', 4 | 'able', 5 | 'about', 6 | 'above', 7 | 'absent', 8 | 'absorb', 9 | 'abstract', 10 | 'absurd', 11 | 'abuse', 12 | 'access', 13 | 'accident', 14 | 'account', 15 | 'accuse', 16 | 'achieve', 17 | 'acid', 18 | 'acoustic', 19 | 'acquire', 20 | 'across', 21 | 'act', 22 | 'action', 23 | 'actor', 24 | 'actress', 25 | 'actual', 26 | 'adapt', 27 | 'add', 28 | 'addict', 29 | 'address', 30 | 'adjust', 31 | 'admit', 32 | 'adult', 33 | 'advance', 34 | 'advice', 35 | 'aerobic', 36 | 'affair', 37 | 'afford', 38 | 'afraid', 39 | 'again', 40 | 'age', 41 | 'agent', 42 | 'agree', 43 | 'ahead', 44 | 'aim', 45 | 'air', 46 | 'airport', 47 | 'aisle', 48 | 'alarm', 49 | 'album', 50 | 'alcohol', 51 | 'alert', 52 | 'alien', 53 | 'all', 54 | 'alley', 55 | 'allow', 56 | 'almost', 57 | 'alone', 58 | 'alpha', 59 | 'already', 60 | 'also', 61 | 'alter', 62 | 'always', 63 | 'amateur', 64 | 'amazing', 65 | 'among', 66 | 'amount', 67 | 'amused', 68 | 'analyst', 69 | 'anchor', 70 | 'ancient', 71 | 'anger', 72 | 'angle', 73 | 'angry', 74 | 'animal', 75 | 'ankle', 76 | 'announce', 77 | 'annual', 78 | 'another', 79 | 'answer', 80 | 'antenna', 81 | 'antique', 82 | 'anxiety', 83 | 'any', 84 | 'apart', 85 | 'apology', 86 | 'appear', 87 | 'apple', 88 | 'approve', 89 | 'april', 90 | 'arch', 91 | 'arctic', 92 | 'area', 93 | 'arena', 94 | 'argue', 95 | 'arm', 96 | 'armed', 97 | 'armor', 98 | 'army', 99 | 'around', 100 | 'arrange', 101 | 'arrest', 102 | 'arrive', 103 | 'arrow', 104 | 'art', 105 | 'artefact', 106 | 'artist', 107 | 'artwork', 108 | 'ask', 109 | 'aspect', 110 | 'assault', 111 | 'asset', 112 | 'assist', 113 | 'assume', 114 | 'asthma', 115 | 'athlete', 116 | 'atom', 117 | 'attack', 118 | 'attend', 119 | 'attitude', 120 | 'attract', 121 | 'auction', 122 | 'audit', 123 | 'august', 124 | 'aunt', 125 | 'author', 126 | 'auto', 127 | 'autumn', 128 | 'average', 129 | 'avocado', 130 | 'avoid', 131 | 'awake', 132 | 'aware', 133 | 'away', 134 | 'awesome', 135 | 'awful', 136 | 'awkward', 137 | 'axis', 138 | 'baby', 139 | 'bachelor', 140 | 'bacon', 141 | 'badge', 142 | 'bag', 143 | 'balance', 144 | 'balcony', 145 | 'ball', 146 | 'bamboo', 147 | 'banana', 148 | 'banner', 149 | 'bar', 150 | 'barely', 151 | 'bargain', 152 | 'barrel', 153 | 'base', 154 | 'basic', 155 | 'basket', 156 | 'battle', 157 | 'beach', 158 | 'bean', 159 | 'beauty', 160 | 'because', 161 | 'become', 162 | 'beef', 163 | 'before', 164 | 'begin', 165 | 'behave', 166 | 'behind', 167 | 'believe', 168 | 'below', 169 | 'belt', 170 | 'bench', 171 | 'benefit', 172 | 'best', 173 | 'betray', 174 | 'better', 175 | 'between', 176 | 'beyond', 177 | 'bicycle', 178 | 'bid', 179 | 'bike', 180 | 'bind', 181 | 'biology', 182 | 'bird', 183 | 'birth', 184 | 'bitter', 185 | 'black', 186 | 'blade', 187 | 'blame', 188 | 'blanket', 189 | 'blast', 190 | 'bleak', 191 | 'bless', 192 | 'blind', 193 | 'blood', 194 | 'blossom', 195 | 'blouse', 196 | 'blue', 197 | 'blur', 198 | 'blush', 199 | 'board', 200 | 'boat', 201 | 'body', 202 | 'boil', 203 | 'bomb', 204 | 'bone', 205 | 'bonus', 206 | 'book', 207 | 'boost', 208 | 'border', 209 | 'boring', 210 | 'borrow', 211 | 'boss', 212 | 'bottom', 213 | 'bounce', 214 | 'box', 215 | 'boy', 216 | 'bracket', 217 | 'brain', 218 | 'brand', 219 | 'brass', 220 | 'brave', 221 | 'bread', 222 | 'breeze', 223 | 'brick', 224 | 'bridge', 225 | 'brief', 226 | 'bright', 227 | 'bring', 228 | 'brisk', 229 | 'broccoli', 230 | 'broken', 231 | 'bronze', 232 | 'broom', 233 | 'brother', 234 | 'brown', 235 | 'brush', 236 | 'bubble', 237 | 'buddy', 238 | 'budget', 239 | 'buffalo', 240 | 'build', 241 | 'bulb', 242 | 'bulk', 243 | 'bullet', 244 | 'bundle', 245 | 'bunker', 246 | 'burden', 247 | 'burger', 248 | 'burst', 249 | 'bus', 250 | 'business', 251 | 'busy', 252 | 'butter', 253 | 'buyer', 254 | 'buzz', 255 | 'cabbage', 256 | 'cabin', 257 | 'cable', 258 | 'cactus', 259 | 'cage', 260 | 'cake', 261 | 'call', 262 | 'calm', 263 | 'camera', 264 | 'camp', 265 | 'can', 266 | 'canal', 267 | 'cancel', 268 | 'candy', 269 | 'cannon', 270 | 'canoe', 271 | 'canvas', 272 | 'canyon', 273 | 'capable', 274 | 'capital', 275 | 'captain', 276 | 'car', 277 | 'carbon', 278 | 'card', 279 | 'cargo', 280 | 'carpet', 281 | 'carry', 282 | 'cart', 283 | 'case', 284 | 'cash', 285 | 'casino', 286 | 'castle', 287 | 'casual', 288 | 'cat', 289 | 'catalog', 290 | 'catch', 291 | 'category', 292 | 'cattle', 293 | 'caught', 294 | 'cause', 295 | 'caution', 296 | 'cave', 297 | 'ceiling', 298 | 'celery', 299 | 'cement', 300 | 'census', 301 | 'century', 302 | 'cereal', 303 | 'certain', 304 | 'chair', 305 | 'chalk', 306 | 'champion', 307 | 'change', 308 | 'chaos', 309 | 'chapter', 310 | 'charge', 311 | 'chase', 312 | 'chat', 313 | 'cheap', 314 | 'check', 315 | 'cheese', 316 | 'chef', 317 | 'cherry', 318 | 'chest', 319 | 'chicken', 320 | 'chief', 321 | 'child', 322 | 'chimney', 323 | 'choice', 324 | 'choose', 325 | 'chronic', 326 | 'chuckle', 327 | 'chunk', 328 | 'churn', 329 | 'cigar', 330 | 'cinnamon', 331 | 'circle', 332 | 'citizen', 333 | 'city', 334 | 'civil', 335 | 'claim', 336 | 'clap', 337 | 'clarify', 338 | 'claw', 339 | 'clay', 340 | 'clean', 341 | 'clerk', 342 | 'clever', 343 | 'click', 344 | 'client', 345 | 'cliff', 346 | 'climb', 347 | 'clinic', 348 | 'clip', 349 | 'clock', 350 | 'clog', 351 | 'close', 352 | 'cloth', 353 | 'cloud', 354 | 'clown', 355 | 'club', 356 | 'clump', 357 | 'cluster', 358 | 'clutch', 359 | 'coach', 360 | 'coast', 361 | 'coconut', 362 | 'code', 363 | 'coffee', 364 | 'coil', 365 | 'coin', 366 | 'collect', 367 | 'color', 368 | 'column', 369 | 'combine', 370 | 'come', 371 | 'comfort', 372 | 'comic', 373 | 'common', 374 | 'company', 375 | 'concert', 376 | 'conduct', 377 | 'confirm', 378 | 'congress', 379 | 'connect', 380 | 'consider', 381 | 'control', 382 | 'convince', 383 | 'cook', 384 | 'cool', 385 | 'copper', 386 | 'copy', 387 | 'coral', 388 | 'core', 389 | 'corn', 390 | 'correct', 391 | 'cost', 392 | 'cotton', 393 | 'couch', 394 | 'country', 395 | 'couple', 396 | 'course', 397 | 'cousin', 398 | 'cover', 399 | 'coyote', 400 | 'crack', 401 | 'cradle', 402 | 'craft', 403 | 'cram', 404 | 'crane', 405 | 'crash', 406 | 'crater', 407 | 'crawl', 408 | 'crazy', 409 | 'cream', 410 | 'credit', 411 | 'creek', 412 | 'crew', 413 | 'cricket', 414 | 'crime', 415 | 'crisp', 416 | 'critic', 417 | 'crop', 418 | 'cross', 419 | 'crouch', 420 | 'crowd', 421 | 'crucial', 422 | 'cruel', 423 | 'cruise', 424 | 'crumble', 425 | 'crunch', 426 | 'crush', 427 | 'cry', 428 | 'crystal', 429 | 'cube', 430 | 'culture', 431 | 'cup', 432 | 'cupboard', 433 | 'curious', 434 | 'current', 435 | 'curtain', 436 | 'curve', 437 | 'cushion', 438 | 'custom', 439 | 'cute', 440 | 'cycle', 441 | 'dad', 442 | 'damage', 443 | 'damp', 444 | 'dance', 445 | 'danger', 446 | 'daring', 447 | 'dash', 448 | 'daughter', 449 | 'dawn', 450 | 'day', 451 | 'deal', 452 | 'debate', 453 | 'debris', 454 | 'decade', 455 | 'december', 456 | 'decide', 457 | 'decline', 458 | 'decorate', 459 | 'decrease', 460 | 'deer', 461 | 'defense', 462 | 'define', 463 | 'defy', 464 | 'degree', 465 | 'delay', 466 | 'deliver', 467 | 'demand', 468 | 'demise', 469 | 'denial', 470 | 'dentist', 471 | 'deny', 472 | 'depart', 473 | 'depend', 474 | 'deposit', 475 | 'depth', 476 | 'deputy', 477 | 'derive', 478 | 'describe', 479 | 'desert', 480 | 'design', 481 | 'desk', 482 | 'despair', 483 | 'destroy', 484 | 'detail', 485 | 'detect', 486 | 'develop', 487 | 'device', 488 | 'devote', 489 | 'diagram', 490 | 'dial', 491 | 'diamond', 492 | 'diary', 493 | 'dice', 494 | 'diesel', 495 | 'diet', 496 | 'differ', 497 | 'digital', 498 | 'dignity', 499 | 'dilemma', 500 | 'dinner', 501 | 'dinosaur', 502 | 'direct', 503 | 'dirt', 504 | 'disagree', 505 | 'discover', 506 | 'disease', 507 | 'dish', 508 | 'dismiss', 509 | 'disorder', 510 | 'display', 511 | 'distance', 512 | 'divert', 513 | 'divide', 514 | 'divorce', 515 | 'dizzy', 516 | 'doctor', 517 | 'document', 518 | 'dog', 519 | 'doll', 520 | 'dolphin', 521 | 'domain', 522 | 'donate', 523 | 'donkey', 524 | 'donor', 525 | 'door', 526 | 'dose', 527 | 'double', 528 | 'dove', 529 | 'draft', 530 | 'dragon', 531 | 'drama', 532 | 'drastic', 533 | 'draw', 534 | 'dream', 535 | 'dress', 536 | 'drift', 537 | 'drill', 538 | 'drink', 539 | 'drip', 540 | 'drive', 541 | 'drop', 542 | 'drum', 543 | 'dry', 544 | 'duck', 545 | 'dumb', 546 | 'dune', 547 | 'during', 548 | 'dust', 549 | 'dutch', 550 | 'duty', 551 | 'dwarf', 552 | 'dynamic', 553 | 'eager', 554 | 'eagle', 555 | 'early', 556 | 'earn', 557 | 'earth', 558 | 'easily', 559 | 'east', 560 | 'easy', 561 | 'echo', 562 | 'ecology', 563 | 'economy', 564 | 'edge', 565 | 'edit', 566 | 'educate', 567 | 'effort', 568 | 'egg', 569 | 'eight', 570 | 'either', 571 | 'elbow', 572 | 'elder', 573 | 'electric', 574 | 'elegant', 575 | 'element', 576 | 'elephant', 577 | 'elevator', 578 | 'elite', 579 | 'else', 580 | 'embark', 581 | 'embody', 582 | 'embrace', 583 | 'emerge', 584 | 'emotion', 585 | 'employ', 586 | 'empower', 587 | 'empty', 588 | 'enable', 589 | 'enact', 590 | 'end', 591 | 'endless', 592 | 'endorse', 593 | 'enemy', 594 | 'energy', 595 | 'enforce', 596 | 'engage', 597 | 'engine', 598 | 'enhance', 599 | 'enjoy', 600 | 'enlist', 601 | 'enough', 602 | 'enrich', 603 | 'enroll', 604 | 'ensure', 605 | 'enter', 606 | 'entire', 607 | 'entry', 608 | 'envelope', 609 | 'episode', 610 | 'equal', 611 | 'equip', 612 | 'era', 613 | 'erase', 614 | 'erode', 615 | 'erosion', 616 | 'error', 617 | 'erupt', 618 | 'escape', 619 | 'essay', 620 | 'essence', 621 | 'estate', 622 | 'eternal', 623 | 'ethics', 624 | 'evidence', 625 | 'evil', 626 | 'evoke', 627 | 'evolve', 628 | 'exact', 629 | 'example', 630 | 'excess', 631 | 'exchange', 632 | 'excite', 633 | 'exclude', 634 | 'excuse', 635 | 'execute', 636 | 'exercise', 637 | 'exhaust', 638 | 'exhibit', 639 | 'exile', 640 | 'exist', 641 | 'exit', 642 | 'exotic', 643 | 'expand', 644 | 'expect', 645 | 'expire', 646 | 'explain', 647 | 'expose', 648 | 'express', 649 | 'extend', 650 | 'extra', 651 | 'eye', 652 | 'eyebrow', 653 | 'fabric', 654 | 'face', 655 | 'faculty', 656 | 'fade', 657 | 'faint', 658 | 'faith', 659 | 'fall', 660 | 'false', 661 | 'fame', 662 | 'family', 663 | 'famous', 664 | 'fan', 665 | 'fancy', 666 | 'fantasy', 667 | 'farm', 668 | 'fashion', 669 | 'fat', 670 | 'fatal', 671 | 'father', 672 | 'fatigue', 673 | 'fault', 674 | 'favorite', 675 | 'feature', 676 | 'february', 677 | 'federal', 678 | 'fee', 679 | 'feed', 680 | 'feel', 681 | 'female', 682 | 'fence', 683 | 'festival', 684 | 'fetch', 685 | 'fever', 686 | 'few', 687 | 'fiber', 688 | 'fiction', 689 | 'field', 690 | 'figure', 691 | 'file', 692 | 'film', 693 | 'filter', 694 | 'final', 695 | 'find', 696 | 'fine', 697 | 'finger', 698 | 'finish', 699 | 'fire', 700 | 'firm', 701 | 'first', 702 | 'fiscal', 703 | 'fish', 704 | 'fit', 705 | 'fitness', 706 | 'fix', 707 | 'flag', 708 | 'flame', 709 | 'flash', 710 | 'flat', 711 | 'flavor', 712 | 'flee', 713 | 'flight', 714 | 'flip', 715 | 'float', 716 | 'flock', 717 | 'floor', 718 | 'flower', 719 | 'fluid', 720 | 'flush', 721 | 'fly', 722 | 'foam', 723 | 'focus', 724 | 'fog', 725 | 'foil', 726 | 'fold', 727 | 'follow', 728 | 'food', 729 | 'foot', 730 | 'force', 731 | 'forest', 732 | 'forget', 733 | 'fork', 734 | 'fortune', 735 | 'forum', 736 | 'forward', 737 | 'fossil', 738 | 'foster', 739 | 'found', 740 | 'fox', 741 | 'fragile', 742 | 'frame', 743 | 'frequent', 744 | 'fresh', 745 | 'friend', 746 | 'fringe', 747 | 'frog', 748 | 'front', 749 | 'frost', 750 | 'frown', 751 | 'frozen', 752 | 'fruit', 753 | 'fuel', 754 | 'fun', 755 | 'funny', 756 | 'furnace', 757 | 'fury', 758 | 'future', 759 | 'gadget', 760 | 'gain', 761 | 'galaxy', 762 | 'gallery', 763 | 'game', 764 | 'gap', 765 | 'garage', 766 | 'garbage', 767 | 'garden', 768 | 'garlic', 769 | 'garment', 770 | 'gas', 771 | 'gasp', 772 | 'gate', 773 | 'gather', 774 | 'gauge', 775 | 'gaze', 776 | 'general', 777 | 'genius', 778 | 'genre', 779 | 'gentle', 780 | 'genuine', 781 | 'gesture', 782 | 'ghost', 783 | 'giant', 784 | 'gift', 785 | 'giggle', 786 | 'ginger', 787 | 'giraffe', 788 | 'girl', 789 | 'give', 790 | 'glad', 791 | 'glance', 792 | 'glare', 793 | 'glass', 794 | 'glide', 795 | 'glimpse', 796 | 'globe', 797 | 'gloom', 798 | 'glory', 799 | 'glove', 800 | 'glow', 801 | 'glue', 802 | 'goat', 803 | 'goddess', 804 | 'gold', 805 | 'good', 806 | 'goose', 807 | 'gorilla', 808 | 'gospel', 809 | 'gossip', 810 | 'govern', 811 | 'gown', 812 | 'grab', 813 | 'grace', 814 | 'grain', 815 | 'grant', 816 | 'grape', 817 | 'grass', 818 | 'gravity', 819 | 'great', 820 | 'green', 821 | 'grid', 822 | 'grief', 823 | 'grit', 824 | 'grocery', 825 | 'group', 826 | 'grow', 827 | 'grunt', 828 | 'guard', 829 | 'guess', 830 | 'guide', 831 | 'guilt', 832 | 'guitar', 833 | 'gun', 834 | 'gym', 835 | 'habit', 836 | 'hair', 837 | 'half', 838 | 'hammer', 839 | 'hamster', 840 | 'hand', 841 | 'happy', 842 | 'harbor', 843 | 'hard', 844 | 'harsh', 845 | 'harvest', 846 | 'hat', 847 | 'have', 848 | 'hawk', 849 | 'hazard', 850 | 'head', 851 | 'health', 852 | 'heart', 853 | 'heavy', 854 | 'hedgehog', 855 | 'height', 856 | 'hello', 857 | 'helmet', 858 | 'help', 859 | 'hen', 860 | 'hero', 861 | 'hidden', 862 | 'high', 863 | 'hill', 864 | 'hint', 865 | 'hip', 866 | 'hire', 867 | 'history', 868 | 'hobby', 869 | 'hockey', 870 | 'hold', 871 | 'hole', 872 | 'holiday', 873 | 'hollow', 874 | 'home', 875 | 'honey', 876 | 'hood', 877 | 'hope', 878 | 'horn', 879 | 'horror', 880 | 'horse', 881 | 'hospital', 882 | 'host', 883 | 'hotel', 884 | 'hour', 885 | 'hover', 886 | 'hub', 887 | 'huge', 888 | 'human', 889 | 'humble', 890 | 'humor', 891 | 'hundred', 892 | 'hungry', 893 | 'hunt', 894 | 'hurdle', 895 | 'hurry', 896 | 'hurt', 897 | 'husband', 898 | 'hybrid', 899 | 'ice', 900 | 'icon', 901 | 'idea', 902 | 'identify', 903 | 'idle', 904 | 'ignore', 905 | 'ill', 906 | 'illegal', 907 | 'illness', 908 | 'image', 909 | 'imitate', 910 | 'immense', 911 | 'immune', 912 | 'impact', 913 | 'impose', 914 | 'improve', 915 | 'impulse', 916 | 'inch', 917 | 'include', 918 | 'income', 919 | 'increase', 920 | 'index', 921 | 'indicate', 922 | 'indoor', 923 | 'industry', 924 | 'infant', 925 | 'inflict', 926 | 'inform', 927 | 'inhale', 928 | 'inherit', 929 | 'initial', 930 | 'inject', 931 | 'injury', 932 | 'inmate', 933 | 'inner', 934 | 'innocent', 935 | 'input', 936 | 'inquiry', 937 | 'insane', 938 | 'insect', 939 | 'inside', 940 | 'inspire', 941 | 'install', 942 | 'intact', 943 | 'interest', 944 | 'into', 945 | 'invest', 946 | 'invite', 947 | 'involve', 948 | 'iron', 949 | 'island', 950 | 'isolate', 951 | 'issue', 952 | 'item', 953 | 'ivory', 954 | 'jacket', 955 | 'jaguar', 956 | 'jar', 957 | 'jazz', 958 | 'jealous', 959 | 'jeans', 960 | 'jelly', 961 | 'jewel', 962 | 'job', 963 | 'join', 964 | 'joke', 965 | 'journey', 966 | 'joy', 967 | 'judge', 968 | 'juice', 969 | 'jump', 970 | 'jungle', 971 | 'junior', 972 | 'junk', 973 | 'just', 974 | 'kangaroo', 975 | 'keen', 976 | 'keep', 977 | 'ketchup', 978 | 'key', 979 | 'kick', 980 | 'kid', 981 | 'kidney', 982 | 'kind', 983 | 'kingdom', 984 | 'kiss', 985 | 'kit', 986 | 'kitchen', 987 | 'kite', 988 | 'kitten', 989 | 'kiwi', 990 | 'knee', 991 | 'knife', 992 | 'knock', 993 | 'know', 994 | 'lab', 995 | 'label', 996 | 'labor', 997 | 'ladder', 998 | 'lady', 999 | 'lake', 1000 | 'lamp', 1001 | 'language', 1002 | 'laptop', 1003 | 'large', 1004 | 'later', 1005 | 'latin', 1006 | 'laugh', 1007 | 'laundry', 1008 | 'lava', 1009 | 'law', 1010 | 'lawn', 1011 | 'lawsuit', 1012 | 'layer', 1013 | 'lazy', 1014 | 'leader', 1015 | 'leaf', 1016 | 'learn', 1017 | 'leave', 1018 | 'lecture', 1019 | 'left', 1020 | 'leg', 1021 | 'legal', 1022 | 'legend', 1023 | 'leisure', 1024 | 'lemon', 1025 | 'lend', 1026 | 'length', 1027 | 'lens', 1028 | 'leopard', 1029 | 'lesson', 1030 | 'letter', 1031 | 'level', 1032 | 'liar', 1033 | 'liberty', 1034 | 'library', 1035 | 'license', 1036 | 'life', 1037 | 'lift', 1038 | 'light', 1039 | 'like', 1040 | 'limb', 1041 | 'limit', 1042 | 'link', 1043 | 'lion', 1044 | 'liquid', 1045 | 'list', 1046 | 'little', 1047 | 'live', 1048 | 'lizard', 1049 | 'load', 1050 | 'loan', 1051 | 'lobster', 1052 | 'local', 1053 | 'lock', 1054 | 'logic', 1055 | 'lonely', 1056 | 'long', 1057 | 'loop', 1058 | 'lottery', 1059 | 'loud', 1060 | 'lounge', 1061 | 'love', 1062 | 'loyal', 1063 | 'lucky', 1064 | 'luggage', 1065 | 'lumber', 1066 | 'lunar', 1067 | 'lunch', 1068 | 'luxury', 1069 | 'lyrics', 1070 | 'machine', 1071 | 'mad', 1072 | 'magic', 1073 | 'magnet', 1074 | 'maid', 1075 | 'mail', 1076 | 'main', 1077 | 'major', 1078 | 'make', 1079 | 'mammal', 1080 | 'man', 1081 | 'manage', 1082 | 'mandate', 1083 | 'mango', 1084 | 'mansion', 1085 | 'manual', 1086 | 'maple', 1087 | 'marble', 1088 | 'march', 1089 | 'margin', 1090 | 'marine', 1091 | 'market', 1092 | 'marriage', 1093 | 'mask', 1094 | 'mass', 1095 | 'master', 1096 | 'match', 1097 | 'material', 1098 | 'math', 1099 | 'matrix', 1100 | 'matter', 1101 | 'maximum', 1102 | 'maze', 1103 | 'meadow', 1104 | 'mean', 1105 | 'measure', 1106 | 'meat', 1107 | 'mechanic', 1108 | 'medal', 1109 | 'media', 1110 | 'melody', 1111 | 'melt', 1112 | 'member', 1113 | 'memory', 1114 | 'mention', 1115 | 'menu', 1116 | 'mercy', 1117 | 'merge', 1118 | 'merit', 1119 | 'merry', 1120 | 'mesh', 1121 | 'message', 1122 | 'metal', 1123 | 'method', 1124 | 'middle', 1125 | 'midnight', 1126 | 'milk', 1127 | 'million', 1128 | 'mimic', 1129 | 'mind', 1130 | 'minimum', 1131 | 'minor', 1132 | 'minute', 1133 | 'miracle', 1134 | 'mirror', 1135 | 'misery', 1136 | 'miss', 1137 | 'mistake', 1138 | 'mix', 1139 | 'mixed', 1140 | 'mixture', 1141 | 'mobile', 1142 | 'model', 1143 | 'modify', 1144 | 'mom', 1145 | 'moment', 1146 | 'monitor', 1147 | 'monkey', 1148 | 'monster', 1149 | 'month', 1150 | 'moon', 1151 | 'moral', 1152 | 'more', 1153 | 'morning', 1154 | 'mosquito', 1155 | 'mother', 1156 | 'motion', 1157 | 'motor', 1158 | 'mountain', 1159 | 'mouse', 1160 | 'move', 1161 | 'movie', 1162 | 'much', 1163 | 'muffin', 1164 | 'mule', 1165 | 'multiply', 1166 | 'muscle', 1167 | 'museum', 1168 | 'mushroom', 1169 | 'music', 1170 | 'must', 1171 | 'mutual', 1172 | 'myself', 1173 | 'mystery', 1174 | 'myth', 1175 | 'naive', 1176 | 'name', 1177 | 'napkin', 1178 | 'narrow', 1179 | 'nasty', 1180 | 'nation', 1181 | 'nature', 1182 | 'near', 1183 | 'neck', 1184 | 'need', 1185 | 'negative', 1186 | 'neglect', 1187 | 'neither', 1188 | 'nephew', 1189 | 'nerve', 1190 | 'nest', 1191 | 'net', 1192 | 'network', 1193 | 'neutral', 1194 | 'never', 1195 | 'news', 1196 | 'next', 1197 | 'nice', 1198 | 'night', 1199 | 'noble', 1200 | 'noise', 1201 | 'nominee', 1202 | 'noodle', 1203 | 'normal', 1204 | 'north', 1205 | 'nose', 1206 | 'notable', 1207 | 'note', 1208 | 'nothing', 1209 | 'notice', 1210 | 'novel', 1211 | 'now', 1212 | 'nuclear', 1213 | 'number', 1214 | 'nurse', 1215 | 'nut', 1216 | 'oak', 1217 | 'obey', 1218 | 'object', 1219 | 'oblige', 1220 | 'obscure', 1221 | 'observe', 1222 | 'obtain', 1223 | 'obvious', 1224 | 'occur', 1225 | 'ocean', 1226 | 'october', 1227 | 'odor', 1228 | 'off', 1229 | 'offer', 1230 | 'office', 1231 | 'often', 1232 | 'oil', 1233 | 'okay', 1234 | 'old', 1235 | 'olive', 1236 | 'olympic', 1237 | 'omit', 1238 | 'once', 1239 | 'one', 1240 | 'onion', 1241 | 'online', 1242 | 'only', 1243 | 'open', 1244 | 'opera', 1245 | 'opinion', 1246 | 'oppose', 1247 | 'option', 1248 | 'orange', 1249 | 'orbit', 1250 | 'orchard', 1251 | 'order', 1252 | 'ordinary', 1253 | 'organ', 1254 | 'orient', 1255 | 'original', 1256 | 'orphan', 1257 | 'ostrich', 1258 | 'other', 1259 | 'outdoor', 1260 | 'outer', 1261 | 'output', 1262 | 'outside', 1263 | 'oval', 1264 | 'oven', 1265 | 'over', 1266 | 'own', 1267 | 'owner', 1268 | 'oxygen', 1269 | 'oyster', 1270 | 'ozone', 1271 | 'pact', 1272 | 'paddle', 1273 | 'page', 1274 | 'pair', 1275 | 'palace', 1276 | 'palm', 1277 | 'panda', 1278 | 'panel', 1279 | 'panic', 1280 | 'panther', 1281 | 'paper', 1282 | 'parade', 1283 | 'parent', 1284 | 'park', 1285 | 'parrot', 1286 | 'party', 1287 | 'pass', 1288 | 'patch', 1289 | 'path', 1290 | 'patient', 1291 | 'patrol', 1292 | 'pattern', 1293 | 'pause', 1294 | 'pave', 1295 | 'payment', 1296 | 'peace', 1297 | 'peanut', 1298 | 'pear', 1299 | 'peasant', 1300 | 'pelican', 1301 | 'pen', 1302 | 'penalty', 1303 | 'pencil', 1304 | 'people', 1305 | 'pepper', 1306 | 'perfect', 1307 | 'permit', 1308 | 'person', 1309 | 'pet', 1310 | 'phone', 1311 | 'photo', 1312 | 'phrase', 1313 | 'physical', 1314 | 'piano', 1315 | 'picnic', 1316 | 'picture', 1317 | 'piece', 1318 | 'pig', 1319 | 'pigeon', 1320 | 'pill', 1321 | 'pilot', 1322 | 'pink', 1323 | 'pioneer', 1324 | 'pipe', 1325 | 'pistol', 1326 | 'pitch', 1327 | 'pizza', 1328 | 'place', 1329 | 'planet', 1330 | 'plastic', 1331 | 'plate', 1332 | 'play', 1333 | 'please', 1334 | 'pledge', 1335 | 'pluck', 1336 | 'plug', 1337 | 'plunge', 1338 | 'poem', 1339 | 'poet', 1340 | 'point', 1341 | 'polar', 1342 | 'pole', 1343 | 'police', 1344 | 'pond', 1345 | 'pony', 1346 | 'pool', 1347 | 'popular', 1348 | 'portion', 1349 | 'position', 1350 | 'possible', 1351 | 'post', 1352 | 'potato', 1353 | 'pottery', 1354 | 'poverty', 1355 | 'powder', 1356 | 'power', 1357 | 'practice', 1358 | 'praise', 1359 | 'predict', 1360 | 'prefer', 1361 | 'prepare', 1362 | 'present', 1363 | 'pretty', 1364 | 'prevent', 1365 | 'price', 1366 | 'pride', 1367 | 'primary', 1368 | 'print', 1369 | 'priority', 1370 | 'prison', 1371 | 'private', 1372 | 'prize', 1373 | 'problem', 1374 | 'process', 1375 | 'produce', 1376 | 'profit', 1377 | 'program', 1378 | 'project', 1379 | 'promote', 1380 | 'proof', 1381 | 'property', 1382 | 'prosper', 1383 | 'protect', 1384 | 'proud', 1385 | 'provide', 1386 | 'public', 1387 | 'pudding', 1388 | 'pull', 1389 | 'pulp', 1390 | 'pulse', 1391 | 'pumpkin', 1392 | 'punch', 1393 | 'pupil', 1394 | 'puppy', 1395 | 'purchase', 1396 | 'purity', 1397 | 'purpose', 1398 | 'purse', 1399 | 'push', 1400 | 'put', 1401 | 'puzzle', 1402 | 'pyramid', 1403 | 'quality', 1404 | 'quantum', 1405 | 'quarter', 1406 | 'question', 1407 | 'quick', 1408 | 'quit', 1409 | 'quiz', 1410 | 'quote', 1411 | 'rabbit', 1412 | 'raccoon', 1413 | 'race', 1414 | 'rack', 1415 | 'radar', 1416 | 'radio', 1417 | 'rail', 1418 | 'rain', 1419 | 'raise', 1420 | 'rally', 1421 | 'ramp', 1422 | 'ranch', 1423 | 'random', 1424 | 'range', 1425 | 'rapid', 1426 | 'rare', 1427 | 'rate', 1428 | 'rather', 1429 | 'raven', 1430 | 'raw', 1431 | 'razor', 1432 | 'ready', 1433 | 'real', 1434 | 'reason', 1435 | 'rebel', 1436 | 'rebuild', 1437 | 'recall', 1438 | 'receive', 1439 | 'recipe', 1440 | 'record', 1441 | 'recycle', 1442 | 'reduce', 1443 | 'reflect', 1444 | 'reform', 1445 | 'refuse', 1446 | 'region', 1447 | 'regret', 1448 | 'regular', 1449 | 'reject', 1450 | 'relax', 1451 | 'release', 1452 | 'relief', 1453 | 'rely', 1454 | 'remain', 1455 | 'remember', 1456 | 'remind', 1457 | 'remove', 1458 | 'render', 1459 | 'renew', 1460 | 'rent', 1461 | 'reopen', 1462 | 'repair', 1463 | 'repeat', 1464 | 'replace', 1465 | 'report', 1466 | 'require', 1467 | 'rescue', 1468 | 'resemble', 1469 | 'resist', 1470 | 'resource', 1471 | 'response', 1472 | 'result', 1473 | 'retire', 1474 | 'retreat', 1475 | 'return', 1476 | 'reunion', 1477 | 'reveal', 1478 | 'review', 1479 | 'reward', 1480 | 'rhythm', 1481 | 'rib', 1482 | 'ribbon', 1483 | 'rice', 1484 | 'rich', 1485 | 'ride', 1486 | 'ridge', 1487 | 'rifle', 1488 | 'right', 1489 | 'rigid', 1490 | 'ring', 1491 | 'riot', 1492 | 'ripple', 1493 | 'risk', 1494 | 'ritual', 1495 | 'rival', 1496 | 'river', 1497 | 'road', 1498 | 'roast', 1499 | 'robot', 1500 | 'robust', 1501 | 'rocket', 1502 | 'romance', 1503 | 'roof', 1504 | 'rookie', 1505 | 'room', 1506 | 'rose', 1507 | 'rotate', 1508 | 'rough', 1509 | 'round', 1510 | 'route', 1511 | 'royal', 1512 | 'rubber', 1513 | 'rude', 1514 | 'rug', 1515 | 'rule', 1516 | 'run', 1517 | 'runway', 1518 | 'rural', 1519 | 'sad', 1520 | 'saddle', 1521 | 'sadness', 1522 | 'safe', 1523 | 'sail', 1524 | 'salad', 1525 | 'salmon', 1526 | 'salon', 1527 | 'salt', 1528 | 'salute', 1529 | 'same', 1530 | 'sample', 1531 | 'sand', 1532 | 'satisfy', 1533 | 'satoshi', 1534 | 'sauce', 1535 | 'sausage', 1536 | 'save', 1537 | 'say', 1538 | 'scale', 1539 | 'scan', 1540 | 'scare', 1541 | 'scatter', 1542 | 'scene', 1543 | 'scheme', 1544 | 'school', 1545 | 'science', 1546 | 'scissors', 1547 | 'scorpion', 1548 | 'scout', 1549 | 'scrap', 1550 | 'screen', 1551 | 'script', 1552 | 'scrub', 1553 | 'sea', 1554 | 'search', 1555 | 'season', 1556 | 'seat', 1557 | 'second', 1558 | 'secret', 1559 | 'section', 1560 | 'security', 1561 | 'seed', 1562 | 'seek', 1563 | 'segment', 1564 | 'select', 1565 | 'sell', 1566 | 'seminar', 1567 | 'senior', 1568 | 'sense', 1569 | 'sentence', 1570 | 'series', 1571 | 'service', 1572 | 'session', 1573 | 'settle', 1574 | 'setup', 1575 | 'seven', 1576 | 'shadow', 1577 | 'shaft', 1578 | 'shallow', 1579 | 'share', 1580 | 'shed', 1581 | 'shell', 1582 | 'sheriff', 1583 | 'shield', 1584 | 'shift', 1585 | 'shine', 1586 | 'ship', 1587 | 'shiver', 1588 | 'shock', 1589 | 'shoe', 1590 | 'shoot', 1591 | 'shop', 1592 | 'short', 1593 | 'shoulder', 1594 | 'shove', 1595 | 'shrimp', 1596 | 'shrug', 1597 | 'shuffle', 1598 | 'shy', 1599 | 'sibling', 1600 | 'sick', 1601 | 'side', 1602 | 'siege', 1603 | 'sight', 1604 | 'sign', 1605 | 'silent', 1606 | 'silk', 1607 | 'silly', 1608 | 'silver', 1609 | 'similar', 1610 | 'simple', 1611 | 'since', 1612 | 'sing', 1613 | 'siren', 1614 | 'sister', 1615 | 'situate', 1616 | 'six', 1617 | 'size', 1618 | 'skate', 1619 | 'sketch', 1620 | 'ski', 1621 | 'skill', 1622 | 'skin', 1623 | 'skirt', 1624 | 'skull', 1625 | 'slab', 1626 | 'slam', 1627 | 'sleep', 1628 | 'slender', 1629 | 'slice', 1630 | 'slide', 1631 | 'slight', 1632 | 'slim', 1633 | 'slogan', 1634 | 'slot', 1635 | 'slow', 1636 | 'slush', 1637 | 'small', 1638 | 'smart', 1639 | 'smile', 1640 | 'smoke', 1641 | 'smooth', 1642 | 'snack', 1643 | 'snake', 1644 | 'snap', 1645 | 'sniff', 1646 | 'snow', 1647 | 'soap', 1648 | 'soccer', 1649 | 'social', 1650 | 'sock', 1651 | 'soda', 1652 | 'soft', 1653 | 'solar', 1654 | 'soldier', 1655 | 'solid', 1656 | 'solution', 1657 | 'solve', 1658 | 'someone', 1659 | 'song', 1660 | 'soon', 1661 | 'sorry', 1662 | 'sort', 1663 | 'soul', 1664 | 'sound', 1665 | 'soup', 1666 | 'source', 1667 | 'south', 1668 | 'space', 1669 | 'spare', 1670 | 'spatial', 1671 | 'spawn', 1672 | 'speak', 1673 | 'special', 1674 | 'speed', 1675 | 'spell', 1676 | 'spend', 1677 | 'sphere', 1678 | 'spice', 1679 | 'spider', 1680 | 'spike', 1681 | 'spin', 1682 | 'spirit', 1683 | 'split', 1684 | 'spoil', 1685 | 'sponsor', 1686 | 'spoon', 1687 | 'sport', 1688 | 'spot', 1689 | 'spray', 1690 | 'spread', 1691 | 'spring', 1692 | 'spy', 1693 | 'square', 1694 | 'squeeze', 1695 | 'squirrel', 1696 | 'stable', 1697 | 'stadium', 1698 | 'staff', 1699 | 'stage', 1700 | 'stairs', 1701 | 'stamp', 1702 | 'stand', 1703 | 'start', 1704 | 'state', 1705 | 'stay', 1706 | 'steak', 1707 | 'steel', 1708 | 'stem', 1709 | 'step', 1710 | 'stereo', 1711 | 'stick', 1712 | 'still', 1713 | 'sting', 1714 | 'stock', 1715 | 'stomach', 1716 | 'stone', 1717 | 'stool', 1718 | 'story', 1719 | 'stove', 1720 | 'strategy', 1721 | 'street', 1722 | 'strike', 1723 | 'strong', 1724 | 'struggle', 1725 | 'student', 1726 | 'stuff', 1727 | 'stumble', 1728 | 'style', 1729 | 'subject', 1730 | 'submit', 1731 | 'subway', 1732 | 'success', 1733 | 'such', 1734 | 'sudden', 1735 | 'suffer', 1736 | 'sugar', 1737 | 'suggest', 1738 | 'suit', 1739 | 'summer', 1740 | 'sun', 1741 | 'sunny', 1742 | 'sunset', 1743 | 'super', 1744 | 'supply', 1745 | 'supreme', 1746 | 'sure', 1747 | 'surface', 1748 | 'surge', 1749 | 'surprise', 1750 | 'surround', 1751 | 'survey', 1752 | 'suspect', 1753 | 'sustain', 1754 | 'swallow', 1755 | 'swamp', 1756 | 'swap', 1757 | 'swarm', 1758 | 'swear', 1759 | 'sweet', 1760 | 'swift', 1761 | 'swim', 1762 | 'swing', 1763 | 'switch', 1764 | 'sword', 1765 | 'symbol', 1766 | 'symptom', 1767 | 'syrup', 1768 | 'system', 1769 | 'table', 1770 | 'tackle', 1771 | 'tag', 1772 | 'tail', 1773 | 'talent', 1774 | 'talk', 1775 | 'tank', 1776 | 'tape', 1777 | 'target', 1778 | 'task', 1779 | 'taste', 1780 | 'tattoo', 1781 | 'taxi', 1782 | 'teach', 1783 | 'team', 1784 | 'tell', 1785 | 'ten', 1786 | 'tenant', 1787 | 'tennis', 1788 | 'tent', 1789 | 'term', 1790 | 'test', 1791 | 'text', 1792 | 'thank', 1793 | 'that', 1794 | 'theme', 1795 | 'then', 1796 | 'theory', 1797 | 'there', 1798 | 'they', 1799 | 'thing', 1800 | 'this', 1801 | 'thought', 1802 | 'three', 1803 | 'thrive', 1804 | 'throw', 1805 | 'thumb', 1806 | 'thunder', 1807 | 'ticket', 1808 | 'tide', 1809 | 'tiger', 1810 | 'tilt', 1811 | 'timber', 1812 | 'time', 1813 | 'tiny', 1814 | 'tip', 1815 | 'tired', 1816 | 'tissue', 1817 | 'title', 1818 | 'toast', 1819 | 'tobacco', 1820 | 'today', 1821 | 'toddler', 1822 | 'toe', 1823 | 'together', 1824 | 'toilet', 1825 | 'token', 1826 | 'tomato', 1827 | 'tomorrow', 1828 | 'tone', 1829 | 'tongue', 1830 | 'tonight', 1831 | 'tool', 1832 | 'tooth', 1833 | 'top', 1834 | 'topic', 1835 | 'topple', 1836 | 'torch', 1837 | 'tornado', 1838 | 'tortoise', 1839 | 'toss', 1840 | 'total', 1841 | 'tourist', 1842 | 'toward', 1843 | 'tower', 1844 | 'town', 1845 | 'toy', 1846 | 'track', 1847 | 'trade', 1848 | 'traffic', 1849 | 'tragic', 1850 | 'train', 1851 | 'transfer', 1852 | 'trap', 1853 | 'trash', 1854 | 'travel', 1855 | 'tray', 1856 | 'treat', 1857 | 'tree', 1858 | 'trend', 1859 | 'trial', 1860 | 'tribe', 1861 | 'trick', 1862 | 'trigger', 1863 | 'trim', 1864 | 'trip', 1865 | 'trophy', 1866 | 'trouble', 1867 | 'truck', 1868 | 'true', 1869 | 'truly', 1870 | 'trumpet', 1871 | 'trust', 1872 | 'truth', 1873 | 'try', 1874 | 'tube', 1875 | 'tuition', 1876 | 'tumble', 1877 | 'tuna', 1878 | 'tunnel', 1879 | 'turkey', 1880 | 'turn', 1881 | 'turtle', 1882 | 'twelve', 1883 | 'twenty', 1884 | 'twice', 1885 | 'twin', 1886 | 'twist', 1887 | 'two', 1888 | 'type', 1889 | 'typical', 1890 | 'ugly', 1891 | 'umbrella', 1892 | 'unable', 1893 | 'unaware', 1894 | 'uncle', 1895 | 'uncover', 1896 | 'under', 1897 | 'undo', 1898 | 'unfair', 1899 | 'unfold', 1900 | 'unhappy', 1901 | 'uniform', 1902 | 'unique', 1903 | 'unit', 1904 | 'universe', 1905 | 'unknown', 1906 | 'unlock', 1907 | 'until', 1908 | 'unusual', 1909 | 'unveil', 1910 | 'update', 1911 | 'upgrade', 1912 | 'uphold', 1913 | 'upon', 1914 | 'upper', 1915 | 'upset', 1916 | 'urban', 1917 | 'urge', 1918 | 'usage', 1919 | 'use', 1920 | 'used', 1921 | 'useful', 1922 | 'useless', 1923 | 'usual', 1924 | 'utility', 1925 | 'vacant', 1926 | 'vacuum', 1927 | 'vague', 1928 | 'valid', 1929 | 'valley', 1930 | 'valve', 1931 | 'van', 1932 | 'vanish', 1933 | 'vapor', 1934 | 'various', 1935 | 'vast', 1936 | 'vault', 1937 | 'vehicle', 1938 | 'velvet', 1939 | 'vendor', 1940 | 'venture', 1941 | 'venue', 1942 | 'verb', 1943 | 'verify', 1944 | 'version', 1945 | 'very', 1946 | 'vessel', 1947 | 'veteran', 1948 | 'viable', 1949 | 'vibrant', 1950 | 'vicious', 1951 | 'victory', 1952 | 'video', 1953 | 'view', 1954 | 'village', 1955 | 'vintage', 1956 | 'violin', 1957 | 'virtual', 1958 | 'virus', 1959 | 'visa', 1960 | 'visit', 1961 | 'visual', 1962 | 'vital', 1963 | 'vivid', 1964 | 'vocal', 1965 | 'voice', 1966 | 'void', 1967 | 'volcano', 1968 | 'volume', 1969 | 'vote', 1970 | 'voyage', 1971 | 'wage', 1972 | 'wagon', 1973 | 'wait', 1974 | 'walk', 1975 | 'wall', 1976 | 'walnut', 1977 | 'want', 1978 | 'warfare', 1979 | 'warm', 1980 | 'warrior', 1981 | 'wash', 1982 | 'wasp', 1983 | 'waste', 1984 | 'water', 1985 | 'wave', 1986 | 'way', 1987 | 'wealth', 1988 | 'weapon', 1989 | 'wear', 1990 | 'weasel', 1991 | 'weather', 1992 | 'web', 1993 | 'wedding', 1994 | 'weekend', 1995 | 'weird', 1996 | 'welcome', 1997 | 'west', 1998 | 'wet', 1999 | 'whale', 2000 | 'what', 2001 | 'wheat', 2002 | 'wheel', 2003 | 'when', 2004 | 'where', 2005 | 'whip', 2006 | 'whisper', 2007 | 'wide', 2008 | 'width', 2009 | 'wife', 2010 | 'wild', 2011 | 'will', 2012 | 'win', 2013 | 'window', 2014 | 'wine', 2015 | 'wing', 2016 | 'wink', 2017 | 'winner', 2018 | 'winter', 2019 | 'wire', 2020 | 'wisdom', 2021 | 'wise', 2022 | 'wish', 2023 | 'witness', 2024 | 'wolf', 2025 | 'woman', 2026 | 'wonder', 2027 | 'wood', 2028 | 'wool', 2029 | 'word', 2030 | 'work', 2031 | 'world', 2032 | 'worry', 2033 | 'worth', 2034 | 'wrap', 2035 | 'wreck', 2036 | 'wrestle', 2037 | 'wrist', 2038 | 'write', 2039 | 'wrong', 2040 | 'yard', 2041 | 'year', 2042 | 'yellow', 2043 | 'you', 2044 | 'young', 2045 | 'youth', 2046 | 'zebra', 2047 | 'zero', 2048 | 'zone', 2049 | 'zoo' 2050 | ] 2051 | export default wordlist 2052 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "alwaysStrict": true, 5 | "baseUrl": ".", 6 | "declaration": true, 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "lib": ["es2017", "dom"], 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "noImplicitAny": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "outDir": "dist", 19 | "rootDir": "src", 20 | "strictNullChecks": true, 21 | "suppressImplicitAnyIndexErrors": true, 22 | "target": "es5" 23 | }, 24 | "include": [ 25 | "src/**/*" 26 | ], 27 | "ignore": [ 28 | "node_modules" 29 | ], 30 | "types": [ 31 | "jest" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-standard"], 3 | "linterOptions": { 4 | "exclude": ["config/**/*.js", "node_modules/**/*.ts", "src/storybook/**/*"] 5 | }, 6 | "rules": { 7 | "await-promise": false, 8 | "jsx-alignment": false, 9 | "jsx-boolean-value": false, 10 | "jsx-curly-spacing": false, 11 | "jsx-no-multiline-js": false, 12 | "jsx-wrap-multiline": false, 13 | "member-ordering": false, 14 | "no-empty": false, 15 | "no-unused-variable": false, 16 | "no-use-before-declare": false, 17 | "semicolon": false, 18 | "space-before-function-paren": false, 19 | "ter-func-call-spacing": false, 20 | "ter-indent": false 21 | } 22 | } 23 | --------------------------------------------------------------------------------