├── fastxpub ├── .gitignore ├── build │ └── fastxpub.wasm ├── additional_sources │ ├── rand-mock.c │ ├── pre.js │ └── post.js ├── package.json ├── README.md ├── tests │ ├── benchmark.html │ ├── test.js │ └── benchmark.js └── Makefile ├── gh-pages ├── fastxpub.wasm ├── example.html └── ui.js ├── .eslintignore ├── .gitmodules ├── src ├── .flowconfig ├── build-tx │ ├── coinselect-lib │ │ ├── sorts.js │ │ ├── index.js │ │ ├── LICENSE │ │ ├── outputs │ │ │ └── split.js │ │ ├── inputs │ │ │ ├── accumulative.js │ │ │ └── bnb.js │ │ ├── tryconfirmed.js │ │ └── utils.js │ ├── permutation.js │ ├── result.js │ ├── index.js │ ├── request.js │ ├── transaction.js │ └── coinselect.js ├── index.js ├── utils │ ├── bchaddr.js │ ├── deferred.js │ ├── unique-random.js │ └── simple-worker-channel.js ├── discovery │ ├── worker │ │ ├── utils.js │ │ ├── inside │ │ │ ├── blocks.js │ │ │ ├── dates.js │ │ │ ├── channel.js │ │ │ ├── derive-utxos.js │ │ │ └── index.js │ │ ├── outside │ │ │ ├── channel.js │ │ │ └── index.js │ │ └── types.js │ └── index.js ├── address-source.js └── socketio-worker │ ├── inside.js │ └── outside.js ├── .prettierrc ├── jest.config.js ├── .babelrc ├── test ├── utils │ └── bchaddrjs.test.js ├── coinselect-lib │ ├── index-errors.js │ ├── fixtures │ │ ├── index-errors.json │ │ └── break.json │ ├── index.js │ ├── split.js │ ├── accumulative.js │ ├── bnb.js │ ├── _utils.js │ └── utils.js ├── _worker-helper.js ├── build-tx.js ├── _mock-worker.js ├── monitor-account.js └── discover-account.js ├── .gitignore ├── type ├── shims.js └── socket.io-client.js ├── shell.nix ├── .npmignore ├── .eslintrc ├── README.md ├── Makefile ├── karma.conf.js ├── package.json ├── example └── index.js ├── .github └── workflows │ └── tests.yml └── COPYING /fastxpub/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | benchmark-browserify.js 3 | -------------------------------------------------------------------------------- /gh-pages/fastxpub.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trezor/hd-wallet/HEAD/gh-pages/fastxpub.wasm -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | fastxpub/build/fastxpub.js 2 | trezor-crypto/* 3 | fastxpub/** 4 | type/** 5 | coverage/** 6 | -------------------------------------------------------------------------------- /fastxpub/build/fastxpub.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trezor/hd-wallet/HEAD/fastxpub/build/fastxpub.wasm -------------------------------------------------------------------------------- /fastxpub/additional_sources/rand-mock.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | uint32_t random32(void) 4 | { 5 | return 1; 6 | } 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "fastxpub/trezor-crypto"] 2 | path = fastxpub/trezor-crypto 3 | url = https://github.com/trezor/trezor-crypto.git 4 | branch = master 5 | -------------------------------------------------------------------------------- /src/.flowconfig: -------------------------------------------------------------------------------- 1 | [libs] 2 | ../type 3 | 4 | [options] 5 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue 6 | include_warnings=true 7 | 8 | [lints] 9 | all=warn 10 | -------------------------------------------------------------------------------- /fastxpub/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastxpub", 3 | "version": "1.0.0", 4 | "description": "", 5 | "dependencies": {}, 6 | "devDependencies": { 7 | "bitcoinjs-lib": "^5.2.0", 8 | "browserify": "^17.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /fastxpub/README.md: -------------------------------------------------------------------------------- 1 | fastxpub is a "little" helper for making addresses out of xpubs very fast. 2 | 3 | It is generated from C code in trezor-crypto, and uses emscripten + webassembly + webworkers. 4 | 5 | Note: for all the node tests, you need to use node >=8 6 | -------------------------------------------------------------------------------- /fastxpub/tests/benchmark.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Open Console in Developer Tools (Ctrl+Shift+I) to see the results ... 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "flow", 3 | "printWidth": 100, 4 | "arrowParens": "avoid", 5 | "bracketSpacing": true, 6 | "singleQuote": true, 7 | "semi": true, 8 | "trailingComma": "all", 9 | "tabWidth": 4, 10 | "useTabs": false, 11 | "jsxBracketSameLine": false 12 | } 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: '.', 3 | testRegex: './test/.*.js$', 4 | collectCoverage: true, 5 | testPathIgnorePatterns: [ 6 | '/node_modules/', 7 | '/fastxpub/', 8 | '/test_bitcore/', 9 | '/build/', 10 | 'helper', 11 | 'test/coinselect-lib/_utils.js', 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | "@babel/plugin-transform-flow-strip-types", 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-proposal-object-rest-spread", 9 | ], 10 | "env": { 11 | "test": { 12 | "plugins": [ 13 | "istanbul" 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/build-tx/coinselect-lib/sorts.js: -------------------------------------------------------------------------------- 1 | import * as utils from './utils'; 2 | 3 | export function score(feeRate) { 4 | return (a, b) => { 5 | const difference = utils.utxoScore(a, feeRate).comparedTo(utils.utxoScore(b, feeRate)); 6 | if (difference === 0) { 7 | return a.i - b.i; 8 | } 9 | return -difference; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export * from './address-source'; 4 | export * from './bitcore'; 5 | export * from './discovery'; 6 | export * from './discovery/worker-discovery'; 7 | export * from './utils/stream'; 8 | export * from './utils/simple-worker-channel'; 9 | export * from './build-tx'; 10 | export { setLogCommunication } from './socketio-worker/outside'; 11 | -------------------------------------------------------------------------------- /test/utils/bchaddrjs.test.js: -------------------------------------------------------------------------------- 1 | import { convertCashAddress } from '../../src/utils/bchaddr'; 2 | 3 | describe('bchaddrjs address', () => { 4 | it('convert cash address', () => { 5 | expect(convertCashAddress('1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX')).toBe('1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX'); 6 | expect(convertCashAddress('bitcoincash:pqkh9ahfj069qv8l6eysyufazpe4fdjq3u4hna323j')).toBe('35qL43qYwLdKtnR7yMfGNDvzv6WyZ8yT2n'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/utils/bchaddr.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import bchaddrjs from 'bchaddrjs'; 3 | 4 | // Cashaddr format is neither base58 nor bech32, so it would fail 5 | // in bitcoinjs-lib-zchash. For this reason use legacy format 6 | export const convertCashAddress = (address: string): string => { 7 | try { 8 | if (bchaddrjs.isCashAddress(address)) { 9 | return bchaddrjs.toLegacyAddress(address); 10 | } 11 | } catch (e) { 12 | // noting 13 | } 14 | return address; 15 | }; 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | gh-pages/index.js 5 | gh-pages/index.js.map 6 | gh-pages/example.js 7 | gh-pages/discovery-worker.js 8 | gh-pages/socket-worker.js 9 | gh-pages/trezor-crypto.js 10 | gh-pages/example.js.map 11 | gh-pages/fastxpub.js 12 | gh-pages/fastxpub.wasm 13 | .nyc_output 14 | 15 | # Npm library 16 | lib 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | # Editors 22 | .idea 23 | .vscode 24 | *.sublime-project 25 | *.sublime-workspace 26 | *.swp 27 | 28 | coverage 29 | 30 | # NixOS project history 31 | .bash_history_nix -------------------------------------------------------------------------------- /type/shims.js: -------------------------------------------------------------------------------- 1 | // this is idiotic, but flow doesn't work otherwise 2 | // (I can also add whole node_modules, but that takes FOREVER) 3 | 4 | 5 | declare module 'whatwg-fetch' { 6 | } 7 | 8 | declare module 'queue' { 9 | declare type Queue = (options: any) => any; 10 | declare export default Queue 11 | } 12 | 13 | declare module 'bchaddrjs' { 14 | declare module.exports: { 15 | isCashAddress(address: string): boolean; 16 | toCashAddress(address: string): string; 17 | isLegacyAddress(address: string): boolean; 18 | toLegacyAddress(address: string): string; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | # the last successful build of nixos-20.09 (stable) as of 2020-10-11 2 | with import 3 | (builtins.fetchTarball { 4 | url = "https://github.com/NixOS/nixpkgs/archive/0b8799ecaaf0dc6b4c11583a3c96ca5b40fcfdfb.tar.gz"; 5 | sha256 = "11m4aig6cv0zi3gbq2xn9by29cfvnsxgzf9qsvz67qr0yq29ryyz"; 6 | }) 7 | { }; 8 | 9 | stdenv.mkDerivation { 10 | name = "hd-wallet-dev"; 11 | buildInputs = [ 12 | autoPatchelfHook 13 | git 14 | nodejs 15 | yarn 16 | ]; 17 | shellHook = '' 18 | export HISTFILE=".bash_history_nix" 19 | export PATH="$PATH:$(pwd)/node_modules/.bin" 20 | autoPatchelf $(pwd)/node_modules/flow-bin/ 21 | ''; 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/deferred.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export type Deferred = { 4 | promise: Promise, 5 | resolve: (t: T) => void, 6 | reject: (e: Error) => void, 7 | }; 8 | 9 | export function deferred(): Deferred { 10 | // ignoring coverage on functions that are just for 11 | // type correctness 12 | /* istanbul ignore next */ 13 | let outResolve = () => {}; 14 | /* istanbul ignore next */ 15 | let outReject = () => {}; 16 | const promise = new Promise((resolve, reject) => { 17 | outResolve = resolve; 18 | outReject = reject; 19 | }); 20 | return { 21 | promise, 22 | resolve: outResolve, 23 | reject: outReject, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/build-tx/coinselect-lib/index.js: -------------------------------------------------------------------------------- 1 | // I am using coinselect like this; the end-goal is, however, to merge all the changes 2 | // back into the upstream and use coinselect from npm 3 | 4 | import accumulative from './inputs/accumulative'; 5 | import bnb from './inputs/bnb'; 6 | import * as sorts from './sorts'; 7 | import * as utils from './utils'; 8 | import tryConfirmed from './tryconfirmed'; 9 | 10 | export default function coinSelect(inputs, outputs, feeRate, options) { 11 | const sortedInputs = options.skipPermutation ? inputs : inputs.sort(sorts.score(feeRate)); 12 | 13 | const algorithm = tryConfirmed( 14 | utils.anyOf([bnb(0.5), accumulative]), 15 | options, 16 | ); 17 | 18 | return algorithm(sortedInputs, outputs, feeRate, options); 19 | } 20 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Dev directories 2 | src 3 | .babelrc 4 | .eslintrc 5 | .eslintignore 6 | .gitignore 7 | .gitmodules 8 | jest.config.js 9 | karma.conf.js 10 | karma.conf.js-bitcore 11 | karma.conf.js-buildtx 12 | karma.conf.js-discovery 13 | Makefile 14 | docker_test 15 | example 16 | test 17 | test_helpers 18 | type 19 | .nyc_output 20 | coverage 21 | 22 | # CI 23 | .github 24 | 25 | # Example directories 26 | examples 27 | gh-pages 28 | 29 | # Fastxpub source 30 | fastxpub 31 | !lib/fastxpub/ 32 | 33 | # Dependency directory 34 | node_modules 35 | yarn.lock 36 | 37 | # OSX 38 | .DS_Store 39 | 40 | # Editors 41 | .editorconfig 42 | .idea 43 | .vscode 44 | *.sublime-project 45 | *.sublime-workspace 46 | 47 | # Logs 48 | logs 49 | *.log 50 | .yarnclean 51 | 52 | # NixOS 53 | shell.nix 54 | .bash_history_nix 55 | -------------------------------------------------------------------------------- /src/discovery/worker/utils.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { Input as BitcoinJsInput } from '@trezor/utxo-lib'; 3 | 4 | export function getInputId( 5 | i: BitcoinJsInput, 6 | ): string { 7 | const { hash } = i; 8 | Array.prototype.reverse.call(hash); 9 | const res = (hash.toString('hex')); 10 | Array.prototype.reverse.call(hash); 11 | return res; 12 | } 13 | 14 | export function objectValues(k: {[k: any]: T}): Array { 15 | return Object.keys(k).map(key => k[key]); 16 | } 17 | 18 | export function filterNull(k: Array, throwErrorOnNull: boolean): Array { 19 | const res: Array = []; 20 | k.forEach((t) => { 21 | if (t != null) { 22 | res.push(t); 23 | } else if (throwErrorOnNull) { 24 | throw new Error('Unexpected null'); 25 | } 26 | }); 27 | return res; 28 | } 29 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2018, 4 | "ecmaFeatures": { 5 | "modules": true 6 | }, 7 | "sourceType": "module" 8 | }, 9 | "parser": "babel-eslint", 10 | "plugins": ["node", "prettier", "import", "jest"], 11 | "extends": [ 12 | "airbnb-base", 13 | "eslint:recommended", 14 | "prettier" 15 | ], 16 | "env": { 17 | "es6": true, 18 | "node": true, 19 | "browser": true, 20 | "webextensions": true, 21 | "jest": true 22 | }, 23 | "rules": { 24 | "no-underscore-dangle": "off", 25 | "no-use-before-define": "off", 26 | "no-console": "off", 27 | "max-classes-per-file": "off", 28 | "no-plusplus": "off", // airbnb-base: irrelevant 29 | "import/prefer-default-export": "off" // irrelevant 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/coinselect-lib/index-errors.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import coinAccum from '../../src/build-tx/coinselect-lib/index'; 4 | import fixtures from './fixtures/index-errors.json'; 5 | import * as utils from './_utils'; 6 | 7 | describe('coinselect errors', () => { 8 | fixtures.forEach((f) => { 9 | it(f.description, () => { 10 | const { inputLength, outputLength, dustThreshold } = f; 11 | 12 | const inputs = utils.expand(f.inputs, true, inputLength); 13 | const outputs = utils.expand(f.outputs, false, outputLength); 14 | 15 | assert.throws(() => { 16 | coinAccum( 17 | inputs, 18 | outputs, 19 | f.feeRate, 20 | { inputLength, changeOutputLength: outputLength, dustThreshold }, 21 | ); 22 | }, new RegExp(f.expected)); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /fastxpub/additional_sources/pre.js: -------------------------------------------------------------------------------- 1 | // stub importScripts for the faulty detection of web worker env 2 | if (typeof importScripts === 'undefined' 3 | && typeof WorkerGlobalScope !== 'undefined' 4 | && this instanceof WorkerGlobalScope 5 | ) { 6 | this.importScripts = function () { 7 | throw new Error('importScripts is a stub'); 8 | }; 9 | } 10 | 11 | // helpful script for deferred promise 12 | function deferred() { 13 | var outResolve = function() {}; // will be overwritten 14 | var outReject = function() {}; // will be overwritten 15 | var promise = new Promise(function (resolve, reject) { 16 | outResolve = resolve; 17 | outReject = reject; 18 | }); 19 | return { 20 | promise: promise, 21 | resolve: outResolve, 22 | reject: outReject, 23 | }; 24 | } 25 | 26 | // prepareModule loads the wasm binary 27 | // returns objects with the functions that you call directly and return results synchronously 28 | function prepareModule(binary) { 29 | var Module = {}; 30 | Module['wasmBinary'] = new Uint8Array(binary); 31 | -------------------------------------------------------------------------------- /src/utils/unique-random.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint-disable */ 3 | 4 | 5 | // Simple pseudo-randomness that's based on a simple fingerprinting 6 | // Used so the order of backends is always the same on a computer 7 | 8 | import crypto from 'crypto'; 9 | 10 | function iisNode(): boolean { 11 | return typeof process !== 'undefined' && !!process.version; 12 | } 13 | 14 | export function uniqueRandom(maxNonInclusive: number) { 15 | const isNode = iisNode(); 16 | const version = isNode 17 | ? process.version 18 | : navigator.userAgent; 19 | const offset = new Date().getTimezoneOffset(); 20 | const languages = isNode 21 | ? 'node' 22 | : ( 23 | navigator.languages == null 24 | ? navigator.language 25 | : navigator.languages.toString() 26 | ); 27 | const allString = version + offset + languages; 28 | 29 | const hash = crypto.createHash('md5').update(allString).digest(); 30 | let r = 0; 31 | for (let i = 0; i < hash.length; i++) { 32 | r += hash[i]; 33 | r %= maxNonInclusive; 34 | } 35 | 36 | return r; 37 | } 38 | -------------------------------------------------------------------------------- /src/build-tx/coinselect-lib/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Daniel Cousens 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /test/coinselect-lib/fixtures/index-errors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "forgotten inputLength", 4 | "feeRate": "10", 5 | "inputs": [ 6 | { 7 | "value": "102001", 8 | "coinbase": false, 9 | "own": true, 10 | "confirmations": 100 11 | } 12 | ], 13 | "outputs": [ 14 | "100000" 15 | ], 16 | "expected": "Null script length", 17 | "inputLength": null, 18 | "outputLength": 25, 19 | "dustThreshold": 546 20 | }, 21 | { 22 | "description": "forgotten outputLength", 23 | "feeRate": "10", 24 | "inputs": [ 25 | { 26 | "value": "102001", 27 | "coinbase": false, 28 | "own": true, 29 | "confirmations": 100 30 | } 31 | ], 32 | "outputs": [ 33 | "100000" 34 | ], 35 | "expected": "Null script length", 36 | "inputLength": 25, 37 | "outputLength": null 38 | }, 39 | { 40 | "description": "missing input info", 41 | "feeRate": "10", 42 | "inputs": [ 43 | { 44 | "value": "102001" 45 | } 46 | ], 47 | "outputs": [ 48 | "100000" 49 | ], 50 | "expected": "Missing information", 51 | "inputLength": 25, 52 | "outputLength": null 53 | } 54 | ] 55 | -------------------------------------------------------------------------------- /gh-pages/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | HD Web Wallet 4 | 5 | 6 | 28 | hd-wallet Library version 9.0.0 29 |
30 |
31 | Xpubs, separated by ";" 32 | 33 | Backend urls, separated by ";" 34 | 35 | 36 | 37 | 38 | 39 |
40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /test/coinselect-lib/index.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import coinAccum from '../../src/build-tx/coinselect-lib/index'; 4 | import fixtures from './fixtures/index.json'; 5 | import * as utils from './_utils'; 6 | 7 | describe('coinselect index', () => { 8 | fixtures.forEach((f) => { 9 | it(f.description, () => { 10 | const { inputLength, outputLength, dustThreshold } = f; 11 | 12 | const inputs = utils.expand(f.inputs, true, inputLength); 13 | const outputs = utils.expand(f.outputs, false, outputLength); 14 | const expected = utils.addScriptLengthToExpected(f.expected, inputLength, outputLength); 15 | 16 | const actual = coinAccum( 17 | inputs, 18 | outputs, 19 | f.feeRate, 20 | { inputLength, changeOutputLength: outputLength, dustThreshold }, 21 | ); 22 | 23 | assert.deepStrictEqual(actual, expected); 24 | if (actual.inputs) { 25 | const feedback = coinAccum( 26 | actual.inputs, 27 | actual.outputs, 28 | f.feeRate, 29 | { inputLength, changeOutputLength: outputLength, dustThreshold }, 30 | ); 31 | assert.deepStrictEqual(feedback, expected); 32 | } 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/build-tx/permutation.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // Helper class for permutation 4 | export class Permutation { 5 | sorted: Array = []; 6 | 7 | // Permutation is an array, 8 | // where on Ith position is J, which means that Jth element in the original, unsorted 9 | // output array 10 | // is Ith in the new array. 11 | _permutation: Array; 12 | 13 | constructor(sorted: Array, permutation: Array) { 14 | this.sorted = sorted; 15 | this._permutation = permutation; 16 | } 17 | 18 | static fromFunction(original: Array, sort: ((a: Y, b: Y) => number)): Permutation { 19 | const range = [...original.keys()]; 20 | 21 | // I am "sorting range" - (0,1,2,3,...) 22 | // so I got the indexes and not the actual values inside 23 | const permutation = range.sort((a, b) => sort(original[a], original[b])); 24 | const res = new Permutation([], permutation); 25 | 26 | res.forEach((originalIx, newIx) => { 27 | res.sorted[newIx] = original[originalIx]; 28 | }); 29 | return res; 30 | } 31 | 32 | forEach(f: (originalIx: number, sortedIx: number) => void) { 33 | this._permutation.forEach(f); 34 | } 35 | 36 | map(fun: (p: X) => Y): Permutation { 37 | const original: Array = this.sorted.map(fun); 38 | const perm: Array = this._permutation; 39 | const res: Permutation = new Permutation(original, perm); 40 | return res; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # High-performance Bitcoin HD Wallet 2 | 3 | [![Build Status](https://github.com/trezor/hd-wallet/actions/workflows/tests.yml/badge.svg)](https://github.com/trezor/hd-wallet/actions/workflows/tests.yml) 4 | [![NPM](https://img.shields.io/npm/v/hd-wallet.svg)](https://www.npmjs.org/package/hd-wallet) 5 | 6 | For now, mostly a PoC. Uses 7 | [bitcore-node](https://github.com/bitpay/bitcore-node) 8 | for transaction lookup and 9 | [trezor-crypto](https://github.com/trezor/trezor-crypto) 10 | for address derivation, compiled through emscripten and run in a web worker. 11 | Supports persisting discovered state and doing partial update later on. 12 | Should out-perform all wallets available today that do client-side chain 13 | discovery. 14 | 15 | ## Example usage 16 | 17 | Example is in `example/index.js`; it is compiled in makefile to `gh-pages` directory by `make example`. 18 | 19 | Built version is in `gh-pages` branch. 20 | 21 | You can also try it yourself here - http://trezor.github.io/hd-wallet/example.html (note that xpubs are preloaded there, but some simple GUI for inputing the XPUBs could be probably done). 22 | 23 | ## Running regtest tests 24 | 25 | Running the tests require an installed regtest-bitcore *and* an empty regtest blockchain, but there is a docker that runs the bitcore in background. 26 | 27 | Before running coverage, do 28 | 29 | * `make bitcore-test-docker` 30 | 31 | And you can normally run coverage tests. 32 | 33 | ## License 34 | 35 | LGPLv3, (C) 2016 Karel Bilek, Jan Pochyla 36 | 37 | Coinselect MIT, (C) 2015 Daniel Cousens 38 | -------------------------------------------------------------------------------- /test/coinselect-lib/split.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import coinAccum from '../../src/build-tx/coinselect-lib/outputs/split'; 4 | import fixtures from './fixtures/split.json'; 5 | import * as utils from './_utils'; 6 | 7 | describe('coinselect split', () => { 8 | fixtures.forEach((f) => { 9 | it(f.description, () => { 10 | const inputs = utils.expand(f.inputs, true, f.inputLength); 11 | const outputs = utils.expand(f.outputs, false, f.outputLength); 12 | const expected = utils.addScriptLengthToExpected( 13 | f.expected, f.inputLength, f.outputLength, 14 | ); 15 | const options = { 16 | inputLength: f.inputLength, 17 | changeOutputLength: f.outputLength, 18 | dustThreshold: f.dustThreshold, 19 | baseFee: f.baseFee, 20 | floorBaseFee: f.floorBaseFee, 21 | dustOutputFee: f.dustOutputFee, 22 | }; 23 | const actual = coinAccum( 24 | inputs, 25 | outputs, 26 | f.feeRate, 27 | options, 28 | ); 29 | 30 | assert.deepStrictEqual(actual, expected); 31 | if (actual.inputs) { 32 | const feedback = coinAccum( 33 | actual.inputs, 34 | actual.outputs, 35 | f.feeRate, 36 | options, 37 | ); 38 | assert.deepStrictEqual(feedback, expected); 39 | } 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/coinselect-lib/accumulative.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import coinAccum from '../../src/build-tx/coinselect-lib/inputs/accumulative'; 4 | import fixtures from './fixtures/accumulative.json'; 5 | import * as utils from './_utils'; 6 | 7 | describe('coinselect accumulative', () => { 8 | fixtures.forEach((f) => { 9 | it(f.description, () => { 10 | const inputs = utils.expand(f.inputs, true, f.inputLength); 11 | const outputs = utils.expand(f.outputs, false, f.outputLength); 12 | const expected = utils.addScriptLengthToExpected( 13 | f.expected, f.inputLength, f.outputLength, f.dustThreshold, 14 | ); 15 | const options = { 16 | inputLength: f.inputLength, 17 | changeOutputLength: f.outputLength, 18 | dustThreshold: f.dustThreshold, 19 | baseFee: f.baseFee, 20 | floorBaseFee: f.floorBaseFee, 21 | dustOutputFee: f.dustOutputFee, 22 | }; 23 | 24 | const actual = coinAccum( 25 | inputs, 26 | outputs, 27 | f.feeRate, 28 | options, 29 | ); 30 | 31 | assert.deepStrictEqual(actual, expected); 32 | if (actual.inputs) { 33 | const feedback = coinAccum( 34 | actual.inputs, 35 | actual.outputs, 36 | f.feeRate, 37 | options, 38 | ); 39 | assert.deepStrictEqual(feedback, expected); 40 | } 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/utils/simple-worker-channel.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // Super simple webworker interface. 4 | 5 | // Used ONLY for the address generation; 6 | // socket worker + discovery workers have more complicated protocols 7 | 8 | // requires an exclusive access to worker. 9 | // does NOT require worker to reply in a linear manner 10 | export class WorkerChannel { 11 | lastI: number = 0; 12 | 13 | worker: Worker; 14 | 15 | pending: {[i: number]: ((f: any) => any)}; 16 | 17 | onMessage: (event: Event) => void; 18 | 19 | constructor(worker: Worker) { 20 | this.worker = worker; 21 | this.pending = {}; 22 | this.onMessage = this.receiveMessage.bind(this); 23 | // this.onError = this.receiveError.bind(this); 24 | this.open(); 25 | } 26 | 27 | open() { 28 | this.worker.onmessage = this.onMessage; 29 | } 30 | 31 | // this is used only for testing 32 | destroy() { 33 | this.worker.onmessage = () => {}; 34 | } 35 | 36 | postMessage(msg: Object): Promise { 37 | return new Promise((resolve) => { 38 | this.pending[this.lastI] = resolve; 39 | this.worker.postMessage({ ...msg, i: this.lastI }); 40 | this.lastI++; 41 | }); 42 | } 43 | 44 | receiveMessage(oevent: any) { 45 | const event = oevent; 46 | const { i } = event.data; 47 | const dfd = this.pending[i]; 48 | if (dfd) { 49 | delete event.data.i; 50 | dfd(event.data); 51 | delete this.pending[i]; 52 | } else { 53 | console.warn(new Error('Strange incoming message')); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/coinselect-lib/bnb.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import coinAccum from '../../src/build-tx/coinselect-lib/inputs/bnb'; 4 | import fixtures from './fixtures/bnb.json'; 5 | import * as utils from './_utils'; 6 | 7 | describe('coinselect bnb', () => { 8 | fixtures.forEach((f) => { 9 | it(f.description, () => { 10 | const inputs = utils.expand(f.inputs, true, f.inputLength); 11 | const outputs = utils.expand(f.outputs, false, f.outputLength); 12 | const expected = utils.addScriptLengthToExpected( 13 | f.expected, f.inputLength, f.outputLength, f.dustThreshold, 14 | ); 15 | const options = { 16 | inputLength: f.inputLength, 17 | changeOutputLength: f.outputLength, 18 | dustThreshold: f.dustThreshold, 19 | baseFee: f.baseFee, 20 | floorBaseFee: f.floorBaseFee, 21 | dustOutputFee: f.dustOutputFee, 22 | }; 23 | 24 | const actual = coinAccum( 25 | f.factor, 26 | )( 27 | inputs, 28 | outputs, 29 | f.feeRate, 30 | options, 31 | ); 32 | 33 | assert.deepStrictEqual(actual, expected); 34 | if (actual.inputs) { 35 | const feedback = coinAccum( 36 | f.factor, 37 | )( 38 | actual.inputs, 39 | actual.outputs, 40 | f.feeRate, 41 | options, 42 | ); 43 | assert.deepStrictEqual(feedback, expected); 44 | } 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/build-tx/result.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as mtransaction from './transaction'; 4 | import * as coinselect from './coinselect'; 5 | 6 | // ---------- Output from algorigthm 7 | // 'nonfinal' - contains info about the outputs, but not Trezor tx 8 | // 'final' - contains info about outputs + Trezor tx 9 | // 'error' - some error, so far only NOT-ENOUGH-FUNDS and EMPTY strings 10 | export type Result = { 11 | type: 'error', 12 | error: string, 13 | } | { 14 | type: 'nonfinal', 15 | max: string, 16 | totalSpent: string, // all the outputs, no fee, no change 17 | fee: string, 18 | feePerByte: string, 19 | bytes: number, 20 | } | { 21 | type: 'final', 22 | max: string, 23 | totalSpent: string, // all the outputs, no fee, no change 24 | fee: string, 25 | feePerByte: string, 26 | bytes: number, 27 | transaction: mtransaction.Transaction, 28 | }; 29 | 30 | export const empty: Result = { 31 | type: 'error', 32 | error: 'EMPTY', 33 | }; 34 | 35 | export function getNonfinalResult(result: coinselect.CompleteResult): Result { 36 | const { 37 | max, fee, feePerByte, bytes, totalSpent, 38 | } = result.result; 39 | 40 | return { 41 | type: 'nonfinal', 42 | fee, 43 | feePerByte, 44 | bytes, 45 | max, 46 | totalSpent, 47 | }; 48 | } 49 | 50 | export function getFinalResult( 51 | result: coinselect.CompleteResult, 52 | transaction: mtransaction.Transaction, 53 | ): Result { 54 | const { 55 | max, fee, feePerByte, bytes, totalSpent, 56 | } = result.result; 57 | 58 | return { 59 | type: 'final', 60 | fee, 61 | feePerByte, 62 | bytes, 63 | transaction, 64 | max, 65 | totalSpent, 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /fastxpub/tests/test.js: -------------------------------------------------------------------------------- 1 | var fastxpub = require('../build/fastxpub'); 2 | var fs = require('fs'); 3 | var file = fs.readFileSync('../build/fastxpub.wasm'); 4 | var bitcoin = require('bitcoinjs-lib'); 5 | 6 | function test_derivation(xpub, version, addressFormat, filename) { 7 | var node = bitcoin.HDNode.fromBase58(xpub).derive(0); 8 | 9 | var nodeStruct = { 10 | depth: node.depth, 11 | child_num: node.index, 12 | fingerprint: node.parentFingerprint, 13 | chain_code: node.chainCode, 14 | public_key: node.keyPair.getPublicKeyBuffer() 15 | }; 16 | 17 | var addresses = fastxpub.deriveAddressRange(nodeStruct, 0, 999, version, addressFormat); 18 | 19 | var correct = fs.readFileSync(filename).toString().split("\n"); 20 | 21 | for (var i = 0; i <= 999; i++) { 22 | if (correct[i] !== addresses[i]) { 23 | console.log("FAILED", i, correct[i], addresses[i]); 24 | return false; 25 | } 26 | } 27 | return true; 28 | } 29 | 30 | function sanity_check_xpub() { 31 | var master = "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw"; 32 | var child = bitcoin.HDNode.fromBase58(master).derive(1).toBase58(); 33 | return fastxpub.deriveNode(master, 1, bitcoin.networks.bitcoin.bip32.public) === child; 34 | } 35 | 36 | fastxpub.init(file).then(() => { 37 | 38 | var success; 39 | 40 | success = test_derivation('xpub6BiVtCpG9fQPxnPmHXG8PhtzQdWC2Su4qWu6XW9tpWFYhxydCLJGrWBJZ5H6qTAHdPQ7pQhtpjiYZVZARo14qHiay2fvrX996oEP42u8wZy', 0, 0, 'test-addresses.txt'); 41 | if (!success) process.exit(1); 42 | 43 | success = test_derivation('xpub6CVKsQYXc9awxgV1tWbG4foDvdcnieK2JkbpPEBKB5WwAPKBZ1mstLbKVB4ov7QzxzjaxNK6EfmNY5Jsk2cG26EVcEkycGW4tchT2dyUhrx', 5, 1, 'test-addresses-segwit-p2sh.txt'); 44 | if (!success) process.exit(1); 45 | 46 | if (!(sanity_check_xpub())) process.exit(1); 47 | 48 | console.log('PASSED'); 49 | process.exit(0); 50 | }, () => { 51 | process.exit(1); 52 | }); 53 | -------------------------------------------------------------------------------- /src/discovery/worker/inside/blocks.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { 3 | AccountInfo, 4 | } from '../../index'; 5 | 6 | import type { 7 | Block, 8 | BlockRange, 9 | } from '../types'; 10 | 11 | import { 12 | lookupSyncStatus, 13 | lookupBlockHash, 14 | } from './channel'; 15 | 16 | // Some helper functions for loading block status 17 | // from blockchain 18 | 19 | // from which to which block do I need to do discovery 20 | // based on whether there was a reorg, detected by last height/hash 21 | export function loadBlockRange(initialState: AccountInfo): Promise { 22 | const pBlock: Block = initialState.lastBlock; 23 | 24 | // first, I ask for last block I will do 25 | return getCurrentBlock().then((last) => { 26 | // then I detect first block I will do 27 | // detect based on whether reorg is needed 28 | // I do not do reorgs inteligently, I always discard all 29 | const firstHeight: Promise = pBlock.height !== 0 30 | 31 | ? getBlock(pBlock.height).then((block) => { 32 | if (block.hash === pBlock.hash) { 33 | return pBlock.height; 34 | } 35 | console.warn('Blockhash mismatch', pBlock, block); 36 | return 0; 37 | }, (err) => { 38 | if (err.message === 'RPCError: Block height out of range') { 39 | console.warn('Block height out of range', pBlock.height); 40 | return 0; 41 | } 42 | throw err; 43 | }) 44 | 45 | : Promise.resolve(0); 46 | return firstHeight.then(h => ({ firstHeight: h, last })); 47 | }); 48 | } 49 | 50 | function getBlock(height: number): Promise { 51 | return lookupBlockHash(height) 52 | .then(hash => ({ hash, height })); 53 | } 54 | 55 | function getCurrentBlock(): Promise { 56 | return lookupSyncStatus() 57 | .then(height => getBlock(height)); 58 | } 59 | -------------------------------------------------------------------------------- /src/discovery/worker/inside/dates.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { TransactionInfo } from '../../index'; 3 | 4 | // Functions for date formatting 5 | export function deriveDateFormats( 6 | t: ?number, 7 | wantedOffset: number, // what (new Date().getTimezoneOffset()) returns 8 | ): { 9 | timestamp: ?number, 10 | dateInfoDayFormat: ?string, 11 | dateInfoTimeFormat: ?string, 12 | } { 13 | if (t == null) { 14 | return { 15 | timestamp: null, 16 | dateInfoDayFormat: null, 17 | dateInfoTimeFormat: null, 18 | }; 19 | } 20 | const t_: number = t; 21 | const date = new Date((t_ - wantedOffset * 60) * 1000); 22 | return { 23 | timestamp: t_, 24 | dateInfoDayFormat: dateToDayFormat(date), 25 | dateInfoTimeFormat: dateToTimeFormat(date), 26 | }; 27 | } 28 | 29 | function dateToTimeFormat(date: Date): string { 30 | const hh = addZero(date.getUTCHours().toString()); 31 | const mm = addZero(date.getUTCMinutes().toString()); 32 | const ss = addZero(date.getUTCSeconds().toString()); 33 | return `${hh}:${mm}:${ss}`; 34 | } 35 | 36 | function dateToDayFormat(date: Date): string { 37 | const yyyy = date.getUTCFullYear().toString(); 38 | const mm = addZero((date.getUTCMonth() + 1).toString()); // getMonth() is zero-based 39 | const dd = addZero(date.getUTCDate().toString()); 40 | return `${yyyy}-${mm}-${dd}`; 41 | } 42 | 43 | function addZero(s: string): string { 44 | if (s.length === 1) { 45 | return `0${s}`; 46 | } 47 | return s; 48 | } 49 | 50 | export function recomputeDateFormats( 51 | ts: Array, 52 | wantedOffset: number, 53 | ) { 54 | ts.forEach((t) => { 55 | const r = deriveDateFormats(t.timestamp, wantedOffset); 56 | // eslint-disable-next-line no-param-reassign 57 | t.dateInfoDayFormat = r.dateInfoDayFormat; 58 | // eslint-disable-next-line no-param-reassign 59 | t.dateInfoTimeFormat = r.dateInfoTimeFormat; 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /test/_worker-helper.js: -------------------------------------------------------------------------------- 1 | // this is a hackish way to keep workers both in node and in karma 2 | /* eslint-disable */ 3 | 4 | export const discoveryWorkerFactory = () => { 5 | if (typeof Worker === 'undefined') { 6 | const TinyWorker = require('tiny-worker'); 7 | 8 | return new TinyWorker(() => { 9 | // Terrible hack 10 | // Browserify throws error if I don't do this 11 | // Maybe it could be fixed with noParse instead of eval, but I don't know how, 12 | // since this is all pretty hacky anyway 13 | // eslint-disable-next-line no-eval 14 | const requireHack = eval('req' + 'uire'); 15 | requireHack('@babel/register')({ cache: true }); 16 | requireHack('../../../src/discovery/worker/inside/index.js'); 17 | }); 18 | } 19 | return new Worker('../../src/discovery/worker/inside/index.js'); 20 | }; 21 | 22 | const fastXpubWorkerFactory = () => { 23 | if (typeof Worker === 'undefined') { 24 | const TinyWorker = require('tiny-worker'); 25 | const worker = new TinyWorker('./fastxpub/build/fastxpub.js'); 26 | const fs = require('fs'); 27 | const filePromise = require('util').promisify(fs.readFile)('./fastxpub/build/fastxpub.wasm') 28 | // issue with tiny-worker - https://github.com/avoidwork/tiny-worker/issues/18 29 | .then(buf => Array.from(buf)); 30 | return { worker, filePromise }; 31 | } 32 | // using this, so Workerify doesn't try to browserify this 33 | // eslint-disable-next-line no-eval 34 | const WorkerHack = eval('Work' + 'er'); 35 | // files are served by karma on base/lib/... 36 | const worker = new WorkerHack('./base/fastxpub/build/fastxpub.js'); 37 | const filePromise = fetch('base/fastxpub/build/fastxpub.wasm') 38 | .then(response => (response.ok ? response.arrayBuffer() : Promise.reject('failed to load'))); 39 | return { worker, filePromise }; 40 | }; 41 | 42 | export const { worker: xpubWorker, filePromise: xpubFilePromise } = fastXpubWorkerFactory(); 43 | -------------------------------------------------------------------------------- /test/coinselect-lib/_utils.js: -------------------------------------------------------------------------------- 1 | function addScriptLength(values, scriptLength) { 2 | return values.map((xx) => { 3 | const x = xx; 4 | if (x.script === undefined) { 5 | x.script = { length: scriptLength }; 6 | } 7 | return x; 8 | }); 9 | } 10 | 11 | export function addScriptLengthToExpected(expected, inputLength, outputLength) { 12 | const newExpected = { ...expected }; 13 | 14 | if (expected.inputs != null) { 15 | newExpected.inputs = expected.inputs.map((input) => { 16 | const newInput = { ...input }; 17 | if (newInput.script == null) { 18 | newInput.script = { length: inputLength }; 19 | } 20 | return newInput; 21 | }); 22 | } 23 | 24 | if (expected.outputs != null) { 25 | newExpected.outputs = expected.outputs.map((output) => { 26 | const newOutput = { ...output }; 27 | if (newOutput.script == null) { 28 | newOutput.script = { length: outputLength }; 29 | } 30 | return newOutput; 31 | }); 32 | } 33 | 34 | return newExpected; 35 | } 36 | 37 | export function expand(values, indices, scriptLength) { 38 | if (indices) { 39 | return addScriptLength(values.map((x, i) => { 40 | if (typeof x === 'string') return { i, value: x }; 41 | 42 | const y = { i }; 43 | Object.keys(x).forEach((k) => { y[k] = x[k]; }); 44 | return y; 45 | }), scriptLength); 46 | } 47 | 48 | return addScriptLength(values.map(x => (typeof x === 'object' ? x : { value: x })), scriptLength); 49 | } 50 | 51 | export function testValues(t, actual, expected) { 52 | t.equal(typeof actual, typeof expected, 'types match'); 53 | if (!expected) return; 54 | 55 | t.equal(actual.length, expected.length, 'lengths match'); 56 | 57 | actual.forEach((ai, i) => { 58 | const ei = expected[i]; 59 | 60 | if (ai.i !== undefined) { 61 | t.equal(ai.i, ei, 'indexes match'); 62 | } else if (typeof ei === 'number') { 63 | t.equal(ai.value, ei, 'values match'); 64 | } else { 65 | t.same(ai, ei, 'objects match'); 66 | } 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /test/build-tx.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import bitcoin from '@trezor/utxo-lib'; 3 | import { buildTx } from '../src/build-tx'; 4 | import { Permutation } from '../src/build-tx/permutation'; 5 | 6 | 7 | import fixtures from './fixtures/build-tx.json'; 8 | 9 | // eslint-disable-next-line no-unused-vars 10 | import accumulative from './coinselect-lib/accumulative'; 11 | // eslint-disable-next-line no-unused-vars 12 | import bnb from './coinselect-lib/bnb'; 13 | // eslint-disable-next-line no-unused-vars 14 | import errors from './coinselect-lib/index-errors'; 15 | // eslint-disable-next-line no-unused-vars 16 | import index from './coinselect-lib/index'; 17 | // eslint-disable-next-line no-unused-vars 18 | import split from './coinselect-lib/split'; 19 | // eslint-disable-next-line no-unused-vars 20 | import utils from './coinselect-lib/utils'; 21 | 22 | describe('build tx', () => { 23 | fixtures.forEach(({ description, request, result: r }) => { 24 | const result = r; 25 | it(description, () => { 26 | request.network = bitcoin.networks.bitcoin; 27 | if (result.transaction) { 28 | result.transaction.inputs.forEach((oinput) => { 29 | const input = oinput; 30 | input.hash = reverseBuffer(Buffer.from(input.REV_hash, 'hex')); 31 | delete input.REV_hash; 32 | }); 33 | const o = result.transaction.PERM_outputs; 34 | const sorted = JSON.parse(JSON.stringify(o.sorted)); 35 | sorted.forEach((ss) => { 36 | const s = ss; 37 | if (s.opReturnData != null) { 38 | s.opReturnData = Buffer.from(s.opReturnData); 39 | } 40 | }); 41 | result.transaction.outputs = new Permutation(sorted, o.permutation); 42 | 43 | delete result.transaction.PERM_outputs; 44 | } 45 | assert.deepStrictEqual(buildTx(request), result); 46 | }); 47 | }); 48 | }); 49 | 50 | function reverseBuffer(src: Buffer): Buffer { 51 | const buffer = Buffer.alloc(src.length); 52 | for (let i = 0, j = src.length - 1; i <= j; ++i, --j) { 53 | buffer[i] = src[j]; 54 | buffer[j] = src[i]; 55 | } 56 | return buffer; 57 | } 58 | -------------------------------------------------------------------------------- /fastxpub/Makefile: -------------------------------------------------------------------------------- 1 | CRYPTODIR = trezor-crypto 2 | 3 | EMSDK_TAG = emscripten/emsdk:2.0.27 4 | 5 | EMFLAGS = \ 6 | -Oz \ 7 | --closure 1 \ 8 | --pre-js additional_sources/pre.js \ 9 | --post-js additional_sources/post.js \ 10 | -I $(CRYPTODIR) \ 11 | -I $(CRYPTODIR)/ed25519-donna \ 12 | -s EXPORTED_FUNCTIONS='["_hdnode_public_ckd_address_optimized", "_ecdsa_read_pubkey", "_hdnode_deserialize", "_hdnode_fingerprint", "_hdnode_public_ckd", "_hdnode_serialize_public"]' \ 13 | -s NO_EXIT_RUNTIME=1 \ 14 | -s NO_FILESYSTEM=1 \ 15 | -s WASM=1 16 | 17 | SRC += $(CRYPTODIR)/bignum.c 18 | SRC += $(CRYPTODIR)/ecdsa.c 19 | SRC += $(CRYPTODIR)/secp256k1.c 20 | SRC += $(CRYPTODIR)/hmac.c 21 | SRC += $(CRYPTODIR)/bip32.c 22 | SRC += $(CRYPTODIR)/base58.c 23 | SRC += $(CRYPTODIR)/ripemd160.c 24 | SRC += $(CRYPTODIR)/sha2.c 25 | SRC += $(CRYPTODIR)/sha3.c 26 | SRC += $(CRYPTODIR)/address.c 27 | SRC += $(CRYPTODIR)/curves.c 28 | SRC += $(CRYPTODIR)/nist256p1.c 29 | SRC += $(CRYPTODIR)/ed25519-donna/ed25519.c 30 | SRC += $(CRYPTODIR)/ed25519-donna/ed25519-keccak.c 31 | SRC += $(CRYPTODIR)/ed25519-donna/ed25519-sha3.c 32 | SRC += $(CRYPTODIR)/ed25519-donna/ed25519-donna-basepoint-table.c 33 | SRC += additional_sources/rand-mock.c 34 | 35 | all: build/fastxpub.js tests/benchmark-browserify.js 36 | 37 | build/fastxpub.js: $(SRC) 38 | emcc $(EMFLAGS) -o $@ $^ 39 | 40 | tests/benchmark-browserify.js: node_modules build/fastxpub.js tests/benchmark.js 41 | $(shell npm bin)/browserify tests/benchmark.js -o $@ --noparse=`pwd`/build/fastxpub.js 42 | 43 | node_modules: 44 | npm install 45 | 46 | benchmark: node_modules build/fastxpub.js tests/benchmark.js 47 | cd tests && node benchmark.js 48 | 49 | test: node_modules build/fastxpub.js 50 | cd tests && node test.js 51 | 52 | clean: 53 | rm -f build/fastxpub.js build/fastxpub.wasm tests/benchmark-browserify.js 54 | 55 | .docker: 56 | docker pull $(EMSDK_TAG) 57 | 58 | docker-shell: .docker 59 | docker run --rm -i -v $(shell pwd)/..:/src -t $(EMSDK_TAG) /bin/bash 60 | 61 | docker-build: .docker 62 | docker run --rm -v $(shell pwd)/..:/src $(EMSDK_TAG) /bin/bash -c 'cd fastxpub && make' 63 | 64 | docker-benchmark: .docker 65 | docker run --rm -v $(shell pwd)/..:/src $(EMSDK_TAG) /bin/bash -c 'cd fastxpub && make benchmark' 66 | 67 | docker-test: .docker 68 | docker run --rm -v $(shell pwd)/..:/src $(EMSDK_TAG) /bin/bash -c 'cd fastxpub && make test' 69 | -------------------------------------------------------------------------------- /test/coinselect-lib/utils.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import BigNumber from 'bignumber.js'; 4 | import { uintOrNaN, bignumberOrNaN, getFee } from '../../src/build-tx/coinselect-lib/utils'; 5 | 6 | describe('coinselect utils', () => { 7 | it('uintOrNaN', () => { 8 | assert.deepStrictEqual(uintOrNaN(1), 1); 9 | assert.deepStrictEqual(Number.isNaN(uintOrNaN('')), true); 10 | assert.deepStrictEqual(Number.isNaN(uintOrNaN(Infinity)), true); 11 | assert.deepStrictEqual(Number.isNaN(uintOrNaN(NaN)), true); 12 | assert.deepStrictEqual(Number.isNaN(uintOrNaN('1')), true); 13 | assert.deepStrictEqual(Number.isNaN(uintOrNaN('1.1')), true); 14 | assert.deepStrictEqual(Number.isNaN(uintOrNaN(1.1)), true); 15 | assert.deepStrictEqual(Number.isNaN(uintOrNaN(-1)), true); 16 | }); 17 | it('bignumberOrNaN', () => { 18 | assert.deepStrictEqual(bignumberOrNaN('1'), new BigNumber('1')); 19 | assert.deepStrictEqual(bignumberOrNaN('').isNaN(), true); 20 | assert.deepStrictEqual(bignumberOrNaN('deadbeef').isNaN(), true); 21 | assert.deepStrictEqual(bignumberOrNaN('0x dead beef').isNaN(), true); 22 | assert.deepStrictEqual(bignumberOrNaN(Infinity).isNaN(), true); 23 | assert.deepStrictEqual(bignumberOrNaN(NaN).isNaN(), true); 24 | assert.deepStrictEqual(bignumberOrNaN(1).isNaN(), true); 25 | assert.deepStrictEqual(bignumberOrNaN('1.1').isNaN(), true); 26 | assert.deepStrictEqual(bignumberOrNaN(1.1).isNaN(), true); 27 | assert.deepStrictEqual(bignumberOrNaN(-1).isNaN(), true); 28 | }); 29 | it('getBaseFee', () => { 30 | assert.deepStrictEqual(getFee(1, 100), 100); 31 | assert.deepStrictEqual(getFee(1, 200, {}), 200); 32 | // without floor 33 | assert.deepStrictEqual(getFee(1, 200, { baseFee: 1000 }), 1200); 34 | assert.deepStrictEqual( 35 | getFee( 36 | 2, 37 | 127, 38 | { baseFee: 1000, dustOutputFee: 1000, dustThreshold: 9 }, 39 | [{ value: 8 }, { value: 7 }], 40 | ), 41 | 3254, 42 | ); 43 | 44 | // with floor 45 | assert.deepStrictEqual(getFee(1, 200, { baseFee: 1000, floorBaseFee: true }), 1000); 46 | assert.deepStrictEqual( 47 | getFee( 48 | 2, 49 | 1000, 50 | { 51 | baseFee: 1000, 52 | dustOutputFee: 1000, 53 | dustThreshold: 9, 54 | floorBaseFee: true, 55 | }, 56 | [{ value: 8 }, { value: 7 }], 57 | ), 58 | 5000, 59 | ); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/build-tx/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as request from './request'; 4 | import * as result from './result'; 5 | import * as transaction from './transaction'; 6 | import * as coinselect from './coinselect'; 7 | 8 | export { empty as BuildTxEmptyResult } from './result'; 9 | export { Request as BuildTxRequest, OutputRequest as BuildTxOutputRequest } from './request'; 10 | export { Result as BuildTxResult } from './result'; 11 | export { Transaction as BuildTxTransaction, Output as BuildTxOutput, Input as BuildTxInput } from './transaction'; 12 | 13 | export function buildTx( 14 | { 15 | utxos, 16 | outputs, 17 | height, 18 | feeRate, 19 | segwit, 20 | inputAmounts, 21 | basePath, 22 | network, 23 | changeId, 24 | changeAddress, 25 | dustThreshold, 26 | baseFee, 27 | floorBaseFee, 28 | dustOutputFee, 29 | skipUtxoSelection, 30 | skipPermutation, 31 | }: request.Request, 32 | ): result.Result { 33 | if (outputs.length === 0) { 34 | return result.empty; 35 | } 36 | if (utxos.length === 0) { 37 | return { type: 'error', error: 'NOT-ENOUGH-FUNDS' }; 38 | } 39 | 40 | let countMax = { exists: false, id: 0 }; 41 | try { 42 | countMax = request.getMax(outputs); 43 | } catch (e) { 44 | return { type: 'error', error: e.message }; 45 | } 46 | const splitOutputs = request.splitByCompleteness(outputs); 47 | 48 | let csResult: coinselect.Result = { type: 'false' }; 49 | try { 50 | csResult = coinselect.coinselect( 51 | utxos, 52 | outputs, 53 | height, 54 | feeRate, 55 | segwit, 56 | countMax.exists, 57 | countMax.id, 58 | dustThreshold, 59 | network, 60 | baseFee, 61 | floorBaseFee, 62 | dustOutputFee, 63 | skipUtxoSelection, 64 | skipPermutation, 65 | ); 66 | } catch (e) { 67 | return { type: 'error', error: e.message }; 68 | } 69 | 70 | if (csResult.type === 'false') { 71 | return { type: 'error', error: 'NOT-ENOUGH-FUNDS' }; 72 | } 73 | if (splitOutputs.incomplete.length > 0) { 74 | return result.getNonfinalResult(csResult); 75 | } 76 | 77 | const resTransaction = transaction.createTransaction( 78 | utxos, 79 | csResult.result.inputs, 80 | splitOutputs.complete, 81 | csResult.result.outputs, 82 | segwit, 83 | inputAmounts, 84 | basePath, 85 | changeId, 86 | changeAddress, 87 | network, 88 | skipPermutation, 89 | ); 90 | return result.getFinalResult(csResult, resTransaction); 91 | } 92 | -------------------------------------------------------------------------------- /src/build-tx/coinselect-lib/outputs/split.js: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import * as utils from '../utils'; 3 | 4 | function filterCoinbase(utxos, minConfCoinbase) { 5 | return utxos.filter((utxo) => { 6 | if (utxo.coinbase) { 7 | return utxo.confirmations >= minConfCoinbase; 8 | } 9 | return true; 10 | }); 11 | } 12 | 13 | // split utxos between each output, ignores outputs with .value defined 14 | export default function split(utxosOrig, outputs, feeRate, options) { 15 | const coinbase = options.coinbase || 100; 16 | 17 | const feeRateBigInt = utils.bignumberOrNaN(feeRate); 18 | if (feeRateBigInt.isNaN() || !feeRateBigInt.isInteger()) return {}; 19 | const feeRateNumber = feeRateBigInt.toNumber(); 20 | 21 | const utxos = filterCoinbase(utxosOrig, coinbase); 22 | 23 | const bytesAccum = utils.transactionBytes(utxos, outputs); 24 | const fee = utils.getFee(feeRateNumber, bytesAccum, options, outputs); 25 | const FEE_RESPONSE = { fee: fee.toString() }; 26 | if (outputs.length === 0) return FEE_RESPONSE; 27 | 28 | const inAccum = utils.sumOrNaN(utxos); 29 | if (inAccum.isNaN()) return FEE_RESPONSE; 30 | const outAccum = utils.sumOrNaN(outputs, true); 31 | const remaining = inAccum.minus(outAccum).minus(new BigNumber(fee)); 32 | if (remaining.comparedTo(new BigNumber(0)) < 0) return FEE_RESPONSE; 33 | 34 | const unspecified = outputs.reduce( 35 | (a, x) => a + (utils.bignumberOrNaN(x.value).isNaN() ? 1 : 0), 36 | 0, 37 | ); 38 | 39 | if (remaining.toString() === '0' && unspecified === 0) { 40 | return utils.finalize(utxos, outputs, feeRateNumber, options); 41 | } 42 | 43 | // this is the same as "unspecified" 44 | // const splitOutputsCount = outputs.reduce((a, x) => a + !Number.isFinite(x.value), 0); 45 | const splitValue = remaining.div(new BigNumber(unspecified)); 46 | const dustThreshold = utils.dustThreshold( 47 | feeRateNumber, 48 | options.inputLength, 49 | options.changeOutputLength, 50 | options.dustThreshold, 51 | ); 52 | 53 | // ensure every output is either user defined, or over the threshold 54 | if (unspecified && splitValue.lte(dustThreshold)) return FEE_RESPONSE; 55 | 56 | // assign splitValue to outputs not user defined 57 | const outputsSplit = outputs.map((x) => { 58 | if (x.value !== undefined) return x; 59 | 60 | // not user defined, but still copy over any non-value fields 61 | const y = {}; 62 | Object.keys(x).forEach((k) => { y[k] = x[k]; }); 63 | y.value = splitValue.toString(); 64 | return y; 65 | }); 66 | 67 | return utils.finalize( 68 | utxos, 69 | outputsSplit, 70 | feeRateNumber, 71 | options, 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /type/socket.io-client.js: -------------------------------------------------------------------------------- 1 | declare module "socket.io-client" { 2 | declare type Callback = (...args: any[]) => void; 3 | 4 | declare type ManagerOptions = $Shape<{ 5 | path: string, 6 | reconnection: boolean, 7 | reconnectionAttempts: number, 8 | reconnectionDelay: number, 9 | reconnectionDelayMax: number, 10 | randomizationFactor: number, 11 | timeout: number, 12 | transports: ("polling" | "websocket")[], 13 | transportOptions: { 14 | polling: { 15 | extraHeaders: {[string]:string} 16 | } 17 | }, 18 | autoConnect: boolean, 19 | query: { [string]: string }, 20 | parser: any 21 | }>; 22 | 23 | declare type SocketOptions = $Shape<{ 24 | query: string 25 | }>; 26 | 27 | declare class Emitter { 28 | on(event: string, cb: Callback): T; 29 | addEventListener(event: string, cb: Callback): T; 30 | once(event: string, cb: Callback): T; 31 | off(event: string, cb: Callback): T; 32 | removeListener(event: string, cb: Callback): T; 33 | removeAllListeners(event?: string): T; 34 | removeEventListener(event: string, cb: Callback): T; 35 | emit(event: string, payload: mixed): T; 36 | listeners(event: string): Callback[]; 37 | hasListeners(event: string): boolean; 38 | } 39 | 40 | declare export class Manager extends Emitter { 41 | constructor(uri?: string, opts?: ManagerOptions): Manager; 42 | opts: ManagerOptions; 43 | reconnection(boolean): Manager; 44 | reconnectionAttempts(number): Manager; 45 | reconnectionDelay(number): Manager; 46 | randomizationFactor(number): Manager; 47 | reconnectionDelayMax(number): Manager; 48 | timeout(number): Manager; 49 | open(fn?: (err?: Error) => void): Manager; 50 | connect(fn?: (err?: Error) => void): Manager; 51 | socket(namespace: string, opts?: SocketOptions): Socket; 52 | } 53 | 54 | declare export class Socket extends Emitter { 55 | constructor(io: Manager, nsp: string, opts?: SocketOptions): Socket; 56 | id: string; 57 | open(): Socket; 58 | connect(): Socket; 59 | send(...args: any[]): Socket; 60 | emit(event: string, ...args: any[]): Socket; // overrides Emitter#emit 61 | close(): Socket; 62 | disconnect(): Socket; 63 | compress(boolean): Socket; 64 | io: Manager; 65 | } 66 | 67 | // all of ManagerOptions with a few additions 68 | declare type LookupOptions = $Shape< 69 | { 70 | forceNew: boolean, 71 | "force new connection": true, 72 | multiplex: boolean 73 | } & ManagerOptions 74 | >; 75 | 76 | declare type Lookup = (uri?: string, opts?: LookupOptions) => Socket; 77 | 78 | declare export var protocol: 4; 79 | declare export var connect: Lookup; 80 | declare export default Lookup 81 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN=`npm bin` 2 | 3 | LIB=src/index.js 4 | 5 | TEST=test/*.js 6 | 7 | EXAMPLE=example/index.js 8 | EXAMPLE_TARGET=gh-pages/example.js 9 | SOCKET_WORKER=src/socketio-worker/inside.js 10 | DISCOVERY_WORKER=src/discovery/worker/inside/index.js 11 | SOCKET_TARGET=gh-pages/socket-worker.js 12 | DISCOVERY_TARGET=gh-pages/discovery-worker.js 13 | 14 | .PHONY: all check lib test example watch server clean 15 | 16 | all: lib 17 | 18 | example: node_modules 19 | ${BIN}/browserify ${EXAMPLE} -g [ uglifyify ] -d > ${EXAMPLE_TARGET} 20 | ${BIN}/browserify ${SOCKET_WORKER} -g [ uglifyify ] -d > ${SOCKET_TARGET} 21 | ${BIN}/browserify ${DISCOVERY_WORKER} -g [ uglifyify ] -d > ${DISCOVERY_TARGET} 22 | cp fastxpub/build/fastxpub.js gh-pages 23 | cp fastxpub/build/fastxpub.wasm gh-pages 24 | 25 | clean: 26 | rm -f \ 27 | ${EXAMPLE_TARGET} ${EXAMPLE_TARGET}.map 28 | rm -rf lib 29 | 30 | node_modules: 31 | yarn 32 | 33 | lib: 34 | `npm bin`/babel src --out-dir lib 35 | cp -r ./fastxpub/build ./lib/fastxpub 36 | cd ./src && find . -name '*.js' | xargs -I {} cp {} ../lib/{}.flow 37 | 38 | unit: 39 | `npm bin`/mocha --require @babel/register --exit 40 | 41 | unit-build-tx: 42 | `npm bin`/mocha --require @babel/register --exit test/build-tx.js 43 | 44 | unit-discovery: 45 | `npm bin`/mocha --require @babel/register --exit test/discover-account.js 46 | 47 | unit-bitcore: 48 | `npm bin`/mocha --require @babel/register --exit test/bitcore.js 49 | 50 | unit-utils: 51 | `npm bin`/mocha --require @babel/register --exit test/utils.js 52 | 53 | 54 | coverage-html: 55 | NODE_ENV=test `npm bin`/nyc --cache --babel-cache=true --reporter=html --check-coverage --lines 97 --branches 93 `npm bin`/mocha --require @babel/register --exit 56 | 57 | run-coverage: 58 | NODE_ENV=test `npm bin`/nyc --check-coverage --lines 97 --branches 93 --babel-cache=true `npm bin`/mocha --require @babel/register --exit 59 | 60 | flow: 61 | `npm bin`/flow check src 62 | 63 | eslint: 64 | cd src && `npm bin`/eslint . 65 | cd ./test && `npm bin`/eslint . 66 | cd ./example && `npm bin`/eslint . 67 | 68 | eslint-fix: 69 | cd src && `npm bin`/eslint --fix . || true 70 | cd ./test && `npm bin`/eslint --fix . || true 71 | cd ./example && `npm bin`/eslint --fix . || true 72 | 73 | 74 | karma-firefox: 75 | `npm bin`/karma start --browsers Firefox --single-run 76 | 77 | karma-chrome: 78 | `npm bin`/karma start --browsers Chrome --single-run 79 | 80 | git-ancestor: 81 | git fetch origin 82 | git merge-base --is-ancestor origin/master master 83 | 84 | .version: clean git-clean git-ancestor flow eslint lib 85 | npm version ${TYPE} 86 | npm publish 87 | git push 88 | git push --tags 89 | 90 | version-patch: TYPE = patch 91 | version-patch: .version 92 | 93 | version-minor: TYPE = minor 94 | version-minor: .version 95 | 96 | version-major: TYPE = major 97 | version-major: .version 98 | 99 | git-clean: 100 | test ! -n "$$(git status --porcelain)" 101 | -------------------------------------------------------------------------------- /test/_mock-worker.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | const TICK_MS = 10; 4 | export class MockWorker { 5 | addEventListener(something, listener) { 6 | this.onmessage = listener; 7 | } 8 | 9 | constructor(spec, doneError, serialize) { 10 | this.specLock = false; 11 | this.onmessage = () => {}; 12 | this.errored = false; 13 | this.spec = JSON.parse(JSON.stringify(spec)); 14 | this.doneError = doneError; 15 | this.doneError = (f) => { 16 | console.error(f); 17 | if (!this.errored) { 18 | doneError(f); 19 | } 20 | this.errored = true; 21 | }; 22 | this.sendOut(); 23 | this.serialize = serialize; 24 | } 25 | 26 | sendOut() { 27 | if (this.spec.length > 0) { 28 | const sspec = this.spec[0]; 29 | if (sspec.type === 'out') { 30 | this.specLock = true; 31 | const { spec } = sspec; 32 | this.spec.shift(); 33 | setTimeout(() => { 34 | this.specLock = false; 35 | if (this.serialize) { 36 | this.onmessage({ data: JSON.stringify(spec) }); 37 | } else { 38 | this.onmessage({ data: spec }); 39 | } 40 | this.sendOut(); 41 | }, TICK_MS); 42 | } 43 | } 44 | } 45 | 46 | terminated = false; 47 | 48 | terminate() { 49 | if (this.terminated) { 50 | const error = new Error('Terminate twice'); 51 | this.doneError(error); 52 | throw error; 53 | } 54 | this.terminated = true; 55 | if (this.spec.length !== 0) { 56 | const error = new Error('Spec left on terminate'); 57 | this.doneError(error); 58 | throw error; 59 | } 60 | } 61 | 62 | postMessage(message) { 63 | const omessage = this.serialize ? JSON.parse(message) : message; 64 | let error = null; 65 | if (this.spec.length === 0) { 66 | assert.deepStrictEqual(omessage, null); 67 | /* console.warn 68 | error = new Error('In spec not defined'); 69 | this.doneError(error); 70 | throw error; */ 71 | } 72 | if (this.specLock) { 73 | error = new Error('Got postMessage while waiting'); 74 | this.doneError(error); 75 | throw error; 76 | } 77 | const sspec = this.spec[0]; 78 | if (sspec.type !== 'in') { 79 | error = new Error('Got in while expecting out'); 80 | this.doneError(error); 81 | throw error; 82 | } 83 | const { spec } = sspec; 84 | this.spec.shift(); 85 | try { 86 | assert.deepStrictEqual(omessage, spec); 87 | } catch (e) { 88 | error = e; 89 | this.doneError(error); 90 | throw error; 91 | } 92 | this.sendOut(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/build-tx/coinselect-lib/inputs/accumulative.js: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import * as utils from '../utils'; 3 | 4 | // add inputs until we reach or surpass the target value (or deplete) 5 | // worst-case: O(n) 6 | export default function accumulative(utxos0, outputs, feeRate, options) { 7 | const feeRateBigInt = utils.bignumberOrNaN(feeRate); 8 | if (feeRateBigInt.isNaN() || !feeRateBigInt.isInteger()) return {}; 9 | const feeRateNumber = feeRateBigInt.toNumber(); 10 | let bytesAccum = utils.transactionBytes([], outputs); 11 | 12 | let inAccum = new BigNumber(0); 13 | const inputs = []; 14 | const outAccum = utils.sumOrNaN(outputs); 15 | 16 | // split utxos into required and the rest 17 | const requiredUtxos = []; 18 | const utxos = []; 19 | utxos0.forEach((u) => { 20 | if (u.required) { 21 | requiredUtxos.push(u); 22 | const utxoBytes = utils.inputBytes(u); 23 | const utxoValue = utils.bignumberOrNaN(u.value); 24 | bytesAccum += utxoBytes; 25 | inAccum = inAccum.plus(utxoValue); 26 | inputs.push(u); 27 | } else { 28 | utxos.push(u); 29 | } 30 | }); 31 | 32 | // check if required utxo is enough 33 | if (requiredUtxos.length > 0) { 34 | const requiredIsEnough = utils.finalize( 35 | requiredUtxos, 36 | outputs, 37 | feeRateNumber, 38 | options, 39 | ); 40 | if (requiredIsEnough.inputs) { 41 | return requiredIsEnough; 42 | } 43 | } 44 | 45 | // continue with the rest 46 | for (let i = 0; i < utxos.length; ++i) { 47 | const utxo = utxos[i]; 48 | const utxoBytes = utils.inputBytes(utxo); 49 | const utxoFee = feeRateNumber * utxoBytes; 50 | const utxoValue = utils.bignumberOrNaN(utxo.value); 51 | 52 | // skip detrimental input 53 | if (utxoValue.isNaN() 54 | || utxoValue.comparedTo(new BigNumber(utxoFee)) < 0) { 55 | if (i === utxos.length - 1) { 56 | const fee = utils.getFee(feeRateNumber, bytesAccum + utxoBytes, options, outputs); 57 | return { 58 | fee: fee.toString(), 59 | }; 60 | } 61 | } else { 62 | bytesAccum += utxoBytes; 63 | inAccum = inAccum.plus(utxoValue); 64 | inputs.push(utxo); 65 | 66 | const fee = utils.getFee(feeRateNumber, bytesAccum, options, outputs); 67 | const outAccumWithFee = outAccum.isNaN() 68 | ? new BigNumber(0) : outAccum.plus(fee); 69 | 70 | // go again? 71 | if (inAccum.comparedTo(outAccumWithFee) >= 0) { 72 | return utils.finalize( 73 | inputs, 74 | outputs, 75 | feeRateNumber, 76 | options, 77 | ); 78 | } 79 | } 80 | } 81 | 82 | const fee = utils.getFee(feeRateNumber, bytesAccum, options, outputs); 83 | return { fee: fee.toString() }; 84 | } 85 | -------------------------------------------------------------------------------- /src/build-tx/request.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { 3 | Network as BitcoinJsNetwork, 4 | } from '@trezor/utxo-lib'; 5 | 6 | import type { UtxoInfo } from '../discovery'; 7 | // -------- Input to algoritm 8 | // array of Request, which is either 9 | // - 'complete' - address + amount 10 | // - 'send-max' - address 11 | // - 'noaddress' - just amount 12 | // - 'send-max-noaddress' - no other info 13 | export type OutputRequestWithAddress = { // TODO rename 14 | type: 'complete', 15 | address: string, 16 | amount: string, // in satoshis 17 | } | { 18 | type: 'send-max', // only one in TX request 19 | address: string, 20 | } | { 21 | type: 'opreturn', // this is misnomer, since it doesn't need to have address 22 | dataHex: string, 23 | }; 24 | 25 | export type OutputRequest = { 26 | type: 'send-max-noaddress', // only one in TX request 27 | } | { 28 | type: 'noaddress', 29 | amount: string, 30 | } | OutputRequestWithAddress; 31 | 32 | export type Request = { 33 | utxos: Array, // all inputs 34 | outputs: Array, // all output "requests" 35 | height: number, 36 | feeRate: string, // in sat/byte, virtual size 37 | segwit: boolean, 38 | inputAmounts: boolean, // BIP 143 - not same as segwit (BCash) 39 | basePath: Array, // for trezor inputs 40 | network: BitcoinJsNetwork, 41 | changeId: number, 42 | changeAddress: string, 43 | dustThreshold: number, // explicit dust threshold, in satoshis 44 | baseFee?: number; // DOGE base fee 45 | floorBaseFee?: boolean; // DOGE floor base fee to the nearest integer 46 | dustOutputFee?: number; // DOGE fee for every output below dust limit 47 | skipUtxoSelection?: boolean; // use custom utxo selection, without algorithm 48 | skipPermutation?: boolean; // Do not sort inputs/outputs and preserve the given order. Handy for RBF. 49 | }; 50 | 51 | export function splitByCompleteness( 52 | outputs: Array, 53 | ): { 54 | complete: Array, 55 | incomplete: Array, 56 | } { 57 | const result : { 58 | complete: Array, 59 | incomplete: Array, 60 | } = { 61 | complete: [], 62 | incomplete: [], 63 | }; 64 | 65 | outputs.forEach((output) => { 66 | if (output.type === 'complete' || output.type === 'send-max' || output.type === 'opreturn') { 67 | result.complete.push(output); 68 | } else { 69 | result.incomplete.push(output); 70 | } 71 | }); 72 | 73 | return result; 74 | } 75 | 76 | export function getMax( 77 | outputs: Array, 78 | ): { 79 | exists: boolean, 80 | id: number, 81 | } { 82 | // first, call coinselect - either sendMax or bnb 83 | // and if the input data are complete, also make the whole transaction 84 | 85 | const countMaxRequests = outputs.filter(output => output.type === 'send-max-noaddress' || output.type === 'send-max'); 86 | if (countMaxRequests.length >= 2) { 87 | throw new Error('TWO-SEND-MAX'); 88 | } 89 | 90 | const id = outputs.findIndex(output => output.type === 'send-max-noaddress' || output.type === 'send-max'); 91 | const exists = countMaxRequests.length === 1; 92 | 93 | return { 94 | id, exists, 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /src/build-tx/coinselect-lib/tryconfirmed.js: -------------------------------------------------------------------------------- 1 | function filterCoinbase(utxos, minConfCoinbase) { 2 | return utxos.filter((utxo) => { 3 | if (utxo.coinbase && !utxo.required) { 4 | return utxo.confirmations >= minConfCoinbase; 5 | } 6 | return true; 7 | }); 8 | } 9 | 10 | function filterUtxos(utxos, minConfOwn, minConfOther) { 11 | const usable = []; 12 | const unusable = []; 13 | 14 | for (let i = 0; i < utxos.length; i++) { 15 | const utxo = utxos[i]; 16 | 17 | const isUsed = (utxo.own) 18 | ? utxo.confirmations >= minConfOwn 19 | : utxo.confirmations >= minConfOther; 20 | 21 | if (isUsed || utxo.required) { 22 | usable.push(utxo); 23 | } else { 24 | unusable.push(utxo); 25 | } 26 | } 27 | return { 28 | usable, 29 | unusable, 30 | }; 31 | } 32 | 33 | export default function tryConfirmed(algorithm, options) { 34 | const own = options.own || 1; 35 | const other = options.other || 6; 36 | const coinbase = options.coinbase || 100; 37 | 38 | return (utxosO, outputs, feeRate, optionsIn) => { 39 | utxosO.forEach((utxo) => { 40 | if (utxo.coinbase == null || utxo.own == null || utxo.confirmations == null) { 41 | throw new Error('Missing information.'); 42 | } 43 | }); 44 | 45 | const utxos = filterCoinbase(utxosO, coinbase); 46 | 47 | if (utxos.length === 0) { 48 | return {}; 49 | } 50 | 51 | const trials = []; 52 | 53 | let i; 54 | // first - let's keep others at options.other and let's try decrease own, but not to 0 55 | for (i = own; i > 0; i--) { 56 | trials.push({ other, own: i }); 57 | } 58 | 59 | // if that did not work, let's try to decrease other, keeping own at 1 60 | for (i = other - 1; i > 0; i--) { 61 | trials.push({ other: i, own: 1 }); 62 | } 63 | 64 | // if that did not work, first allow own unconfirmed, then all unconfirmed 65 | trials.push({ other: 1, own: 0 }); 66 | trials.push({ other: 0, own: 0 }); 67 | 68 | let unusable = utxos; 69 | let usable = []; 70 | 71 | for (i = 0; i < trials.length; i++) { 72 | const trial = trials[i]; 73 | 74 | // since the restrictions are always loosening, we can just filter the unusable so far 75 | const filterResult = filterUtxos(unusable, trial.own, trial.other, coinbase); 76 | 77 | // and we can try the algorithm only if there are some newly usable utxos 78 | if (filterResult.usable.length > 0) { 79 | usable = usable.concat(filterResult.usable); 80 | const unusableH = filterResult.unusable; 81 | unusable = unusableH; 82 | 83 | const result = algorithm(usable, outputs, feeRate, optionsIn); 84 | if (result.inputs) { 85 | return result; 86 | } 87 | 88 | // we tried all inputs already 89 | if (unusable.length === 0) { 90 | return result; 91 | } 92 | } 93 | } 94 | 95 | /* istanbul ignore next */ // we should never end here 96 | throw new Error('Unexpected unreturned result'); 97 | }; 98 | } 99 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Wed Apr 12 2017 14:04:18 GMT+0200 (CEST) 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | // frameworks to use 11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 12 | frameworks: ['browserify', 'mocha'], 13 | 14 | browserify: { 15 | debug: true, 16 | transform: [ 17 | ['babelify', 18 | { 19 | "presets": [ 20 | "@babel/preset-env" 21 | ], 22 | "plugins": [ 23 | "@babel/plugin-transform-flow-strip-types", 24 | "@babel/plugin-proposal-class-properties", 25 | "@babel/plugin-proposal-object-rest-spread" 26 | ] 27 | } 28 | ], 29 | ['workerify'], 30 | ['browserify-shim'], 31 | ], 32 | }, 33 | 34 | // list of files / patterns to load in the browser 35 | files: [ 36 | 'test/utils.js', 37 | 'test/bitcore.js', 38 | 'test/build-tx.js', 39 | 'test/discover-account.js', 40 | 'test/monitor-account.js', 41 | {pattern: 'fastxpub/build/fastxpub.js', included: false}, 42 | {pattern: 'fastxpub/build/fastxpub.wasm', included: false} 43 | 44 | ], 45 | 46 | // list of files to exclude 47 | exclude: [ 48 | ], 49 | 50 | // preprocess matching files before serving them to the browser 51 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 52 | preprocessors: { 53 | 'test/utils.js': [ 'browserify' ], 54 | 'test/utils.js': [ 'browserify' ], 55 | 'test/bitcore.js': [ 'browserify' ], 56 | 'test/build-tx.js': [ 'browserify' ], 57 | 'test/discover-account.js': [ 'browserify' ], 58 | 'test/monitor-account.js': [ 'browserify' ], 59 | }, 60 | 61 | // test results reporter to use 62 | // possible values: 'dots', 'progress' 63 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 64 | reporters: ['dots'], 65 | 66 | browserNoActivityTimeout: 60 * 1000, 67 | 68 | // web server port 69 | port: 9876, 70 | 71 | // enable / disable colors in the output (reporters and logs) 72 | colors: true, 73 | 74 | // level of logging 75 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 76 | logLevel: config.LOG_INFO, 77 | 78 | // enable / disable watching file and executing tests whenever any file changes 79 | autoWatch: false, 80 | 81 | // start these browsers 82 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 83 | browsers: ['Chrome', 'Firefox'], 84 | 85 | // Continuous Integration mode 86 | // if true, Karma captures browsers, runs the tests and exits 87 | singleRun: false, 88 | 89 | // Concurrency level 90 | // how many browser should be started simultaneous 91 | concurrency: Infinity, 92 | }); 93 | }; 94 | -------------------------------------------------------------------------------- /src/address-source.js: -------------------------------------------------------------------------------- 1 | /* @flow 2 | * Derivation of addresses from HD nodes 3 | */ 4 | 5 | import type { HDNode, Network } from '@trezor/utxo-lib'; 6 | import { 7 | crypto, 8 | address, 9 | } from '@trezor/utxo-lib'; 10 | import type { WorkerChannel } from './utils/simple-worker-channel'; 11 | 12 | export type AddressSource = { 13 | derive( 14 | firstIndex: number, 15 | lastIndex: number 16 | ): Promise>, 17 | }; 18 | 19 | export class BrowserAddressSource { 20 | network: Network; 21 | 22 | segwit: boolean; 23 | 24 | node: HDNode; 25 | 26 | constructor(hdnode: HDNode, network: Network, segwit: boolean) { 27 | this.network = network; 28 | this.segwit = segwit; 29 | this.node = hdnode; 30 | } 31 | 32 | derive( 33 | first: number, 34 | last: number, 35 | ): Promise> { 36 | const addresses: Array = []; 37 | // const chainNode = HDNode.fromBase58(this.xpub, this.network).derive(this.chainId); 38 | for (let i = first; i <= last; i++) { 39 | const addressNode = this.node.derive(i); 40 | let naddress = ''; 41 | 42 | if (!this.segwit) { 43 | naddress = addressNode.getAddress(); 44 | } else { 45 | // see https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki 46 | // address derivation + test vectors 47 | const pkh = addressNode.getIdentifier(); 48 | const scriptSig = Buffer.alloc(pkh.length + 2); 49 | scriptSig[0] = 0; 50 | scriptSig[1] = 0x14; 51 | pkh.copy(scriptSig, 2); 52 | const addressBytes = crypto.hash160(scriptSig); 53 | naddress = address.toBase58Check(addressBytes, this.network.scriptHash); 54 | } 55 | addresses.push(naddress); 56 | } 57 | return Promise.resolve(addresses); 58 | } 59 | } 60 | 61 | export class WorkerAddressSource { 62 | channel: WorkerChannel; 63 | 64 | node: { 65 | depth: number, 66 | child_num: number, 67 | fingerprint: number, 68 | chain_code: Array, 69 | public_key: Array, 70 | }; 71 | 72 | version: number; 73 | 74 | segwit: 'p2sh' | 'off'; 75 | 76 | constructor(channel: WorkerChannel, node: HDNode, version: number, segwit: 'p2sh' | 'off') { 77 | this.channel = channel; 78 | this.node = { 79 | depth: node.depth, 80 | child_num: node.index, 81 | fingerprint: node.parentFingerprint, 82 | chain_code: Array.prototype.slice.call(node.chainCode), 83 | public_key: Array.prototype.slice.call(node.keyPair.getPublicKeyBuffer()), 84 | }; 85 | this.version = version; 86 | this.segwit = segwit; 87 | } 88 | 89 | derive(firstIndex: number, lastIndex: number): Promise> { 90 | const request = { 91 | type: 'deriveAddressRange', 92 | node: this.node, 93 | version: this.version, 94 | firstIndex, 95 | lastIndex, 96 | addressFormat: this.segwit === 'p2sh' ? 1 : 0, 97 | }; 98 | return this.channel.postMessage(request) 99 | .then(({ addresses }) => addresses); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hd-wallet", 3 | "version": "9.1.2", 4 | "description": "Data structures and algorithms for Bitcoin HD wallet.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build-lib": "make clean;make lib", 8 | "build-example": "make example", 9 | "test": "make eslint && make flow && make unit", 10 | "unit": "make unit", 11 | "unit-build-tx": "make unit-build-tx", 12 | "unit-discovery": "make unit-discovery", 13 | "unit-bitcore-with-docker": "make bitcore-test-docker && make unit-bitcore", 14 | "unit-bitcore": "make unit-bitcore", 15 | "coverage-html": "make coverage-html", 16 | "coverage": "make run-coverage", 17 | "flow": "make flow", 18 | "eslint": "make eslint", 19 | "lint": "make eslint-fix", 20 | "karma-firefox": "make karma-firefox", 21 | "karma-chrome": "make karma-chrome", 22 | "jest": "jest test" 23 | }, 24 | "author": "TREZOR ", 25 | "repository": "https://github.com/trezor/hd-wallet", 26 | "license": "LGPL-3.0+", 27 | "browserify": { 28 | "transform": [ 29 | [ 30 | "babelify", 31 | { 32 | "presets": [ 33 | "@babel/preset-env" 34 | ], 35 | "plugins": [ 36 | "@babel/plugin-transform-flow-strip-types", 37 | "@babel/plugin-proposal-class-properties", 38 | "@babel/plugin-proposal-object-rest-spread" 39 | ] 40 | } 41 | ] 42 | ] 43 | }, 44 | "nyc": { 45 | "sourceMap": false, 46 | "instrument": false 47 | }, 48 | "browserify-shim": { 49 | "../../../src/socketio-worker/inside.js": "global:thisIsJustForKarmaTestButIHaveToWriteItHere" 50 | }, 51 | "browserslist": "> 5%, IE > 9, last 10 versions", 52 | "dependencies": { 53 | "@trezor/utxo-lib": "0.1.2", 54 | "bchaddrjs": "^0.5.2", 55 | "bignumber.js": "^9.0.1", 56 | "queue": "^6.0.2", 57 | "socket.io-client": "^4.1.2" 58 | }, 59 | "devDependencies": { 60 | "@babel/cli": "7.14.3", 61 | "@babel/core": "7.14.3", 62 | "@babel/plugin-proposal-class-properties": "7.13.0", 63 | "@babel/plugin-proposal-object-rest-spread": "7.14.4", 64 | "@babel/plugin-transform-flow-strip-types": "7.13.0", 65 | "@babel/preset-env": "7.14.4", 66 | "@babel/register": "7.13.16", 67 | "babel-eslint": "^10.1.0", 68 | "babel-jest": "^27.0.2", 69 | "babel-plugin-istanbul": "^6.0.0", 70 | "babelify": "^10.0.0", 71 | "browserify": "^17.0.0", 72 | "browserify-shim": "^3.8.14", 73 | "eslint": "^7.27.0", 74 | "eslint-config-airbnb-base": "^14.2.1", 75 | "eslint-config-prettier": "^8.3.0", 76 | "eslint-plugin-flowtype": "^5.7.2", 77 | "eslint-plugin-import": "^2.23.4", 78 | "eslint-plugin-jest": "^24.3.6", 79 | "eslint-plugin-node": "^11.1.0", 80 | "eslint-plugin-prettier": "^3.4.0", 81 | "eslint-plugin-promise": "^5.1.0", 82 | "eslint-plugin-standard": "^5.0.0", 83 | "flow-bin": "0.59.0", 84 | "jest": "^27.0.4", 85 | "karma": "^6.3.3", 86 | "karma-browserify": "^8.0.0", 87 | "karma-chai": "^0.1.0", 88 | "karma-chrome-launcher": "^3.1.0", 89 | "karma-firefox-launcher": "^2.1.1", 90 | "karma-mocha": "^2.0.1", 91 | "mocha": "^8.4.0", 92 | "nyc": "^15.1.0", 93 | "prettier": "^2.3.0", 94 | "tiny-worker": "^2.3.0", 95 | "uglifyify": "^5.0.2", 96 | "virtual-dom": "2.1.1", 97 | "workerify": "1.1.0" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /fastxpub/tests/benchmark.js: -------------------------------------------------------------------------------- 1 | var fastxpub = require('../build/fastxpub'); 2 | var bitcoin = require('bitcoinjs-lib'); 3 | 4 | var XPUB = 5 | 'xpub6BiVtCpG9fQPxnPmHXG8PhtzQdWC2Su4qWu6XW9tpWFYhxydCLJGrWBJZ5H6qTAHdPQ7pQhtpjiYZVZARo14qHiay2fvrX996oEP42u8wZy'; 6 | var node = bitcoin.HDNode.fromBase58(XPUB).derive(0); 7 | 8 | var nodeStruct = { 9 | depth: node.depth, 10 | child_num: node.index, 11 | fingerprint: node.parentFingerprint, 12 | chain_code: node.chainCode, 13 | public_key: node.keyPair.getPublicKeyBuffer() 14 | }; 15 | 16 | var suite; 17 | var worker; 18 | 19 | var wasmBinaryFile = '../build/fastxpub.wasm'; 20 | 21 | if (typeof Worker !== 'undefined') { 22 | var promise = fetch(wasmBinaryFile, { credentials: 'same-origin' }).then(function(response) { 23 | if (!response['ok']) { 24 | throw "failed to load wasm binary file at '" + wasmBinaryFile + "'"; 25 | } 26 | return response['arrayBuffer'](); 27 | }); 28 | 29 | promise.then(binary => { 30 | fastxpub.init(binary).then(() => { 31 | worker = new Worker('../build/fastxpub.js'); 32 | worker.onerror = function (error) { 33 | console.error('worker:', error); 34 | }; 35 | worker.postMessage({ 36 | type: 'init', 37 | binary 38 | }); 39 | 40 | suite = [ 41 | benchBitcoinJS, 42 | benchBrowserify, 43 | benchWorker 44 | ]; 45 | benchmark(suite, 1000, 1000); 46 | }); 47 | }); 48 | 49 | } else { 50 | var fs = require('fs'); 51 | var file = fs.readFileSync(wasmBinaryFile); 52 | fastxpub.init(file).then(() => { 53 | suite = [ 54 | benchBitcoinJS, 55 | benchBrowserify 56 | ]; 57 | benchmark(suite, 1000, 1000); 58 | }); 59 | } 60 | 61 | function benchmark(suite, delay, ops) { 62 | (function cycle(i) { 63 | setTimeout(function () { 64 | var benchmark = suite[i]; 65 | runBenchmark(benchmark, ops, function (runtime) { 66 | printResult(benchmark, ops, runtime); 67 | cycle(i+1 < suite.length ? i+1 : 0); 68 | }); 69 | }, delay); 70 | }(0)); 71 | } 72 | 73 | function benchBitcoinJS(ops, fn) { 74 | for (var i = 0; i < ops; i++) { 75 | node.derive(i).getAddress(); 76 | } 77 | fn(); 78 | } 79 | 80 | function benchBrowserify(ops, fn) { 81 | fastxpub.loadNode(nodeStruct); 82 | for (var i = 0; i < ops; i++) { 83 | fastxpub.deriveAddress(i, 0); 84 | } 85 | fn(); 86 | } 87 | 88 | function benchWorker(ops, fn) { 89 | worker.onmessage = function (event) { 90 | fn(); 91 | }; 92 | worker.postMessage({ 93 | type: 'deriveAddressRange', 94 | node: nodeStruct, 95 | firstIndex: 0, 96 | lastIndex: ops - 1, 97 | version: 0 98 | }); 99 | } 100 | 101 | function runBenchmark(benchmark, ops, fn) { 102 | var start = new Date(); 103 | benchmark(ops, function () { 104 | var end = new Date(); 105 | fn(end - start); 106 | }); 107 | } 108 | 109 | function printResult(benchmark, ops, runtime) { 110 | var opssec = (ops / runtime) * 1000; 111 | console.log( 112 | benchmark.name, 113 | 'ops #', ops, 114 | 'runtime', runtime / 1000, 115 | 'sec, ops/sec', opssec 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /src/socketio-worker/inside.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // socket.io is enclosed inside worker, 4 | // because it behaves unpredictably + is slow 5 | // (I don't like socket.io.) 6 | import socketIO from 'socket.io-client'; 7 | import type { Socket as SocketIO } from 'socket.io-client'; 8 | 9 | export type InMessage = { 10 | type: 'init', 11 | endpoint: string, 12 | connectionType: string, 13 | } | { 14 | type: 'observe', 15 | event: string, 16 | } | { 17 | type: 'unobserve', 18 | event: string, 19 | } | { 20 | type: 'subscribe', 21 | event: string, 22 | values: Array, 23 | } | { 24 | type: 'send', 25 | message: Object, 26 | id: number, 27 | } | { 28 | type: 'close', 29 | } 30 | 31 | export type OutMessage = { 32 | type: 'emit', 33 | event: string, 34 | data: any, 35 | } | { 36 | type: 'sendReply', 37 | reply: any, 38 | id: number, 39 | } | { 40 | type: 'initDone', 41 | } | { 42 | type: 'initError', 43 | } 44 | 45 | let socket: ?SocketIO; 46 | const events: {[key: number]: Function} = {}; 47 | 48 | // eslint-disable-next-line no-undef 49 | onmessage = (event: {data: string}) => { 50 | const data = JSON.parse(event.data); 51 | 52 | if (data.type === 'init') { 53 | const { endpoint, connectionType } = data; 54 | socket = socketIO(endpoint, { 55 | transports: [connectionType], 56 | reconnection: false, 57 | }); 58 | socket.on('connect', () => doPostMessage({ 59 | type: 'initDone', 60 | })); 61 | socket.on('connect_error', () => { 62 | doPostMessage({ 63 | type: 'initError', 64 | }); 65 | // eslint-disable-next-line no-restricted-globals,no-undef 66 | close(); 67 | }); 68 | } 69 | 70 | if (data.type === 'close') { 71 | // a hack to prevent Firefox errors in karma tests 72 | // it doesn't break anything - since on closing the worker, 73 | // no timeouts will ever happen anyway 74 | try { 75 | // eslint-disable-next-line no-global-assign,no-native-reassign 76 | setTimeout = function fun() {}; 77 | } catch (e) { 78 | // intentionally empty - thread is closing anyway 79 | } 80 | 81 | if (socket) { 82 | socket.disconnect(); 83 | } 84 | socket = null; 85 | // eslint-disable-next-line no-restricted-globals,no-undef 86 | close(); 87 | } 88 | 89 | if (data.type === 'observe') { 90 | const eventFunction = (reply) => { 91 | doPostMessage({ 92 | type: 'emit', 93 | event: data.event, 94 | data: reply, 95 | }); 96 | }; 97 | events[data.id] = eventFunction; 98 | if (socket) { 99 | socket.on(data.event, eventFunction); 100 | } 101 | } 102 | 103 | if (data.type === 'unobserve') { 104 | const eventFunction = events[data.id]; 105 | if (socket != null) { 106 | socket.removeListener(data.event, eventFunction); 107 | } 108 | delete events[data.id]; 109 | } 110 | 111 | if (data.type === 'subscribe') { 112 | if (socket) { 113 | socket.emit('subscribe', data.event, ...data.values); 114 | } 115 | } 116 | 117 | if (data.type === 'send' && socket) { 118 | socket.send(data.message, (reply) => { 119 | doPostMessage({ 120 | type: 'sendReply', 121 | reply, 122 | id: data.id, 123 | }); 124 | }); 125 | } 126 | }; 127 | 128 | function doPostMessage(data: Object) { 129 | /* $FlowIssue worker postMessage missing */ // eslint-disable-next-line no-undef 130 | postMessage( 131 | JSON.stringify(data), 132 | ); 133 | } 134 | -------------------------------------------------------------------------------- /test/monitor-account.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-global-assign */ 2 | import { MockBitcore } from './_mock-bitcore'; 3 | import { WorkerDiscovery } from '../src/discovery/worker-discovery'; 4 | import fixtures from './fixtures/monitor-account.json'; 5 | 6 | import { discoveryWorkerFactory, xpubWorker, xpubFilePromise } from './_worker-helper'; 7 | 8 | const hasWasm = typeof WebAssembly !== 'undefined'; 9 | if (hasWasm) { 10 | monitorAccount(true); 11 | } 12 | monitorAccount(false); 13 | 14 | function monitorAccount(enableWebassembly) { 15 | const desc = enableWebassembly ? ' wasm' : ' no wasm'; 16 | describe(`monitor account${desc}`, () => { 17 | fixtures.forEach((fixtureOrig) => { 18 | const fixture = JSON.parse(JSON.stringify(fixtureOrig)); 19 | it(fixture.name, function f(doneOrig) { 20 | if (typeof jest !== 'undefined') { 21 | console.warn(fixture.name); 22 | jest.setTimeout(30 * 1000); 23 | } else { 24 | this.timeout(30 * 1000); 25 | } 26 | let wasmOld; 27 | if (!enableWebassembly && hasWasm) { 28 | wasmOld = WebAssembly; 29 | WebAssembly = undefined; 30 | } 31 | const { spec } = fixture; 32 | const doneWasm = (x) => { 33 | if (!enableWebassembly && hasWasm) { 34 | WebAssembly = wasmOld; 35 | } 36 | doneOrig(x); 37 | }; 38 | const blockchain = new MockBitcore(spec, doneWasm); 39 | const discovery = new WorkerDiscovery( 40 | discoveryWorkerFactory, 41 | xpubWorker, 42 | xpubFilePromise, 43 | blockchain, 44 | ); 45 | const stream = discovery.monitorAccountActivity( 46 | fixture.start, 47 | fixture.xpub, 48 | fixture.network, 49 | fixture.segwit, 50 | fixture.cashaddr, 51 | fixture.gap, 52 | fixture.timeOffset, 53 | ); 54 | const done = (x) => { 55 | stream.dispose(); 56 | doneWasm(x); 57 | }; 58 | 59 | stream.values.attach((res) => { 60 | if (!(res instanceof Error)) { 61 | if (!blockchain.errored) { 62 | if (JSON.stringify(res) !== JSON.stringify(fixture.end)) { 63 | console.log('Discovery result', JSON.stringify(res, null, 2)); 64 | console.log('Fixture', JSON.stringify(fixture.end, null, 2)); 65 | done(new Error('Result not the same')); 66 | } else if (blockchain.spec.length > 0) { 67 | console.log(JSON.stringify(blockchain.spec)); 68 | done(new Error('Some spec left on end')); 69 | } else { 70 | done(); 71 | } 72 | } 73 | } else { 74 | const err = res.message; 75 | if (!(err.startsWith(fixture.endError))) { 76 | console.log('Discovery result', JSON.stringify(err, null, 2)); 77 | console.log('Fixture', JSON.stringify(fixture.endError, null, 2)); 78 | done(new Error('Result not the same')); 79 | } else if (blockchain.spec.length > 0) { 80 | console.log(JSON.stringify(blockchain.spec)); 81 | done(new Error('Some spec left on end')); 82 | } else { 83 | done(); 84 | } 85 | } 86 | }); 87 | }); 88 | }); 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /test/discover-account.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-global-assign */ 2 | import assert from 'assert'; 3 | import { MockBitcore } from './_mock-bitcore'; 4 | import { WorkerDiscovery } from '../src/discovery/worker-discovery'; 5 | import fixtures from './fixtures/discover-account.json'; 6 | 7 | import { discoveryWorkerFactory, xpubWorker, xpubFilePromise } from './_worker-helper'; 8 | 9 | const hasWasm = typeof WebAssembly !== 'undefined'; 10 | if (hasWasm) { 11 | discoverAccount(true); 12 | } 13 | discoverAccount(false); 14 | 15 | function discoverAccount(enableWebassembly) { 16 | const desc = enableWebassembly ? ' wasm' : ' no wasm'; 17 | describe(`discover account${desc}`, () => { 18 | fixtures.forEach((fixtureOrig) => { 19 | const fixture = JSON.parse(JSON.stringify(fixtureOrig)); 20 | it(fixture.name, function f(doneOrig) { 21 | if (typeof jest !== 'undefined') { 22 | console.warn(fixture.name); 23 | jest.setTimeout(30 * 1000); 24 | } else { 25 | this.timeout(30 * 1000); 26 | } 27 | let wasmOld; 28 | if (!enableWebassembly && hasWasm) { 29 | wasmOld = WebAssembly; 30 | WebAssembly = undefined; 31 | } 32 | const doneWasm = (x) => { 33 | if (!enableWebassembly && hasWasm) { 34 | WebAssembly = wasmOld; 35 | } 36 | doneOrig(x); 37 | }; 38 | const blockchain = new MockBitcore(fixture.spec, doneWasm); 39 | const discovery = new WorkerDiscovery( 40 | discoveryWorkerFactory, 41 | xpubWorker, 42 | xpubFilePromise, 43 | blockchain, 44 | ); 45 | const stream = discovery.discoverAccount( 46 | fixture.start, 47 | fixture.xpub, 48 | fixture.network, 49 | fixture.segwit, 50 | fixture.cashaddr, 51 | fixture.gap, 52 | fixture.timeOffset, 53 | ); 54 | const done = (x) => { 55 | stream.dispose(); 56 | discovery.destroy(); 57 | doneWasm(x); 58 | }; 59 | stream.ending.then((res) => { 60 | if (!blockchain.errored) { 61 | if (JSON.stringify(res) !== JSON.stringify(fixture.end)) { 62 | console.log('Discovery result', JSON.stringify(res, null, 2)); 63 | console.log('Fixture', JSON.stringify(fixture.end, null, 2)); 64 | assert.deepStrictEqual(res, fixture.end); 65 | done(new Error('Result not the same')); 66 | } else if (blockchain.spec.length > 0) { 67 | console.log(JSON.stringify(blockchain.spec)); 68 | done(new Error('Some spec left on end')); 69 | } else { 70 | done(); 71 | } 72 | } 73 | }, (oerr) => { 74 | let err = oerr; 75 | if (oerr instanceof Error) { 76 | err = err.message; 77 | } 78 | if (!(err.startsWith(fixture.endError))) { 79 | console.log('Discovery result', JSON.stringify(err, null, 2)); 80 | console.log('Fixture', JSON.stringify(fixture.endError, null, 2)); 81 | done(new Error('Result not the same')); 82 | } else if (blockchain.spec.length > 0) { 83 | console.log(JSON.stringify(blockchain.spec)); 84 | done(new Error('Some spec left on end')); 85 | } else { 86 | done(); 87 | } 88 | }); 89 | }); 90 | }); 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import h from 'virtual-dom/h'; 3 | // eslint-disable-next-line import/no-extraneous-dependencies 4 | import diff from 'virtual-dom/diff'; 5 | // eslint-disable-next-line import/no-extraneous-dependencies 6 | import patch from 'virtual-dom/patch'; 7 | // eslint-disable-next-line import/no-extraneous-dependencies 8 | import createElement from 'virtual-dom/create-element'; 9 | import { BitcoreBlockchain } from '../src/bitcore'; 10 | import { WorkerDiscovery } from '../src/discovery/worker-discovery'; 11 | 12 | // setting up workers 13 | const fastXpubWorker = new Worker('fastxpub.js'); 14 | const fastXpubWasmFilePromise = fetch('fastxpub.wasm') 15 | .then(response => (response.ok ? response.arrayBuffer() : Promise.reject(new Error('failed to load')))); 16 | 17 | const socketWorkerFactory = () => new Worker('./socket-worker.js'); 18 | const discoveryWorkerFactory = () => new Worker('./discovery-worker.js'); 19 | 20 | function renderTx(tx) { 21 | const className = tx.invalidTransaction ? 'invalid' : 'valid'; 22 | return h('tr', { className }, [ 23 | h('td', tx.hash), 24 | h('td', tx.height ? tx.height.toString() : 'unconfirmed'), 25 | h('td', tx.value.toString()), 26 | h('td', tx.type), 27 | h('td', tx.targets.map(t => h('span', `${t.address} (${t.value}) `))), 28 | ]); 29 | } 30 | 31 | function renderAccount(account) { 32 | if (typeof account.info === 'number') { 33 | return [ 34 | h('h3', `${account.xpub}`), 35 | h('div', `Loading ${account.info} transactions)`), 36 | ]; 37 | } 38 | return [ 39 | h('h3', `${account.xpub}`), 40 | h('div', `Balance: ${account.info.balance}`), 41 | h('table', account.info.transactions.map(renderTx)), 42 | ]; 43 | } 44 | 45 | function render(state) { 46 | return h('div', state.map(renderAccount)); 47 | } 48 | 49 | const appState = []; 50 | const processes = []; 51 | let tree = render(appState); 52 | let rootNode = createElement(tree); 53 | 54 | document.body.appendChild(rootNode); 55 | 56 | function refresh() { 57 | const newTree = render(appState); 58 | const patches = diff(tree, newTree); 59 | rootNode = patch(rootNode, patches); 60 | tree = newTree; 61 | } 62 | 63 | function discover(xpubs, discovery, network, segwit, cashaddr) { 64 | let done = 0; 65 | xpubs.forEach((xpub, i) => { 66 | const process = discovery.discoverAccount(null, xpub, network, segwit ? 'p2sh' : 'off', cashaddr, 20, 0); 67 | appState[i] = { xpub, info: 0 }; 68 | 69 | process.stream.values.attach((status) => { 70 | appState[i] = { xpub, info: status.transactions }; 71 | refresh(); 72 | }); 73 | process.ending.then((info) => { 74 | appState[i] = { xpub, info }; 75 | refresh(); 76 | done++; 77 | if (done === xpubs.length) { 78 | console.timeEnd('portfolio'); 79 | } 80 | }); 81 | processes.push(process); 82 | refresh(); 83 | }); 84 | console.time('portfolio'); 85 | } 86 | 87 | 88 | window.run = () => { 89 | window.clear(); 90 | const XPUBS = document.getElementById('xpubs').value.split(';'); 91 | const BITCORE_URLS = document.getElementById('urls').value.split(';'); 92 | const selected = document.getElementById('network').value; 93 | const d = window.data[selected]; 94 | 95 | const blockchain = new BitcoreBlockchain(BITCORE_URLS, socketWorkerFactory, d.network); 96 | 97 | const discovery = new WorkerDiscovery( 98 | discoveryWorkerFactory, 99 | fastXpubWorker, 100 | fastXpubWasmFilePromise, 101 | blockchain, 102 | ); 103 | const cashaddr = selected === 'bitcoincash'; 104 | const segwit = selected.indexOf('Segwit') >= 0; 105 | discover(XPUBS, discovery, d.network, segwit, cashaddr); 106 | }; 107 | 108 | window.stop = () => { 109 | processes.forEach(p => p.dispose()); 110 | console.timeEnd('portfolio'); 111 | }; 112 | 113 | window.clear = () => { 114 | appState.splice(0, appState.length); 115 | refresh(); 116 | }; 117 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | setup: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [12.x, 14.x, 15.x] 15 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - uses: actions/cache@v2 24 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 25 | with: 26 | path: node_modules 27 | key: ${{ runner.os }}-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }} 28 | restore-keys: | 29 | ${{ runner.os }}-${{ matrix.node-version }}- 30 | - run: yarn install 31 | 32 | flow: 33 | needs: setup 34 | runs-on: ubuntu-latest 35 | strategy: 36 | matrix: 37 | node-version: [12.x, 14.x, 15.x] 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions/cache@v2 41 | with: 42 | path: node_modules 43 | key: ${{ runner.os }}-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }} 44 | - run: yarn flow 45 | 46 | eslint: 47 | needs: setup 48 | runs-on: ubuntu-latest 49 | strategy: 50 | matrix: 51 | node-version: [12.x, 14.x, 15.x] 52 | steps: 53 | - uses: actions/checkout@v2 54 | - name: Load node_modules 55 | uses: actions/cache@v2 56 | with: 57 | path: node_modules 58 | key: ${{ runner.os }}-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }} 59 | - run: yarn eslint 60 | 61 | build-example: 62 | needs: ["flow", "eslint"] 63 | runs-on: ubuntu-latest 64 | strategy: 65 | matrix: 66 | node-version: [12.x, 14.x, 15.x] 67 | steps: 68 | - uses: actions/checkout@v2 69 | - name: Load node_modules 70 | uses: actions/cache@v2 71 | with: 72 | path: node_modules 73 | key: ${{ runner.os }}-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }} 74 | - run: yarn build-example 75 | 76 | unit: 77 | needs: ["flow", "eslint"] 78 | runs-on: ubuntu-latest 79 | strategy: 80 | matrix: 81 | node-version: [12.x, 14.x, 15.x] 82 | steps: 83 | - uses: actions/checkout@v2 84 | - name: Load node_modules 85 | uses: actions/cache@v2 86 | with: 87 | path: node_modules 88 | key: ${{ runner.os }}-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }} 89 | - run: yarn unit 90 | 91 | coverage: 92 | needs: ["flow", "eslint"] 93 | runs-on: ubuntu-latest 94 | strategy: 95 | matrix: 96 | node-version: [12.x, 14.x, 15.x] 97 | steps: 98 | - uses: actions/checkout@v2 99 | - name: Load node_modules 100 | uses: actions/cache@v2 101 | with: 102 | path: node_modules 103 | key: ${{ runner.os }}-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }} 104 | - run: yarn coverage 105 | 106 | fastxpub: 107 | needs: ["flow", "eslint"] 108 | runs-on: ubuntu-latest 109 | strategy: 110 | matrix: 111 | node-version: [12.x, 14.x, 15.x] 112 | steps: 113 | - uses: actions/checkout@v2 114 | - run: git submodule update --init --recursive 115 | - run: cd fastxpub && make clean && make docker-build && make test && cd .. 116 | # TODO: cache fast xpub 117 | 118 | karma-chrome: 119 | needs: ["fastxpub"] 120 | runs-on: ubuntu-latest 121 | strategy: 122 | matrix: 123 | node-version: [12.x, 14.x, 15.x] 124 | steps: 125 | - uses: actions/checkout@v2 126 | - name: Load node_modules 127 | uses: actions/cache@v2 128 | with: 129 | path: node_modules 130 | key: ${{ runner.os }}-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }} 131 | # xvfb is required to run karma tests 132 | - run: sudo apt-get install xvfb 133 | - run: xvfb-run --auto-servernum yarn karma-chrome 134 | 135 | karma-firefox: 136 | needs: ["fastxpub"] 137 | runs-on: ubuntu-latest 138 | strategy: 139 | matrix: 140 | node-version: [12.x, 14.x, 15.x] 141 | steps: 142 | - uses: actions/checkout@v2 143 | - name: Load node_modules 144 | uses: actions/cache@v2 145 | with: 146 | path: node_modules 147 | key: ${{ runner.os }}-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }} 148 | # xvfb is required to run karma tests 149 | - run: sudo apt-get install xvfb 150 | - run: xvfb-run --auto-servernum yarn karma-firefox 151 | 152 | -------------------------------------------------------------------------------- /src/discovery/worker/outside/channel.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import queue from 'queue'; 4 | import type { 5 | InMessage, 6 | OutMessage, 7 | PromiseRequestOutMessage, 8 | PromiseResponseType, 9 | PromiseRequestType, 10 | StreamRequestOutMessage, 11 | StreamRequestType, 12 | } from '../types'; 13 | 14 | import { Emitter, Stream } from '../../../utils/stream'; 15 | import type { AccountInfo } from '../../index'; 16 | 17 | // eslint-disable-next-line no-undef 18 | type WorkerFactory = () => Worker; 19 | 20 | // will get injected 21 | type GetPromise = (p: PromiseRequestType) => Promise; 22 | type GetStream = (p: StreamRequestType) => Stream; 23 | 24 | const CONCURRENT_WORKERS = 4; 25 | const q = queue({ concurrency: CONCURRENT_WORKERS, autostart: true }); 26 | 27 | export class WorkerChannel { 28 | // eslint-disable-next-line no-undef 29 | w: Promise<{worker: Worker, finish: () => void}>; 30 | 31 | messageEmitter: Emitter = new Emitter(); 32 | 33 | getPromise: GetPromise; 34 | 35 | getStream: GetStream; 36 | 37 | constructor( 38 | f: WorkerFactory, 39 | getPromise: GetPromise, 40 | getStream: GetStream, 41 | ) { 42 | this.getPromise = getPromise; 43 | this.getStream = getStream; 44 | 45 | this.w = new Promise((resolve) => { 46 | q.push((cb) => { 47 | const worker = f(); 48 | const finish = cb; 49 | 50 | // $FlowIssue 51 | worker.onmessage = (event: {data: OutMessage}) => { 52 | // eslint-disable-next-line 53 | const data: OutMessage = event.data; 54 | this.messageEmitter.emit(data); 55 | }; 56 | 57 | resolve({ worker, finish }); 58 | }); 59 | }); 60 | 61 | this.messageEmitter.attach((message) => { 62 | if (message.type === 'promiseRequest') { 63 | this.handlePromiseRequest(message); 64 | } 65 | if (message.type === 'streamRequest') { 66 | this.handleStreamRequest(message); 67 | } 68 | }); 69 | } 70 | 71 | postToWorker(m: InMessage) { 72 | this.w.then((w) => { 73 | w.worker.postMessage(m); 74 | }); 75 | } 76 | 77 | resPromise(onFinish: () => void): Promise { 78 | return new Promise((resolve, reject) => { 79 | this.messageEmitter.attach((message, detach) => { 80 | if (message.type === 'result') { 81 | resolve(message.result); 82 | detach(); 83 | onFinish(); 84 | this.w.then((w) => { 85 | w.worker.terminate(); 86 | w.finish(); 87 | }); 88 | } 89 | if (message.type === 'error') { 90 | reject(new Error(message.error)); 91 | detach(); 92 | onFinish(); 93 | this.w.then((w) => { 94 | w.worker.terminate(); 95 | w.finish(); 96 | }); 97 | } 98 | }); 99 | }); 100 | } 101 | 102 | handlePromiseRequest(request: PromiseRequestOutMessage) { 103 | const promise = this.getPromise(request.request); 104 | 105 | promise.then((result) => { 106 | // $FlowIssue I overload Flow logic a bit here 107 | const r: PromiseResponseType = { 108 | type: request.request.type, 109 | response: result, 110 | }; 111 | this.postToWorker({ 112 | type: 'promiseResponseSuccess', 113 | id: request.id, 114 | response: r, 115 | }); 116 | }, (error) => { 117 | const message = error.message == null ? error.toString() : error.message.toString(); 118 | this.postToWorker({ 119 | type: 'promiseResponseFailure', 120 | failure: message, 121 | id: request.id, 122 | }); 123 | }); 124 | } 125 | 126 | handleStreamRequest(request: StreamRequestOutMessage) { 127 | const stream = this.getStream(request.request); 128 | 129 | stream.values.attach((value) => { 130 | this.postToWorker({ 131 | type: 'streamResponseUpdate', 132 | id: request.id, 133 | update: { 134 | type: request.request.type, 135 | response: value, 136 | }, 137 | }); 138 | }); 139 | stream.finish.attach(() => { 140 | this.postToWorker({ 141 | type: 'streamResponseFinish', 142 | id: request.id, 143 | }); 144 | }); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/discovery/worker/inside/channel.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type { Network as BitcoinJsNetwork } from '@trezor/utxo-lib'; 4 | import { deferred } from '../../../utils/deferred'; 5 | import { Emitter, Stream } from '../../../utils/stream'; 6 | import type { 7 | InMessage, 8 | PromiseRequestType, 9 | StreamRequestType, 10 | ChunkDiscoveryInfo, 11 | OutMessage, 12 | } from '../types'; 13 | 14 | import type { 15 | AccountInfo, 16 | } from '../../index'; 17 | 18 | // Code for all communication with outside 19 | 20 | // There is a mechanism for "sending" Promise from outside here 21 | // - first I send promiseRequest from worker to outside, 22 | // and I either get promiseResponseSuccess or promiseResponseFailure 23 | // 24 | // Similar logic for Stream - I get streamRequest and 25 | // streamResponseUpdate and streamResponseFinish 26 | // 27 | // It's maybe a little overkill :( but it allows me to have multiple streams 28 | // and promises over one worker communication 29 | 30 | let lastId: number = 0; 31 | 32 | const messageEmitter: Emitter = new Emitter(); 33 | 34 | function askPromise(request: PromiseRequestType): Promise { 35 | const id = lastId + 1; 36 | lastId++; 37 | doPostMessage({ 38 | type: 'promiseRequest', 39 | request, 40 | id, 41 | }); 42 | const dfd = deferred(); 43 | messageEmitter.attach((message, detach) => { 44 | if (message.id === id) { 45 | if (message.type === 'promiseResponseSuccess') { 46 | detach(); 47 | dfd.resolve(message.response.response); 48 | } 49 | if (message.type === 'promiseResponseFailure') { 50 | detach(); 51 | dfd.reject(new Error(message.failure)); 52 | } 53 | } 54 | }); 55 | return dfd.promise; 56 | } 57 | 58 | function askStream(request: StreamRequestType): Stream { 59 | const id = lastId + 1; 60 | lastId++; 61 | doPostMessage({ 62 | type: 'streamRequest', 63 | request, 64 | id, 65 | }); 66 | return new Stream((update, finish) => { 67 | let emitterDetach = () => {}; 68 | messageEmitter.attach((message: InMessage, detach) => { 69 | emitterDetach = detach; 70 | if (message.id === id) { 71 | if (message.type === 'streamResponseUpdate') { 72 | update(message.update.response); 73 | } 74 | if (message.type === 'streamResponseFinish') { 75 | detach(); 76 | finish(); 77 | } 78 | } 79 | }); 80 | return () => { 81 | emitterDetach(); 82 | }; 83 | }); 84 | } 85 | 86 | export function lookupSyncStatus(): Promise { 87 | return askPromise({ type: 'lookupSyncStatus' }); 88 | } 89 | 90 | export function lookupBlockHash(height: number): Promise { 91 | return askPromise({ type: 'lookupBlockHash', height }); 92 | } 93 | 94 | export function chunkTransactions( 95 | chainId: number, 96 | firstIndex: number, 97 | lastIndex: number, 98 | startBlock: number, 99 | endBlock: number, 100 | pseudoCount: number, 101 | addresses: ?Array, 102 | ): Stream { 103 | return askStream({ 104 | type: 'chunkTransactions', 105 | chainId, 106 | firstIndex, 107 | lastIndex, 108 | startBlock, 109 | endBlock, 110 | pseudoCount, 111 | addresses, 112 | }).map((k: ChunkDiscoveryInfo | string): (ChunkDiscoveryInfo | Error) => { 113 | if (typeof k === 'string') { 114 | return new Error(k); 115 | } 116 | return k; 117 | }); 118 | } 119 | 120 | export function returnSuccess(result: AccountInfo): void { 121 | doPostMessage({ type: 'result', result }); 122 | } 123 | 124 | export function returnError(error: Error | string): void { 125 | const errorMessage: string = error instanceof Error ? error.message : error.toString(); 126 | doPostMessage({ type: 'error', error: errorMessage }); 127 | } 128 | 129 | function doPostMessage(data: OutMessage) { 130 | // eslint-disable-next-line no-undef,no-restricted-globals 131 | self.postMessage( 132 | data, 133 | ); 134 | } 135 | 136 | // eslint-disable-next-line no-undef,no-restricted-globals 137 | self.onmessage = (event: {data: InMessage}) => { 138 | const { data } = event; 139 | messageEmitter.emit(data); 140 | }; 141 | 142 | const initDfd = deferred(); 143 | export const initPromise: Promise<{ 144 | accountInfo: ?AccountInfo, 145 | network: BitcoinJsNetwork, 146 | xpub: string, 147 | segwit: boolean, 148 | webassembly: boolean, 149 | cashAddress: boolean, 150 | gap: number, 151 | timeOffset: number, 152 | }> = initDfd.promise; 153 | 154 | messageEmitter.attach((message, detach) => { 155 | if (message.type === 'init') { 156 | detach(); 157 | initDfd.resolve({ 158 | accountInfo: message.state, 159 | network: message.network, 160 | xpub: message.xpub, 161 | segwit: message.segwit, 162 | webassembly: message.webassembly, 163 | cashAddress: message.cashAddress, 164 | gap: message.gap, 165 | timeOffset: message.timeOffset, 166 | }); 167 | } 168 | }); 169 | 170 | const startDiscoveryDfd = deferred(); 171 | export const startDiscoveryPromise: Promise = startDiscoveryDfd.promise; 172 | 173 | messageEmitter.attach((message, detach) => { 174 | if (message.type === 'startDiscovery') { 175 | detach(); 176 | startDiscoveryDfd.resolve(); 177 | } 178 | }); 179 | -------------------------------------------------------------------------------- /src/discovery/worker/types.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // Only types, no actual code here 4 | 5 | import type { 6 | Transaction as BitcoinJsTransaction, 7 | Network as BitcoinJsNetwork, 8 | } from '@trezor/utxo-lib'; 9 | import type { 10 | TransactionWithHeight, 11 | } from '../../bitcore'; 12 | 13 | import type { 14 | AccountInfo, 15 | TargetInfo, 16 | } from '../index'; 17 | 18 | 19 | /* ----- messages INTO into worker, from handler ------ */ 20 | 21 | // All the info about addresses plus transactions 22 | // It is sent when we ask for transactions 23 | export type ChunkDiscoveryInfo = { 24 | addresses: Array, 25 | transactions: Array, 26 | }; 27 | 28 | // responses for general promises 29 | export type PromiseResponseType = { 30 | type: 'lookupSyncStatus', 31 | response: number, 32 | } | { 33 | type: 'lookupBlockHash', 34 | response: string, 35 | } | { 36 | type: 'doesTransactionExist', 37 | response: boolean, 38 | }; 39 | 40 | // response for transactions 41 | type StreamUpdateType = { 42 | type: 'chunkTransactions', 43 | response: ChunkDiscoveryInfo | string, 44 | }; 45 | 46 | // This is message INTO worker 47 | export type InMessage = { 48 | // starting worker 49 | type: 'init', 50 | state: ?AccountInfo, 51 | network: BitcoinJsNetwork, 52 | webassembly: boolean, 53 | xpub: string, 54 | segwit: boolean, 55 | cashAddress: boolean, 56 | gap: number, 57 | 58 | // what (new Date().getTimezoneOffset()) returns 59 | // note that it is NEGATIVE from the UTC string timezone 60 | // so, UTC+2 timezone returns -120... 61 | // it's javascript, it's insane by default 62 | timeOffset: number, 63 | } | { 64 | // starting discovery after init 65 | type: 'startDiscovery', 66 | } | { 67 | // general message for sending promise result into worker 68 | type: 'promiseResponseSuccess', 69 | response: PromiseResponseType, 70 | id: number, 71 | } | { 72 | // general message for sending promise result into worker 73 | type: 'promiseResponseFailure', 74 | failure: string, 75 | id: number, 76 | } | { 77 | // general message for sending stream update into worker 78 | type: 'streamResponseUpdate', 79 | update: StreamUpdateType, 80 | id: number, 81 | } | { 82 | // general message for sending stream update into worker 83 | type: 'streamResponseFinish', 84 | id: number, 85 | }; 86 | 87 | /* ----- messages OUT from worker, into handler ------ */ 88 | 89 | // Promises I can ask for 90 | export type PromiseRequestType = { 91 | type: 'lookupSyncStatus', 92 | } | { 93 | type: 'lookupBlockHash', 94 | height: number, 95 | } | { 96 | type: 'doesTransactionExist', 97 | txid: string, 98 | } 99 | 100 | // Streams I can ask for (right now only one) 101 | export type StreamRequestType = { 102 | type: 'chunkTransactions', 103 | chainId: number, 104 | firstIndex: number, 105 | lastIndex: number, 106 | startBlock: number, 107 | endBlock: number, 108 | pseudoCount: number, 109 | addresses: ?Array, 110 | } 111 | 112 | // general message, asking for promise 113 | export type PromiseRequestOutMessage = { 114 | type: 'promiseRequest', 115 | request: PromiseRequestType, 116 | id: number, 117 | } 118 | 119 | // general message, asking for stream 120 | export type StreamRequestOutMessage = { 121 | type: 'streamRequest', 122 | request: StreamRequestType, 123 | id: number, 124 | }; 125 | 126 | export type OutMessage = 127 | PromiseRequestOutMessage | 128 | StreamRequestOutMessage | { 129 | // result from the discovery 130 | type: 'result', 131 | result: AccountInfo, 132 | } | { 133 | // error from the discovery (shouldn't happen, but can :)) 134 | type: 'error', 135 | error: string, 136 | }; 137 | 138 | /* ----- types used internally IN the worker ------ */ 139 | 140 | // Info about transaction, with some derived information 141 | export type ChainNewTransaction = { 142 | tx: BitcoinJsTransaction, 143 | invalidTransaction: boolean, 144 | height: ?number, 145 | inputAddresses: Array, // might be undecodable 146 | outputAddresses: Array, 147 | timestamp: ?number, 148 | hash: string, 149 | vsize: number, 150 | } 151 | 152 | // New transactions on a chain 153 | export type ChainNewTransactions = {[id: string]: ChainNewTransaction}; 154 | 155 | // Simple map address => id, id 156 | export type AddressToPath = {[address: string]: [number, number]}; 157 | 158 | // What gets out of discovery 159 | export type ChainNewInfo = { 160 | allAddresses: Array, 161 | newTransactions: ChainNewTransactions, 162 | } 163 | 164 | // New additional info about an account, on two chains 165 | export type AccountNewInfo = { 166 | main: ChainNewInfo, 167 | change: ChainNewInfo, 168 | } 169 | 170 | export type Block = { hash: string, height: number }; 171 | export type BlockRange = { firstHeight: number, last: Block }; 172 | 173 | // export type InputsForAnalysis = Array<{id: string, index: number}> 174 | 175 | export type TransactionInfoBalanceless = { 176 | isCoinbase: boolean, 177 | timestamp: ?number, 178 | dateInfoDayFormat: ?string, 179 | dateInfoTimeFormat: ?string, 180 | hash: string, 181 | 182 | height: ?number, 183 | confirmations: ?number, 184 | 185 | targets: Array, 186 | myOutputs: {[i: number]: TargetInfo}, 187 | 188 | type: 'self' | 'recv' | 'sent', 189 | 190 | value: string, 191 | 192 | inputs: Array<{id: string, index: number}>, // needing this for analysis 193 | 194 | tsize: number, // total size - in case of segwit, total, with segwit data 195 | vsize: number, // virtual size - segwit concept - same as size in non-segwit 196 | invalidTransaction?: boolean, // true if we are not able to parse transaction correctly 197 | } 198 | 199 | export type TargetsType = { 200 | targets: Array, 201 | myOutputs: {[i: number]: TargetInfo}, 202 | type: 'self' | 'recv' | 'sent', 203 | value: string, 204 | }; 205 | -------------------------------------------------------------------------------- /src/discovery/worker/inside/derive-utxos.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type { Input as BitcoinJsInput } from '@trezor/utxo-lib'; 4 | import { 5 | Transaction as BitcoinJsTransaction, 6 | } from '@trezor/utxo-lib'; 7 | import type { 8 | ChainNewTransactions, 9 | AddressToPath, 10 | AccountNewInfo, 11 | } from '../types'; 12 | import type { 13 | TransactionInfo, 14 | AccountInfo, 15 | UtxoInfo, 16 | } from '../../index'; 17 | 18 | import { 19 | objectValues, 20 | getInputId, 21 | } from '../utils'; 22 | 23 | 24 | // what is hapenning here: 25 | // I have a state with old utxo set 26 | // and some new transactions 27 | // and the only thing that can happen is that new utxos arrive 28 | // from the new transactions, or the old utxos are spent 29 | // The way this is done, no new utxos are added "back" 30 | // from the old transactions. 31 | // This is to save time - we do not need to go through old 32 | // transactions, just through the new ones 33 | // 34 | // Note that this on itself could cause a problem 35 | // If there is outgoing transaction in a mempool in the old state 36 | // that is later removed, 37 | // old utxos need to be added; 38 | // I find such old utxos in index.js in findDeleted 39 | // and later pass them here 40 | export function deriveUtxos( 41 | newInfo: AccountNewInfo, 42 | oldInfo: AccountInfo, 43 | addressToPath: AddressToPath, 44 | joined: ChainNewTransactions, 45 | ) { 46 | // First do preparations 47 | // Make set of all my transaction IDs, old and new 48 | const allTransactionHashes = deriveAllTransactionHashes( 49 | newInfo.main.newTransactions, 50 | newInfo.change.newTransactions, 51 | oldInfo.transactions, 52 | ); 53 | 54 | // Then, make set of spent outputs 55 | // (tx + ":" + id) 56 | const spentOutputs = deriveSpentOutputs( 57 | allTransactionHashes, 58 | newInfo.main.newTransactions, 59 | newInfo.change.newTransactions, 60 | oldInfo.transactions, 61 | ); 62 | 63 | // actual logic 64 | const utxos = _deriveUtxos( 65 | oldInfo.utxos, 66 | joined, 67 | addressToPath, 68 | spentOutputs, 69 | ); 70 | 71 | return utxos; 72 | } 73 | 74 | function deriveAllTransactionHashes( 75 | main: ChainNewTransactions, 76 | change: ChainNewTransactions, 77 | old: Array, 78 | ): Set { 79 | const res = new Set(); 80 | 81 | Object.keys(main).forEach((id) => { 82 | res.add(id); 83 | }); 84 | Object.keys(change).forEach((id) => { 85 | res.add(id); 86 | }); 87 | old.forEach((t) => { 88 | res.add(t.hash); 89 | }); 90 | 91 | return res; 92 | } 93 | 94 | function deriveSpentOutputs( 95 | allTransactionHashes: Set, 96 | main: ChainNewTransactions, 97 | change: ChainNewTransactions, 98 | old: Array, 99 | ): Set { 100 | const res = new Set(); 101 | 102 | // saving only mine spent outputs 103 | // (to save some time) 104 | function canTxBeMine(id: string): boolean { 105 | return allTransactionHashes.has(id); 106 | } 107 | 108 | function saveNew(ts: ChainNewTransactions) { 109 | objectValues(ts).forEach((tx) => { 110 | tx.tx.ins.forEach((inp: BitcoinJsInput) => { 111 | const i = inp.index; 112 | const id = getInputId(inp); 113 | if (canTxBeMine(id)) { 114 | res.add(`${id}:${i}`); 115 | } 116 | }); 117 | }); 118 | } 119 | 120 | old.forEach((t) => { 121 | t.inputs.forEach(({ id, index }) => { 122 | if (canTxBeMine(id)) { 123 | res.add(`${id}:${index}`); 124 | } 125 | }); 126 | }); 127 | 128 | saveNew(main); 129 | saveNew(change); 130 | 131 | return res; 132 | } 133 | 134 | function _deriveUtxos( 135 | currentUtxos: Array, 136 | newTransactions: ChainNewTransactions, 137 | addressToPath: AddressToPath, 138 | spentOutputs: Set, 139 | ): Array { 140 | const res: {[i: string]: UtxoInfo} = {}; 141 | 142 | const isOwnAddress = address => address != null 143 | && addressToPath[address] != null; 144 | 145 | const isCoinbase = tx => tx.ins.some(i => BitcoinJsTransaction.isCoinbaseHash(i.hash)); 146 | 147 | // first, delete spent utxos from current batch from staying 148 | const filteredUtxos = currentUtxos.filter((utxo) => { 149 | const ix = `${utxo.transactionHash}:${utxo.index}`; 150 | return !(spentOutputs.has(ix)); 151 | }); 152 | 153 | // second, add them to hash, so if there is new and confirmed utxo, 154 | // it will overwrite existing utxo 155 | filteredUtxos.forEach((utxo) => { 156 | const ix = `${utxo.transactionHash}:${utxo.index}`; 157 | res[ix] = utxo; 158 | }); 159 | 160 | // third, find utxos in new txs and maybe overwrite existing 161 | const newTxs = objectValues(newTransactions); 162 | newTxs.forEach(({ 163 | hash, tx, height, outputAddresses, inputAddresses, vsize, 164 | }) => { 165 | const coinbase = isCoinbase(tx); 166 | const own = inputAddresses.some(address => isOwnAddress(address)); 167 | 168 | tx.outs.forEach((o, index) => { 169 | const ix = `${hash}:${index}`; 170 | const address = outputAddresses[index]; 171 | if ((spentOutputs.has(ix)) || !isOwnAddress(address)) { 172 | return; 173 | } 174 | 175 | const addressPath = addressToPath[address]; 176 | const resIx: UtxoInfo = { 177 | index, 178 | value: typeof o.value === 'string' ? o.value : o.value.toString(), 179 | transactionHash: hash, 180 | height, 181 | coinbase, 182 | addressPath, 183 | vsize, 184 | tsize: tx.byteLength(), 185 | own, 186 | }; 187 | res[ix] = resIx; 188 | }); 189 | }); 190 | 191 | return objectValues(res); 192 | } 193 | -------------------------------------------------------------------------------- /src/build-tx/coinselect-lib/utils.js: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | // baseline estimates, used to improve performance 3 | const TX_EMPTY_SIZE = 4 + 1 + 1 + 4 + 1; 4 | // 8 bytes start, 2 times 1 byte count in/out, 1 extra byte for segwit start 5 | 6 | const TX_INPUT_BASE = 32 + 4 + 1 + 4; 7 | const TX_OUTPUT_BASE = 8 + 1; 8 | 9 | export function inputBytes(input) { 10 | if (input.script.length == null) { 11 | throw new Error('Null script length'); 12 | } 13 | return TX_INPUT_BASE + input.script.length; 14 | } 15 | 16 | export function outputBytes(output) { 17 | if (output.script.length == null) { 18 | throw new Error('Null script length'); 19 | } 20 | return TX_OUTPUT_BASE + output.script.length; 21 | } 22 | 23 | export function dustThreshold( 24 | feeRate, 25 | inputLength, 26 | outputLength, 27 | explicitDustThreshold, 28 | ) { 29 | const size = transactionBytes([ 30 | { 31 | script: { 32 | length: inputLength, 33 | }, 34 | }, 35 | ], [ 36 | { 37 | script: { 38 | length: outputLength, 39 | }, 40 | }, 41 | ]); 42 | const price = size * feeRate; 43 | const threshold = Math.max(explicitDustThreshold, price); 44 | return threshold; 45 | } 46 | 47 | export function transactionBytes(inputs, outputs) { 48 | return TX_EMPTY_SIZE 49 | + inputs.reduce((a, x) => a + inputBytes(x), 0) 50 | + outputs.reduce((a, x) => a + outputBytes(x), 0); 51 | } 52 | 53 | export function uintOrNaN(v) { 54 | if (typeof v !== 'number') return NaN; 55 | if (!Number.isFinite(v)) return NaN; 56 | if (Math.floor(v) !== v) return NaN; 57 | if (v < 0) return NaN; 58 | return v; 59 | } 60 | 61 | export function bignumberOrNaN(v): BigNumber { 62 | if (v instanceof BigNumber) return v; 63 | if (typeof v !== 'string') return new BigNumber(NaN); 64 | try { 65 | const value = new BigNumber(v); 66 | return (value.toFixed() === v && value.isInteger()) ? value : new BigNumber(NaN); 67 | } catch (error) { 68 | return new BigNumber(NaN); 69 | } 70 | } 71 | 72 | export function sumOrNaN(range, forgiving = false): BigNumber { 73 | return range.reduce((a, x) => { 74 | if (Number.isNaN(a)) return new BigNumber(NaN); 75 | const value = bignumberOrNaN(x.value); 76 | if (value.isNaN()) return forgiving ? new BigNumber(0).plus(a) : new BigNumber(NaN); 77 | return value.plus(a); 78 | }, new BigNumber(0)); 79 | } 80 | 81 | // DOGE fee policy https://github.com/dogecoin/dogecoin/issues/1650#issuecomment-722229742 82 | // 1 DOGE base fee + 1 DOGE per every started kb + 1 DOGE for every output below 1 DOGE (dust limit) 83 | export function getFee(feeRate, bytes = 0, options = {}, outputs = []) { 84 | const defaultFee = feeRate * bytes; 85 | let baseFee = options.baseFee || 0; 86 | if (baseFee && bytes) { 87 | if (options.floorBaseFee) { 88 | // increase baseFee for every started kb 89 | baseFee *= parseInt((baseFee + defaultFee) / baseFee, 10); 90 | } else { 91 | // simple increase baseFee 92 | baseFee += defaultFee; 93 | } 94 | } 95 | if (options.dustOutputFee) { 96 | // find all outputs below dust limit 97 | for (let i = 0; i < outputs.length; i++) { 98 | if (outputs[i].value && outputs[i].value - options.dustThreshold <= 0) { 99 | // increase for every output below dustThreshold 100 | baseFee += options.dustOutputFee; 101 | } 102 | } 103 | } 104 | return baseFee || defaultFee; 105 | } 106 | 107 | export function finalize( 108 | inputs, 109 | outputsO, 110 | feeRate, 111 | options, 112 | ) { 113 | const { 114 | inputLength, 115 | changeOutputLength, 116 | dustThreshold: explicitDustThreshold, 117 | } = options; 118 | let outputs = outputsO; 119 | const bytesAccum = transactionBytes(inputs, outputs); 120 | const blankOutputBytes = outputBytes({ script: { length: changeOutputLength } }); 121 | const fee = getFee(feeRate, bytesAccum, options, outputs); 122 | const feeAfterExtraOutput = getFee(feeRate, bytesAccum + blankOutputBytes, options, outputs); 123 | const sumInputs = sumOrNaN(inputs); 124 | const sumOutputs = sumOrNaN(outputs); 125 | // if sum inputs/outputs is NaN 126 | // or `fee` is greater than sum of inputs reduced by sum of outputs (use case: baseFee) 127 | // no further calculation required (not enough funds) 128 | if (sumInputs.isNaN() || sumOutputs.isNaN() || sumInputs.minus(sumOutputs).lt(fee)) { 129 | return { fee: fee.toString() }; 130 | } 131 | 132 | const remainderAfterExtraOutput = sumInputs.minus(sumOutputs.plus(feeAfterExtraOutput)); 133 | const dust = dustThreshold( 134 | feeRate, 135 | inputLength, 136 | changeOutputLength, 137 | explicitDustThreshold, 138 | ); 139 | 140 | // is it worth a change output? 141 | if (remainderAfterExtraOutput.gt(dust)) { 142 | outputs = outputs.concat({ 143 | value: remainderAfterExtraOutput.toString(), 144 | script: { 145 | length: changeOutputLength, 146 | }, 147 | }); 148 | } 149 | 150 | return { 151 | inputs, 152 | outputs, 153 | fee: sumInputs.minus(sumOrNaN(outputs)).toString(), 154 | }; 155 | } 156 | 157 | export function anyOf(algorithms) { 158 | return (utxos, outputs, feeRate, inputLength, outputLength) => { 159 | let result = { fee: Infinity }; 160 | 161 | for (let i = 0; i < algorithms.length; i++) { 162 | const algorithm = algorithms[i]; 163 | result = algorithm(utxos, outputs, feeRate, inputLength, outputLength); 164 | if (result.inputs) { 165 | return result; 166 | } 167 | } 168 | 169 | return result; 170 | }; 171 | } 172 | 173 | export function utxoScore(x, feeRate) { 174 | return new BigNumber(x.value).minus(new BigNumber(feeRate * inputBytes(x))); 175 | } 176 | -------------------------------------------------------------------------------- /src/build-tx/transaction.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { 3 | address as BitcoinJsAddress, 4 | script as BitcoinJsScript, 5 | } from '@trezor/utxo-lib'; 6 | 7 | import type { 8 | Network as BitcoinJsNetwork, 9 | } from '@trezor/utxo-lib'; 10 | import BigNumber from 'bignumber.js'; 11 | import { Permutation } from './permutation'; 12 | 13 | import type { UtxoInfo } from '../discovery'; 14 | import * as coinselect from './coinselect'; 15 | import * as request from './request'; 16 | import { convertCashAddress } from '../utils/bchaddr'; 17 | 18 | function inputComparator(aHash: Buffer, aVout: number, bHash: Buffer, bVout: number) { 19 | return reverseBuffer(aHash).compare(reverseBuffer(bHash)) || aVout - bVout; 20 | } 21 | 22 | function outputComparator(aScript: Buffer, aValue: string, bScript: Buffer, bValue: string) { 23 | return new BigNumber(aValue).comparedTo(new BigNumber(bValue)) || aScript.compare(bScript); 24 | } 25 | 26 | // types for building the transaction in trezor.js 27 | export type Output = {| 28 | path: Array, 29 | value: string, 30 | segwit: boolean, 31 | |} | {| 32 | address: string, 33 | value: string, 34 | |} | {| 35 | opReturnData: Buffer, 36 | |}; 37 | 38 | export type Input = { 39 | hash: Buffer, 40 | index: number, 41 | path: Array, // necessary for trezor.js 42 | segwit: boolean, 43 | amount?: string, // only with segwit 44 | }; 45 | 46 | export type Transaction = { 47 | inputs: Array, 48 | outputs: Permutation, // not in trezor.js, but needed for metadata saving 49 | }; 50 | 51 | export function createTransaction( 52 | allInputs: Array, 53 | selectedInputs: Array, 54 | allOutputs: Array, 55 | selectedOutputs: Array, 56 | segwit: boolean, 57 | inputAmounts: boolean, 58 | basePath: Array, 59 | changeId: number, 60 | changeAddress: string, 61 | network: BitcoinJsNetwork, 62 | skipPermutation?: boolean, 63 | ): Transaction { 64 | const convertedInputs = selectedInputs.map((input) => { 65 | const { id } = input; 66 | const richInput = allInputs[id]; 67 | return convertInput( 68 | richInput, 69 | segwit, 70 | inputAmounts, 71 | basePath, 72 | ); 73 | }); 74 | const convertedOutputs = selectedOutputs.map((output, i) => { 75 | // change is always last 76 | const isChange = i === allOutputs.length; 77 | 78 | const original: request.OutputRequestWithAddress = allOutputs[i]; // null if change 79 | 80 | if ((!isChange) && original.type === 'opreturn') { 81 | const opReturnData: string = original.dataHex; 82 | return convertOpReturnOutput(opReturnData); 83 | } 84 | // TODO refactor and get rid of FlowIssues everywhere 85 | // $FlowIssue 86 | const address = isChange ? changeAddress : original.address; 87 | const amount = output.value; 88 | return convertOutput( 89 | address, 90 | amount, 91 | network, 92 | basePath, 93 | changeId, 94 | isChange, 95 | segwit, 96 | ); 97 | }); 98 | 99 | // this syntax is forced by flow: sketchy-null-bool 100 | // i don't like it but needs to be refactored anyway... 101 | if (skipPermutation === true) { 102 | return { 103 | inputs: convertedInputs, 104 | outputs: new Permutation(convertedOutputs.map(o => o.output), convertedOutputs.map((_o, i) => i)) 105 | } 106 | } 107 | convertedInputs.sort((a, b) => inputComparator(a.hash, a.index, b.hash, b.index)); 108 | const permutedOutputs = Permutation.fromFunction(convertedOutputs, (a, b) => { 109 | const aValue: string = typeof a.output.value === 'string' ? a.output.value : '0'; 110 | const bValue: string = typeof b.output.value === 'string' ? b.output.value : '0'; 111 | return outputComparator(a.script, aValue, b.script, bValue); 112 | }).map(o => o.output); 113 | return { 114 | inputs: convertedInputs, 115 | outputs: permutedOutputs, 116 | }; 117 | } 118 | 119 | function convertInput( 120 | utxo: UtxoInfo, 121 | segwit: boolean, 122 | inputAmounts: boolean, 123 | basePath: Array, 124 | ): Input { 125 | const res = { 126 | hash: reverseBuffer(Buffer.from(utxo.transactionHash, 'hex')), 127 | index: utxo.index, 128 | path: basePath.concat([...utxo.addressPath]), 129 | segwit, 130 | }; 131 | if (inputAmounts) { 132 | return { 133 | ...res, 134 | amount: utxo.value, 135 | }; 136 | } 137 | return res; 138 | } 139 | 140 | function convertOpReturnOutput( 141 | opReturnData: string, 142 | ): { 143 | output: Output, 144 | script: Buffer, 145 | } { 146 | const opReturnDataBuffer = Buffer.from(opReturnData, 'hex'); 147 | const output = { 148 | opReturnData: opReturnDataBuffer, 149 | }; 150 | const script = BitcoinJsScript.nullData.output.encode(opReturnDataBuffer); 151 | return { 152 | output, 153 | script, 154 | }; 155 | } 156 | 157 | function convertOutput( 158 | address: string, 159 | value: string, 160 | network: BitcoinJsNetwork, 161 | basePath: Array, 162 | changeId: number, 163 | isChange: boolean, 164 | segwit: boolean, 165 | ): { 166 | output: Output, 167 | script: Buffer, 168 | } { 169 | const output: Output = isChange ? { 170 | path: [...basePath, 1, changeId], 171 | segwit, 172 | value, 173 | } : { 174 | address, 175 | value, 176 | }; 177 | 178 | return { 179 | output, 180 | script: BitcoinJsAddress.toOutputScript(convertCashAddress(address), network), 181 | }; 182 | } 183 | 184 | function reverseBuffer(src: Buffer): Buffer { 185 | const buffer = Buffer.alloc(src.length); 186 | for (let i = 0, j = src.length - 1; i <= j; ++i, --j) { 187 | buffer[i] = src[j]; 188 | buffer[j] = src[i]; 189 | } 190 | return buffer; 191 | } 192 | -------------------------------------------------------------------------------- /src/build-tx/coinselect-lib/inputs/bnb.js: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import * as utils from '../utils'; 3 | 4 | const maxTries = 1000000; 5 | 6 | function calculateEffectiveValues(utxos, feeRate) { 7 | return utxos.map((utxo) => { 8 | const value = utils.bignumberOrNaN(utxo.value); 9 | if (value.isNaN()) { 10 | return { 11 | utxo, 12 | effectiveValue: new BigNumber(0), 13 | }; 14 | } 15 | 16 | const effectiveFee = utils.inputBytes(utxo) * feeRate; 17 | const effectiveValue = value.minus(effectiveFee); 18 | return { 19 | utxo, 20 | effectiveValue, 21 | }; 22 | }); 23 | } 24 | 25 | export default function branchAndBound(factor) { 26 | return (utxos, outputs, feeRate, options) => { 27 | const { inputLength, changeOutputLength } = options; 28 | if (options.baseFee) return {}; // TODO: enable bnb algorithm for DOGE 29 | // TODO: enable bnb algorithm if required utxos are defined 30 | if (utxos.find(u => u.required)) return {}; 31 | 32 | const feeRateBigInt = utils.bignumberOrNaN(feeRate); 33 | if (feeRateBigInt.isNaN() || !feeRateBigInt.isInteger()) return {}; 34 | const feeRateNumber = feeRateBigInt.toNumber(); 35 | 36 | const costPerChangeOutput = utils.outputBytes({ 37 | script: { 38 | length: changeOutputLength, 39 | }, 40 | }) * feeRateNumber; 41 | 42 | const costPerInput = utils.inputBytes({ 43 | script: { 44 | length: inputLength, 45 | }, 46 | }) * feeRateNumber; 47 | 48 | const costOfChange = Math.floor((costPerInput + costPerChangeOutput) * factor); 49 | const txBytes = utils.transactionBytes([], outputs); 50 | 51 | const bytesAndFee = feeRateBigInt.times(txBytes); 52 | 53 | const outSum = utils.sumOrNaN(outputs); 54 | if (outSum.isNaN()) { 55 | return { fee: '0' }; 56 | } 57 | 58 | const outAccum = outSum.plus(bytesAndFee); 59 | 60 | const effectiveUtxos = calculateEffectiveValues(utxos, feeRateNumber) 61 | .filter(x => x.effectiveValue.comparedTo(new BigNumber(0)) > 0) 62 | .sort((a, b) => { 63 | const subtract = b.effectiveValue.minus(a.effectiveValue).toNumber(); 64 | if (subtract !== 0) { 65 | return subtract; 66 | } 67 | return a.utxo.i - b.utxo.i; 68 | }); 69 | 70 | const selected = search(effectiveUtxos, outAccum, costOfChange); 71 | if (selected !== null) { 72 | const inputs = []; 73 | 74 | for (let i = 0; i < effectiveUtxos.length; i++) { 75 | if (selected[i]) { 76 | inputs.push(effectiveUtxos[i].utxo); 77 | } 78 | } 79 | 80 | return utils.finalize( 81 | inputs, 82 | outputs, 83 | feeRateNumber, 84 | options, 85 | ); 86 | } 87 | 88 | return { fee: '0' }; 89 | }; 90 | } 91 | 92 | // Depth first search 93 | // Inclusion branch first (Largest First Exploration), then exclusion branch 94 | function search(effectiveUtxos, target, costOfChange) { 95 | if (effectiveUtxos.length === 0) { 96 | return null; 97 | } 98 | 99 | let tries = maxTries; 100 | 101 | const selected = []; // true -> select the utxo at this index 102 | let selectedAccum = new BigNumber(0); // sum of effective values 103 | 104 | let done = false; 105 | let backtrack = false; 106 | 107 | let remaining = effectiveUtxos.reduce((a, x) => x.effectiveValue.plus(a), new BigNumber(0)); 108 | const costRange = target.plus(costOfChange); 109 | 110 | let depth = 0; 111 | while (!done) { 112 | if (tries <= 0) { // Too many tries, exit 113 | return null; 114 | } 115 | 116 | if (selectedAccum.comparedTo(costRange) > 0) { 117 | // Selected value is out of range, go back and try other branch 118 | backtrack = true; 119 | } else if (selectedAccum.comparedTo(target) >= 0) { 120 | // Selected value is within range 121 | done = true; 122 | } else if (depth >= effectiveUtxos.length) { 123 | // Reached a leaf node, no solution here 124 | backtrack = true; 125 | } else if (selectedAccum.plus(remaining).comparedTo(target) < 0) { 126 | // Cannot possibly reach target with amount remaining 127 | if (depth === 0) { 128 | // At the first utxo, no possible selections, so exit 129 | return null; 130 | } 131 | backtrack = true; 132 | } else { // Continue down this branch 133 | // Remove this utxo from the remaining utxo amount 134 | remaining = remaining.minus(effectiveUtxos[depth].effectiveValue); 135 | // Inclusion branch first (Largest First Exploration) 136 | selected[depth] = true; 137 | selectedAccum = selectedAccum.plus(effectiveUtxos[depth].effectiveValue); 138 | depth++; 139 | } 140 | 141 | // Step back to the previous utxo and try the other branch 142 | if (backtrack) { 143 | backtrack = false; // Reset 144 | depth--; 145 | 146 | // Walk backwards to find the first utxo which has not has its second branch traversed 147 | while (!selected[depth]) { 148 | remaining = remaining.plus(effectiveUtxos[depth].effectiveValue); 149 | 150 | // Step back one 151 | depth--; 152 | 153 | if (depth < 0) { 154 | // We have walked back to the first utxo 155 | // and no branch is untraversed. No solution, exit. 156 | return null; 157 | } 158 | } 159 | 160 | // Now traverse the second branch of the utxo we have arrived at. 161 | selected[depth] = false; 162 | selectedAccum = selectedAccum.minus(effectiveUtxos[depth].effectiveValue); 163 | depth++; 164 | } 165 | tries--; 166 | } 167 | 168 | return selected; 169 | } 170 | -------------------------------------------------------------------------------- /src/discovery/worker/inside/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // This is the entry to the worker, doing account discovery + analysis 4 | 5 | import type { Network as BitcoinJsNetwork } from '@trezor/utxo-lib'; 6 | 7 | import type { AccountInfo, TransactionInfo } from '../../index'; 8 | import * as channel from './channel'; 9 | import { loadBlockRange } from './blocks'; 10 | import { recomputeDateFormats } from './dates'; 11 | import type { BlockRange, AccountNewInfo } from '../types'; 12 | 13 | import { GetChainTransactions } from './get-chain-transactions'; 14 | import { integrateNewTxs } from './integrate-new-txs'; 15 | 16 | // increase version for forced redownload of txs 17 | // (on either format change, or on some widespread data corruption) 18 | // 19 | // version 1 added infos about fees and sizes; we cannot calculate that 20 | // version 2 was correction in mytrezor 21 | // v3 added info, whether utxo is my own or not 22 | // so we have to re-download everything -> setting initial state as if nothing is known 23 | // v4 changed timestamp format 24 | // v5 is just to force re-download on forceAdded data corruption 25 | // v6 changed types (tx amounts/values, fee, balance ect) from number to string 26 | // across the whole library (discovery and buildtx) 27 | const LATEST_VERSION = 6; 28 | 29 | // Default starting info being used, when there is null 30 | const defaultInfo: AccountInfo = { 31 | utxos: [], 32 | transactions: [], 33 | usedAddresses: [], 34 | unusedAddresses: [], 35 | changeIndex: 0, 36 | balance: '0', 37 | sentAddresses: {}, 38 | lastBlock: { height: 0, hash: 'abcd' }, 39 | transactionHashes: {}, 40 | changeAddresses: [], 41 | allowChange: false, 42 | lastConfirmedChange: -1, 43 | lastConfirmedMain: -1, 44 | version: LATEST_VERSION, 45 | }; 46 | 47 | let recvInfo: ?AccountInfo; 48 | let recvNetwork: BitcoinJsNetwork; 49 | let recvXpub: string; 50 | let recvSegwit: boolean; 51 | let recvWebAssembly: boolean; 52 | let recvGap: number; 53 | let recvCashAddress: boolean; 54 | 55 | // what (new Date().getTimezoneOffset()) returns 56 | // note that it is NEGATIVE from the UTC string timezone 57 | // so, UTC+2 timezone returns -120... 58 | // it's javascript, it's insane by default 59 | let recvTimeOffset: number; 60 | 61 | // init on worker start 62 | channel.initPromise.then(({ 63 | accountInfo, 64 | network, 65 | xpub, 66 | segwit, 67 | webassembly, 68 | cashAddress, 69 | gap, 70 | timeOffset, 71 | }) => { 72 | recvInfo = accountInfo; 73 | recvNetwork = network; 74 | recvSegwit = segwit; 75 | recvXpub = xpub; 76 | recvWebAssembly = webassembly; 77 | recvCashAddress = cashAddress; 78 | recvGap = gap; 79 | 80 | recvTimeOffset = timeOffset; 81 | }); 82 | 83 | channel.startDiscoveryPromise.then(() => { 84 | let initialState = recvInfo == null ? defaultInfo : recvInfo; 85 | 86 | if (initialState.version == null || initialState.version < LATEST_VERSION) { 87 | initialState = defaultInfo; 88 | } 89 | 90 | recomputeDateFormats(initialState.transactions, recvTimeOffset); 91 | 92 | // first load blocks, then count last used indexes, 93 | // then start asking for new transactions, 94 | // then integrate new transactions into old transactions 95 | loadBlockRange(initialState).then((range) => { 96 | // when starting from 0, take as if there is no info 97 | const oldState = range.firstHeight === 0 98 | ? defaultInfo 99 | : initialState; 100 | 101 | const { 102 | changeAddresses, 103 | lastConfirmedMain, 104 | lastConfirmedChange, 105 | } = oldState; 106 | 107 | const unconfirmedTxids = oldState.transactions 108 | .filter(t => t.height == null) 109 | .map(t => t.hash); 110 | 111 | const mainAddresses = oldState.usedAddresses 112 | .map(a => a.address) 113 | .concat(oldState.unusedAddresses); 114 | 115 | 116 | // get all the new info, then... 117 | return discoverAccount( 118 | range, 119 | [lastConfirmedMain, lastConfirmedChange], 120 | oldState.transactions, 121 | mainAddresses, 122 | changeAddresses, 123 | ).then((newInfo: AccountNewInfo): AccountInfo => { 124 | // then find out deleted info 125 | const deleted: Array = findDeleted( 126 | unconfirmedTxids, 127 | newInfo, 128 | ); 129 | // ... then integrate 130 | const res: AccountInfo = integrateNewTxs( 131 | newInfo, 132 | oldState, 133 | range.last, 134 | deleted, 135 | recvGap, 136 | recvTimeOffset, 137 | ); 138 | return res; 139 | }); 140 | }).then( 141 | // either success or failure 142 | // (other side will shut down the worker then) 143 | (result: AccountInfo) => channel.returnSuccess(result), 144 | error => channel.returnError(error), 145 | ); 146 | }); 147 | 148 | function discoverAccount( 149 | range: BlockRange, 150 | lastUsedAddresses: [number, number], 151 | transactions: Array, 152 | mainAddresses: Array, 153 | changeAddresses: Array, 154 | ): Promise { 155 | function d(i: number) { 156 | return new GetChainTransactions( 157 | i, 158 | range, 159 | lastUsedAddresses[i], 160 | channel.chunkTransactions, 161 | i === 0 ? transactions : [], // used for visual counting 162 | i === 0 ? mainAddresses : changeAddresses, 163 | recvNetwork, 164 | recvXpub, 165 | recvSegwit, 166 | recvWebAssembly, 167 | recvCashAddress, 168 | recvGap, 169 | ).discover(); 170 | } 171 | 172 | return d(0) 173 | .then(main => d(1).then(change => ({ main, change }))); 174 | } 175 | 176 | function findDeleted( 177 | txids: Array, 178 | newInfo: AccountNewInfo, 179 | ): Array { 180 | return txids.filter((id) => { 181 | if (newInfo.main.newTransactions[id] != null) { 182 | return false; 183 | } 184 | if (newInfo.change.newTransactions[id] != null) { 185 | return false; 186 | } 187 | return true; 188 | }); 189 | } 190 | -------------------------------------------------------------------------------- /src/discovery/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // `discovery` is for account discovery process, plus monitoring. 3 | // In this file, there is only Flow "interface", no actual code. 4 | // 5 | // Discovery for 1 trezor is from several account discoveries; we discover 6 | // accounts one after another; howver, this is NOT dealt with in this library 7 | // at all. This library deals with just one account. 8 | // 9 | // One class "implement" this - workeŕDiscovery 10 | // 11 | // In the comments and in the names, "hash" and "id" are used interchangably 12 | // and both mean the "reverse endian" hash that bitcoin uses for 13 | // identification, not actual hash. 14 | // Actual hash is not used anywhere in the API. 15 | 16 | import type { Network as BitcoinJsNetwork } from '@trezor/utxo-lib'; 17 | import { 18 | Stream, 19 | StreamWithEnding, 20 | } from '../utils/stream'; 21 | 22 | // First, we describe all the types that go out of discovery 23 | // and that are directly used in web wallet. 24 | 25 | // Information about utxos 26 | // UTXO == unspent transaction output = all I can spend 27 | export type UtxoInfo = { 28 | index: number, // index of output IN THE TRANSACTION 29 | transactionHash: string, // hash of the transaction 30 | value: string, // how much money sent 31 | addressPath: [number, number], // path 32 | height: ?number, // null == unconfirmed 33 | coinbase: boolean, 34 | tsize: number, // total size - in case of segwit, total, with segwit data 35 | vsize: number, // virtual size - segwit concept - same as size in non-segwit 36 | own: boolean, // is the ORIGIN me (the same account) 37 | required?: boolean, // must be included into transaction 38 | } 39 | 40 | // Some info about output 41 | export type TargetInfo = { 42 | address: string, 43 | value: string, 44 | i: number, // index in the transaction 45 | } 46 | 47 | export type TransactionInfo = { 48 | isCoinbase: boolean, 49 | 50 | // unix timestamp 51 | timestamp: ?number, 52 | 53 | // I am saving it like this, because mytrezor is in angular 54 | // and directly displays this 55 | // and it saves suprisingly a lot of time 56 | // since otherwise Angular would recompute it repeatedly 57 | // only day YYYY-MM-DD 58 | dateInfoDayFormat: ?string, 59 | // only time HH:MM:SS 60 | dateInfoTimeFormat: ?string, 61 | 62 | height: ?number, 63 | confirmations: ?number, 64 | 65 | hash: string, 66 | 67 | // targets - only the shown and "relevant" outputs 68 | // note: should not be used in any advanced logic, it's heuristic 69 | targets: Array, 70 | 71 | // all outputs that belong to my addresses 72 | myOutputs: {[i: number]: TargetInfo}, 73 | 74 | type: 'self' | 'recv' | 'sent', 75 | 76 | // value - tx itself 77 | // balance - balance on account after this tx 78 | // both are little heuristics! it is "relevant" value/balance 79 | value: string, 80 | balance: string, 81 | 82 | inputs: Array<{id: string, index: number}>, // needing this for later analysis 83 | 84 | tsize: number, // total size - in case of segwit, total, with segwit data 85 | vsize: number, // virtual size - segwit concept - same as size in non-segwit 86 | invalidTransaction?: boolean, // true if we are not able to parse transaction correctly 87 | } 88 | 89 | // This is used for used addresses 90 | // Where we display address and number of received BTC. 91 | // NOTE: received does *not* mean current balance on address!!! 92 | // We don't expose that to the user. 93 | // It's really just sum of received outputs. 94 | export type AddressWithReceived = { 95 | // regular base58check address 96 | address: string, 97 | // received, in satoshis 98 | received: string, 99 | }; 100 | 101 | // Complete info about one account. 102 | // (trezor usually has several accounts, 1 at minimum) 103 | export type AccountInfo = { 104 | utxos: Array, 105 | transactions: Array, 106 | 107 | // all addresses FROM THE MAIN CHAIN that has at least 1 received transaction 108 | usedAddresses: Array, 109 | 110 | // addresses that has <1 transaction 111 | // (max 20, but can be less! can be even 0) 112 | unusedAddresses: Array, 113 | 114 | // in mytrezor, I would need just one change address, but useful for setting up watching 115 | changeAddresses: Array, 116 | 117 | // first unused change index 118 | changeIndex: number, 119 | 120 | // not used in mytrezor, useful in discovery 121 | lastConfirmedChange: number, 122 | lastConfirmedMain: number, 123 | 124 | // if there is 20 change addresses in a row all used, but unconfirmed (rarely happens) 125 | // we don't allow change and we don't allow sending 126 | // (not yet implemented in GUI, since it happens super rarely) 127 | allowChange: boolean, 128 | 129 | // balance (== all utxos added) 130 | balance: string, 131 | 132 | // index for outgoing addresses; not including mine self-sents 133 | sentAddresses: {[txPlusIndex: string]: string}, 134 | 135 | // what is last block I saw 136 | lastBlock: {height: number, hash: string}, 137 | 138 | // version of saved data - allows updates 139 | // version null => original version 140 | // version 1 => added fees and sizes to utxos+history - needs re-download 141 | version: number, 142 | }; 143 | 144 | // This is number of currently loaded transactions. 145 | // Used only for displaying "Loading..." status. 146 | export type AccountLoadStatus = { 147 | transactions: number, 148 | }; 149 | 150 | export type ForceAddedTransaction = { 151 | hex: string, 152 | network: BitcoinJsNetwork, 153 | hash: string, 154 | inputAddresses: Array, 155 | outputAddresses: Array, 156 | vsize: number, 157 | fee: string, 158 | }; 159 | 160 | export type Discovery = { 161 | +discoverAccount: ( 162 | initial: ?AccountInfo, 163 | xpub: string, 164 | network: BitcoinJsNetwork, 165 | segwit: 'off' | 'p2sh', 166 | cashAddress: boolean, 167 | gap: number, 168 | timeOffset: number 169 | ) => StreamWithEnding, 170 | 171 | +monitorAccountActivity: ( 172 | initial: AccountInfo, 173 | xpub: string, 174 | network: BitcoinJsNetwork, 175 | segwit: 'off' | 'p2sh', 176 | cashAddress: boolean, 177 | gap: number, 178 | timeOffset: number 179 | ) => Stream, 180 | 181 | // force-adds transaction to multiple addresses 182 | // (useful for adding transactions right after succesful send) 183 | +forceAddTransaction: ( 184 | transaction: ForceAddedTransaction 185 | ) => void, 186 | 187 | // helper function for the rest of wallet for xpub derivation - 188 | // it is here because WASM is done here... 189 | +deriveXpub: ( 190 | xpub: string, 191 | network: BitcoinJsNetwork, 192 | index: number 193 | ) => Promise, 194 | } 195 | -------------------------------------------------------------------------------- /src/build-tx/coinselect.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // this is converting in/to coinselect format 4 | // 5 | // I am using the coinselect format, since the end-goal is 6 | // to merge the changes back to upstream; it didn't work out so far 7 | import type { 8 | Network as BitcoinJsNetwork, 9 | } from '@trezor/utxo-lib'; 10 | import { 11 | address as BitcoinJsAddress, 12 | } from '@trezor/utxo-lib'; 13 | import BigNumber from 'bignumber.js'; 14 | import bitcoinJsSplit from './coinselect-lib/outputs/split'; 15 | import bitcoinJsCoinselect from './coinselect-lib'; 16 | import { transactionBytes, finalize } from './coinselect-lib/utils'; 17 | 18 | 19 | import type { UtxoInfo } from '../discovery'; 20 | import * as request from './request'; 21 | import { convertCashAddress } from '../utils/bchaddr'; 22 | 23 | const SEGWIT_INPUT_SCRIPT_LENGTH = 51; // actually 50.25, but let's make extra room 24 | const INPUT_SCRIPT_LENGTH = 109; 25 | const P2PKH_OUTPUT_SCRIPT_LENGTH = 25; 26 | const P2SH_OUTPUT_SCRIPT_LENGTH = 23; 27 | const P2WPKH_OUTPUT_SCRIPT_LENGTH = 22; 28 | const P2WSH_OUTPUT_SCRIPT_LENGTH = 34; 29 | 30 | export type Input = { 31 | id: number, 32 | script: { 33 | length: number, 34 | }, 35 | value: string, 36 | 37 | own: boolean, 38 | coinbase: boolean, 39 | confirmations: number, 40 | } 41 | 42 | type OutputIn = { 43 | value?: string, 44 | script: { 45 | length: number, 46 | }, 47 | } 48 | 49 | export type OutputOut = { 50 | value: string, 51 | script?: { 52 | length: number, 53 | }, 54 | } 55 | 56 | export type CompleteResult = { 57 | type: 'true', 58 | result: { 59 | inputs: Array, 60 | outputs: Array, 61 | max: string, 62 | totalSpent: string, 63 | fee: string, 64 | feePerByte: string, 65 | bytes: number, 66 | }, 67 | } 68 | 69 | export type Result = CompleteResult | { 70 | type: 'false', 71 | } 72 | 73 | export function coinselect( 74 | utxos: Array, 75 | rOutputs: Array, 76 | height: number, 77 | feeRate: string, 78 | segwit: boolean, 79 | countMax: boolean, 80 | countMaxId: number, 81 | dustThreshold: number, 82 | network: BitcoinJsNetwork, 83 | baseFee?: number, 84 | floorBaseFee?: boolean, 85 | dustOutputFee?: number, 86 | skipUtxoSelection?: boolean, 87 | skipPermutation?: boolean, 88 | ): Result { 89 | const inputs = convertInputs(utxos, height, segwit); 90 | const outputs = convertOutputs(rOutputs, network); 91 | const options = { 92 | inputLength: segwit ? SEGWIT_INPUT_SCRIPT_LENGTH : INPUT_SCRIPT_LENGTH, 93 | changeOutputLength: segwit ? P2SH_OUTPUT_SCRIPT_LENGTH : P2PKH_OUTPUT_SCRIPT_LENGTH, 94 | dustThreshold, 95 | baseFee, 96 | floorBaseFee, 97 | dustOutputFee, 98 | skipPermutation, 99 | }; 100 | 101 | const algorithm = countMax ? bitcoinJsSplit : bitcoinJsCoinselect; 102 | // finalize using requested custom inputs or use coin select algorith 103 | const result = skipUtxoSelection != null && !countMax 104 | ? finalize(inputs, outputs, parseInt(feeRate, 10), options) 105 | : algorithm(inputs, outputs, feeRate, options); 106 | if (!result.inputs) { 107 | return { 108 | type: 'false', 109 | }; 110 | } 111 | 112 | const { fee } = result; 113 | const max = countMaxId === -1 ? -1 : result.outputs[countMaxId].value; 114 | 115 | const totalSpent = (result.outputs 116 | .filter((output, i) => i !== rOutputs.length) 117 | .map(o => o.value) 118 | .reduce((a, b) => new BigNumber(a).plus(b), new BigNumber(0)) 119 | ).plus(new BigNumber(result.fee)); 120 | 121 | 122 | const allSize = transactionBytes(result.inputs, result.outputs); 123 | // javascript WTF: fee is a string, allSize is a number, therefore it's working 124 | const feePerByte = fee / allSize; 125 | 126 | return { 127 | type: 'true', 128 | result: { 129 | ...result, 130 | fee: result.fee.toString(), 131 | 132 | feePerByte: feePerByte.toString(), 133 | bytes: allSize, 134 | max, 135 | totalSpent: totalSpent.toString(), 136 | }, 137 | }; 138 | } 139 | 140 | function convertInputs( 141 | inputs: Array, 142 | height: number, 143 | segwit: boolean, 144 | ): Array { 145 | const bytesPerInput = segwit ? SEGWIT_INPUT_SCRIPT_LENGTH : INPUT_SCRIPT_LENGTH; 146 | return inputs.map((input, i) => ({ 147 | id: i, 148 | script: { length: bytesPerInput }, 149 | value: input.value, 150 | own: input.own, 151 | coinbase: input.coinbase, 152 | confirmations: input.height == null 153 | ? 0 154 | : (1 + height - input.height), 155 | required: input.required, 156 | })); 157 | } 158 | 159 | function isBech32(address: string): boolean { 160 | try { 161 | BitcoinJsAddress.fromBech32(address); 162 | return true; 163 | } catch (e) { 164 | return false; 165 | } 166 | } 167 | 168 | function getScriptAddress(address: string, network: BitcoinJsNetwork): {length: number} { 169 | const bech = isBech32(address); 170 | let pubkeyhash; 171 | if (!bech) { 172 | const decoded = BitcoinJsAddress.fromBase58Check(convertCashAddress(address)); 173 | pubkeyhash = decoded.version === network.pubKeyHash; 174 | } else { 175 | const decoded = BitcoinJsAddress.fromBech32(address); 176 | pubkeyhash = decoded.data.length === 20; 177 | } 178 | 179 | const becLength = pubkeyhash ? P2WPKH_OUTPUT_SCRIPT_LENGTH : P2WSH_OUTPUT_SCRIPT_LENGTH; 180 | const norLength = pubkeyhash ? P2PKH_OUTPUT_SCRIPT_LENGTH : P2SH_OUTPUT_SCRIPT_LENGTH; 181 | const length = bech 182 | ? becLength 183 | : norLength; 184 | return { length }; 185 | } 186 | 187 | function convertOutputs( 188 | outputs: Array, 189 | network: BitcoinJsNetwork, 190 | ): Array { 191 | // most scripts are P2PKH; default is P2PKH 192 | const defaultScript = { length: P2PKH_OUTPUT_SCRIPT_LENGTH }; 193 | return outputs.map((output) => { 194 | if (output.type === 'complete') { 195 | return { 196 | value: output.amount, 197 | script: getScriptAddress(output.address, network), 198 | }; 199 | } 200 | if (output.type === 'noaddress') { 201 | return { 202 | value: output.amount, 203 | script: defaultScript, 204 | }; 205 | } 206 | if (output.type === 'opreturn') { 207 | return { 208 | value: '0', 209 | script: { length: 2 + (output.dataHex.length / 2) }, 210 | }; 211 | } 212 | if (output.type === 'send-max') { 213 | return { 214 | script: getScriptAddress(output.address, network), 215 | }; 216 | } 217 | if (output.type === 'send-max-noaddress') { 218 | return { 219 | script: defaultScript, 220 | }; 221 | } 222 | throw new Error('WRONG-OUTPUT-TYPE'); 223 | }); 224 | } 225 | -------------------------------------------------------------------------------- /test/coinselect-lib/fixtures/break.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "1:1, no remainder", 4 | "feeRate": "10", 5 | "inputs": [ 6 | "11920" 7 | ], 8 | "output": "10000", 9 | "expected": { 10 | "inputs": [ 11 | { 12 | "value": "11920" 13 | } 14 | ], 15 | "outputs": [ 16 | { 17 | "value": "10000" 18 | } 19 | ], 20 | "fee": "1920" 21 | }, 22 | "inputLength": 107, 23 | "outputLength": 25, 24 | "dustThreshold": 546 25 | }, 26 | { 27 | "description": "1:1", 28 | "feeRate": "10", 29 | "inputs": [ 30 | "12000" 31 | ], 32 | "output": { 33 | "address": "woop", 34 | "value": "10000" 35 | }, 36 | "expected": { 37 | "fee": "2000", 38 | "inputs": [ 39 | { 40 | "value": "12000" 41 | } 42 | ], 43 | "outputs": [ 44 | { 45 | "address": "woop", 46 | "value": "10000" 47 | } 48 | ] 49 | }, 50 | "inputLength": 107, 51 | "outputLength": 25, 52 | "dustThreshold": 546 53 | }, 54 | { 55 | "description": "1:1, w/ change", 56 | "feeRate": "10", 57 | "inputs": [ 58 | "12000" 59 | ], 60 | "output": "8000", 61 | "expected": { 62 | "inputs": [ 63 | { 64 | "value": "12000" 65 | } 66 | ], 67 | "outputs": [ 68 | { 69 | "value": "8000" 70 | }, 71 | { 72 | "value": "1740" 73 | } 74 | ], 75 | "fee": "2260" 76 | }, 77 | "inputLength": 107, 78 | "outputLength": 25, 79 | "dustThreshold": 546 80 | }, 81 | { 82 | "description": "1:2, strange output type", 83 | "feeRate": "10", 84 | "inputs": [ 85 | "27000" 86 | ], 87 | "output": { 88 | "script": { 89 | "length": 220 90 | }, 91 | "value": "10000" 92 | }, 93 | "expected": { 94 | "inputs": [ 95 | { 96 | "value": "27000" 97 | } 98 | ], 99 | "outputs": [ 100 | { 101 | "script": { 102 | "length": 220 103 | }, 104 | "value": "10000" 105 | }, 106 | { 107 | "script": { 108 | "length": 220 109 | }, 110 | "value": "10000" 111 | } 112 | ], 113 | "fee": "7000" 114 | }, 115 | "inputLength": 107, 116 | "outputLength": 25, 117 | "dustThreshold": 546 118 | }, 119 | { 120 | "description": "1:4", 121 | "feeRate": "10", 122 | "inputs": [ 123 | "12000" 124 | ], 125 | "output": "2000", 126 | "expected": { 127 | "inputs": [ 128 | { 129 | "value": "12000" 130 | } 131 | ], 132 | "outputs": [ 133 | { 134 | "value": "2000" 135 | }, 136 | { 137 | "value": "2000" 138 | }, 139 | { 140 | "value": "2000" 141 | }, 142 | { 143 | "value": "2000" 144 | } 145 | ], 146 | "fee": "4000" 147 | }, 148 | "inputLength": 107, 149 | "outputLength": 25, 150 | "dustThreshold": 546 151 | }, 152 | { 153 | "description": "2:5", 154 | "feeRate": "10", 155 | "inputs": [ 156 | "3000", 157 | "12000" 158 | ], 159 | "output": "2000", 160 | "expected": { 161 | "inputs": [ 162 | { 163 | "value": "3000" 164 | }, 165 | { 166 | "value": "12000" 167 | } 168 | ], 169 | "outputs": [ 170 | { 171 | "value": "2000" 172 | }, 173 | { 174 | "value": "2000" 175 | }, 176 | { 177 | "value": "2000" 178 | }, 179 | { 180 | "value": "2000" 181 | }, 182 | { 183 | "value": "2000" 184 | } 185 | ], 186 | "fee": "5000" 187 | }, 188 | "inputLength": 107, 189 | "outputLength": 25, 190 | "dustThreshold": 546 191 | }, 192 | { 193 | "description": "2:5, no fee", 194 | "feeRate": "0", 195 | "inputs": [ 196 | "5000", 197 | "10000" 198 | ], 199 | "output": "3000", 200 | "expected": { 201 | "inputs": [ 202 | { 203 | "value": "5000" 204 | }, 205 | { 206 | "value": "10000" 207 | } 208 | ], 209 | "outputs": [ 210 | { 211 | "value": "3000" 212 | }, 213 | { 214 | "value": "3000" 215 | }, 216 | { 217 | "value": "3000" 218 | }, 219 | { 220 | "value": "3000" 221 | }, 222 | { 223 | "value": "3000" 224 | } 225 | ], 226 | "fee": "0" 227 | }, 228 | "inputLength": 107, 229 | "outputLength": 25, 230 | "dustThreshold": 546 231 | }, 232 | { 233 | "description": "2:2 (+1), w/ change", 234 | "feeRate": "7", 235 | "inputs": [ 236 | "16000" 237 | ], 238 | "output": "6000", 239 | "expected": { 240 | "inputs": [ 241 | { 242 | "value": "16000" 243 | } 244 | ], 245 | "outputs": [ 246 | { 247 | "value": "6000" 248 | }, 249 | { 250 | "value": "6000" 251 | }, 252 | { 253 | "value": "2180" 254 | } 255 | ], 256 | "fee": "1820" 257 | }, 258 | "inputLength": 107, 259 | "outputLength": 25, 260 | "dustThreshold": 546 261 | }, 262 | { 263 | "description": "2:3 (+1), no fee, w/ change", 264 | "feeRate": "0", 265 | "inputs": [ 266 | "5000", 267 | "10000" 268 | ], 269 | "output": "4000", 270 | "expected": { 271 | "inputs": [ 272 | { 273 | "value": "5000" 274 | }, 275 | { 276 | "value": "10000" 277 | } 278 | ], 279 | "outputs": [ 280 | { 281 | "value": "4000" 282 | }, 283 | { 284 | "value": "4000" 285 | }, 286 | { 287 | "value": "4000" 288 | }, 289 | { 290 | "value": "3000" 291 | } 292 | ], 293 | "fee": "0" 294 | }, 295 | "inputLength": 107, 296 | "outputLength": 25, 297 | "dustThreshold": 546 298 | }, 299 | { 300 | "description": "not enough funds", 301 | "feeRate": "10", 302 | "inputs": [ 303 | "41000", 304 | "1000" 305 | ], 306 | "output": "40000", 307 | "expected": { 308 | "fee": "3400" 309 | }, 310 | "inputLength": 107, 311 | "outputLength": 25, 312 | "dustThreshold": 546 313 | }, 314 | { 315 | "description": "no inputs", 316 | "feeRate": "10", 317 | "inputs": [], 318 | "output": "2000", 319 | "expected": { 320 | "fee": "440" 321 | }, 322 | "inputLength": 107, 323 | "outputLength": 25, 324 | "dustThreshold": 546 325 | }, 326 | { 327 | "description": "invalid output (NaN)", 328 | "feeRate": "10", 329 | "inputs": [], 330 | "output": {}, 331 | "expected": { 332 | "fee": "100" 333 | }, 334 | "inputLength": 107, 335 | "outputLength": 25, 336 | "dustThreshold": 546 337 | }, 338 | { 339 | "description": "input with float values (NaN)", 340 | "feeRate": "10", 341 | "inputs": [ 342 | "10000.5" 343 | ], 344 | "output": "5000", 345 | "expected": { 346 | "fee": "1580" 347 | }, 348 | "inputLength": 107, 349 | "outputLength": 25, 350 | "dustThreshold": 546 351 | }, 352 | { 353 | "description": "inputs and outputs, bad feeRate - number (NaN)", 354 | "feeRate": "1", 355 | "inputs": [ 356 | "20000" 357 | ], 358 | "output": "10000", 359 | "expected": {}, 360 | "inputLength": 107, 361 | "outputLength": 25, 362 | "dustThreshold": 546 363 | }, 364 | { 365 | "description": "inputs and outputs, bad feeRate - decimal (NaN)", 366 | "feeRate": "1.5", 367 | "inputs": [ 368 | "20000" 369 | ], 370 | "output": "10000", 371 | "expected": {}, 372 | "inputLength": 107, 373 | "outputLength": 25, 374 | "dustThreshold": 546 375 | } 376 | ] 377 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /fastxpub/additional_sources/post.js: -------------------------------------------------------------------------------- 1 | /* 2 | typedef struct { 3 | uint32_t depth; 4 | uint32_t child_num; 5 | uint8_t chain_code[32]; 6 | uint8_t private_key[32]; 7 | uint8_t public_key[33]; 8 | const curve_info *curve; 9 | } HDNode; 10 | */ 11 | 12 | var readyDfd = deferred(); 13 | 14 | Module['onRuntimeInitialized'] = function() { 15 | var HEAPU8 = Module['HEAPU8']; 16 | 17 | var _malloc = Module['_malloc']; 18 | var _hdnode_public_ckd_xpub_optimized = Module['_hdnode_public_ckd_xpub_optimized']; 19 | var _hdnode_public_ckd_address_optimized = Module['_hdnode_public_ckd_address_optimized']; 20 | var _hdnode_deserialize = Module['_hdnode_deserialize']; 21 | var _hdnode_fingerprint = Module['_hdnode_fingerprint']; 22 | var _hdnode_public_ckd = Module['_hdnode_public_ckd']; 23 | var _hdnode_serialize_public = Module['_hdnode_serialize_public']; 24 | var _ecdsa_read_pubkey = Module['_ecdsa_read_pubkey']; 25 | var Pointer_stringify = Module['Pointer_stringify']; 26 | 27 | // HDNode structs global 28 | var PUBPOINT_SIZE = 2 * 9 * 4; // (2 * bignum256 = 2 * 9 * uint32_t) 29 | var _pubpoint = _malloc(PUBPOINT_SIZE); 30 | var PUBKEY_SIZE = 33; 31 | var _pubkey = _malloc(PUBKEY_SIZE); 32 | var CHAINCODE_SIZE = 32; 33 | var _chaincode = _malloc(CHAINCODE_SIZE); 34 | 35 | var HDNODE_SIZE = 2 * 4 + 32 + 32 + 33 + 4; 36 | var _hdnode = _malloc(HDNODE_SIZE); 37 | 38 | // address string global 39 | var ADDRESS_SIZE = 60; // maximum size 40 | var _address = _malloc(ADDRESS_SIZE); 41 | 42 | var XPUB_SIZE = 200; // maximum size 43 | var _xpub = _malloc(XPUB_SIZE); 44 | 45 | var fingerprint = 0; 46 | 47 | /* 48 | * public library interface 49 | */ 50 | 51 | function loadNodeStruct(xpub, version_public) { 52 | stringToUTF8(xpub, _xpub, XPUB_SIZE); 53 | if (_hdnode_deserialize(_xpub, version_public, 0, _hdnode, 0) !== 0) { 54 | throw new Error("Wrong XPUB type!!"); // abort everything (should not happen, should be catched already outside of asm.js, but this is emergency) 55 | }; 56 | fingerprint = _hdnode_fingerprint(_hdnode); 57 | } 58 | 59 | /** 60 | * @param {HDNode} node HDNode struct, see the definition above 61 | */ 62 | function loadNode(node) { 63 | var u8_pubkey = new Uint8Array(PUBKEY_SIZE); 64 | u8_pubkey.set(node['public_key'], 0); 65 | HEAPU8.set(u8_pubkey, _pubkey); 66 | 67 | var u8_chaincode = new Uint8Array(CHAINCODE_SIZE); 68 | u8_chaincode.set(node['chain_code'], 0); 69 | HEAPU8.set(u8_chaincode, _chaincode); 70 | 71 | _ecdsa_read_pubkey(0, _pubkey, _pubpoint); 72 | } 73 | 74 | function deriveNodeFromStruct(index, version_public) { 75 | if (_hdnode_public_ckd(_hdnode, index) !== 1) { 76 | throw new Error("Strange return type"); 77 | }; 78 | if (_hdnode_serialize_public(_hdnode, fingerprint, version_public, _xpub, XPUB_SIZE) === 0) { 79 | throw new Error("Strange return type"); 80 | }; 81 | return UTF8ToString(_xpub); 82 | } 83 | 84 | function deriveNode(xpub, index, version_public) { 85 | loadNodeStruct(xpub, version_public); 86 | return deriveNodeFromStruct(index, version_public); 87 | } 88 | 89 | /** 90 | * @param {Number} index BIP32 index of the address 91 | * @param {Number} version address version byte 92 | * @param {Number} addressFormat address format (0 = normal, 1 = segwit-in-p2sh) 93 | * @return {String} 94 | */ 95 | function deriveAddress(index, version, addressFormat) { 96 | _hdnode_public_ckd_address_optimized(_pubpoint, _chaincode, index, version, _address, ADDRESS_SIZE, addressFormat); 97 | return Pointer_stringify(_address); 98 | } 99 | 100 | /** 101 | * @param {HDNode} node HDNode struct, see the definition above 102 | * @param {Number} firstIndex index of the first address 103 | * @param {Number} lastIndex index of the last address 104 | * @param {Number} version address version byte 105 | * @param {Number} addressFormat address format (0 = normal, 1 = segwit-in-p2sh) 106 | * @return {Array} 107 | */ 108 | function deriveAddressRange(node, firstIndex, lastIndex, version, addressFormat) { 109 | var addresses = []; 110 | loadNode(node); 111 | for (var i = firstIndex; i <= lastIndex; i++) { 112 | addresses.push(deriveAddress(i, version, addressFormat)); 113 | } 114 | return addresses; 115 | } 116 | 117 | /* 118 | * Web worker processing 119 | */ 120 | 121 | function processMessage(event) { 122 | var data = event['data']; 123 | var type = data['type']; 124 | 125 | switch (type) { 126 | case 'deriveAddressRange': 127 | var addresses = deriveAddressRange( 128 | data['node'], 129 | data['firstIndex'], 130 | data['lastIndex'], 131 | data['version'], 132 | data['addressFormat'] 133 | ); 134 | self.postMessage({ 135 | 'addresses': addresses, 136 | 'firstIndex': data['firstIndex'], 137 | 'lastIndex': data['lastIndex'], 138 | 'i': data['i'] 139 | }); 140 | break; 141 | case 'deriveNode': 142 | var node = deriveNode( 143 | data['xpub'], 144 | data['index'], 145 | data['version'] 146 | ); 147 | self.postMessage({ 148 | 'xpub': node, 149 | 'i': data['i'] 150 | }); 151 | break; 152 | default: 153 | throw new Error('Unknown message type: ' + type); 154 | } 155 | } 156 | readyDfd.resolve({ 157 | 'processMessage': processMessage, 158 | 'loadNode': loadNode, 159 | 'deriveAddress': deriveAddress, 160 | 'deriveNode': deriveNode, 161 | 'deriveAddressRange': deriveAddressRange, 162 | }); 163 | } 164 | return readyDfd.promise; 165 | } 166 | 167 | 168 | // ------ asynchronous wasm file setup ------ 169 | 170 | // setting up calls for webworker 171 | // (not for browserify/node import) 172 | // init() loads the wasm file; unless the wasm file is loaded, the other functions wait 173 | 174 | var ENVIRONMENT_IS_WORKER = typeof importScripts === 'function'; 175 | if (ENVIRONMENT_IS_WORKER) { 176 | 177 | // callsDfd is resolved when init is finished 178 | var callsDfd = deferred(); 179 | 180 | self.onmessage = function(event) { 181 | var data = event['data']; 182 | var type = data['type']; 183 | 184 | // type is either init or something else 185 | // init inits right away, else it waits for init 186 | if (type === 'init') { 187 | var binary = data['binary']; 188 | prepareModule(binary).then(function (result) { 189 | callsDfd.resolve(result); 190 | }); 191 | } else { 192 | callsDfd.promise.then(function (calls) { 193 | // self.postMessage is called in the processMessage call 194 | calls['processMessage'](event); 195 | }); 196 | }; 197 | }; 198 | } 199 | 200 | // setting up exports for node / browserify import 201 | // (not in webworker environment) 202 | // init() loads the wasm file; unless the wasm file is loaded, the other functions return error 203 | // init() returns promise, resolved when init is done 204 | if (typeof module !== 'undefined') { 205 | var calls = null; 206 | 207 | // this is a function that is exported and that loads the binary 208 | var init = function(binary) { 209 | return prepareModule(binary).then(function(retCalls) { 210 | calls = retCalls; 211 | }) 212 | } 213 | 214 | function callFunctionIfInited(name) { 215 | return function () { 216 | if (calls === null) { 217 | throw new Error('fastxpub not yet inited.'); 218 | } else { 219 | return calls[name].apply(undefined, arguments); 220 | } 221 | } 222 | } 223 | 224 | module['exports'] = { 225 | 'deriveNode': callFunctionIfInited('deriveNode'), 226 | 'loadNode': callFunctionIfInited('loadNode'), 227 | 'deriveAddress': callFunctionIfInited('deriveAddress'), 228 | 'deriveAddressRange': callFunctionIfInited('deriveAddressRange'), 229 | 'init': init 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/socketio-worker/outside.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { Stream, Emitter } from '../utils/stream'; 4 | 5 | import type { 6 | InMessage as SocketWorkerInMessage, 7 | OutMessage as SocketWorkerOutMessage, 8 | } from './inside'; 9 | import { deferred } from '../utils/deferred'; 10 | 11 | type SocketWorkerFactory = () => Worker; 12 | 13 | let logCommunication = false; 14 | export function setLogCommunication() { 15 | logCommunication = true; 16 | } 17 | 18 | export class Socket { 19 | endpoint: string; 20 | 21 | socket: SocketWorkerHandler; 22 | 23 | _socketInited: Promise; 24 | 25 | streams: Array> = []; 26 | 27 | destroyerOnInit: Emitter; 28 | 29 | constructor( 30 | workerFactory: SocketWorkerFactory, 31 | endpoint: string, 32 | destroyerOnInit: Emitter, 33 | ) { 34 | this.endpoint = endpoint; 35 | this.socket = new SocketWorkerHandler(workerFactory, destroyerOnInit); 36 | this._socketInited = this.socket.init(this.endpoint); 37 | this.destroyerOnInit = destroyerOnInit; 38 | } 39 | 40 | send(message: Object): Promise { 41 | return this._socketInited.then(() => this.socket.send(message)); 42 | } 43 | 44 | close() { 45 | if (!this.destroyerOnInit.destroyed) { 46 | this.destroyerOnInit.emit(); 47 | } 48 | this.streams.forEach(stream => stream.dispose()); 49 | this._socketInited.then(() => { 50 | this.socket.close(); 51 | }, () => {}); 52 | } 53 | 54 | observe(event: string): Stream { 55 | const res = Stream.fromPromise(this._socketInited.then(() => this.socket.observe(event))); 56 | this.streams.push(res); 57 | return res; 58 | } 59 | 60 | subscribe(event: string, ...values: Array) { 61 | return this._socketInited.then( 62 | () => this.socket.subscribe(event, ...values), 63 | ).catch(() => {}); 64 | } 65 | } 66 | 67 | const errorTypes = ['connect_error', 'reconnect_error', 'error', 'close', 'disconnect']; 68 | const disconnectErrorTypes = ['connect_error', 'reconnect_error', 'close', 'disconnect']; 69 | 70 | class SocketWorkerHandler { 71 | _worker: ?Worker; 72 | 73 | workerFactory: () => Worker; 74 | 75 | _emitter: ?Emitter; 76 | 77 | counter: number; 78 | 79 | destroyerOnInit: Emitter; 80 | 81 | constructor(workerFactory: () => Worker, destroyerOnInit: Emitter) { 82 | this.workerFactory = workerFactory; 83 | this.counter = 0; 84 | this.destroyerOnInit = destroyerOnInit; 85 | } 86 | 87 | _tryWorker(endpoint: string, type: string): Promise { 88 | const worker = this.workerFactory(); 89 | const dfd = deferred(); 90 | 91 | let destroyed = false; 92 | 93 | const funOnDestroy = (n, detach) => { 94 | destroyed = true; 95 | worker.terminate(); 96 | detach(); 97 | }; 98 | this.destroyerOnInit.attach(funOnDestroy); 99 | 100 | worker.onmessage = ({ data }) => { 101 | this.destroyerOnInit.detach(funOnDestroy); 102 | if (typeof data === 'string') { 103 | const parsed = JSON.parse(data); 104 | if (parsed.type === 'initDone') { 105 | dfd.resolve(worker); 106 | } else { 107 | if (!destroyed) { 108 | worker.terminate(); 109 | } 110 | dfd.reject(new Error('Connection failed.')); 111 | } 112 | } 113 | }; 114 | 115 | worker.postMessage(JSON.stringify({ 116 | type: 'init', 117 | endpoint, 118 | connectionType: type, 119 | })); 120 | return dfd.promise; 121 | } 122 | 123 | init(endpoint: string): Promise { 124 | return this._tryWorker(endpoint, 'websocket') 125 | .catch(() => this._tryWorker(endpoint, 'polling')) 126 | .then((worker) => { 127 | const cworker = worker; 128 | this._worker = cworker; 129 | const emitter = new Emitter(); 130 | cworker.onmessage = ({ data }) => { 131 | if (typeof data === 'string') { 132 | if (!this.stopped) { 133 | emitter.emit(JSON.parse(data)); 134 | } 135 | } 136 | }; 137 | this._emitter = emitter; 138 | 139 | disconnectErrorTypes.forEach((type) => { 140 | this.observe(type).map(() => { 141 | // almost the same as this.close(), 142 | // but doesn't call destroy() 143 | // since that would also delete all handlers attached on emitters 144 | // and we want to observe errors from more places 145 | this._sendMessage({ 146 | type: 'close', 147 | }); 148 | this._emitter = null; 149 | return null; 150 | }); 151 | }); 152 | }); 153 | } 154 | 155 | stopped: boolean = false; 156 | 157 | close(): Promise { 158 | this.stopped = true; 159 | this._sendMessage({ 160 | type: 'close', 161 | }); 162 | if (this._emitter != null) { 163 | this._emitter.destroy(); 164 | this._emitter = null; 165 | } 166 | return new Promise((resolve) => { 167 | setTimeout(() => { 168 | if (this._worker != null) { 169 | this._worker.terminate(); 170 | } 171 | resolve(); 172 | }, 10); 173 | }); 174 | } 175 | 176 | send(imessage: Object): Promise { 177 | this.counter++; 178 | const { counter } = this; 179 | this._sendMessage({ 180 | type: 'send', 181 | message: imessage, 182 | id: counter, 183 | }); 184 | const dfd = deferred(); 185 | if (this._emitter == null) { 186 | return Promise.reject(new Error('Server disconnected.')); 187 | } 188 | this._emitter.attach((message, detach) => { 189 | if (logCommunication) { 190 | console.log('[socket.io] in message', message); 191 | } 192 | 193 | if (message.type === 'sendReply' && message.id === counter) { 194 | const { result, error } = message.reply; 195 | if (error != null) { 196 | dfd.reject(error); 197 | } else { 198 | dfd.resolve(result); 199 | } 200 | detach(); 201 | } 202 | // This is not covered by coverage, because it's hard to simulate 203 | // Happens when the server is disconnected during some long operation 204 | // It's hard to simulate long operation on regtest (very big transactions) 205 | // but happens in real life 206 | if (message.type === 'emit' && (errorTypes.indexOf(message.event) !== -1)) { 207 | dfd.reject(new Error('Server disconnected.')); 208 | detach(); 209 | } 210 | }); 211 | return dfd.promise; 212 | } 213 | 214 | observers: {[name: string]: Stream} = {} 215 | 216 | observe(event: string): Stream { 217 | if (this.observers[event] != null) { 218 | return this.observers[event]; 219 | } 220 | const observer = this._newObserve(event); 221 | this.observers[event] = observer; 222 | return observer; 223 | } 224 | 225 | _newObserve(event: string): Stream { 226 | this.counter++; 227 | const { counter } = this; 228 | this._sendMessage({ 229 | type: 'observe', 230 | event, 231 | id: counter, 232 | }); 233 | 234 | // $FlowIssue - this can't be null if used from bitcore.js 235 | const emitter: Emitter = this._emitter; 236 | 237 | const r = Stream.fromEmitter( 238 | emitter, 239 | () => { 240 | this._sendMessage({ 241 | type: 'unobserve', 242 | event, 243 | id: counter, 244 | }); 245 | delete this.observers[event]; 246 | }, 247 | ) 248 | .filter(message => (message.type === 'emit' && message.event === event)) 249 | // $FlowIssue 250 | .map((message: SocketWorkerOutMessage) => message.data); 251 | return r; 252 | } 253 | 254 | subscribe(event: string, ...values: Array) { 255 | this._sendMessage({ 256 | type: 'subscribe', 257 | event, 258 | values, 259 | }); 260 | } 261 | 262 | _sendMessage(message: SocketWorkerInMessage) { 263 | // $FlowIssue - this can't be null if used from bitcore.js 264 | const worker: Worker = this._worker; 265 | if (logCommunication) { 266 | console.log('[socket.io] out message', message); 267 | } 268 | 269 | worker.postMessage(JSON.stringify(message)); 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /gh-pages/ui.js: -------------------------------------------------------------------------------- 1 | const data = { 2 | bitcoinSegwit: { 3 | xpubs: 'xpub6CVKsQYXc9awxgV1tWbG4foDvdcnieK2JkbpPEBKB5WwAPKBZ1mstLbKVB4ov7QzxzjaxNK6EfmNY5Jsk2cG26EVcEkycGW4tchT2dyUhrx;xpub6CVKsQYXc9ax22ig3KAZMRiJL1xT9Me1sFX3t34mnVVzr6FkciU74qk7AqBkePQ2sM9pKeWp88KfPT2qcVQ19ykqGHMDioJhwywGuJ96Xt8', 4 | urls: 'https://btc1.trezor.io', 5 | network: { 6 | messagePrefix: '\x18Bitcoin Signed Message:\n', 7 | bip32: { 8 | private: 76066276, 9 | public: 76067358, 10 | }, 11 | pubKeyHash: 0x00, 12 | scriptHash: 0x05, 13 | wif: 0x80, 14 | coin: 'btc', 15 | }, 16 | }, 17 | bitcoinLegacy: { 18 | xpubs: 'xpub6BiVtCpG9fQPxnPmHXG8PhtzQdWC2Su4qWu6XW9tpWFYhxydCLJGrWBJZ5H6qTAHdPQ7pQhtpjiYZVZARo14qHiay2fvrX996oEP42u8wZy;xpub6BiVtCpG9fQQ1EW99bMSYwySbPWvzTFRQZCFgTmV3samLSZAYU7C3f4Je9vkNh7h1GAWi5Fn93BwoGBy9EAXbWTTgTnVKAbthHpxM1fXVRL', 19 | urls: 'https://btc1.trezor.io', 20 | network: { 21 | messagePrefix: '\x18Bitcoin Signed Message:\n', 22 | bip32: { 23 | private: 76066276, 24 | public: 76067358, 25 | }, 26 | pubKeyHash: 0x00, 27 | scriptHash: 0x05, 28 | wif: 0x80, 29 | coin: 'btc', 30 | }, 31 | }, 32 | bitcoincash: { 33 | xpubs: 'xpub6DFYZ2FZwJHL4WULnRKTyMAaE9sM5Vi3QoWW9kYWGzR4HxDJ42Gbbdj7bpBAtATpaNeSVqSD3gdFFmZZYK9BVo96rhxPY7SWZWsfmdHpZ7e', 34 | urls: 'https://bch1.trezor.io', 35 | network: { 36 | messagePrefix: '\x18Bitcoin Signed Message:\n', 37 | bip32: { 38 | private: 76066276, 39 | public: 76067358, 40 | }, 41 | pubKeyHash: 0, 42 | scriptHash: 5, 43 | wif: 0x80, 44 | coin: 'bch', 45 | }, 46 | }, 47 | bitcoinGoldSegwit: { 48 | xpubs: 'xpub6BezrjBueDupWpyMVsztAHjR5Sw5fzmHAz9bMUSAsfs62TFaU1qdytKJuXBL4oba2XFoXptxXefT7tyaBQbaQDouHaqaogMHoRG7pUrJZsf;xpub6BezrjBueDupZvwM1PGcPS5fFTQ7ZNQahTzQ7St8qMjXcRBraEZLbwYe38vQ1qxZckd3CHQio3pzSDPXX8wsf5Abxha11aYssjA48SHd85J', 49 | urls: 'https://btg1.trezor.io', 50 | network: { 51 | messagePrefix: '\x18Bitcoin Gold Signed Message:\n', 52 | bip32: { 53 | private: 76066276, 54 | public: 76067358, 55 | }, 56 | pubKeyHash: 38, 57 | scriptHash: 23, 58 | wif: 0x80, 59 | coin: 'btg', 60 | }, 61 | }, 62 | bitcoinGold: { 63 | xpubs: 'xpub6Ci3YvWwttrxAQjegzMQUfBVBTUyXG5brnoypdzrjqpC6cPfEWLpJnGy5AMgM64EcpFeM1zH6e585FLRo9nXyvHDhrBHrXYa2kNdQnq6iYE', 64 | urls: 'https://btg1.trezor.io', 65 | network: { 66 | messagePrefix: '\x18Bitcoin Gold Signed Message:\n', 67 | bip32: { 68 | private: 76066276, 69 | public: 76067358, 70 | }, 71 | pubKeyHash: 38, 72 | scriptHash: 23, 73 | wif: 0x80, 74 | coin: 'btg', 75 | }, 76 | }, 77 | dash: { 78 | xpubs: 'drkpRzoAZxGzS6tCmAzbkg9ZgQmr2oaLK2u89SxTfc7FW9Mray7oEHusTRbb8kKXbQCMh4vBUiXWsxsFHToHA4AeLCiGKQqDgcE291PqDf3zEUT', 79 | urls: 'https://dash1.trezor.io', 80 | network: { 81 | messagePrefix: '\x19DarkCoin Signed Message:\n', 82 | bip32: { 83 | private: 50221816, 84 | public: 50221772, 85 | }, 86 | pubKeyHash: 76, 87 | scriptHash: 16, 88 | wif: 0xcc, 89 | coin: 'dash', 90 | }, 91 | }, 92 | digibyteSegwit: { 93 | xpubs: 'xpub6CHZ4pRtkriEMy6d7r6yaQ3jQEVP9Hz14kqNdgWDmKugzLhFtP8ZHYFe1uGihkbVSEKTei5RapVd76Gyf1B4We7nSFfuLgRffbtYM3vR2VS', 94 | urls: 'https://dgb1.trezor.io', 95 | network: { 96 | messagePrefix: '\x18DigiByte Signed Message:\n', 97 | bip32: { 98 | private: 76066276, 99 | public: 76067358, 100 | }, 101 | pubKeyHash: 30, 102 | scriptHash: 63, 103 | wif: 0x80, 104 | coin: 'dgb', 105 | }, 106 | }, 107 | digibyte: { 108 | xpubs: 'xpub6CqTerEbegsjrhHTuRNGVdkqfksFMjrZsmf3Lp5JtbiFSgJgCSeodsjAUvXXksAkvAYHYhMeZcjmDRhixpZwYkuMAWf5fAW5ncPzmkcBrHp', 109 | urls: 'https://dgb1.trezor.io', 110 | network: { 111 | messagePrefix: '\x18DigiByte Signed Message:\n', 112 | bip32: { 113 | private: 76066276, 114 | public: 76067358, 115 | }, 116 | pubKeyHash: 30, 117 | scriptHash: 63, 118 | wif: 0x80, 119 | coin: 'dgb', 120 | }, 121 | }, 122 | doge: { 123 | xpubs: 'dgub8s6ZqRM7y5YaswHTn2EiE1dz8BWCdswdi2TTZWtU86Cv78qAgeHib9X7ntGvVaHXPqh4WQogMYthCtCzKwudS3JSnYbVFSdEGPqWnT6B8FB', 124 | urls: 'https://doge1.trezor.io', 125 | network: { 126 | messagePrefix: '\x19DogeCoin Signed Message:\n', 127 | bip32: { 128 | private: 49988504, 129 | public: 49990397, 130 | }, 131 | pubKeyHash: 30, 132 | scriptHash: 22, 133 | wif: 0x80, 134 | coin: 'doge', 135 | }, 136 | }, 137 | litecoinSegwit: { 138 | xpubs: 'Ltub2Z9JbADcx5xj75V5Enx5p9FVnkXXgo3Z3V9EJY7HEPwnBXBxcDdSEWt1ZfzqJhkg4unSdog77UTGPHuqh1rDbHdcjsYJtUS3CGNNHmD5K3X', 139 | urls: 'https://ltc1.trezor.io', 140 | network: { 141 | messagePrefix: '\x18Litecoin Signed Message:\n', 142 | bip32: { 143 | private: 27106558, 144 | public: 27108450, 145 | }, 146 | pubKeyHash: 48, 147 | scriptHash: 50, 148 | wif: 0x80, 149 | coin: 'ltc', 150 | }, 151 | }, 152 | litecoin: { 153 | xpubs: 'Ltub2Y8PyEMWQVgiX4L4gVzU8PakBTQ2WBxFdS6tJARQeasUUfXmBut2jGShnQyD3jgyBf7mmvs5jPNgmgXad5J6M8a8FiZK78dbT21fYtTAC9a', 154 | urls: 'https://ltc1.trezor.io', 155 | network: { 156 | messagePrefix: '\x18Litecoin Signed Message:\n', 157 | bip32: { 158 | private: 27106558, 159 | public: 27108450, 160 | }, 161 | pubKeyHash: 48, 162 | scriptHash: 50, 163 | wif: 0x80, 164 | coin: 'ltc', 165 | }, 166 | }, 167 | namecoin: { 168 | xpubs: 'xpub6CG5oukvBQsT57G4cRpCed53yHY2MknS84DvPEfwHbKPM8FF3sgAWcpAscc1xaMBnfrNWwteTbgL2xBVkHvrLUw6HQM9mgwsuwY1Pv7h4jh', 169 | urls: 'https://nmc1.trezor.io', 170 | network: { 171 | messagePrefix: '\x18Namecoin Gold Signed Message:\n', 172 | bip32: { 173 | private: 76066276, 174 | public: 76067358, 175 | }, 176 | pubKeyHash: 52, 177 | scriptHash: 5, 178 | wif: 0x80, 179 | coin: 'nmc', 180 | }, 181 | }, 182 | vertcoinSegwit: { 183 | xpubs: 'xpub6C8zLxpvrSYvugWob1fmum8tVTzyVJ6apbQT2xxPgNiiGo3QB12FDBuBH8CiYrMx5bMtfeCubKtXjtXeZE7AbV2qJyGsRRiZ31eFRs9dupW', 184 | urls: 'https://vtc1.trezor.io', 185 | network: { 186 | messagePrefix: '\x18Vertcoin Signed Message:\n', 187 | bip32: { 188 | private: 76066276, 189 | public: 76067358, 190 | }, 191 | pubKeyHash: 71, 192 | scriptHash: 5, 193 | wif: 0x80, 194 | coin: 'vtc', 195 | }, 196 | }, 197 | vertcoin: { 198 | xpubs: 'xpub6BwcwbAxw5PtCEZXsXsYrtdpz8eF6YFxUJdJJrsGq4pSqgKygipJQRwjUdVsUXbooQSvZsW2xhU5qdxmFqcurfEJ7ndeiB5776TgzTWTEFR', 199 | urls: 'https://vtc1.trezor.io', 200 | network: { 201 | messagePrefix: '\x18Vertcoin Signed Message:\n', 202 | bip32: { 203 | private: 76066276, 204 | public: 76067358, 205 | }, 206 | pubKeyHash: 71, 207 | scriptHash: 5, 208 | wif: 0x80, 209 | coin: 'vtc', 210 | }, 211 | }, 212 | zcash: { 213 | xpubs: 'xpub6CQdEahwhKRSn9BFc7oWpzNoeqG2ygv3xdofyk7He93NMjvDpGvcQ2o4dZfBNXpqzKydaHp5rhXRT3zYhRYJAErXxarH37f9hgRZ6UPiqfg;xpub6CQdEahwhKRSrZ3ij6AbaVo2Cogb1woDYXEimbJUyzDwm5P6iGZMw6S4JjrHZLawzCjnQG5X4hYdAF3kHEybPHkGxAbskq9gqLDaPznobzn', 214 | urls: 'https://zec1.trezor.io', 215 | network: { 216 | messagePrefix: '\x18ZCash Signed Message:\n', 217 | bip32: { 218 | private: 76066276, 219 | public: 76067358, 220 | }, 221 | pubKeyHash: 7352, 222 | scriptHash: 7357, 223 | wif: 0x80, 224 | coin: 'zcash', 225 | consensusBranchId: { 226 | 1: 0x00, 227 | 2: 0x00, 228 | 3: 0x5ba81b19, 229 | 4: 0x76b809bb, 230 | }, 231 | }, 232 | }, 233 | }; 234 | 235 | const onSelectChange = () => { 236 | const select = document.getElementById('network'); 237 | const d = data[select.value]; 238 | document.getElementById('xpubs').value = d.xpubs; 239 | document.getElementById('urls').value = d.urls; 240 | }; 241 | 242 | window.onload = () => { 243 | const select = document.getElementById('network'); 244 | select.onchange = onSelectChange; 245 | Object.keys(data).forEach((key) => { 246 | const option = document.createElement('option'); 247 | option.text = key; 248 | option.value = key; 249 | select.appendChild(option); 250 | }); 251 | onSelectChange(); 252 | }; 253 | 254 | window.data = data; 255 | -------------------------------------------------------------------------------- /src/discovery/worker/outside/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type { Network as BitcoinJsNetwork } from '@trezor/utxo-lib'; 4 | import bchaddrjs from 'bchaddrjs'; 5 | import type { 6 | PromiseRequestType, 7 | StreamRequestType, 8 | ChunkDiscoveryInfo, 9 | } from '../types'; 10 | 11 | 12 | import { Emitter, Stream, StreamWithEnding } from '../../../utils/stream'; 13 | import type { Blockchain, TransactionWithHeight } from '../../../bitcore'; 14 | import type { AddressSource } from '../../../address-source'; 15 | import type { AccountInfo, AccountLoadStatus, ForceAddedTransaction } from '../../index'; 16 | 17 | import { WorkerChannel } from './channel'; 18 | 19 | // eslint-disable-next-line no-undef 20 | type WorkerFactory = () => Worker; 21 | 22 | export class WorkerDiscoveryHandler { 23 | blockchain: Blockchain; 24 | 25 | addressSources: Array; 26 | 27 | workerChannel: WorkerChannel; 28 | 29 | network: BitcoinJsNetwork; 30 | 31 | cashAddress: boolean; 32 | 33 | // this array is the SAME object as in the WorkerDiscovery object 34 | // it will be changed here - this is intentional 35 | // we want to delete from this array if we actually see the tx in the wild 36 | forceAddedTransactions: Array = []; 37 | 38 | constructor( 39 | f: WorkerFactory, 40 | blockchain: Blockchain, 41 | addressSources: Array, 42 | network: BitcoinJsNetwork, 43 | cashAddress: boolean, 44 | forceAddedTransactions: Array, 45 | ) { 46 | this.blockchain = blockchain; 47 | this.addressSources = addressSources; 48 | 49 | this.workerChannel = new WorkerChannel(f, r => this.getPromise(r), r => this.getStream(r)); 50 | this.network = network; 51 | this.cashAddress = cashAddress; 52 | this.forceAddedTransactions = forceAddedTransactions; 53 | } 54 | 55 | discovery( 56 | ai: ?AccountInfo, 57 | xpub: string, 58 | segwit: boolean, 59 | gap: number, 60 | 61 | // what (new Date().getTimezoneOffset()) returns 62 | // note that it is NEGATIVE from the UTC string timezone 63 | // so, UTC+2 timezone returns -120... 64 | // it's javascript, it's insane by default 65 | timeOffset: number, 66 | ): StreamWithEnding { 67 | // $FlowIssue 68 | const webassembly = typeof WebAssembly !== 'undefined'; 69 | this.workerChannel.postToWorker({ 70 | type: 'init', 71 | state: ai, 72 | network: this.network, 73 | webassembly, 74 | xpub, 75 | segwit, 76 | cashAddress: this.cashAddress, 77 | gap, 78 | timeOffset, 79 | }); 80 | this.workerChannel.postToWorker({ type: 'startDiscovery' }); 81 | 82 | const promise = this.workerChannel.resPromise(() => { 83 | this.counter.finisher.emit(); 84 | this.counter.stream.dispose(); 85 | }); 86 | 87 | const res: StreamWithEnding = StreamWithEnding 88 | .fromStreamAndPromise( 89 | this.counter.stream, 90 | promise, 91 | ); 92 | return res; 93 | } 94 | 95 | counter = new TransactionCounter(); 96 | 97 | getStream(p: StreamRequestType): Stream { 98 | if (p.type === 'chunkTransactions') { 99 | const source = this.addressSources[p.chainId]; 100 | if (p.chainId === 0) { 101 | this.counter.setCount(p.pseudoCount); 102 | } 103 | return this.getChunkStream( 104 | source, 105 | p.firstIndex, 106 | p.lastIndex, 107 | p.startBlock, 108 | p.endBlock, 109 | p.chainId === 0, 110 | p.addresses, 111 | ); 112 | } 113 | return Stream.simple(`Unknown request ${p.type}`); 114 | } 115 | 116 | getPromise(p: PromiseRequestType): Promise { 117 | if (p.type === 'lookupBlockHash') { 118 | return this.blockchain.lookupBlockHash(p.height); 119 | } 120 | if (p.type === 'lookupSyncStatus') { 121 | return this.blockchain.lookupSyncStatus().then(({ height }) => height); 122 | } 123 | if (p.type === 'doesTransactionExist') { 124 | return this.blockchain.lookupTransaction(p.txid) 125 | .then(() => true, () => false); 126 | } 127 | return Promise.reject(new Error(`Unknown request ${p.type}`)); 128 | } 129 | 130 | static deriveAddresses( 131 | source: ?AddressSource, 132 | addresses: ?Array, 133 | firstIndex: number, 134 | lastIndex: number, 135 | ): Promise> { 136 | if (addresses == null) { 137 | if (source == null) { 138 | return Promise.reject(new Error('Cannot derive addresses in worker without webassembly')); 139 | } 140 | return source.derive(firstIndex, lastIndex); 141 | } 142 | return Promise.resolve(addresses); 143 | } 144 | 145 | getChunkStream( 146 | source: ?AddressSource, 147 | firstIndex: number, 148 | lastIndex: number, 149 | startBlock: number, 150 | endBlock: number, 151 | add: boolean, 152 | oaddresses: ?Array, 153 | ): Stream { 154 | const addressPromise = WorkerDiscoveryHandler.deriveAddresses( 155 | source, 156 | oaddresses, 157 | firstIndex, 158 | lastIndex, 159 | ); 160 | 161 | const errStream: Stream = Stream.fromPromise( 162 | addressPromise.then((paddresses) => { 163 | const addresses = this.cashAddress 164 | ? paddresses.map(a => bchaddrjs.toCashAddress(a)) 165 | : paddresses; 166 | 167 | return this.blockchain.lookupTransactionsStream(addresses, endBlock, startBlock) 168 | .map( 169 | (transactions) => { 170 | if (transactions instanceof Error) { 171 | return transactions.message; 172 | } 173 | const transactions_: Array = transactions; 174 | 175 | // code for handling forceAdded transactions 176 | const addedTransactions = []; 177 | this.forceAddedTransactions.slice().forEach((transaction, i) => { 178 | const transaction_: TransactionWithHeight = { 179 | ...transaction, 180 | height: null, 181 | timestamp: null, 182 | }; 183 | if (transactions_ 184 | .map(t => t.hash) 185 | .some(hash => transaction.hash === hash)) { 186 | // transaction already came from blockchain again 187 | this.forceAddedTransactions.splice(i, 1); 188 | } else { 189 | const txAddresses = new Set(); 190 | transaction 191 | .inputAddresses 192 | .concat(transaction.outputAddresses) 193 | .forEach((a) => { 194 | if (a != null) { 195 | txAddresses.add(a); 196 | } 197 | }); 198 | if (addresses.some(address => txAddresses.has(address))) { 199 | addedTransactions.push(transaction_); 200 | } 201 | } 202 | }); 203 | 204 | this.counter.setCount(this.counter.count + transactions.length); 205 | 206 | const ci: ChunkDiscoveryInfo = { 207 | transactions: transactions.concat(addedTransactions), addresses, 208 | }; 209 | return ci; 210 | }, 211 | ); 212 | }), 213 | ); 214 | const resStream: Stream = errStream 215 | .map( 216 | (k: (ChunkDiscoveryInfo | string | Error)): (ChunkDiscoveryInfo | string) => { 217 | if (k instanceof Error) { 218 | return k.message; 219 | } 220 | return k; 221 | }, 222 | ); 223 | return resStream; 224 | } 225 | } 226 | 227 | class TransactionCounter { 228 | count: number = 0; 229 | 230 | emitter: Emitter = new Emitter(); 231 | 232 | finisher: Emitter = new Emitter(); 233 | 234 | stream: Stream = Stream 235 | .fromEmitterFinish( 236 | this.emitter, 237 | this.finisher, 238 | () => { }, 239 | ); 240 | 241 | setCount(i: number) { 242 | if (i > this.count) { 243 | this.count = i; 244 | this.emitter.emit({ transactions: this.count }); 245 | } 246 | } 247 | } 248 | --------------------------------------------------------------------------------