├── Images.xcassets ├── Contents.json ├── icon.imageset │ ├── icon.png │ ├── icon@2x.png │ ├── icon@3x.png │ └── Contents.json └── AppIcon.appiconset │ ├── Icon-120.png │ ├── Icon-121.png │ ├── Icon-152.png │ ├── Icon-167.png │ ├── Icon-180.png │ ├── Icon-20.png │ ├── Icon-29.png │ ├── Icon-40.png │ ├── Icon-41.png │ ├── Icon-42.png │ ├── Icon-58.png │ ├── Icon-59.png │ ├── Icon-60.png │ ├── Icon-76.png │ ├── Icon-80.png │ ├── Icon-81.png │ ├── Icon-87.png │ ├── Icon-1024.png │ └── Contents.json ├── constants.js ├── Appfile ├── AppDelegate.h ├── main.m ├── hd-segwit-bech32-wallet.js ├── index.js ├── placeholder-wallet.js ├── Fastfile ├── .gitignore ├── LICENSE ├── segwit-bech-wallet.js ├── Base.lproj ├── LaunchScreen.xib └── MainInterface.storyboard ├── onAppLaunch.js ├── quickActions.js ├── biometrics.js ├── AppDelegate.m ├── New Group └── API.swift ├── hd-legacy-p2pkh-wallet.js ├── walletGradient.js ├── abstract-wallet.js ├── segwit-p2sh-wallet.js ├── hd-legacy-breadwallet-wallet.js ├── Info.plist ├── hd-segwit-p2sh-wallet.js ├── watch-only-wallet.js ├── walletImport.js ├── deeplinkSchemaMatch.js ├── hd-segwit-bech32-transaction.js ├── legacy-wallet.js ├── app-storage.js ├── abstract-hd-wallet.js ├── lightning-custodian-wallet.js └── abstract-hd-electrum-wallet.js /Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Let's keep config vars, constants and definitions here 3 | */ 4 | 5 | export const useBlockcypherTokens = false; 6 | -------------------------------------------------------------------------------- /Images.xcassets/icon.imageset/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/icon.imageset/icon.png -------------------------------------------------------------------------------- /Images.xcassets/icon.imageset/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/icon.imageset/icon@2x.png -------------------------------------------------------------------------------- /Images.xcassets/icon.imageset/icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/icon.imageset/icon@3x.png -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Icon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/AppIcon.appiconset/Icon-120.png -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Icon-121.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/AppIcon.appiconset/Icon-121.png -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/AppIcon.appiconset/Icon-152.png -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Icon-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/AppIcon.appiconset/Icon-167.png -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/AppIcon.appiconset/Icon-180.png -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/AppIcon.appiconset/Icon-20.png -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/AppIcon.appiconset/Icon-29.png -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/AppIcon.appiconset/Icon-40.png -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Icon-41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/AppIcon.appiconset/Icon-41.png -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Icon-42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/AppIcon.appiconset/Icon-42.png -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Icon-58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/AppIcon.appiconset/Icon-58.png -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Icon-59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/AppIcon.appiconset/Icon-59.png -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/AppIcon.appiconset/Icon-60.png -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/AppIcon.appiconset/Icon-76.png -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Icon-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/AppIcon.appiconset/Icon-80.png -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Icon-81.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/AppIcon.appiconset/Icon-81.png -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Icon-87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/AppIcon.appiconset/Icon-87.png -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedster0629/Smart-Contract-Development/HEAD/Images.xcassets/AppIcon.appiconset/Icon-1024.png -------------------------------------------------------------------------------- /Appfile: -------------------------------------------------------------------------------- 1 | # app_identifier("[[APP_IDENTIFIER]]") # The bundle identifier of your app 2 | # apple_id("[[APPLE_ID]]") # Your Apple email address 3 | 4 | 5 | # For more information about the Appfile, see: 6 | # https://docs.fastlane.tools/advanced/#appfile 7 | -------------------------------------------------------------------------------- /AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | 10 | @interface AppDelegate : UIResponder 11 | 12 | @property (nonatomic, strong) UIWindow *window; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /main.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Images.xcassets/icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "icon@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "icon@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /hd-segwit-bech32-wallet.js: -------------------------------------------------------------------------------- 1 | import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet'; 2 | 3 | /** 4 | * HD Wallet (BIP39). 5 | * In particular, BIP84 (Bech32 Native Segwit) 6 | * @see https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki 7 | */ 8 | export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet { 9 | static type = 'HDsegwitBech32'; 10 | static typeReadable = 'HD SegWit (BIP84 Bech32 Native)'; 11 | 12 | allowSend() { 13 | return true; 14 | } 15 | 16 | allowBatchSend() { 17 | return true; 18 | } 19 | 20 | allowSendMax() { 21 | return true; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export * from './abstract-wallet'; 2 | export * from './app-storage'; 3 | export * from './constants'; 4 | export * from './legacy-wallet'; 5 | export * from './segwit-bech-wallet'; 6 | export * from './segwit-p2sh-wallet'; 7 | export * from './hd-segwit-p2sh-wallet'; 8 | export * from './hd-legacy-breadwallet-wallet'; 9 | export * from './hd-legacy-p2pkh-wallet'; 10 | export * from './watch-only-wallet'; 11 | export * from './lightning-custodian-wallet'; 12 | export * from './abstract-hd-wallet'; 13 | export * from './hd-segwit-bech32-wallet'; 14 | export * from './hd-segwit-bech32-transaction'; 15 | export * from './placeholder-wallet'; 16 | -------------------------------------------------------------------------------- /placeholder-wallet.js: -------------------------------------------------------------------------------- 1 | import { AbstractWallet } from './abstract-wallet'; 2 | 3 | export class PlaceholderWallet extends AbstractWallet { 4 | static type = 'placeholder'; 5 | static typeReadable = 'Placeholder'; 6 | 7 | constructor() { 8 | super(); 9 | this._isFailure = false; 10 | } 11 | 12 | allowSend() { 13 | return false; 14 | } 15 | 16 | getLabel() { 17 | return this.getIsFailure() ? 'Wallet Import' : 'Importing Wallet...'; 18 | } 19 | 20 | allowReceive() { 21 | return false; 22 | } 23 | 24 | getIsFailure() { 25 | return this._isFailure; 26 | } 27 | 28 | setIsFailure(value) { 29 | this._isFailure = value; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | # update_fastlane 15 | 16 | default_platform(:ios) 17 | 18 | platform :ios do 19 | desc "Description of what the lane does" 20 | lane :custom_lane do 21 | # add actions here: https://docs.fastlane.tools/actions 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # C++ objects and libs 2 | *.slo 3 | *.lo 4 | *.o 5 | *.a 6 | *.la 7 | *.lai 8 | *.so 9 | *.so.* 10 | *.dll 11 | *.dylib 12 | 13 | # Qt-es 14 | object_script.*.Release 15 | object_script.*.Debug 16 | *_plugin_import.cpp 17 | /.qmake.cache 18 | /.qmake.stash 19 | *.pro.user 20 | *.pro.user.* 21 | *.qbs.user 22 | *.qbs.user.* 23 | *.moc 24 | moc_*.cpp 25 | moc_*.h 26 | qrc_*.cpp 27 | ui_*.h 28 | *.qmlc 29 | *.jsc 30 | Makefile* 31 | *build-* 32 | *.qm 33 | *.prl 34 | 35 | # Qt unit tests 36 | target_wrapper.* 37 | 38 | # QtCreator 39 | *.autosave 40 | 41 | # QtCreator Qml 42 | *.qmlproject.user 43 | *.qmlproject.user.* 44 | 45 | # QtCreator CMake 46 | CMakeLists.txt.user* 47 | 48 | # QtCreator 4.8< compilation database 49 | compile_commands.json 50 | 51 | # QtCreator local machine specific files for imported projects 52 | *creator.user* 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Blockchain Help 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 | -------------------------------------------------------------------------------- /segwit-bech-wallet.js: -------------------------------------------------------------------------------- 1 | import { LegacyWallet } from './legacy-wallet'; 2 | const bitcoin = require('bitcoinjs-lib'); 3 | 4 | export class SegwitBech32Wallet extends LegacyWallet { 5 | static type = 'segwitBech32'; 6 | static typeReadable = 'P2 WPKH'; 7 | 8 | getAddress() { 9 | if (this._address) return this._address; 10 | let address; 11 | try { 12 | let keyPair = bitcoin.ECPair.fromWIF(this.secret); 13 | address = bitcoin.payments.p2wpkh({ 14 | pubkey: keyPair.publicKey, 15 | }).address; 16 | } catch (err) { 17 | return false; 18 | } 19 | this._address = address; 20 | 21 | return this._address; 22 | } 23 | 24 | static witnessToAddress(witness) { 25 | const pubKey = Buffer.from(witness, 'hex'); 26 | return bitcoin.payments.p2wpkh({ 27 | pubkey: pubKey, 28 | network: bitcoin.networks.bitcoin, 29 | }).address; 30 | } 31 | 32 | /** 33 | * Converts script pub key to bech32 address if it can. Returns FALSE if it cant. 34 | * 35 | * @param scriptPubKey 36 | * @returns {boolean|string} Either bech32 address or false 37 | */ 38 | static scriptPubKeyToAddress(scriptPubKey) { 39 | const scriptPubKey2 = Buffer.from(scriptPubKey, 'hex'); 40 | let ret; 41 | try { 42 | ret = bitcoin.payments.p2wpkh({ 43 | output: scriptPubKey2, 44 | network: bitcoin.networks.bitcoin, 45 | }).address; 46 | } catch (_) { 47 | return false; 48 | } 49 | return ret; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /onAppLaunch.js: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-community/async-storage'; 2 | const BlueApp = require('../BlueApp'); 3 | 4 | export default class OnAppLaunch { 5 | static STORAGE_KEY = 'ONAPP_LAUNCH_SELECTED_DEFAULT_WALLET_KEY'; 6 | 7 | static async isViewAllWalletsEnabled() { 8 | try { 9 | const selectedDefaultWallet = await AsyncStorage.getItem(OnAppLaunch.STORAGE_KEY); 10 | return selectedDefaultWallet === '' || selectedDefaultWallet === null; 11 | } catch (_e) { 12 | return true; 13 | } 14 | } 15 | 16 | static async setViewAllWalletsEnabled(value) { 17 | if (!value) { 18 | const selectedDefaultWallet = await OnAppLaunch.getSelectedDefaultWallet(); 19 | if (!selectedDefaultWallet) { 20 | const firstWallet = BlueApp.getWallets()[0]; 21 | await OnAppLaunch.setSelectedDefaultWallet(firstWallet.getID()); 22 | } 23 | } else { 24 | await AsyncStorage.setItem(OnAppLaunch.STORAGE_KEY, ''); 25 | } 26 | } 27 | 28 | static async getSelectedDefaultWallet() { 29 | let selectedWallet = false; 30 | try { 31 | const selectedWalletID = JSON.parse(await AsyncStorage.getItem(OnAppLaunch.STORAGE_KEY)); 32 | selectedWallet = BlueApp.getWallets().find(wallet => wallet.getID() === selectedWalletID); 33 | if (!selectedWallet) { 34 | await AsyncStorage.setItem(OnAppLaunch.STORAGE_KEY, ''); 35 | } 36 | } catch (_e) { 37 | return false; 38 | } 39 | return selectedWallet; 40 | } 41 | 42 | static async setSelectedDefaultWallet(value) { 43 | await AsyncStorage.setItem(OnAppLaunch.STORAGE_KEY, JSON.stringify(value)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /quickActions.js: -------------------------------------------------------------------------------- 1 | import QuickActions from 'react-native-quick-actions'; 2 | import { Platform } from 'react-native'; 3 | 4 | export default class DeviceQuickActions { 5 | static shared = new DeviceQuickActions(); 6 | wallets; 7 | 8 | static setWallets(wallets) { 9 | DeviceQuickActions.shared.wallets = wallets.slice(0, 4); 10 | } 11 | 12 | static removeAllWallets() { 13 | DeviceQuickActions.shared.wallets = undefined; 14 | } 15 | 16 | static setQuickActions() { 17 | if (DeviceQuickActions.shared.wallets === undefined) { 18 | return; 19 | } 20 | QuickActions.isSupported((error, _supported) => { 21 | if (error === null) { 22 | let shortcutItems = []; 23 | const loc = require('../loc/'); 24 | for (const wallet of DeviceQuickActions.shared.wallets) { 25 | shortcutItems.push({ 26 | type: 'Wallets', // Required 27 | title: wallet.getLabel(), // Optional, if empty, `type` will be used instead 28 | subtitle: 29 | wallet.hideBalance || wallet.getBalance() <= 0 30 | ? '' 31 | : loc.formatBalance(Number(wallet.getBalance()), wallet.getPreferredBalanceUnit(), true), 32 | userInfo: { 33 | url: `bluewallet://wallet/${wallet.getID()}`, // Provide any custom data like deep linking URL 34 | }, 35 | icon: Platform.select({ android: 'quickactions', ios: 'bookmark' }), 36 | }); 37 | } 38 | QuickActions.setShortcutItems(shortcutItems); 39 | } 40 | }); 41 | } 42 | 43 | static clearShortcutItems() { 44 | QuickActions.clearShortcutItems(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /biometrics.js: -------------------------------------------------------------------------------- 1 | import Biometrics from 'react-native-biometrics'; 2 | const BlueApp = require('../BlueApp'); 3 | 4 | export default class Biometric { 5 | static STORAGEKEY = 'Biometrics'; 6 | static FaceID = Biometrics.FaceID; 7 | static TouchID = Biometrics.TouchID; 8 | static Biometrics = Biometrics.Biometrics; 9 | 10 | static async isDeviceBiometricCapable() { 11 | const isDeviceBiometricCapable = await Biometrics.isSensorAvailable(); 12 | if (isDeviceBiometricCapable.available) { 13 | return true; 14 | } 15 | Biometric.setBiometricUseEnabled(false); 16 | return false; 17 | } 18 | 19 | static async biometricType() { 20 | try { 21 | const isSensorAvailable = await Biometrics.isSensorAvailable(); 22 | return isSensorAvailable.biometryType; 23 | } catch (e) { 24 | console.log(e); 25 | } 26 | return false; 27 | } 28 | 29 | static async isBiometricUseEnabled() { 30 | try { 31 | const enabledBiometrics = await BlueApp.getItem(Biometric.STORAGEKEY); 32 | return !!enabledBiometrics; 33 | } catch (_e) { 34 | await BlueApp.setItem(Biometric.STORAGEKEY, ''); 35 | return false; 36 | } 37 | } 38 | 39 | static async isBiometricUseCapableAndEnabled() { 40 | const isBiometricUseEnabled = await Biometric.isBiometricUseEnabled(); 41 | const isDeviceBiometricCapable = await Biometric.isDeviceBiometricCapable(); 42 | return isBiometricUseEnabled && isDeviceBiometricCapable; 43 | } 44 | 45 | static async setBiometricUseEnabled(value) { 46 | await BlueApp.setItem(Biometric.STORAGEKEY, value === true ? '1' : ''); 47 | } 48 | 49 | static async unlockWithBiometrics() { 50 | const isDeviceBiometricCapable = await Biometric.isDeviceBiometricCapable(); 51 | if (isDeviceBiometricCapable) { 52 | try { 53 | const isConfirmed = await Biometrics.simplePrompt({ promptMessage: 'Please confirm your identity.' }); 54 | return isConfirmed.success; 55 | } catch (_e) { 56 | return false; 57 | } 58 | } 59 | return false; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /AppDelegate.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import "AppDelegate.h" 9 | 10 | #import 11 | #import 12 | #import 13 | #import "RNQuickActionManager.h" 14 | 15 | @implementation AppDelegate 16 | 17 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 18 | { 19 | NSURL *jsCodeLocation; 20 | 21 | jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 22 | 23 | RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation 24 | moduleName:@"BlueWallet" 25 | initialProperties:nil 26 | launchOptions:launchOptions]; 27 | rootView.backgroundColor = [UIColor whiteColor]; 28 | 29 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 30 | UIViewController *rootViewController = [UIViewController new]; 31 | rootViewController.view = rootView; 32 | self.window.rootViewController = rootViewController; 33 | [self.window makeKeyAndVisible]; 34 | return YES; 35 | } 36 | 37 | - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { 38 | return [RCTLinkingManager application:app openURL:url options:options]; 39 | } 40 | 41 | - (BOOL)application:(UIApplication *)application shouldAllowExtensionPointIdentifier:(UIApplicationExtensionPointIdentifier)extensionPointIdentifier { 42 | return NO; 43 | } 44 | 45 | - (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL succeeded)) completionHandler { 46 | [RNQuickActionManager onQuickActionPress:shortcutItem completionHandler:completionHandler]; 47 | } 48 | 49 | @end 50 | -------------------------------------------------------------------------------- /New Group/API.swift: -------------------------------------------------------------------------------- 1 | // 2 | // API.swift 3 | // TodayExtension 4 | // 5 | // Created by Marcos Rodriguez on 11/2/19. 6 | // Copyright © 2019 Facebook. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class API { 12 | 13 | static func fetchPrice(currency: String, completion: @escaping ((Dictionary?, Error?) -> Void)) { 14 | guard let url = URL(string: "https://api.coindesk.com/v1/bpi/currentPrice/\(currency).json") else {return} 15 | 16 | URLSession.shared.dataTask(with: url) { (data, response, error) in 17 | guard let dataResponse = data, 18 | let json = try? JSONSerialization.jsonObject(with: dataResponse, options: .mutableContainers) as? Dictionary, 19 | error == nil else { 20 | print(error?.localizedDescription ?? "Response Error") 21 | completion(nil, error) 22 | return } 23 | 24 | completion(json, nil) 25 | }.resume() 26 | } 27 | 28 | static func getUserPreferredCurrency() -> String { 29 | guard let userDefaults = UserDefaults(suiteName: "group.io.bluewallet.bluewallet"), 30 | let preferredCurrency = userDefaults.string(forKey: "preferredCurrency") 31 | else { 32 | return "USD" 33 | } 34 | 35 | if preferredCurrency != API.getLastSelectedCurrency() { 36 | UserDefaults.standard.removeObject(forKey: TodayData.TodayCachedDataStoreKey) 37 | UserDefaults.standard.removeObject(forKey: TodayData.TodayDataStoreKey) 38 | UserDefaults.standard.synchronize() 39 | } 40 | 41 | return preferredCurrency 42 | } 43 | 44 | static func getUserPreferredCurrencyLocale() -> String { 45 | guard let userDefaults = UserDefaults(suiteName: "group.io.bluewallet.bluewallet"), 46 | let preferredCurrency = userDefaults.string(forKey: "preferredCurrencyLocale") 47 | else { 48 | return "en_US" 49 | } 50 | return preferredCurrency 51 | } 52 | 53 | static func getLastSelectedCurrency() -> String { 54 | guard let dataStore = UserDefaults.standard.string(forKey: "currency") else { 55 | return "USD" 56 | } 57 | 58 | return dataStore 59 | } 60 | 61 | static func saveNewSelectedCurrency() { 62 | UserDefaults.standard.setValue(API.getUserPreferredCurrency(), forKey: "currency") 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-40.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-60.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-58.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-87.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-80.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-121.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-120.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-180.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "Icon-20.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-41.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-29.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-59.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-42.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-81.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-76.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-152.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-167.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "Icon-1024.png", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } -------------------------------------------------------------------------------- /hd-legacy-p2pkh-wallet.js: -------------------------------------------------------------------------------- 1 | import { AbstractHDWallet } from './abstract-hd-wallet'; 2 | import bip39 from 'bip39'; 3 | import BigNumber from 'bignumber.js'; 4 | import signer from '../models/signer'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | const HDNode = require('bip32'); 7 | 8 | /** 9 | * HD Wallet (BIP39). 10 | * In particular, BIP44 (P2PKH legacy addressess) 11 | * @see https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki 12 | */ 13 | export class HDLegacyP2PKHWallet extends AbstractHDWallet { 14 | static type = 'HDlegacyP2PKH'; 15 | static typeReadable = 'HD Legacy (BIP44 P2PKH)'; 16 | 17 | allowSend() { 18 | return true; 19 | } 20 | 21 | getXpub() { 22 | if (this._xpub) { 23 | return this._xpub; // cache hit 24 | } 25 | const mnemonic = this.secret; 26 | const seed = bip39.mnemonicToSeed(mnemonic); 27 | const root = bitcoin.bip32.fromSeed(seed); 28 | 29 | const path = "m/44'/0'/0'"; 30 | const child = root.derivePath(path).neutered(); 31 | this._xpub = child.toBase58(); 32 | 33 | return this._xpub; 34 | } 35 | 36 | _getExternalWIFByIndex(index) { 37 | return this._getWIFByIndex(false, index); 38 | } 39 | 40 | _getInternalWIFByIndex(index) { 41 | return this._getWIFByIndex(true, index); 42 | } 43 | 44 | /** 45 | * Get internal/external WIF by wallet index 46 | * @param {Boolean} internal 47 | * @param {Number} index 48 | * @returns {*} 49 | * @private 50 | */ 51 | _getWIFByIndex(internal, index) { 52 | const mnemonic = this.secret; 53 | const seed = bip39.mnemonicToSeed(mnemonic); 54 | 55 | const root = HDNode.fromSeed(seed); 56 | const path = `m/44'/0'/0'/${internal ? 1 : 0}/${index}`; 57 | const child = root.derivePath(path); 58 | 59 | return child.toWIF(); 60 | } 61 | 62 | _getExternalAddressByIndex(index) { 63 | index = index * 1; // cast to int 64 | if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit 65 | 66 | const node = bitcoin.bip32.fromBase58(this.getXpub()); 67 | const address = bitcoin.payments.p2pkh({ 68 | pubkey: node.derive(0).derive(index).publicKey, 69 | }).address; 70 | 71 | return (this.external_addresses_cache[index] = address); 72 | } 73 | 74 | _getInternalAddressByIndex(index) { 75 | index = index * 1; // cast to int 76 | if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit 77 | 78 | const node = bitcoin.bip32.fromBase58(this.getXpub()); 79 | const address = bitcoin.payments.p2pkh({ 80 | pubkey: node.derive(1).derive(index).publicKey, 81 | }).address; 82 | 83 | return (this.internal_addresses_cache[index] = address); 84 | } 85 | 86 | createTx(utxos, amount, fee, address) { 87 | for (let utxo of utxos) { 88 | utxo.wif = this._getWifForAddress(utxo.address); 89 | } 90 | 91 | let amountPlusFee = parseFloat(new BigNumber(amount).plus(fee).toString(10)); 92 | return signer.createHDTransaction( 93 | utxos, 94 | address, 95 | amountPlusFee, 96 | fee, 97 | this._getInternalAddressByIndex(this.next_free_change_address_index), 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /walletGradient.js: -------------------------------------------------------------------------------- 1 | import { LegacyWallet } from './legacy-wallet'; 2 | import { HDSegwitP2SHWallet } from './hd-segwit-p2sh-wallet'; 3 | import { LightningCustodianWallet } from './lightning-custodian-wallet'; 4 | import { HDLegacyBreadwalletWallet } from './hd-legacy-breadwallet-wallet'; 5 | import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet'; 6 | import { WatchOnlyWallet } from './watch-only-wallet'; 7 | import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet'; 8 | import { PlaceholderWallet } from './placeholder-wallet'; 9 | 10 | export default class WalletGradient { 11 | static hdSegwitP2SHWallet = ['#65ceef', '#68bbe1']; 12 | static hdSegwitBech32Wallet = ['#68bbe1', '#3b73d4']; 13 | static watchOnlyWallet = ['#7d7d7d', '#4a4a4a']; 14 | static legacyWallet = ['#40fad1', '#15be98']; 15 | static hdLegacyP2PKHWallet = ['#e36dfa', '#bd10e0']; 16 | static hdLegacyBreadWallet = ['#fe6381', '#f99c42']; 17 | static defaultGradients = ['#c65afb', '#9053fe']; 18 | static lightningCustodianWallet = ['#f1be07', '#f79056']; 19 | static createWallet = ['#eef0f4', '#eef0f4']; 20 | 21 | static gradientsFor(type) { 22 | let gradient; 23 | switch (type) { 24 | case WatchOnlyWallet.type: 25 | gradient = WalletGradient.watchOnlyWallet; 26 | break; 27 | case LegacyWallet.type: 28 | gradient = WalletGradient.legacyWallet; 29 | break; 30 | case HDLegacyP2PKHWallet.type: 31 | gradient = WalletGradient.hdLegacyP2PKHWallet; 32 | break; 33 | case HDLegacyBreadwalletWallet.type: 34 | gradient = WalletGradient.hdLegacyBreadWallet; 35 | break; 36 | case HDSegwitP2SHWallet.type: 37 | gradient = WalletGradient.hdSegwitP2SHWallet; 38 | break; 39 | case HDSegwitBech32Wallet.type: 40 | gradient = WalletGradient.hdSegwitBech32Wallet; 41 | break; 42 | case LightningCustodianWallet.type: 43 | gradient = WalletGradient.lightningCustodianWallet; 44 | break; 45 | case PlaceholderWallet.type: 46 | gradient = WalletGradient.watchOnlyWallet; 47 | break; 48 | case 'CreateWallet': 49 | gradient = WalletGradient.createWallet; 50 | break; 51 | default: 52 | gradient = WalletGradient.defaultGradients; 53 | break; 54 | } 55 | return gradient; 56 | } 57 | 58 | static headerColorFor(type) { 59 | let gradient; 60 | switch (type) { 61 | case WatchOnlyWallet.type: 62 | gradient = WalletGradient.watchOnlyWallet; 63 | break; 64 | case LegacyWallet.type: 65 | gradient = WalletGradient.legacyWallet; 66 | break; 67 | case HDLegacyP2PKHWallet.type: 68 | gradient = WalletGradient.hdLegacyP2PKHWallet; 69 | break; 70 | case HDLegacyBreadwalletWallet.type: 71 | gradient = WalletGradient.hdLegacyBreadWallet; 72 | break; 73 | case HDSegwitP2SHWallet.type: 74 | gradient = WalletGradient.hdSegwitP2SHWallet; 75 | break; 76 | case HDSegwitBech32Wallet.type: 77 | gradient = WalletGradient.hdSegwitBech32Wallet; 78 | break; 79 | case LightningCustodianWallet.type: 80 | gradient = WalletGradient.lightningCustodianWallet; 81 | break; 82 | case 'CreateWallet': 83 | gradient = WalletGradient.createWallet; 84 | break; 85 | default: 86 | gradient = WalletGradient.defaultGradients; 87 | break; 88 | } 89 | return gradient[0]; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /abstract-wallet.js: -------------------------------------------------------------------------------- 1 | import { BitcoinUnit, Chain } from '../models/bitcoinUnits'; 2 | const createHash = require('create-hash'); 3 | export class AbstractWallet { 4 | static type = 'abstract'; 5 | static typeReadable = 'abstract'; 6 | 7 | static fromJson(obj) { 8 | let obj2 = JSON.parse(obj); 9 | let temp = new this(); 10 | for (let key2 of Object.keys(obj2)) { 11 | temp[key2] = obj2[key2]; 12 | } 13 | 14 | return temp; 15 | } 16 | 17 | constructor() { 18 | this.type = this.constructor.type; 19 | this.typeReadable = this.constructor.typeReadable; 20 | this.label = ''; 21 | this.secret = ''; // private key or recovery phrase 22 | this.balance = 0; 23 | this.unconfirmed_balance = 0; 24 | this.transactions = []; 25 | this._address = false; // cache 26 | this.utxo = []; 27 | this._lastTxFetch = 0; 28 | this._lastBalanceFetch = 0; 29 | this.preferredBalanceUnit = BitcoinUnit.BTC; 30 | this.chain = Chain.ONCHAIN; 31 | this.hideBalance = false; 32 | this.userHasSavedExport = false; 33 | } 34 | 35 | getID() { 36 | return createHash('sha256') 37 | .update(this.getSecret()) 38 | .digest() 39 | .toString('hex'); 40 | } 41 | 42 | getTransactions() { 43 | return this.transactions; 44 | } 45 | 46 | getUserHasSavedExport() { 47 | return this.userHasSavedExport; 48 | } 49 | 50 | setUserHasSavedExport(value) { 51 | this.userHasSavedExport = value; 52 | } 53 | 54 | /** 55 | * 56 | * @returns {string} 57 | */ 58 | getLabel() { 59 | if (this.label.trim().length === 0) { 60 | return 'Wallet'; 61 | } 62 | return this.label; 63 | } 64 | 65 | getXpub() { 66 | return this._address; 67 | } 68 | 69 | /** 70 | * 71 | * @returns {number} Available to spend amount, int, in sats 72 | */ 73 | getBalance() { 74 | return this.balance; 75 | } 76 | 77 | getPreferredBalanceUnit() { 78 | for (let value of Object.values(BitcoinUnit)) { 79 | if (value === this.preferredBalanceUnit) { 80 | return this.preferredBalanceUnit; 81 | } 82 | } 83 | return BitcoinUnit.BTC; 84 | } 85 | 86 | allowReceive() { 87 | return true; 88 | } 89 | 90 | allowSend() { 91 | return true; 92 | } 93 | 94 | allowSendMax(): boolean { 95 | return false; 96 | } 97 | 98 | allowRBF() { 99 | return false; 100 | } 101 | 102 | allowBatchSend() { 103 | return false; 104 | } 105 | 106 | weOwnAddress(address) { 107 | return this._address === address; 108 | } 109 | 110 | /** 111 | * Returns delta of unconfirmed balance. For example, if theres no 112 | * unconfirmed balance its 0 113 | * 114 | * @return {number} 115 | */ 116 | getUnconfirmedBalance() { 117 | return this.unconfirmed_balance; 118 | } 119 | 120 | setLabel(newLabel) { 121 | this.label = newLabel; 122 | return this; 123 | } 124 | 125 | getSecret() { 126 | return this.secret; 127 | } 128 | 129 | setSecret(newSecret) { 130 | this.secret = newSecret.trim(); 131 | return this; 132 | } 133 | 134 | getLatestTransactionTime() { 135 | return 0; 136 | } 137 | 138 | // createTx () { throw Error('not implemented') } 139 | 140 | getAddress() { 141 | throw Error('not implemented'); 142 | } 143 | 144 | getAddressAsync() { 145 | return new Promise(resolve => resolve(this.getAddress())); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /segwit-p2sh-wallet.js: -------------------------------------------------------------------------------- 1 | import { LegacyWallet } from './legacy-wallet'; 2 | const bitcoin = require('bitcoinjs-lib'); 3 | const signer = require('../models/signer'); 4 | const BigNumber = require('bignumber.js'); 5 | 6 | /** 7 | * Creates Segwit P2SH Bitcoin address 8 | * @param pubkey 9 | * @param network 10 | * @returns {String} 11 | */ 12 | function pubkeyToP2shSegwitAddress(pubkey, network) { 13 | network = network || bitcoin.networks.bitcoin; 14 | const { address } = bitcoin.payments.p2sh({ 15 | redeem: bitcoin.payments.p2wpkh({ pubkey, network }), 16 | network, 17 | }); 18 | return address; 19 | } 20 | 21 | export class SegwitP2SHWallet extends LegacyWallet { 22 | static type = 'segwitP2SH'; 23 | static typeReadable = 'SegWit (P2SH)'; 24 | 25 | allowRBF() { 26 | return true; 27 | } 28 | 29 | static witnessToAddress(witness) { 30 | const pubKey = Buffer.from(witness, 'hex'); 31 | return pubkeyToP2shSegwitAddress(pubKey); 32 | } 33 | 34 | /** 35 | * Converts script pub key to p2sh address if it can. Returns FALSE if it cant. 36 | * 37 | * @param scriptPubKey 38 | * @returns {boolean|string} Either p2sh address or false 39 | */ 40 | static scriptPubKeyToAddress(scriptPubKey) { 41 | const scriptPubKey2 = Buffer.from(scriptPubKey, 'hex'); 42 | let ret; 43 | try { 44 | ret = bitcoin.payments.p2sh({ 45 | output: scriptPubKey2, 46 | network: bitcoin.networks.bitcoin, 47 | }).address; 48 | } catch (_) { 49 | return false; 50 | } 51 | return ret; 52 | } 53 | 54 | getAddress() { 55 | if (this._address) return this._address; 56 | let address; 57 | try { 58 | let keyPair = bitcoin.ECPair.fromWIF(this.secret); 59 | let pubKey = keyPair.publicKey; 60 | if (!keyPair.compressed) { 61 | console.warn('only compressed public keys are good for segwit'); 62 | return false; 63 | } 64 | address = pubkeyToP2shSegwitAddress(pubKey); 65 | } catch (err) { 66 | return false; 67 | } 68 | this._address = address; 69 | 70 | return this._address; 71 | } 72 | 73 | /** 74 | * Takes UTXOs (as presented by blockcypher api), transforms them into 75 | * format expected by signer module, creates tx and returns signed string txhex. 76 | * 77 | * @param utxos Unspent outputs, expects blockcypher format 78 | * @param amount 79 | * @param fee 80 | * @param address 81 | * @param memo 82 | * @param sequence By default zero. Increased with each transaction replace. 83 | * @return string Signed txhex ready for broadcast 84 | */ 85 | createTx(utxos, amount, fee, address, memo, sequence) { 86 | // TODO: memo is not used here, get rid of it 87 | if (sequence === undefined) { 88 | sequence = 0; 89 | } 90 | // transforming UTXOs fields to how module expects it 91 | for (let u of utxos) { 92 | u.confirmations = 6; // hack to make module accept 0 confirmations 93 | u.txid = u.tx_hash; 94 | u.vout = u.tx_output_n; 95 | u.amount = new BigNumber(u.value); 96 | u.amount = u.amount.dividedBy(100000000); 97 | u.amount = u.amount.toString(10); 98 | } 99 | // console.log('creating tx ', amount, ' with fee ', fee, 'secret=', this.getSecret(), 'from address', this.getAddress()); 100 | let amountPlusFee = parseFloat(new BigNumber(amount).plus(fee).toString(10)); 101 | // to compensate that module substracts fee from amount 102 | return signer.createSegwitTransaction(utxos, address, amountPlusFee, fee, this.getSecret(), this.getAddress(), sequence); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /hd-legacy-breadwallet-wallet.js: -------------------------------------------------------------------------------- 1 | import { AbstractHDWallet } from './abstract-hd-wallet'; 2 | import Frisbee from 'frisbee'; 3 | import bip39 from 'bip39'; 4 | const bip32 = require('bip32'); 5 | const bitcoinjs = require('bitcoinjs-lib'); 6 | 7 | /** 8 | * HD Wallet (BIP39). 9 | * In particular, Breadwallet-compatible (Legacy addresses) 10 | */ 11 | export class HDLegacyBreadwalletWallet extends AbstractHDWallet { 12 | static type = 'HDLegacyBreadwallet'; 13 | static typeReadable = 'HD Legacy Breadwallet (P2PKH)'; 14 | 15 | /** 16 | * @see https://github.com/bitcoinjs/bitcoinjs-lib/issues/584 17 | * @see https://github.com/bitcoinjs/bitcoinjs-lib/issues/914 18 | * @see https://github.com/bitcoinjs/bitcoinjs-lib/issues/997 19 | */ 20 | getXpub() { 21 | if (this._xpub) { 22 | return this._xpub; // cache hit 23 | } 24 | const mnemonic = this.secret; 25 | const seed = bip39.mnemonicToSeed(mnemonic); 26 | const root = bip32.fromSeed(seed); 27 | 28 | const path = "m/0'"; 29 | const child = root.derivePath(path).neutered(); 30 | this._xpub = child.toBase58(); 31 | 32 | return this._xpub; 33 | } 34 | 35 | _getExternalAddressByIndex(index) { 36 | index = index * 1; // cast to int 37 | if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit 38 | const mnemonic = this.secret; 39 | const seed = bip39.mnemonicToSeed(mnemonic); 40 | const root = bip32.fromSeed(seed); 41 | 42 | const path = "m/0'/0/" + index; 43 | const child = root.derivePath(path); 44 | 45 | const address = bitcoinjs.payments.p2pkh({ 46 | pubkey: child.publicKey, 47 | }).address; 48 | 49 | return (this.external_addresses_cache[index] = address); 50 | } 51 | 52 | _getInternalAddressByIndex(index) { 53 | index = index * 1; // cast to int 54 | if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit 55 | const mnemonic = this.secret; 56 | const seed = bip39.mnemonicToSeed(mnemonic); 57 | const root = bip32.fromSeed(seed); 58 | 59 | const path = "m/0'/1/" + index; 60 | const child = root.derivePath(path); 61 | 62 | const address = bitcoinjs.payments.p2pkh({ 63 | pubkey: child.publicKey, 64 | }).address; 65 | 66 | return (this.internal_addresses_cache[index] = address); 67 | } 68 | 69 | _getExternalWIFByIndex(index) { 70 | return this._getWIFByIndex(false, index); 71 | } 72 | 73 | _getInternalWIFByIndex(index) { 74 | return this._getWIFByIndex(true, index); 75 | } 76 | 77 | /** 78 | * Get internal/external WIF by wallet index 79 | * @param {Boolean} internal 80 | * @param {Number} index 81 | * @returns {*} 82 | * @private 83 | */ 84 | _getWIFByIndex(internal, index) { 85 | const mnemonic = this.secret; 86 | const seed = bip39.mnemonicToSeed(mnemonic); 87 | const root = bitcoinjs.bip32.fromSeed(seed); 88 | const path = `m/0'/${internal ? 1 : 0}/${index}`; 89 | const child = root.derivePath(path); 90 | 91 | return child.keyPair.toWIF(); 92 | } 93 | 94 | /** 95 | * @inheritDoc 96 | */ 97 | async fetchBalance() { 98 | try { 99 | const api = new Frisbee({ baseURI: 'https://blockchain.info' }); 100 | 101 | let response = await api.get('/balance?active=' + this.getXpub()); 102 | 103 | if (response && response.body) { 104 | for (let xpub of Object.keys(response.body)) { 105 | this.balance = response.body[xpub].final_balance / 100000000; 106 | } 107 | this._lastBalanceFetch = +new Date(); 108 | } else { 109 | throw new Error('Could not fetch balance from API: ' + response.err); 110 | } 111 | } catch (err) { 112 | console.warn(err); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIUserInterfaceStyle 6 | Light 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleDisplayName 10 | BlueWallet 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 4.9.4 23 | CFBundleSignature 24 | ???? 25 | CFBundleURLTypes 26 | 27 | 28 | CFBundleTypeRole 29 | Editor 30 | CFBundleURLSchemes 31 | 32 | bitcoin 33 | lightning 34 | bluewallet 35 | lapp 36 | blue 37 | 38 | 39 | 40 | CFBundleVersion 41 | 232 42 | ITSAppUsesNonExemptEncryption 43 | 44 | LSRequiresIPhoneOS 45 | 46 | NSAppTransportSecurity 47 | 48 | NSAllowsArbitraryLoads 49 | 50 | NSExceptionDomains 51 | 52 | localhost 53 | 54 | NSExceptionAllowsInsecureHTTPLoads 55 | 56 | 57 | 58 | 59 | NSAppleMusicUsageDescription 60 | This alert should not show up as we do not require this data 61 | NSFaceIDUsageDescription 62 | In order to confirm your identity, we need your permission to use FaceID. 63 | NSBluetoothPeripheralUsageDescription 64 | This alert should not show up as we do not require this data 65 | NSCalendarsUsageDescription 66 | This alert should not show up as we do not require this data 67 | NSCameraUsageDescription 68 | In order to quickly scan the recipient's address, we need your permission to use the camera to scan their QR Code. 69 | NSLocationWhenInUseUsageDescription 70 | This alert should not show up as we do not require this data 71 | NSLocationAlwaysUsageDescription 72 | This alert should not show up as we do not require this data 73 | NSMicrophoneUsageDescription 74 | This alert should not show up as we do not require this data 75 | NSMotionUsageDescription 76 | This alert should not show up as we do not require this data 77 | NSPhotoLibraryAddUsageDescription 78 | This alert should not show up as we do not require this data 79 | NSPhotoLibraryUsageDescription 80 | In order to import an image for scanning, we need your permission to access your photo library. 81 | NSSpeechRecognitionUsageDescription 82 | This alert should not show up as we do not require this data 83 | UIAppFonts 84 | 85 | AntDesign.ttf 86 | Entypo.ttf 87 | EvilIcons.ttf 88 | Feather.ttf 89 | FontAwesome.ttf 90 | FontAwesome5_Brands.ttf 91 | FontAwesome5_Regular.ttf 92 | FontAwesome5_Solid.ttf 93 | Foundation.ttf 94 | Ionicons.ttf 95 | MaterialCommunityIcons.ttf 96 | MaterialIcons.ttf 97 | Octicons.ttf 98 | SimpleLineIcons.ttf 99 | Zocial.ttf 100 | 101 | UILaunchStoryboardName 102 | LaunchScreen 103 | UIRequiredDeviceCapabilities 104 | 105 | armv7 106 | 107 | UISupportedInterfaceOrientations 108 | 109 | UIInterfaceOrientationPortrait 110 | UIInterfaceOrientationPortraitUpsideDown 111 | 112 | UISupportedInterfaceOrientations~ipad 113 | 114 | UIInterfaceOrientationPortrait 115 | UIInterfaceOrientationLandscapeLeft 116 | UIInterfaceOrientationLandscapeRight 117 | UIInterfaceOrientationPortraitUpsideDown 118 | 119 | UIViewControllerBasedStatusBarAppearance 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /hd-segwit-p2sh-wallet.js: -------------------------------------------------------------------------------- 1 | import bip39 from 'bip39'; 2 | import BigNumber from 'bignumber.js'; 3 | import b58 from 'bs58check'; 4 | import signer from '../models/signer'; 5 | import { BitcoinUnit } from '../models/bitcoinUnits'; 6 | import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet'; 7 | const bitcoin = require('bitcoinjs-lib'); 8 | const HDNode = require('bip32'); 9 | 10 | /** 11 | * HD Wallet (BIP39). 12 | * In particular, BIP49 (P2SH Segwit) 13 | * @see https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki 14 | */ 15 | export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet { 16 | static type = 'HDsegwitP2SH'; 17 | static typeReadable = 'HD SegWit (BIP49 P2SH)'; 18 | 19 | allowSend() { 20 | return true; 21 | } 22 | 23 | allowSendMax(): boolean { 24 | return true; 25 | } 26 | 27 | /** 28 | * Get internal/external WIF by wallet index 29 | * @param {Boolean} internal 30 | * @param {Number} index 31 | * @returns {*} 32 | * @private 33 | */ 34 | _getWIFByIndex(internal, index) { 35 | const mnemonic = this.secret; 36 | const seed = bip39.mnemonicToSeed(mnemonic); 37 | const root = bitcoin.bip32.fromSeed(seed); 38 | const path = `m/49'/0'/0'/${internal ? 1 : 0}/${index}`; 39 | const child = root.derivePath(path); 40 | 41 | return bitcoin.ECPair.fromPrivateKey(child.privateKey).toWIF(); 42 | } 43 | 44 | _getExternalAddressByIndex(index) { 45 | index = index * 1; // cast to int 46 | if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit 47 | 48 | if (!this._node0) { 49 | const xpub = this.constructor._ypubToXpub(this.getXpub()); 50 | const hdNode = HDNode.fromBase58(xpub); 51 | this._node0 = hdNode.derive(0); 52 | } 53 | const address = this.constructor._nodeToP2shSegwitAddress(this._node0.derive(index)); 54 | 55 | return (this.external_addresses_cache[index] = address); 56 | } 57 | 58 | _getInternalAddressByIndex(index) { 59 | index = index * 1; // cast to int 60 | if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit 61 | 62 | if (!this._node1) { 63 | const xpub = this.constructor._ypubToXpub(this.getXpub()); 64 | const hdNode = HDNode.fromBase58(xpub); 65 | this._node1 = hdNode.derive(1); 66 | } 67 | const address = this.constructor._nodeToP2shSegwitAddress(this._node1.derive(index)); 68 | 69 | return (this.internal_addresses_cache[index] = address); 70 | } 71 | 72 | /** 73 | * Returning ypub actually, not xpub. Keeping same method name 74 | * for compatibility. 75 | * 76 | * @return {String} ypub 77 | */ 78 | getXpub() { 79 | if (this._xpub) { 80 | return this._xpub; // cache hit 81 | } 82 | // first, getting xpub 83 | const mnemonic = this.secret; 84 | const seed = bip39.mnemonicToSeed(mnemonic); 85 | const root = HDNode.fromSeed(seed); 86 | 87 | const path = "m/49'/0'/0'"; 88 | const child = root.derivePath(path).neutered(); 89 | const xpub = child.toBase58(); 90 | 91 | // bitcoinjs does not support ypub yet, so we just convert it from xpub 92 | let data = b58.decode(xpub); 93 | data = data.slice(4); 94 | data = Buffer.concat([Buffer.from('049d7cb2', 'hex'), data]); 95 | this._xpub = b58.encode(data); 96 | 97 | return this._xpub; 98 | } 99 | 100 | /** 101 | * 102 | * @param utxos 103 | * @param amount Either float (BTC) or string 'MAX' (BitcoinUnit.MAX) to send all 104 | * @param fee 105 | * @param address 106 | * @returns {string} 107 | */ 108 | createTx(utxos, amount, fee, address) { 109 | for (let utxo of utxos) { 110 | utxo.wif = this._getWifForAddress(utxo.address); 111 | } 112 | 113 | let amountPlusFee = parseFloat(new BigNumber(amount).plus(fee).toString(10)); 114 | 115 | if (amount === BitcoinUnit.MAX) { 116 | amountPlusFee = new BigNumber(0); 117 | for (let utxo of utxos) { 118 | amountPlusFee = amountPlusFee.plus(utxo.amount); 119 | } 120 | amountPlusFee = amountPlusFee.dividedBy(100000000).toString(10); 121 | } 122 | 123 | return signer.createHDSegwitTransaction( 124 | utxos, 125 | address, 126 | amountPlusFee, 127 | fee, 128 | this._getInternalAddressByIndex(this.next_free_change_address_index), 129 | ); 130 | } 131 | 132 | /** 133 | * Converts ypub to xpub 134 | * @param {String} ypub - wallet ypub 135 | * @returns {*} 136 | */ 137 | static _ypubToXpub(ypub) { 138 | let data = b58.decode(ypub); 139 | if (data.readUInt32BE() !== 0x049d7cb2) throw new Error('Not a valid ypub extended key!'); 140 | data = data.slice(4); 141 | data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]); 142 | 143 | return b58.encode(data); 144 | } 145 | 146 | /** 147 | * Creates Segwit P2SH Bitcoin address 148 | * @param hdNode 149 | * @returns {String} 150 | */ 151 | static _nodeToP2shSegwitAddress(hdNode) { 152 | const { address } = bitcoin.payments.p2sh({ 153 | redeem: bitcoin.payments.p2wpkh({ pubkey: hdNode.publicKey }), 154 | }); 155 | return address; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /watch-only-wallet.js: -------------------------------------------------------------------------------- 1 | import { LegacyWallet } from './legacy-wallet'; 2 | import { HDSegwitP2SHWallet } from './hd-segwit-p2sh-wallet'; 3 | import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet'; 4 | import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | 7 | export class WatchOnlyWallet extends LegacyWallet { 8 | static type = 'watchOnly'; 9 | static typeReadable = 'Watch-only'; 10 | 11 | constructor() { 12 | super(); 13 | this.use_with_hardware_wallet = false; 14 | } 15 | 16 | allowSend() { 17 | return !!this.use_with_hardware_wallet && this._hdWalletInstance instanceof HDSegwitBech32Wallet && this._hdWalletInstance.allowSend(); 18 | } 19 | 20 | allowBatchSend() { 21 | return ( 22 | !!this.use_with_hardware_wallet && this._hdWalletInstance instanceof HDSegwitBech32Wallet && this._hdWalletInstance.allowBatchSend() 23 | ); 24 | } 25 | 26 | allowSendMax() { 27 | return ( 28 | !!this.use_with_hardware_wallet && this._hdWalletInstance instanceof HDSegwitBech32Wallet && this._hdWalletInstance.allowSendMax() 29 | ); 30 | } 31 | 32 | getAddress() { 33 | return this.secret; 34 | } 35 | 36 | createTx(utxos, amount, fee, toAddress, memo) { 37 | throw new Error('Not supported'); 38 | } 39 | 40 | valid() { 41 | if (this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub')) return true; 42 | 43 | try { 44 | bitcoin.address.toOutputScript(this.getAddress()); 45 | return true; 46 | } catch (e) { 47 | return false; 48 | } 49 | } 50 | 51 | /** 52 | * this method creates appropriate HD wallet class, depending on whether we have xpub, ypub or zpub 53 | * as a property of `this`, and in case such property exists - it recreates it and copies data from old one. 54 | * this is needed after serialization/save/load/deserialization procedure. 55 | */ 56 | init() { 57 | let hdWalletInstance; 58 | if (this.secret.startsWith('xpub')) hdWalletInstance = new HDLegacyP2PKHWallet(); 59 | else if (this.secret.startsWith('ypub')) hdWalletInstance = new HDSegwitP2SHWallet(); 60 | else if (this.secret.startsWith('zpub')) hdWalletInstance = new HDSegwitBech32Wallet(); 61 | else return; 62 | hdWalletInstance._xpub = this.secret; 63 | if (this._hdWalletInstance) { 64 | // now, porting all properties from old object to new one 65 | for (let k of Object.keys(this._hdWalletInstance)) { 66 | hdWalletInstance[k] = this._hdWalletInstance[k]; 67 | } 68 | 69 | // deleting properties that cant survive serialization/deserialization: 70 | delete hdWalletInstance._node1; 71 | delete hdWalletInstance._node0; 72 | } 73 | this._hdWalletInstance = hdWalletInstance; 74 | } 75 | 76 | getBalance() { 77 | if (this._hdWalletInstance) return this._hdWalletInstance.getBalance(); 78 | return super.getBalance(); 79 | } 80 | 81 | getTransactions() { 82 | if (this._hdWalletInstance) return this._hdWalletInstance.getTransactions(); 83 | return super.getTransactions(); 84 | } 85 | 86 | async fetchBalance() { 87 | if (this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub')) { 88 | if (!this._hdWalletInstance) this.init(); 89 | return this._hdWalletInstance.fetchBalance(); 90 | } else { 91 | // return LegacyWallet.prototype.fetchBalance.call(this); 92 | return super.fetchBalance(); 93 | } 94 | } 95 | 96 | async fetchTransactions() { 97 | if (this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub')) { 98 | if (!this._hdWalletInstance) this.init(); 99 | return this._hdWalletInstance.fetchTransactions(); 100 | } else { 101 | // return LegacyWallet.prototype.fetchBalance.call(this); 102 | return super.fetchTransactions(); 103 | } 104 | } 105 | 106 | async getAddressAsync() { 107 | if (this.isAddressValid(this.secret)) return new Promise(resolve => resolve(this.secret)); 108 | if (this._hdWalletInstance) return this._hdWalletInstance.getAddressAsync(); 109 | throw new Error('Not initialized'); 110 | } 111 | 112 | async _getExternalAddressByIndex(index) { 113 | if (this._hdWalletInstance) return this._hdWalletInstance._getExternalAddressByIndex(index); 114 | throw new Error('Not initialized'); 115 | } 116 | 117 | async getChangeAddressAsync() { 118 | if (this._hdWalletInstance) return this._hdWalletInstance.getChangeAddressAsync(); 119 | throw new Error('Not initialized'); 120 | } 121 | 122 | async fetchUtxo() { 123 | if (this._hdWalletInstance) return this._hdWalletInstance.fetchUtxo(); 124 | throw new Error('Not initialized'); 125 | } 126 | 127 | getUtxo() { 128 | if (this._hdWalletInstance) return this._hdWalletInstance.getUtxo(); 129 | throw new Error('Not initialized'); 130 | } 131 | 132 | combinePsbt(base64one, base64two) { 133 | if (this._hdWalletInstance) return this._hdWalletInstance.combinePsbt(base64one, base64two); 134 | throw new Error('Not initialized'); 135 | } 136 | 137 | broadcastTx(hex) { 138 | if (this._hdWalletInstance) return this._hdWalletInstance.broadcastTx(hex); 139 | throw new Error('Not initialized'); 140 | } 141 | 142 | /** 143 | * signature of this method is the same ad BIP84 createTransaction, BUT this method should be used to create 144 | * unsinged PSBT to be used with HW wallet (or other external signer) 145 | * @see HDSegwitBech32Wallet.createTransaction 146 | */ 147 | createTransaction(utxos, targets, feeRate, changeAddress, sequence) { 148 | if (this._hdWalletInstance instanceof HDSegwitBech32Wallet) { 149 | return this._hdWalletInstance.createTransaction(utxos, targets, feeRate, changeAddress, sequence, true); 150 | } else { 151 | throw new Error('Not a zpub watch-only wallet, cant create PSBT (or just not initialized)'); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /walletImport.js: -------------------------------------------------------------------------------- 1 | /* global alert */ 2 | import { 3 | SegwitP2SHWallet, 4 | LegacyWallet, 5 | WatchOnlyWallet, 6 | HDLegacyBreadwalletWallet, 7 | HDSegwitP2SHWallet, 8 | HDLegacyP2PKHWallet, 9 | HDSegwitBech32Wallet, 10 | LightningCustodianWallet, 11 | PlaceholderWallet, 12 | } from '../class'; 13 | import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; 14 | const EV = require('../events'); 15 | const A = require('../analytics'); 16 | /** @type {AppStorage} */ 17 | const BlueApp = require('../BlueApp'); 18 | const loc = require('../loc'); 19 | 20 | export default class WalletImport { 21 | static async _saveWallet(w) { 22 | try { 23 | const wallet = BlueApp.getWallets().some(wallet => wallet.getSecret() === w.secret && wallet.type !== PlaceholderWallet.type); 24 | if (wallet) { 25 | alert('This wallet has been previously imported.'); 26 | WalletImport.removePlaceholderWallet(); 27 | } else { 28 | alert(loc.wallets.import.success); 29 | ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false }); 30 | w.setLabel(loc.wallets.import.imported + ' ' + w.typeReadable); 31 | w.setUserHasSavedExport(true); 32 | WalletImport.removePlaceholderWallet(); 33 | BlueApp.wallets.push(w); 34 | await BlueApp.saveToDisk(); 35 | A(A.ENUM.CREATED_WALLET); 36 | } 37 | EV(EV.enum.WALLETS_COUNT_CHANGED); 38 | } catch (_e) {} 39 | } 40 | 41 | static removePlaceholderWallet() { 42 | const placeholderWalletIndex = BlueApp.wallets.findIndex(wallet => wallet.type === PlaceholderWallet.type); 43 | if (placeholderWalletIndex > -1) { 44 | BlueApp.wallets.splice(placeholderWalletIndex, 1); 45 | } 46 | } 47 | 48 | static addPlaceholderWallet(importText, isFailure = false) { 49 | const wallet = new PlaceholderWallet(); 50 | wallet.setSecret(importText); 51 | wallet.setIsFailure(isFailure); 52 | BlueApp.wallets.push(wallet); 53 | EV(EV.enum.WALLETS_COUNT_CHANGED); 54 | return wallet; 55 | } 56 | 57 | static isCurrentlyImportingWallet() { 58 | return BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type); 59 | } 60 | 61 | static async processImportText(importText) { 62 | if (WalletImport.isCurrentlyImportingWallet()) { 63 | return; 64 | } 65 | const placeholderWallet = WalletImport.addPlaceholderWallet(importText); 66 | // Plan: 67 | // 0. check if its HDSegwitBech32Wallet (BIP84) 68 | // 1. check if its HDSegwitP2SHWallet (BIP49) 69 | // 2. check if its HDLegacyP2PKHWallet (BIP44) 70 | // 3. check if its HDLegacyBreadwalletWallet (no BIP, just "m/0") 71 | // 4. check if its Segwit WIF (P2SH) 72 | // 5. check if its Legacy WIF 73 | // 6. check if its address (watch-only wallet) 74 | // 7. check if its private key (segwit address P2SH) TODO 75 | // 7. check if its private key (legacy address) TODO 76 | 77 | try { 78 | // is it lightning custodian? 79 | if (importText.indexOf('blitzhub://') !== -1 || importText.indexOf('lndhub://') !== -1) { 80 | let lnd = new LightningCustodianWallet(); 81 | if (importText.includes('@')) { 82 | const split = importText.split('@'); 83 | lnd.setBaseURI(split[1]); 84 | lnd.setSecret(split[0]); 85 | } else { 86 | lnd.setBaseURI(LightningCustodianWallet.defaultBaseUri); 87 | lnd.setSecret(importText); 88 | } 89 | lnd.init(); 90 | await lnd.authorize(); 91 | await lnd.fetchTransactions(); 92 | await lnd.fetchUserInvoices(); 93 | await lnd.fetchPendingTransactions(); 94 | await lnd.fetchBalance(); 95 | return WalletImport._saveWallet(lnd); 96 | } 97 | 98 | // trying other wallet types 99 | 100 | let hd4 = new HDSegwitBech32Wallet(); 101 | hd4.setSecret(importText); 102 | if (hd4.validateMnemonic()) { 103 | await hd4.fetchBalance(); 104 | if (hd4.getBalance() > 0) { 105 | await hd4.fetchTransactions(); 106 | return WalletImport._saveWallet(hd4); 107 | } 108 | } 109 | 110 | let segwitWallet = new SegwitP2SHWallet(); 111 | segwitWallet.setSecret(importText); 112 | if (segwitWallet.getAddress()) { 113 | // ok its a valid WIF 114 | 115 | let legacyWallet = new LegacyWallet(); 116 | legacyWallet.setSecret(importText); 117 | 118 | await legacyWallet.fetchBalance(); 119 | if (legacyWallet.getBalance() > 0) { 120 | // yep, its legacy we're importing 121 | await legacyWallet.fetchTransactions(); 122 | return WalletImport._saveWallet(legacyWallet); 123 | } else { 124 | // by default, we import wif as Segwit P2SH 125 | await segwitWallet.fetchBalance(); 126 | await segwitWallet.fetchTransactions(); 127 | return WalletImport._saveWallet(segwitWallet); 128 | } 129 | } 130 | 131 | // case - WIF is valid, just has uncompressed pubkey 132 | 133 | let legacyWallet = new LegacyWallet(); 134 | legacyWallet.setSecret(importText); 135 | if (legacyWallet.getAddress()) { 136 | await legacyWallet.fetchBalance(); 137 | await legacyWallet.fetchTransactions(); 138 | return WalletImport._saveWallet(legacyWallet); 139 | } 140 | 141 | // if we're here - nope, its not a valid WIF 142 | 143 | let hd1 = new HDLegacyBreadwalletWallet(); 144 | hd1.setSecret(importText); 145 | if (hd1.validateMnemonic()) { 146 | await hd1.fetchBalance(); 147 | if (hd1.getBalance() > 0) { 148 | await hd1.fetchTransactions(); 149 | return WalletImport._saveWallet(hd1); 150 | } 151 | } 152 | 153 | let hd2 = new HDSegwitP2SHWallet(); 154 | hd2.setSecret(importText); 155 | if (hd2.validateMnemonic()) { 156 | await hd2.fetchBalance(); 157 | if (hd2.getBalance() > 0) { 158 | await hd2.fetchTransactions(); 159 | return WalletImport._saveWallet(hd2); 160 | } 161 | } 162 | 163 | let hd3 = new HDLegacyP2PKHWallet(); 164 | hd3.setSecret(importText); 165 | if (hd3.validateMnemonic()) { 166 | await hd3.fetchBalance(); 167 | if (hd3.getBalance() > 0) { 168 | await hd3.fetchTransactions(); 169 | return WalletImport._saveWallet(hd3); 170 | } 171 | } 172 | 173 | // no balances? how about transactions count? 174 | 175 | if (hd1.validateMnemonic()) { 176 | await hd1.fetchTransactions(); 177 | if (hd1.getTransactions().length !== 0) { 178 | return WalletImport._saveWallet(hd1); 179 | } 180 | } 181 | if (hd2.validateMnemonic()) { 182 | await hd2.fetchTransactions(); 183 | if (hd2.getTransactions().length !== 0) { 184 | return WalletImport._saveWallet(hd2); 185 | } 186 | } 187 | if (hd3.validateMnemonic()) { 188 | await hd3.fetchTransactions(); 189 | if (hd3.getTransactions().length !== 0) { 190 | return WalletImport._saveWallet(hd3); 191 | } 192 | } 193 | if (hd4.validateMnemonic()) { 194 | await hd4.fetchTransactions(); 195 | if (hd4.getTransactions().length !== 0) { 196 | return WalletImport._saveWallet(hd4); 197 | } 198 | } 199 | 200 | // is it even valid? if yes we will import as: 201 | if (hd4.validateMnemonic()) { 202 | return WalletImport._saveWallet(hd4); 203 | } 204 | 205 | // not valid? maybe its a watch-only address? 206 | 207 | let watchOnly = new WatchOnlyWallet(); 208 | watchOnly.setSecret(importText); 209 | if (watchOnly.valid()) { 210 | await watchOnly.fetchTransactions(); 211 | await watchOnly.fetchBalance(); 212 | return WalletImport._saveWallet(watchOnly); 213 | } 214 | 215 | // nope? 216 | 217 | // TODO: try a raw private key 218 | } catch (Err) { 219 | WalletImport.removePlaceholderWallet(placeholderWallet); 220 | EV(EV.enum.WALLETS_COUNT_CHANGED); 221 | console.warn(Err); 222 | } 223 | WalletImport.removePlaceholderWallet(); 224 | WalletImport.addPlaceholderWallet(importText, true); 225 | ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); 226 | EV(EV.enum.WALLETS_COUNT_CHANGED); 227 | alert(loc.wallets.import.error); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /deeplinkSchemaMatch.js: -------------------------------------------------------------------------------- 1 | import { AppStorage, LightningCustodianWallet } from './'; 2 | import AsyncStorage from '@react-native-community/async-storage'; 3 | import BitcoinBIP70TransactionDecode from '../bip70/bip70'; 4 | import { Chain } from '../models/bitcoinUnits'; 5 | const bitcoin = require('bitcoinjs-lib'); 6 | const BlueApp: AppStorage = require('../BlueApp'); 7 | const url = require('url'); 8 | 9 | class DeeplinkSchemaMatch { 10 | static hasSchema(schemaString) { 11 | if (typeof schemaString !== 'string' || schemaString.length <= 0) return false; 12 | const lowercaseString = schemaString.trim().toLowerCase(); 13 | return ( 14 | lowercaseString.startsWith('bitcoin:') || 15 | lowercaseString.startsWith('lightning:') || 16 | lowercaseString.startsWith('blue:') || 17 | lowercaseString.startsWith('bluewallet:') || 18 | lowercaseString.startsWith('lapp:') 19 | ); 20 | } 21 | 22 | /** 23 | * Examines the content of the event parameter. 24 | * If the content is recognizable, create a dictionary with the respective 25 | * navigation dictionary required by react-navigation 26 | * @param {Object} event 27 | * @param {void} completionHandler 28 | */ 29 | static navigationRouteFor(event, completionHandler) { 30 | if (event.url === null) { 31 | return; 32 | } 33 | if (typeof event.url !== 'string') { 34 | return; 35 | } 36 | let isBothBitcoinAndLightning; 37 | try { 38 | isBothBitcoinAndLightning = DeeplinkSchemaMatch.isBothBitcoinAndLightning(event.url); 39 | } catch (e) { 40 | console.log(e); 41 | } 42 | if (isBothBitcoinAndLightning) { 43 | completionHandler({ 44 | routeName: 'HandleOffchainAndOnChain', 45 | params: { 46 | onWalletSelect: wallet => 47 | completionHandler(DeeplinkSchemaMatch.isBothBitcoinAndLightningOnWalletSelect(wallet, isBothBitcoinAndLightning)), 48 | }, 49 | }); 50 | } else if (DeeplinkSchemaMatch.isBitcoinAddress(event.url) || BitcoinBIP70TransactionDecode.matchesPaymentURL(event.url)) { 51 | completionHandler({ 52 | routeName: 'SendDetails', 53 | params: { 54 | uri: event.url, 55 | }, 56 | }); 57 | } else if (DeeplinkSchemaMatch.isLightningInvoice(event.url)) { 58 | completionHandler({ 59 | routeName: 'ScanLndInvoice', 60 | params: { 61 | uri: event.url, 62 | }, 63 | }); 64 | } else if (DeeplinkSchemaMatch.isLnUrl(event.url)) { 65 | completionHandler({ 66 | routeName: 'LNDCreateInvoice', 67 | params: { 68 | uri: event.url, 69 | }, 70 | }); 71 | } else if (DeeplinkSchemaMatch.isSafelloRedirect(event)) { 72 | let urlObject = url.parse(event.url, true) // eslint-disable-line 73 | 74 | const safelloStateToken = urlObject.query['safello-state-token']; 75 | 76 | completionHandler({ 77 | routeName: 'BuyBitcoin', 78 | params: { 79 | uri: event.url, 80 | safelloStateToken, 81 | }, 82 | }); 83 | } else { 84 | let urlObject = url.parse(event.url, true); // eslint-disable-line 85 | console.log('parsed', urlObject); 86 | (async () => { 87 | if (urlObject.protocol === 'bluewallet:' || urlObject.protocol === 'lapp:' || urlObject.protocol === 'blue:') { 88 | switch (urlObject.host) { 89 | case 'openlappbrowser': 90 | console.log('opening LAPP', urlObject.query.url); 91 | // searching for LN wallet: 92 | let haveLnWallet = false; 93 | for (let w of BlueApp.getWallets()) { 94 | if (w.type === LightningCustodianWallet.type) { 95 | haveLnWallet = true; 96 | } 97 | } 98 | 99 | if (!haveLnWallet) { 100 | // need to create one 101 | let w = new LightningCustodianWallet(); 102 | w.setLabel(w.typeReadable); 103 | 104 | try { 105 | let lndhub = await AsyncStorage.getItem(AppStorage.LNDHUB); 106 | if (lndhub) { 107 | w.setBaseURI(lndhub); 108 | w.init(); 109 | } 110 | await w.createAccount(); 111 | await w.authorize(); 112 | } catch (Err) { 113 | // giving up, not doing anything 114 | return; 115 | } 116 | BlueApp.wallets.push(w); 117 | await BlueApp.saveToDisk(); 118 | } 119 | 120 | // now, opening lapp browser and navigating it to URL. 121 | // looking for a LN wallet: 122 | let lnWallet; 123 | for (let w of BlueApp.getWallets()) { 124 | if (w.type === LightningCustodianWallet.type) { 125 | lnWallet = w; 126 | break; 127 | } 128 | } 129 | 130 | if (!lnWallet) { 131 | // something went wrong 132 | return; 133 | } 134 | 135 | completionHandler({ 136 | routeName: 'LappBrowser', 137 | params: { 138 | fromSecret: lnWallet.getSecret(), 139 | fromWallet: lnWallet, 140 | url: urlObject.query.url, 141 | }, 142 | }); 143 | break; 144 | } 145 | } 146 | })(); 147 | } 148 | } 149 | 150 | static isBothBitcoinAndLightningOnWalletSelect(wallet, uri) { 151 | if (wallet.chain === Chain.ONCHAIN) { 152 | return { 153 | routeName: 'SendDetails', 154 | params: { 155 | uri: uri.bitcoin, 156 | fromWallet: wallet, 157 | }, 158 | }; 159 | } else if (wallet.chain === Chain.OFFCHAIN) { 160 | return { 161 | routeName: 'ScanLndInvoice', 162 | params: { 163 | uri: uri.lndInvoice, 164 | fromSecret: wallet.getSecret(), 165 | }, 166 | }; 167 | } 168 | } 169 | 170 | static isBitcoinAddress(address) { 171 | address = address 172 | .replace('bitcoin:', '') 173 | .replace('bitcoin=', '') 174 | .split('?')[0]; 175 | let isValidBitcoinAddress = false; 176 | try { 177 | bitcoin.address.toOutputScript(address); 178 | isValidBitcoinAddress = true; 179 | } catch (err) { 180 | isValidBitcoinAddress = false; 181 | } 182 | return isValidBitcoinAddress; 183 | } 184 | 185 | static isLightningInvoice(invoice) { 186 | let isValidLightningInvoice = false; 187 | if (invoice.toLowerCase().startsWith('lightning:lnb') || invoice.toLowerCase().startsWith('lnb')) { 188 | isValidLightningInvoice = true; 189 | } 190 | return isValidLightningInvoice; 191 | } 192 | 193 | static isLnUrl(text) { 194 | if (text.toLowerCase().startsWith('lightning:lnurl') || text.toLowerCase().startsWith('lnurl')) { 195 | return true; 196 | } 197 | return false; 198 | } 199 | 200 | static isSafelloRedirect(event) { 201 | let urlObject = url.parse(event.url, true) // eslint-disable-line 202 | 203 | return !!urlObject.query['safello-state-token']; 204 | } 205 | 206 | static isBothBitcoinAndLightning(url) { 207 | if (url.includes('lightning') && url.includes('bitcoin')) { 208 | const txInfo = url.split(/(bitcoin:|lightning:|lightning=|bitcoin=)+/); 209 | let bitcoin; 210 | let lndInvoice; 211 | for (const [index, value] of txInfo.entries()) { 212 | try { 213 | // Inside try-catch. We dont wan't to crash in case of an out-of-bounds error. 214 | if (value.startsWith('bitcoin')) { 215 | bitcoin = `bitcoin:${txInfo[index + 1]}`; 216 | if (!DeeplinkSchemaMatch.isBitcoinAddress(bitcoin)) { 217 | bitcoin = false; 218 | break; 219 | } 220 | } else if (value.startsWith('lightning')) { 221 | lndInvoice = `lightning:${txInfo[index + 1]}`; 222 | if (!this.isLightningInvoice(lndInvoice)) { 223 | lndInvoice = false; 224 | break; 225 | } 226 | } 227 | } catch (e) { 228 | console.log(e); 229 | } 230 | if (bitcoin && lndInvoice) break; 231 | } 232 | if (bitcoin && lndInvoice) { 233 | return { bitcoin, lndInvoice }; 234 | } else { 235 | return undefined; 236 | } 237 | } 238 | return undefined; 239 | } 240 | } 241 | 242 | export default DeeplinkSchemaMatch; 243 | -------------------------------------------------------------------------------- /Base.lproj/MainInterface.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 27 | 33 | 39 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 69 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /hd-segwit-bech32-transaction.js: -------------------------------------------------------------------------------- 1 | import { HDSegwitBech32Wallet, SegwitBech32Wallet } from './'; 2 | const bitcoin = require('bitcoinjs-lib'); 3 | const BlueElectrum = require('../BlueElectrum'); 4 | const reverse = require('buffer-reverse'); 5 | const BigNumber = require('bignumber.js'); 6 | 7 | /** 8 | * Represents transaction of a BIP84 wallet. 9 | * Helpers for RBF, CPFP etc. 10 | */ 11 | export class HDSegwitBech32Transaction { 12 | /** 13 | * @param txhex {string|null} Object is initialized with txhex 14 | * @param txid {string|null} If txhex not present - txid whould be present 15 | * @param wallet {HDSegwitBech32Wallet|null} If set - a wallet object to which transacton belongs 16 | */ 17 | constructor(txhex, txid, wallet) { 18 | if (!txhex && !txid) throw new Error('Bad arguments'); 19 | this._txhex = txhex; 20 | this._txid = txid; 21 | 22 | if (wallet) { 23 | if (wallet.type === HDSegwitBech32Wallet.type) { 24 | /** @type {HDSegwitBech32Wallet} */ 25 | this._wallet = wallet; 26 | } else { 27 | throw new Error('Only HD Bech32 wallets supported'); 28 | } 29 | } 30 | 31 | if (this._txhex) this._txDecoded = bitcoin.Transaction.fromHex(this._txhex); 32 | this._remoteTx = null; 33 | } 34 | 35 | /** 36 | * If only txid present - we fetch hex 37 | * 38 | * @returns {Promise} 39 | * @private 40 | */ 41 | async _fetchTxhexAndDecode() { 42 | let hexes = await BlueElectrum.multiGetTransactionByTxid([this._txid], 10, false); 43 | this._txhex = hexes[this._txid]; 44 | if (!this._txhex) throw new Error("Transaction can't be found in mempool"); 45 | this._txDecoded = bitcoin.Transaction.fromHex(this._txhex); 46 | } 47 | 48 | /** 49 | * Returns max used sequence for this transaction. Next RBF transaction 50 | * should have this sequence + 1 51 | * 52 | * @returns {Promise} 53 | */ 54 | async getMaxUsedSequence() { 55 | if (!this._txDecoded) await this._fetchTxhexAndDecode(); 56 | 57 | let max = 0; 58 | for (let inp of this._txDecoded.ins) { 59 | max = Math.max(inp.sequence, max); 60 | } 61 | 62 | return max; 63 | } 64 | 65 | /** 66 | * Basic check that Sequence num for this TX is replaceable 67 | * 68 | * @returns {Promise} 69 | */ 70 | async isSequenceReplaceable() { 71 | return (await this.getMaxUsedSequence()) < bitcoin.Transaction.DEFAULT_SEQUENCE; 72 | } 73 | 74 | /** 75 | * If internal extended tx data not set - this is a method 76 | * to fetch and set this data from electrum. Its different data from 77 | * decoded hex - it contains confirmations etc. 78 | * 79 | * @returns {Promise} 80 | * @private 81 | */ 82 | async _fetchRemoteTx() { 83 | let result = await BlueElectrum.multiGetTransactionByTxid([this._txid || this._txDecoded.getId()]); 84 | this._remoteTx = Object.values(result)[0]; 85 | } 86 | 87 | /** 88 | * Fetches from electrum actual confirmations number for this tx 89 | * 90 | * @returns {Promise} 91 | */ 92 | async getRemoteConfirmationsNum() { 93 | if (!this._remoteTx) await this._fetchRemoteTx(); 94 | return this._remoteTx.confirmations || 0; // stupid undefined 95 | } 96 | 97 | /** 98 | * Checks that tx belongs to a wallet and also 99 | * tx value is < 0, which means its a spending transaction 100 | * definately initiated by us, can be RBF'ed. 101 | * 102 | * @returns {Promise} 103 | */ 104 | async isOurTransaction() { 105 | if (!this._wallet) throw new Error('Wallet required for this method'); 106 | let found = false; 107 | for (let tx of this._wallet.getTransactions()) { 108 | if (tx.txid === (this._txid || this._txDecoded.getId())) { 109 | // its our transaction, and its spending transaction, which means we initiated it 110 | if (tx.value < 0) found = true; 111 | } 112 | } 113 | return found; 114 | } 115 | 116 | /** 117 | * Checks that tx belongs to a wallet and also 118 | * tx value is > 0, which means its a receiving transaction and thus 119 | * can be CPFP'ed. 120 | * 121 | * @returns {Promise} 122 | */ 123 | async isToUsTransaction() { 124 | if (!this._wallet) throw new Error('Wallet required for this method'); 125 | let found = false; 126 | for (let tx of this._wallet.getTransactions()) { 127 | if (tx.txid === (this._txid || this._txDecoded.getId())) { 128 | if (tx.value > 0) found = true; 129 | } 130 | } 131 | return found; 132 | } 133 | 134 | /** 135 | * Returns all the info about current transaction which is needed to do a replacement TX 136 | * * fee - current tx fee 137 | * * utxos - UTXOs current tx consumes 138 | * * changeAmount - amount of satoshis that sent to change address (or addresses) we control 139 | * * feeRate - sat/byte for current tx 140 | * * targets - destination(s) of funds (outputs we do not control) 141 | * * unconfirmedUtxos - UTXOs created by this transaction (only the ones we control) 142 | * 143 | * @returns {Promise<{fee: number, utxos: Array, unconfirmedUtxos: Array, changeAmount: number, feeRate: number, targets: Array}>} 144 | */ 145 | async getInfo() { 146 | if (!this._wallet) throw new Error('Wallet required for this method'); 147 | if (!this._remoteTx) await this._fetchRemoteTx(); 148 | if (!this._txDecoded) await this._fetchTxhexAndDecode(); 149 | 150 | let prevInputs = []; 151 | for (let inp of this._txDecoded.ins) { 152 | let reversedHash = Buffer.from(reverse(inp.hash)); 153 | reversedHash = reversedHash.toString('hex'); 154 | prevInputs.push(reversedHash); 155 | } 156 | 157 | let prevTransactions = await BlueElectrum.multiGetTransactionByTxid(prevInputs); 158 | 159 | // fetched, now lets count how much satoshis went in 160 | let wentIn = 0; 161 | let utxos = []; 162 | for (let inp of this._txDecoded.ins) { 163 | let reversedHash = Buffer.from(reverse(inp.hash)); 164 | reversedHash = reversedHash.toString('hex'); 165 | if (prevTransactions[reversedHash] && prevTransactions[reversedHash].vout && prevTransactions[reversedHash].vout[inp.index]) { 166 | let value = prevTransactions[reversedHash].vout[inp.index].value; 167 | value = new BigNumber(value).multipliedBy(100000000).toNumber(); 168 | wentIn += value; 169 | let address = SegwitBech32Wallet.witnessToAddress(inp.witness[inp.witness.length - 1]); 170 | utxos.push({ vout: inp.index, value: value, txId: reversedHash, address: address }); 171 | } 172 | } 173 | 174 | // counting how much went into actual outputs 175 | 176 | let wasSpent = 0; 177 | for (let outp of this._txDecoded.outs) { 178 | wasSpent += +outp.value; 179 | } 180 | 181 | let fee = wentIn - wasSpent; 182 | let feeRate = Math.floor(fee / (this._txhex.length / 2)); 183 | if (feeRate === 0) feeRate = 1; 184 | 185 | // lets take a look at change 186 | let changeAmount = 0; 187 | let targets = []; 188 | for (let outp of this._remoteTx.vout) { 189 | let address = outp.scriptPubKey.addresses[0]; 190 | let value = new BigNumber(outp.value).multipliedBy(100000000).toNumber(); 191 | if (this._wallet.weOwnAddress(address)) { 192 | changeAmount += value; 193 | } else { 194 | // this is target 195 | targets.push({ value: value, address: address }); 196 | } 197 | } 198 | 199 | // lets find outputs we own that current transaction creates. can be used in CPFP 200 | let unconfirmedUtxos = []; 201 | for (let outp of this._remoteTx.vout) { 202 | let address = outp.scriptPubKey.addresses[0]; 203 | let value = new BigNumber(outp.value).multipliedBy(100000000).toNumber(); 204 | if (this._wallet.weOwnAddress(address)) { 205 | unconfirmedUtxos.push({ 206 | vout: outp.n, 207 | value: value, 208 | txId: this._txid || this._txDecoded.getId(), 209 | address: address, 210 | }); 211 | } 212 | } 213 | 214 | return { fee, feeRate, targets, changeAmount, utxos, unconfirmedUtxos }; 215 | } 216 | 217 | /** 218 | * Checks if all outputs belong to us, that 219 | * means we already canceled this tx and we can only bump fees 220 | * 221 | * @returns {Promise} 222 | */ 223 | async canCancelTx() { 224 | if (!this._wallet) throw new Error('Wallet required for this method'); 225 | if (!this._txDecoded) await this._fetchTxhexAndDecode(); 226 | 227 | // if theres at least one output we dont own - we can cancel this transaction! 228 | for (let outp of this._txDecoded.outs) { 229 | if (!this._wallet.weOwnAddress(SegwitBech32Wallet.scriptPubKeyToAddress(outp.script))) return true; 230 | } 231 | 232 | return false; 233 | } 234 | 235 | /** 236 | * Creates an RBF transaction that can replace previous one and basically cancel it (rewrite 237 | * output to the one our wallet controls). Note, this cannot add more utxo in RBF transaction if 238 | * newFeerate is too high 239 | * 240 | * @param newFeerate {number} Sat/byte. Should be greater than previous tx feerate 241 | * @returns {Promise<{outputs: Array, tx: Transaction, inputs: Array, fee: Number}>} 242 | */ 243 | async createRBFcancelTx(newFeerate) { 244 | if (!this._wallet) throw new Error('Wallet required for this method'); 245 | if (!this._remoteTx) await this._fetchRemoteTx(); 246 | 247 | let { feeRate, utxos } = await this.getInfo(); 248 | 249 | if (newFeerate <= feeRate) throw new Error('New feerate should be bigger than the old one'); 250 | let myAddress = await this._wallet.getChangeAddressAsync(); 251 | 252 | return this._wallet.createTransaction( 253 | utxos, 254 | [{ address: myAddress }], 255 | newFeerate, 256 | /* meaningless in this context */ myAddress, 257 | (await this.getMaxUsedSequence()) + 1, 258 | ); 259 | } 260 | 261 | /** 262 | * Creates an RBF transaction that can bumps fee of previous one. Note, this cannot add more utxo in RBF 263 | * transaction if newFeerate is too high 264 | * 265 | * @param newFeerate {number} Sat/byte 266 | * @returns {Promise<{outputs: Array, tx: Transaction, inputs: Array, fee: Number}>} 267 | */ 268 | async createRBFbumpFee(newFeerate) { 269 | if (!this._wallet) throw new Error('Wallet required for this method'); 270 | if (!this._remoteTx) await this._fetchRemoteTx(); 271 | 272 | let { feeRate, targets, changeAmount, utxos } = await this.getInfo(); 273 | 274 | if (newFeerate <= feeRate) throw new Error('New feerate should be bigger than the old one'); 275 | let myAddress = await this._wallet.getChangeAddressAsync(); 276 | 277 | if (changeAmount === 0) delete targets[0].value; 278 | // looks like this was sendMAX transaction (because there was no change), so we cant reuse amount in this 279 | // target since fee wont change. removing the amount so `createTransaction` will sendMAX correctly with new feeRate 280 | 281 | if (targets.length === 0) { 282 | // looks like this was cancelled tx with single change output, so it wasnt included in `this.getInfo()` targets 283 | // so we add output paying ourselves: 284 | targets.push({ address: this._wallet._getInternalAddressByIndex(this._wallet.next_free_change_address_index) }); 285 | // not checking emptiness on purpose: it could unpredictably generate too far address because of unconfirmed tx. 286 | } 287 | 288 | return this._wallet.createTransaction(utxos, targets, newFeerate, myAddress, (await this.getMaxUsedSequence()) + 1); 289 | } 290 | 291 | /** 292 | * Creates a CPFP transaction that can bumps fee of previous one (spends created but not confirmed outputs 293 | * that belong to us). Note, this cannot add more utxo in CPFP transaction if newFeerate is too high 294 | * 295 | * @param newFeerate {number} sat/byte 296 | * @returns {Promise<{outputs: Array, tx: Transaction, inputs: Array, fee: Number}>} 297 | */ 298 | async createCPFPbumpFee(newFeerate) { 299 | if (!this._wallet) throw new Error('Wallet required for this method'); 300 | if (!this._remoteTx) await this._fetchRemoteTx(); 301 | 302 | let { feeRate, fee: oldFee, unconfirmedUtxos } = await this.getInfo(); 303 | 304 | if (newFeerate <= feeRate) throw new Error('New feerate should be bigger than the old one'); 305 | let myAddress = await this._wallet.getChangeAddressAsync(); 306 | 307 | // calculating feerate for CPFP tx so that average between current and CPFP tx will equal newFeerate. 308 | // this works well if both txs are +/- equal size in bytes 309 | const targetFeeRate = 2 * newFeerate - feeRate; 310 | 311 | let add = 0; 312 | while (add <= 128) { 313 | var { tx, inputs, outputs, fee } = this._wallet.createTransaction( 314 | unconfirmedUtxos, 315 | [{ address: myAddress }], 316 | targetFeeRate + add, 317 | myAddress, 318 | HDSegwitBech32Wallet.defaultRBFSequence, 319 | ); 320 | let combinedFeeRate = (oldFee + fee) / (this._txhex.length / 2 + tx.toHex().length / 2); // avg 321 | if (Math.round(combinedFeeRate) < newFeerate) { 322 | add *= 2; 323 | if (!add) add = 2; 324 | } else { 325 | // reached target feerate 326 | break; 327 | } 328 | } 329 | 330 | return { tx, inputs, outputs, fee }; 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /legacy-wallet.js: -------------------------------------------------------------------------------- 1 | import { AbstractWallet } from './abstract-wallet'; 2 | import { SegwitBech32Wallet } from './'; 3 | import { useBlockcypherTokens } from './constants'; 4 | import Frisbee from 'frisbee'; 5 | import { NativeModules } from 'react-native'; 6 | const bitcoin = require('bitcoinjs-lib'); 7 | const { RNRandomBytes } = NativeModules; 8 | const BigNumber = require('bignumber.js'); 9 | const signer = require('../models/signer'); 10 | const BlueElectrum = require('../BlueElectrum'); 11 | 12 | /** 13 | * Has private key and single address like "1ABCD....." 14 | * (legacy P2PKH compressed) 15 | */ 16 | export class LegacyWallet extends AbstractWallet { 17 | static type = 'legacy'; 18 | static typeReadable = 'Legacy (P2PKH)'; 19 | 20 | /** 21 | * Simple function which says that we havent tried to fetch balance 22 | * for a long time 23 | * 24 | * @return {boolean} 25 | */ 26 | timeToRefreshBalance() { 27 | if (+new Date() - this._lastBalanceFetch >= 5 * 60 * 1000) { 28 | return true; 29 | } 30 | return false; 31 | } 32 | 33 | /** 34 | * Simple function which says if we hve some low-confirmed transactions 35 | * and we better fetch them 36 | * 37 | * @return {boolean} 38 | */ 39 | timeToRefreshTransaction() { 40 | for (let tx of this.transactions) { 41 | if (tx.confirmations < 7) { 42 | return true; 43 | } 44 | } 45 | return false; 46 | } 47 | 48 | async generate() { 49 | let that = this; 50 | return new Promise(function(resolve) { 51 | if (typeof RNRandomBytes === 'undefined') { 52 | // CLI/CI environment 53 | // crypto should be provided globally by test launcher 54 | return crypto.randomBytes(32, (err, buf) => { // eslint-disable-line 55 | if (err) throw err; 56 | that.secret = bitcoin.ECPair.makeRandom({ 57 | rng: function(length) { 58 | return buf; 59 | }, 60 | }).toWIF(); 61 | resolve(); 62 | }); 63 | } 64 | 65 | // RN environment 66 | RNRandomBytes.randomBytes(32, (err, bytes) => { 67 | if (err) throw new Error(err); 68 | that.secret = bitcoin.ECPair.makeRandom({ 69 | rng: function(length) { 70 | let b = Buffer.from(bytes, 'base64'); 71 | return b; 72 | }, 73 | }).toWIF(); 74 | resolve(); 75 | }); 76 | }); 77 | } 78 | 79 | /** 80 | * 81 | * @returns {string} 82 | */ 83 | getAddress() { 84 | if (this._address) return this._address; 85 | let address; 86 | try { 87 | let keyPair = bitcoin.ECPair.fromWIF(this.secret); 88 | address = bitcoin.payments.p2pkh({ 89 | pubkey: keyPair.publicKey, 90 | }).address; 91 | } catch (err) { 92 | return false; 93 | } 94 | this._address = address; 95 | 96 | return this._address; 97 | } 98 | 99 | /** 100 | * Fetches balance of the Wallet via API. 101 | * Returns VOID. Get the actual balance via getter. 102 | * 103 | * @returns {Promise.} 104 | */ 105 | async fetchBalance() { 106 | try { 107 | const api = new Frisbee({ 108 | baseURI: 'https://api.blockcypher.com/v1/btc/main/addrs/', 109 | }); 110 | 111 | let response = await api.get( 112 | this.getAddress() + '/balance' + ((useBlockcypherTokens && '?token=' + this.getRandomBlockcypherToken()) || ''), 113 | ); 114 | let json = response.body; 115 | if (typeof json === 'undefined' || typeof json.final_balance === 'undefined') { 116 | throw new Error('Could not fetch balance from API: ' + response.err + ' ' + JSON.stringify(response.body)); 117 | } 118 | 119 | this.balance = Number(json.final_balance); 120 | this.unconfirmed_balance = new BigNumber(json.unconfirmed_balance); 121 | this.unconfirmed_balance = this.unconfirmed_balance.dividedBy(100000000).toString() * 1; 122 | this._lastBalanceFetch = +new Date(); 123 | } catch (err) { 124 | console.warn(err); 125 | } 126 | } 127 | 128 | /** 129 | * Fetches UTXO from API. Returns VOID. 130 | * 131 | * @return {Promise.} 132 | */ 133 | async fetchUtxo() { 134 | const api = new Frisbee({ 135 | baseURI: 'https://api.blockcypher.com/v1/btc/main/addrs/', 136 | }); 137 | 138 | let response; 139 | try { 140 | let maxHeight = 0; 141 | this.utxo = []; 142 | let json; 143 | 144 | do { 145 | response = await api.get( 146 | this.getAddress() + 147 | '?limit=2000&after=' + 148 | maxHeight + 149 | ((useBlockcypherTokens && '&token=' + this.getRandomBlockcypherToken()) || ''), 150 | ); 151 | json = response.body; 152 | if (typeof json === 'undefined' || typeof json.final_balance === 'undefined') { 153 | throw new Error('Could not fetch UTXO from API' + response.err); 154 | } 155 | json.txrefs = json.txrefs || []; // case when source address is empty (or maxheight too high, no txs) 156 | 157 | for (let txref of json.txrefs) { 158 | maxHeight = Math.max(maxHeight, txref.block_height) + 1; 159 | if (typeof txref.spent !== 'undefined' && txref.spent === false) { 160 | this.utxo.push(txref); 161 | } 162 | } 163 | } while (json.txrefs.length); 164 | 165 | json.unconfirmed_txrefs = json.unconfirmed_txrefs || []; 166 | this.utxo = this.utxo.concat(json.unconfirmed_txrefs); 167 | } catch (err) { 168 | console.warn(err); 169 | } 170 | } 171 | 172 | /** 173 | * Fetches transactions via API. Returns VOID. 174 | * Use getter to get the actual list. 175 | * 176 | * @return {Promise.} 177 | */ 178 | async fetchTransactions() { 179 | try { 180 | const api = new Frisbee({ 181 | baseURI: 'https://api.blockcypher.com/', 182 | }); 183 | 184 | let after = 0; 185 | let before = 100500100; 186 | 187 | for (let oldTx of this.getTransactions()) { 188 | if (oldTx.block_height && oldTx.confirmations < 7) { 189 | after = Math.max(after, oldTx.block_height); 190 | } 191 | } 192 | 193 | while (1) { 194 | let response = await api.get( 195 | 'v1/btc/main/addrs/' + 196 | this.getAddress() + 197 | '/full?after=' + 198 | after + 199 | '&before=' + 200 | before + 201 | '&limit=50' + 202 | ((useBlockcypherTokens && '&token=' + this.getRandomBlockcypherToken()) || ''), 203 | ); 204 | let json = response.body; 205 | if (typeof json === 'undefined' || !json.txs) { 206 | throw new Error('Could not fetch transactions from API:' + response.err); 207 | } 208 | 209 | let alreadyFetchedTransactions = this.transactions; 210 | this.transactions = json.txs; 211 | this._lastTxFetch = +new Date(); 212 | 213 | // now, calculating value per each transaction... 214 | for (let tx of this.transactions) { 215 | if (tx.block_height) { 216 | before = Math.min(before, tx.block_height); // so next time we fetch older TXs 217 | } 218 | 219 | // now, if we dont have enough outputs or inputs in response we should collect them from API: 220 | if (tx.next_outputs) { 221 | let newOutputs = await this._fetchAdditionalOutputs(tx.next_outputs); 222 | tx.outputs = tx.outputs.concat(newOutputs); 223 | } 224 | if (tx.next_inputs) { 225 | let newInputs = await this._fetchAdditionalInputs(tx.next_inputs); 226 | tx.inputs = tx.inputs.concat(newInputs); 227 | } 228 | 229 | // how much came in... 230 | let value = 0; 231 | for (let out of tx.outputs) { 232 | if (out && out.addresses && out.addresses.indexOf(this.getAddress()) !== -1) { 233 | // found our address in outs of this TX 234 | value += out.value; 235 | } 236 | } 237 | tx.value = value; 238 | // end 239 | 240 | // how much came out 241 | value = 0; 242 | for (let inp of tx.inputs) { 243 | if (!inp.addresses) { 244 | // console.log('inp.addresses empty'); 245 | // console.log('got witness', inp.witness); // TODO 246 | 247 | inp.addresses = []; 248 | if (inp.witness && inp.witness[1]) { 249 | let address = SegwitBech32Wallet.witnessToAddress(inp.witness[1]); 250 | inp.addresses.push(address); 251 | } else { 252 | inp.addresses.push('???'); 253 | } 254 | } 255 | if (inp && inp.addresses && inp.addresses.indexOf(this.getAddress()) !== -1) { 256 | // found our address in outs of this TX 257 | value -= inp.output_value; 258 | } 259 | } 260 | tx.value += value; 261 | // end 262 | } 263 | 264 | this.transactions = alreadyFetchedTransactions.concat(this.transactions); 265 | 266 | let txsUnconf = []; 267 | let txs = []; 268 | let hashPresent = {}; 269 | // now, rearranging TXs. unconfirmed go first: 270 | for (let tx of this.transactions.reverse()) { 271 | if (hashPresent[tx.hash]) continue; 272 | hashPresent[tx.hash] = 1; 273 | if (tx.block_height && tx.block_height === -1) { 274 | // unconfirmed 275 | console.log(tx); 276 | if (+new Date(tx.received) < +new Date() - 3600 * 24 * 1000) { 277 | // nop, too old unconfirmed tx - skipping it 278 | } else { 279 | txsUnconf.push(tx); 280 | } 281 | } else { 282 | txs.push(tx); 283 | } 284 | } 285 | this.transactions = txsUnconf.reverse().concat(txs.reverse()); 286 | // all reverses needed so freshly fetched TXs replace same old TXs 287 | 288 | this.transactions = this.transactions.sort((a, b) => { 289 | return a.received < b.received; 290 | }); 291 | 292 | if (json.txs.length < 50) { 293 | // final batch, so it has les than max txs 294 | break; 295 | } 296 | } 297 | } catch (err) { 298 | console.warn(err); 299 | } 300 | } 301 | 302 | async _fetchAdditionalOutputs(nextOutputs) { 303 | let outputs = []; 304 | let baseURI = nextOutputs.split('/'); 305 | baseURI = baseURI[0] + '/' + baseURI[1] + '/' + baseURI[2] + '/'; 306 | const api = new Frisbee({ 307 | baseURI: baseURI, 308 | }); 309 | 310 | do { 311 | await (() => new Promise(resolve => setTimeout(resolve, 1000)))(); 312 | nextOutputs = nextOutputs.replace(baseURI, ''); 313 | 314 | let response = await api.get(nextOutputs + ((useBlockcypherTokens && '&token=' + this.getRandomBlockcypherToken()) || '')); 315 | let json = response.body; 316 | if (typeof json === 'undefined') { 317 | throw new Error('Could not fetch transactions from API:' + response.err); 318 | } 319 | 320 | if (json.outputs && json.outputs.length) { 321 | outputs = outputs.concat(json.outputs); 322 | nextOutputs = json.next_outputs; 323 | } else { 324 | break; 325 | } 326 | } while (1); 327 | 328 | return outputs; 329 | } 330 | 331 | async _fetchAdditionalInputs(nextInputs) { 332 | let inputs = []; 333 | let baseURI = nextInputs.split('/'); 334 | baseURI = baseURI[0] + '/' + baseURI[1] + '/' + baseURI[2] + '/'; 335 | const api = new Frisbee({ 336 | baseURI: baseURI, 337 | }); 338 | 339 | do { 340 | await (() => new Promise(resolve => setTimeout(resolve, 1000)))(); 341 | nextInputs = nextInputs.replace(baseURI, ''); 342 | 343 | let response = await api.get(nextInputs + ((useBlockcypherTokens && '&token=' + this.getRandomBlockcypherToken()) || '')); 344 | let json = response.body; 345 | if (typeof json === 'undefined') { 346 | throw new Error('Could not fetch transactions from API:' + response.err); 347 | } 348 | 349 | if (json.inputs && json.inputs.length) { 350 | inputs = inputs.concat(json.inputs); 351 | nextInputs = json.next_inputs; 352 | } else { 353 | break; 354 | } 355 | } while (1); 356 | 357 | return inputs; 358 | } 359 | 360 | async broadcastTx(txhex) { 361 | try { 362 | const broadcast = await BlueElectrum.broadcast(txhex); 363 | return broadcast; 364 | } catch (error) { 365 | return error; 366 | } 367 | } 368 | 369 | async _broadcastTxBtczen(txhex) { 370 | const api = new Frisbee({ 371 | baseURI: 'https://btczen.com', 372 | headers: { 373 | Accept: 'application/json', 374 | 'Content-Type': 'application/json', 375 | }, 376 | }); 377 | 378 | let res = await api.get('/broadcast/' + txhex); 379 | console.log('response btczen', res.body); 380 | return res.body; 381 | } 382 | 383 | async _broadcastTxChainso(txhex) { 384 | const api = new Frisbee({ 385 | baseURI: 'https://chain.so', 386 | headers: { 387 | Accept: 'application/json', 388 | 'Content-Type': 'application/json', 389 | }, 390 | }); 391 | 392 | let res = await api.post('/api/v2/send_tx/BTC', { 393 | body: { tx_hex: txhex }, 394 | }); 395 | return res.body; 396 | } 397 | 398 | async _broadcastTxSmartbit(txhex) { 399 | const api = new Frisbee({ 400 | baseURI: 'https://api.smartbit.com.au', 401 | headers: { 402 | Accept: 'application/json', 403 | 'Content-Type': 'application/json', 404 | }, 405 | }); 406 | 407 | let res = await api.post('/v1/blockchain/pushtx', { 408 | body: { hex: txhex }, 409 | }); 410 | return res.body; 411 | } 412 | 413 | async _broadcastTxBlockcypher(txhex) { 414 | const api = new Frisbee({ 415 | baseURI: 'https://api.blockcypher.com', 416 | headers: { 417 | Accept: 'application/json', 418 | 'Content-Type': 'application/json', 419 | }, 420 | }); 421 | 422 | let res = await api.post('/v1/btc/main/txs/push', { body: { tx: txhex } }); 423 | // console.log('blockcypher response', res); 424 | return res.body; 425 | } 426 | 427 | /** 428 | * Takes UTXOs (as presented by blockcypher api), transforms them into 429 | * format expected by signer module, creates tx and returns signed string txhex. 430 | * 431 | * @param utxos Unspent outputs, expects blockcypher format 432 | * @param amount 433 | * @param fee 434 | * @param toAddress 435 | * @param memo 436 | * @return string Signed txhex ready for broadcast 437 | */ 438 | createTx(utxos, amount, fee, toAddress, memo) { 439 | // transforming UTXOs fields to how module expects it 440 | for (let u of utxos) { 441 | u.confirmations = 6; // hack to make module accept 0 confirmations 442 | u.txid = u.tx_hash; 443 | u.vout = u.tx_output_n; 444 | u.amount = new BigNumber(u.value); 445 | u.amount = u.amount.dividedBy(100000000); 446 | u.amount = u.amount.toString(10); 447 | } 448 | // console.log('creating legacy tx ', amount, ' with fee ', fee, 'secret=', this.getSecret(), 'from address', this.getAddress()); 449 | let amountPlusFee = parseFloat(new BigNumber(amount).plus(fee).toString(10)); 450 | return signer.createTransaction(utxos, toAddress, amountPlusFee, fee, this.getSecret(), this.getAddress()); 451 | } 452 | 453 | getLatestTransactionTime() { 454 | if (this.getTransactions().length === 0) { 455 | return 0; 456 | } 457 | let max = 0; 458 | for (let tx of this.getTransactions()) { 459 | max = Math.max(new Date(tx.received) * 1, max); 460 | } 461 | 462 | return new Date(max).toString(); 463 | } 464 | 465 | getRandomBlockcypherToken() { 466 | return (array => { 467 | for (let i = array.length - 1; i > 0; i--) { 468 | let j = Math.floor(Math.random() * (i + 1)); 469 | [array[i], array[j]] = [array[j], array[i]]; 470 | } 471 | return array[0]; 472 | })([ 473 | '0326b7107b4149559d18ce80612ef812', 474 | 'a133eb7ccacd4accb80cb1225de4b155', 475 | '7c2b1628d27b4bd3bf8eaee7149c577f', 476 | 'f1e5a02b9ec84ec4bc8db2349022e5f5', 477 | 'e5926dbeb57145979153adc41305b183', 478 | ]); 479 | } 480 | 481 | isAddressValid(address) { 482 | try { 483 | bitcoin.address.toOutputScript(address); 484 | return true; 485 | } catch (e) { 486 | return false; 487 | } 488 | } 489 | } 490 | -------------------------------------------------------------------------------- /app-storage.js: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-community/async-storage'; 2 | import RNSecureKeyStore, { ACCESSIBLE } from 'react-native-secure-key-store'; 3 | import { 4 | HDLegacyBreadwalletWallet, 5 | HDSegwitP2SHWallet, 6 | HDLegacyP2PKHWallet, 7 | WatchOnlyWallet, 8 | LegacyWallet, 9 | SegwitP2SHWallet, 10 | SegwitBech32Wallet, 11 | HDSegwitBech32Wallet, 12 | PlaceholderWallet, 13 | LightningCustodianWallet, 14 | } from './'; 15 | import WatchConnectivity from '../WatchConnectivity'; 16 | import DeviceQuickActions from './quickActions'; 17 | const encryption = require('../encryption'); 18 | 19 | export class AppStorage { 20 | static FLAG_ENCRYPTED = 'data_encrypted'; 21 | static LANG = 'lang'; 22 | static EXCHANGE_RATES = 'currency'; 23 | static LNDHUB = 'lndhub'; 24 | static ELECTRUM_HOST = 'electrum_host'; 25 | static ELECTRUM_TCP_PORT = 'electrum_tcp_port'; 26 | static PREFERRED_CURRENCY = 'preferredCurrency'; 27 | static ADVANCED_MODE_ENABLED = 'advancedmodeenabled'; 28 | 29 | constructor() { 30 | /** {Array.} */ 31 | this.wallets = []; 32 | this.tx_metadata = {}; 33 | this.cachedPassword = false; 34 | this.settings = { 35 | brandingColor: '#ffffff', 36 | foregroundColor: '#0c2550', 37 | buttonBackgroundColor: '#ccddf9', 38 | buttonTextColor: '#0c2550', 39 | buttonAlternativeTextColor: '#2f5fb3', 40 | buttonDisabledBackgroundColor: '#eef0f4', 41 | buttonDisabledTextColor: '#9aa0aa', 42 | inputBorderColor: '#d2d2d2', 43 | inputBackgroundColor: '#f5f5f5', 44 | alternativeTextColor: '#9aa0aa', 45 | alternativeTextColor2: '#0f5cc0', 46 | buttonBlueBackgroundColor: '#ccddf9', 47 | incomingBackgroundColor: '#d2f8d6', 48 | incomingForegroundColor: '#37c0a1', 49 | outgoingBackgroundColor: '#f8d2d2', 50 | outgoingForegroundColor: '#d0021b', 51 | successColor: '#37c0a1', 52 | failedColor: '#ff0000', 53 | shadowColor: '#000000', 54 | inverseForegroundColor: '#ffffff', 55 | hdborderColor: '#68BBE1', 56 | hdbackgroundColor: '#ECF9FF', 57 | lnborderColor: '#F7C056', 58 | lnbackgroundColor: '#FFFAEF', 59 | }; 60 | } 61 | 62 | /** 63 | * Wrapper for storage call. Secure store works only in RN environment. AsyncStorage is 64 | * used for cli/tests 65 | * 66 | * @param key 67 | * @param value 68 | * @returns {Promise|Promise | Promise | * | Promise | void} 69 | */ 70 | setItem(key, value) { 71 | if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') { 72 | return RNSecureKeyStore.set(key, value, { accessible: ACCESSIBLE.WHEN_UNLOCKED }); 73 | } else { 74 | return AsyncStorage.setItem(key, value); 75 | } 76 | } 77 | 78 | /** 79 | * Wrapper for storage call. Secure store works only in RN environment. AsyncStorage is 80 | * used for cli/tests 81 | * 82 | * @param key 83 | * @returns {Promise|*} 84 | */ 85 | getItem(key) { 86 | if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') { 87 | return RNSecureKeyStore.get(key); 88 | } else { 89 | return AsyncStorage.getItem(key); 90 | } 91 | } 92 | 93 | async storageIsEncrypted() { 94 | let data; 95 | try { 96 | data = await this.getItem(AppStorage.FLAG_ENCRYPTED); 97 | } catch (error) { 98 | return false; 99 | } 100 | 101 | return !!data; 102 | } 103 | 104 | /** 105 | * Iterates through all values of `data` trying to 106 | * decrypt each one, and returns first one successfully decrypted 107 | * 108 | * @param data String (serialized array) 109 | * @param password 110 | */ 111 | decryptData(data, password) { 112 | data = JSON.parse(data); 113 | let decrypted; 114 | for (let value of data) { 115 | try { 116 | decrypted = encryption.decrypt(value, password); 117 | } catch (e) { 118 | console.log(e.message); 119 | } 120 | 121 | if (decrypted) { 122 | return decrypted; 123 | } 124 | } 125 | 126 | return false; 127 | } 128 | 129 | async encryptStorage(password) { 130 | // assuming the storage is not yet encrypted 131 | await this.saveToDisk(); 132 | let data = await this.getItem('data'); 133 | // TODO: refactor ^^^ (should not save & load to fetch data) 134 | 135 | let encrypted = encryption.encrypt(data, password); 136 | data = []; 137 | data.push(encrypted); // putting in array as we might have many buckets with storages 138 | data = JSON.stringify(data); 139 | this.cachedPassword = password; 140 | await this.setItem('data', data); 141 | await this.setItem(AppStorage.FLAG_ENCRYPTED, '1'); 142 | DeviceQuickActions.clearShortcutItems(); 143 | DeviceQuickActions.removeAllWallets(); 144 | } 145 | 146 | /** 147 | * Cleans up all current application data (wallets, tx metadata etc) 148 | * Encrypts the bucket and saves it storage 149 | * 150 | * @returns {Promise.} Success or failure 151 | */ 152 | async createFakeStorage(fakePassword) { 153 | this.wallets = []; 154 | this.tx_metadata = {}; 155 | 156 | let data = { 157 | wallets: [], 158 | tx_metadata: {}, 159 | }; 160 | 161 | let buckets = await this.getItem('data'); 162 | buckets = JSON.parse(buckets); 163 | buckets.push(encryption.encrypt(JSON.stringify(data), fakePassword)); 164 | this.cachedPassword = fakePassword; 165 | const bucketsString = JSON.stringify(buckets); 166 | await this.setItem('data', bucketsString); 167 | return (await this.getItem('data')) === bucketsString; 168 | } 169 | 170 | /** 171 | * Loads from storage all wallets and 172 | * maps them to `this.wallets` 173 | * 174 | * @param password If present means storage must be decrypted before usage 175 | * @returns {Promise.} 176 | */ 177 | async loadFromDisk(password) { 178 | try { 179 | let data = await this.getItem('data'); 180 | if (password) { 181 | data = this.decryptData(data, password); 182 | if (data) { 183 | // password is good, cache it 184 | this.cachedPassword = password; 185 | } 186 | } 187 | if (data !== null) { 188 | data = JSON.parse(data); 189 | if (!data.wallets) return false; 190 | let wallets = data.wallets; 191 | for (let key of wallets) { 192 | // deciding which type is wallet and instatiating correct object 193 | let tempObj = JSON.parse(key); 194 | let unserializedWallet; 195 | switch (tempObj.type) { 196 | case PlaceholderWallet.type: 197 | continue; 198 | case SegwitBech32Wallet.type: 199 | unserializedWallet = SegwitBech32Wallet.fromJson(key); 200 | break; 201 | case SegwitP2SHWallet.type: 202 | unserializedWallet = SegwitP2SHWallet.fromJson(key); 203 | break; 204 | case WatchOnlyWallet.type: 205 | unserializedWallet = WatchOnlyWallet.fromJson(key); 206 | unserializedWallet.init(); 207 | break; 208 | case HDLegacyP2PKHWallet.type: 209 | unserializedWallet = HDLegacyP2PKHWallet.fromJson(key); 210 | break; 211 | case HDSegwitP2SHWallet.type: 212 | unserializedWallet = HDSegwitP2SHWallet.fromJson(key); 213 | break; 214 | case HDSegwitBech32Wallet.type: 215 | unserializedWallet = HDSegwitBech32Wallet.fromJson(key); 216 | break; 217 | case HDLegacyBreadwalletWallet.type: 218 | unserializedWallet = HDLegacyBreadwalletWallet.fromJson(key); 219 | break; 220 | case LightningCustodianWallet.type: 221 | /** @type {LightningCustodianWallet} */ 222 | unserializedWallet = LightningCustodianWallet.fromJson(key); 223 | let lndhub = false; 224 | try { 225 | lndhub = await AsyncStorage.getItem(AppStorage.LNDHUB); 226 | } catch (Error) { 227 | console.warn(Error); 228 | } 229 | 230 | if (unserializedWallet.baseURI) { 231 | unserializedWallet.setBaseURI(unserializedWallet.baseURI); // not really necessary, just for the sake of readability 232 | console.log('using saved uri for for ln wallet:', unserializedWallet.baseURI); 233 | } else if (lndhub) { 234 | console.log('using wallet-wide settings ', lndhub, 'for ln wallet'); 235 | unserializedWallet.setBaseURI(lndhub); 236 | } else { 237 | console.log('using default', LightningCustodianWallet.defaultBaseUri, 'for ln wallet'); 238 | unserializedWallet.setBaseURI(LightningCustodianWallet.defaultBaseUri); 239 | } 240 | unserializedWallet.init(); 241 | break; 242 | case LegacyWallet.type: 243 | default: 244 | unserializedWallet = LegacyWallet.fromJson(key); 245 | break; 246 | } 247 | // done 248 | if (!this.wallets.some(wallet => wallet.getSecret() === unserializedWallet.secret)) { 249 | this.wallets.push(unserializedWallet); 250 | this.tx_metadata = data.tx_metadata; 251 | } 252 | } 253 | WatchConnectivity.shared.wallets = this.wallets; 254 | WatchConnectivity.shared.tx_metadata = this.tx_metadata; 255 | WatchConnectivity.shared.fetchTransactionsFunction = async () => { 256 | await this.fetchWalletTransactions(); 257 | await this.saveToDisk(); 258 | }; 259 | await WatchConnectivity.shared.sendWalletsToWatch(); 260 | DeviceQuickActions.setWallets(this.wallets); 261 | DeviceQuickActions.setQuickActions(); 262 | return true; 263 | } else { 264 | return false; // failed loading data or loading/decryptin data 265 | } 266 | } catch (error) { 267 | console.warn(error.message); 268 | return false; 269 | } 270 | } 271 | 272 | /** 273 | * Lookup wallet in list by it's secret and 274 | * remove it from `this.wallets` 275 | * 276 | * @param wallet {AbstractWallet} 277 | */ 278 | deleteWallet(wallet) { 279 | let secret = wallet.getSecret(); 280 | let tempWallets = []; 281 | 282 | for (let value of this.wallets) { 283 | if (value.getSecret() === secret) { 284 | // the one we should delete 285 | // nop 286 | } else { 287 | // the one we must keep 288 | tempWallets.push(value); 289 | } 290 | } 291 | this.wallets = tempWallets; 292 | } 293 | 294 | /** 295 | * Serializes and saves to storage object data. 296 | * If cached password is saved - finds the correct bucket 297 | * to save to, encrypts and then saves. 298 | * 299 | * @returns {Promise} Result of storage save 300 | */ 301 | async saveToDisk() { 302 | let walletsToSave = []; 303 | for (let key of this.wallets) { 304 | if (typeof key === 'boolean' || key.type === PlaceholderWallet.type) continue; 305 | if (key.prepareForSerialization) key.prepareForSerialization(); 306 | walletsToSave.push(JSON.stringify({ ...key, type: key.type })); 307 | } 308 | 309 | let data = { 310 | wallets: walletsToSave, 311 | tx_metadata: this.tx_metadata, 312 | }; 313 | 314 | if (this.cachedPassword) { 315 | // should find the correct bucket, encrypt and then save 316 | let buckets = await this.getItem('data'); 317 | buckets = JSON.parse(buckets); 318 | let newData = []; 319 | for (let bucket of buckets) { 320 | let decrypted = encryption.decrypt(bucket, this.cachedPassword); 321 | if (!decrypted) { 322 | // no luck decrypting, its not our bucket 323 | newData.push(bucket); 324 | } else { 325 | // decrypted ok, this is our bucket 326 | // we serialize our object's data, encrypt it, and add it to buckets 327 | newData.push(encryption.encrypt(JSON.stringify(data), this.cachedPassword)); 328 | await this.setItem(AppStorage.FLAG_ENCRYPTED, '1'); 329 | } 330 | } 331 | data = newData; 332 | } else { 333 | await this.setItem(AppStorage.FLAG_ENCRYPTED, ''); // drop the flag 334 | } 335 | WatchConnectivity.shared.wallets = this.wallets; 336 | WatchConnectivity.shared.tx_metadata = this.tx_metadata; 337 | WatchConnectivity.shared.sendWalletsToWatch(); 338 | DeviceQuickActions.setWallets(this.wallets); 339 | DeviceQuickActions.setQuickActions(); 340 | return this.setItem('data', JSON.stringify(data)); 341 | } 342 | 343 | /** 344 | * For each wallet, fetches balance from remote endpoint. 345 | * Use getter for a specific wallet to get actual balance. 346 | * Returns void. 347 | * If index is present then fetch only from this specific wallet 348 | * 349 | * @return {Promise.} 350 | */ 351 | async fetchWalletBalances(index) { 352 | console.log('fetchWalletBalances for wallet#', index); 353 | if (index || index === 0) { 354 | let c = 0; 355 | for (let wallet of this.wallets.filter(wallet => wallet.type !== PlaceholderWallet.type)) { 356 | if (c++ === index) { 357 | await wallet.fetchBalance(); 358 | } 359 | } 360 | } else { 361 | for (let wallet of this.wallets.filter(wallet => wallet.type !== PlaceholderWallet.type)) { 362 | await wallet.fetchBalance(); 363 | } 364 | } 365 | } 366 | 367 | /** 368 | * Fetches from remote endpoint all transactions for each wallet. 369 | * Returns void. 370 | * To access transactions - get them from each respective wallet. 371 | * If index is present then fetch only from this specific wallet. 372 | * 373 | * @param index {Integer} Index of the wallet in this.wallets array, 374 | * blank to fetch from all wallets 375 | * @return {Promise.} 376 | */ 377 | async fetchWalletTransactions(index) { 378 | console.log('fetchWalletTransactions for wallet#', index); 379 | if (index || index === 0) { 380 | let c = 0; 381 | for (let wallet of this.wallets.filter(wallet => wallet.type !== PlaceholderWallet.type)) { 382 | if (c++ === index) { 383 | await wallet.fetchTransactions(); 384 | if (wallet.fetchPendingTransactions) { 385 | await wallet.fetchPendingTransactions(); 386 | } 387 | if (wallet.fetchUserInvoices) { 388 | await wallet.fetchUserInvoices(); 389 | } 390 | } 391 | } 392 | } else { 393 | for (let wallet of this.wallets) { 394 | await wallet.fetchTransactions(); 395 | if (wallet.fetchPendingTransactions) { 396 | await wallet.fetchPendingTransactions(); 397 | } 398 | if (wallet.fetchUserInvoices) { 399 | await wallet.fetchUserInvoices(); 400 | } 401 | } 402 | } 403 | } 404 | 405 | /** 406 | * 407 | * @returns {Array.} 408 | */ 409 | getWallets() { 410 | return this.wallets; 411 | } 412 | 413 | /** 414 | * Getter for all transactions in all wallets. 415 | * But if index is provided - only for wallet with corresponding index 416 | * 417 | * @param index {Integer|null} Wallet index in this.wallets. Empty (or null) for all wallets. 418 | * @param limit {Integer} How many txs return, starting from the earliest. Default: all of them. 419 | * @return {Array} 420 | */ 421 | getTransactions(index, limit = Infinity) { 422 | if (index || index === 0) { 423 | let txs = []; 424 | let c = 0; 425 | for (let wallet of this.wallets) { 426 | if (c++ === index) { 427 | txs = txs.concat(wallet.getTransactions()); 428 | } 429 | } 430 | return txs; 431 | } 432 | 433 | let txs = []; 434 | for (let wallet of this.wallets) { 435 | let walletTransactions = wallet.getTransactions(); 436 | for (let t of walletTransactions) { 437 | t.walletPreferredBalanceUnit = wallet.getPreferredBalanceUnit(); 438 | } 439 | txs = txs.concat(walletTransactions); 440 | } 441 | 442 | for (let t of txs) { 443 | t.sort_ts = +new Date(t.received); 444 | } 445 | 446 | return txs 447 | .sort(function(a, b) { 448 | return b.sort_ts - a.sort_ts; 449 | }) 450 | .slice(0, limit); 451 | } 452 | 453 | /** 454 | * Getter for a sum of all balances of all wallets 455 | * 456 | * @return {number} 457 | */ 458 | getBalance() { 459 | let finalBalance = 0; 460 | for (let wal of this.wallets) { 461 | finalBalance += wal.getBalance(); 462 | } 463 | return finalBalance; 464 | } 465 | 466 | /** 467 | * Simple async sleeper function 468 | * 469 | * @param ms {number} Milliseconds to sleep 470 | * @returns {Promise | Promise<*>>} 471 | */ 472 | async sleep(ms) { 473 | return new Promise(resolve => setTimeout(resolve, ms)); 474 | } 475 | } 476 | -------------------------------------------------------------------------------- /abstract-hd-wallet.js: -------------------------------------------------------------------------------- 1 | import { LegacyWallet } from './legacy-wallet'; 2 | import Frisbee from 'frisbee'; 3 | const bitcoin = require('bitcoinjs-lib'); 4 | const bip39 = require('bip39'); 5 | const BlueElectrum = require('../BlueElectrum'); 6 | 7 | export class AbstractHDWallet extends LegacyWallet { 8 | static type = 'abstract'; 9 | static typeReadable = 'abstract'; 10 | 11 | constructor() { 12 | super(); 13 | this.next_free_address_index = 0; 14 | this.next_free_change_address_index = 0; 15 | this.internal_addresses_cache = {}; // index => address 16 | this.external_addresses_cache = {}; // index => address 17 | this._xpub = ''; // cache 18 | this.usedAddresses = []; 19 | this._address_to_wif_cache = {}; 20 | this.gap_limit = 20; 21 | } 22 | 23 | prepareForSerialization() { 24 | // deleting structures that cant be serialized 25 | delete this._node0; 26 | delete this._node1; 27 | } 28 | 29 | generate() { 30 | throw new Error('Not implemented'); 31 | } 32 | 33 | allowSend() { 34 | return false; 35 | } 36 | 37 | getTransactions() { 38 | // need to reformat txs, as we are expected to return them in blockcypher format, 39 | // but they are from blockchain.info actually (for all hd wallets) 40 | 41 | let uniq = {}; 42 | let txs = []; 43 | for (let tx of this.transactions) { 44 | if (uniq[tx.hash]) continue; 45 | uniq[tx.hash] = 1; 46 | txs.push(AbstractHDWallet.convertTx(tx)); 47 | } 48 | 49 | return txs; 50 | } 51 | 52 | static convertTx(tx) { 53 | // console.log('converting', tx); 54 | var clone = Object.assign({}, tx); 55 | clone.received = new Date(clone.time * 1000).toISOString(); 56 | clone.outputs = clone.out; 57 | if (clone.confirmations === undefined) { 58 | clone.confirmations = 0; 59 | } 60 | for (let o of clone.outputs) { 61 | o.addresses = [o.addr]; 62 | } 63 | for (let i of clone.inputs) { 64 | if (i.prev_out && i.prev_out.addr) { 65 | i.addresses = [i.prev_out.addr]; 66 | } 67 | } 68 | 69 | if (!clone.value) { 70 | let value = 0; 71 | for (let inp of clone.inputs) { 72 | if (inp.prev_out && inp.prev_out.xpub) { 73 | // our owned 74 | value -= inp.prev_out.value; 75 | } 76 | } 77 | 78 | for (let out of clone.out) { 79 | if (out.xpub) { 80 | // to us 81 | value += out.value; 82 | } 83 | } 84 | clone.value = value; 85 | } 86 | 87 | return clone; 88 | } 89 | 90 | setSecret(newSecret) { 91 | this.secret = newSecret.trim().toLowerCase(); 92 | this.secret = this.secret.replace(/[^a-zA-Z0-9]/g, ' ').replace(/\s+/g, ' '); 93 | return this; 94 | } 95 | 96 | /** 97 | * @return {Boolean} is mnemonic in `this.secret` valid 98 | */ 99 | validateMnemonic() { 100 | return bip39.validateMnemonic(this.secret); 101 | } 102 | 103 | getMnemonicToSeedHex() { 104 | return bip39.mnemonicToSeedHex(this.secret); 105 | } 106 | 107 | /** 108 | * Derives from hierarchy, returns next free address 109 | * (the one that has no transactions). Looks for several, 110 | * gives up if none found, and returns the used one 111 | * 112 | * @return {Promise.} 113 | */ 114 | async getAddressAsync() { 115 | // looking for free external address 116 | let freeAddress = ''; 117 | let c; 118 | for (c = 0; c < this.gap_limit + 1; c++) { 119 | if (this.next_free_address_index + c < 0) continue; 120 | let address = this._getExternalAddressByIndex(this.next_free_address_index + c); 121 | this.external_addresses_cache[this.next_free_address_index + c] = address; // updating cache just for any case 122 | let txs = []; 123 | try { 124 | txs = await BlueElectrum.getTransactionsByAddress(address); 125 | } catch (Err) { 126 | console.warn('BlueElectrum.getTransactionsByAddress()', Err.message); 127 | } 128 | if (txs.length === 0) { 129 | // found free address 130 | freeAddress = address; 131 | this.next_free_address_index += c; // now points to _this one_ 132 | break; 133 | } 134 | } 135 | 136 | if (!freeAddress) { 137 | // could not find in cycle above, give up 138 | freeAddress = this._getExternalAddressByIndex(this.next_free_address_index + c); // we didnt check this one, maybe its free 139 | this.next_free_address_index += c + 1; // now points to the one _after_ 140 | } 141 | this._address = freeAddress; 142 | return freeAddress; 143 | } 144 | 145 | /** 146 | * Derives from hierarchy, returns next free CHANGE address 147 | * (the one that has no transactions). Looks for several, 148 | * gives up if none found, and returns the used one 149 | * 150 | * @return {Promise.} 151 | */ 152 | async getChangeAddressAsync() { 153 | // looking for free internal address 154 | let freeAddress = ''; 155 | let c; 156 | for (c = 0; c < this.gap_limit + 1; c++) { 157 | if (this.next_free_change_address_index + c < 0) continue; 158 | let address = this._getInternalAddressByIndex(this.next_free_change_address_index + c); 159 | this.internal_addresses_cache[this.next_free_change_address_index + c] = address; // updating cache just for any case 160 | let txs = []; 161 | try { 162 | txs = await BlueElectrum.getTransactionsByAddress(address); 163 | } catch (Err) { 164 | console.warn('BlueElectrum.getTransactionsByAddress()', Err.message); 165 | } 166 | if (txs.length === 0) { 167 | // found free address 168 | freeAddress = address; 169 | this.next_free_change_address_index += c; // now points to _this one_ 170 | break; 171 | } 172 | } 173 | 174 | if (!freeAddress) { 175 | // could not find in cycle above, give up 176 | freeAddress = this._getExternalAddressByIndex(this.next_free_address_index + c); // we didnt check this one, maybe its free 177 | this.next_free_address_index += c + 1; // now points to the one _after_ 178 | } 179 | this._address = freeAddress; 180 | return freeAddress; 181 | } 182 | 183 | /** 184 | * Should not be used in HD wallets 185 | * 186 | * @deprecated 187 | * @return {string} 188 | */ 189 | getAddress() { 190 | return this._address; 191 | } 192 | 193 | _getExternalWIFByIndex(index) { 194 | throw new Error('Not implemented'); 195 | } 196 | 197 | _getInternalWIFByIndex(index) { 198 | throw new Error('Not implemented'); 199 | } 200 | 201 | _getExternalAddressByIndex(index) { 202 | throw new Error('Not implemented'); 203 | } 204 | 205 | _getInternalAddressByIndex(index) { 206 | throw new Error('Not implemented'); 207 | } 208 | 209 | getXpub() { 210 | throw new Error('Not implemented'); 211 | } 212 | 213 | /** 214 | * Async function to fetch all transactions. Use getter to get actual txs. 215 | * Also, sets internals: 216 | * `this.internal_addresses_cache` 217 | * `this.external_addresses_cache` 218 | * 219 | * @returns {Promise} 220 | */ 221 | async fetchTransactions() { 222 | try { 223 | const api = new Frisbee({ baseURI: 'https://blockchain.info' }); 224 | this.transactions = []; 225 | let offset = 0; 226 | 227 | while (1) { 228 | let response = await api.get('/multiaddr?active=' + this.getXpub() + '&n=100&offset=' + offset); 229 | 230 | if (response && response.body) { 231 | if (response.body.txs && response.body.txs.length === 0) { 232 | break; 233 | } 234 | 235 | let latestBlock = false; 236 | if (response.body.info && response.body.info.latest_block) { 237 | latestBlock = response.body.info.latest_block.height; 238 | } 239 | 240 | this._lastTxFetch = +new Date(); 241 | 242 | // processing TXs and adding to internal memory 243 | if (response.body.txs) { 244 | for (let tx of response.body.txs) { 245 | let value = 0; 246 | 247 | for (let input of tx.inputs) { 248 | // ----- INPUTS 249 | if (input.prev_out.xpub) { 250 | // sent FROM US 251 | value -= input.prev_out.value; 252 | 253 | // setting internal caches to help ourselves in future... 254 | let path = input.prev_out.xpub.path.split('/'); 255 | if (path[path.length - 2] === '1') { 256 | // change address 257 | this.next_free_change_address_index = Math.max(path[path.length - 1] * 1 + 1, this.next_free_change_address_index); 258 | // setting to point to last maximum known change address + 1 259 | } 260 | if (path[path.length - 2] === '0') { 261 | // main (aka external) address 262 | this.next_free_address_index = Math.max(path[path.length - 1] * 1 + 1, this.next_free_address_index); 263 | // setting to point to last maximum known main address + 1 264 | } 265 | // done with cache 266 | } 267 | } 268 | 269 | for (let output of tx.out) { 270 | // ----- OUTPUTS 271 | if (output.xpub) { 272 | // sent TO US (change) 273 | value += output.value; 274 | 275 | // setting internal caches to help ourselves in future... 276 | let path = output.xpub.path.split('/'); 277 | if (path[path.length - 2] === '1') { 278 | // change address 279 | this.next_free_change_address_index = Math.max(path[path.length - 1] * 1 + 1, this.next_free_change_address_index); 280 | // setting to point to last maximum known change address + 1 281 | } 282 | if (path[path.length - 2] === '0') { 283 | // main (aka external) address 284 | this.next_free_address_index = Math.max(path[path.length - 1] * 1 + 1, this.next_free_address_index); 285 | // setting to point to last maximum known main address + 1 286 | } 287 | // done with cache 288 | } 289 | } 290 | 291 | tx.value = value; // new BigNumber(value).div(100000000).toString() * 1; 292 | if (!tx.confirmations && latestBlock) { 293 | tx.confirmations = latestBlock - tx.block_height + 1; 294 | } 295 | 296 | this.transactions.push(tx); 297 | } 298 | 299 | if (response.body.txs.length < 100) { 300 | // this fetch yilded less than page size, thus requesting next batch makes no sense 301 | break; 302 | } 303 | } else { 304 | break; // error ? 305 | } 306 | } else { 307 | throw new Error('Could not fetch transactions from API: ' + response.err); // breaks here 308 | } 309 | 310 | offset += 100; 311 | } 312 | } catch (err) { 313 | console.warn(err); 314 | } 315 | } 316 | 317 | /** 318 | * Given that `address` is in our HD hierarchy, try to find 319 | * corresponding WIF 320 | * 321 | * @param address {String} In our HD hierarchy 322 | * @return {String} WIF if found 323 | */ 324 | _getWifForAddress(address) { 325 | if (this._address_to_wif_cache[address]) return this._address_to_wif_cache[address]; // cache hit 326 | 327 | // fast approach, first lets iterate over all addressess we have in cache 328 | for (let index of Object.keys(this.internal_addresses_cache)) { 329 | if (this._getInternalAddressByIndex(index) === address) { 330 | return (this._address_to_wif_cache[address] = this._getInternalWIFByIndex(index)); 331 | } 332 | } 333 | 334 | for (let index of Object.keys(this.external_addresses_cache)) { 335 | if (this._getExternalAddressByIndex(index) === address) { 336 | return (this._address_to_wif_cache[address] = this._getExternalWIFByIndex(index)); 337 | } 338 | } 339 | 340 | // no luck - lets iterate over all addresses we have up to first unused address index 341 | for (let c = 0; c <= this.next_free_change_address_index + this.gap_limit; c++) { 342 | let possibleAddress = this._getInternalAddressByIndex(c); 343 | if (possibleAddress === address) { 344 | return (this._address_to_wif_cache[address] = this._getInternalWIFByIndex(c)); 345 | } 346 | } 347 | 348 | for (let c = 0; c <= this.next_free_address_index + this.gap_limit; c++) { 349 | let possibleAddress = this._getExternalAddressByIndex(c); 350 | if (possibleAddress === address) { 351 | return (this._address_to_wif_cache[address] = this._getExternalWIFByIndex(c)); 352 | } 353 | } 354 | 355 | throw new Error('Could not find WIF for ' + address); 356 | } 357 | 358 | createTx() { 359 | throw new Error('Not implemented'); 360 | } 361 | 362 | async fetchBalance() { 363 | try { 364 | let that = this; 365 | 366 | // refactor me 367 | // eslint-disable-next-line 368 | async function binarySearchIterationForInternalAddress(index, maxUsedIndex = 0, minUnusedIndex = 100500100, depth = 0) { 369 | if (depth >= 20) return maxUsedIndex + 1; // fail 370 | let txs = await BlueElectrum.getTransactionsByAddress(that._getInternalAddressByIndex(index)); 371 | if (txs.length === 0) { 372 | if (index === 0) return 0; 373 | minUnusedIndex = Math.min(minUnusedIndex, index); // set 374 | index = Math.floor((index - maxUsedIndex) / 2 + maxUsedIndex); 375 | } else { 376 | maxUsedIndex = Math.max(maxUsedIndex, index); // set 377 | let txs2 = await BlueElectrum.getTransactionsByAddress(that._getInternalAddressByIndex(index + 1)); 378 | if (txs2.length === 0) return index + 1; // thats our next free address 379 | 380 | index = Math.round((minUnusedIndex - index) / 2 + index); 381 | } 382 | 383 | return binarySearchIterationForInternalAddress(index, maxUsedIndex, minUnusedIndex, depth + 1); 384 | } 385 | 386 | // refactor me 387 | // eslint-disable-next-line 388 | async function binarySearchIterationForExternalAddress(index, maxUsedIndex = 0, minUnusedIndex = 100500100, depth = 0) { 389 | if (depth >= 20) return maxUsedIndex + 1; // fail 390 | let txs = await BlueElectrum.getTransactionsByAddress(that._getExternalAddressByIndex(index)); 391 | if (txs.length === 0) { 392 | if (index === 0) return 0; 393 | minUnusedIndex = Math.min(minUnusedIndex, index); // set 394 | index = Math.floor((index - maxUsedIndex) / 2 + maxUsedIndex); 395 | } else { 396 | maxUsedIndex = Math.max(maxUsedIndex, index); // set 397 | let txs2 = await BlueElectrum.getTransactionsByAddress(that._getExternalAddressByIndex(index + 1)); 398 | if (txs2.length === 0) return index + 1; // thats our next free address 399 | 400 | index = Math.round((minUnusedIndex - index) / 2 + index); 401 | } 402 | 403 | return binarySearchIterationForExternalAddress(index, maxUsedIndex, minUnusedIndex, depth + 1); 404 | } 405 | 406 | if (this.next_free_change_address_index === 0 && this.next_free_address_index === 0) { 407 | // assuming that this is freshly imported/created wallet, with no internal variables set 408 | // wild guess - its completely empty wallet: 409 | let completelyEmptyWallet = false; 410 | let txs = await BlueElectrum.getTransactionsByAddress(that._getInternalAddressByIndex(0)); 411 | if (txs.length === 0) { 412 | let txs2 = await BlueElectrum.getTransactionsByAddress(that._getExternalAddressByIndex(0)); 413 | if (txs2.length === 0) { 414 | // yep, completely empty wallet 415 | completelyEmptyWallet = true; 416 | } 417 | } 418 | 419 | // wrong guess. will have to rescan 420 | if (!completelyEmptyWallet) { 421 | // so doing binary search for last used address: 422 | this.next_free_change_address_index = await binarySearchIterationForInternalAddress(1000); 423 | this.next_free_address_index = await binarySearchIterationForExternalAddress(1000); 424 | } 425 | } // end rescanning fresh wallet 426 | 427 | // finally fetching balance 428 | await this._fetchBalance(); 429 | } catch (err) { 430 | console.warn(err); 431 | } 432 | } 433 | 434 | async _fetchBalance() { 435 | // probing future addressess in hierarchy whether they have any transactions, in case 436 | // our 'next free addr' pointers are lagging behind 437 | let tryAgain = false; 438 | let txs = await BlueElectrum.getTransactionsByAddress( 439 | this._getExternalAddressByIndex(this.next_free_address_index + this.gap_limit - 1), 440 | ); 441 | if (txs.length > 0) { 442 | // whoa, someone uses our wallet outside! better catch up 443 | this.next_free_address_index += this.gap_limit; 444 | tryAgain = true; 445 | } 446 | 447 | txs = await BlueElectrum.getTransactionsByAddress( 448 | this._getInternalAddressByIndex(this.next_free_change_address_index + this.gap_limit - 1), 449 | ); 450 | if (txs.length > 0) { 451 | this.next_free_change_address_index += this.gap_limit; 452 | tryAgain = true; 453 | } 454 | 455 | // FIXME: refactor me ^^^ can be batched in single call 456 | 457 | if (tryAgain) return this._fetchBalance(); 458 | 459 | // next, business as usuall. fetch balances 460 | 461 | this.usedAddresses = []; 462 | // generating all involved addresses: 463 | for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { 464 | this.usedAddresses.push(this._getExternalAddressByIndex(c)); 465 | } 466 | for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { 467 | this.usedAddresses.push(this._getInternalAddressByIndex(c)); 468 | } 469 | let balance = await BlueElectrum.multiGetBalanceByAddress(this.usedAddresses); 470 | this.balance = balance.balance; 471 | this.unconfirmed_balance = balance.unconfirmed_balance; 472 | this._lastBalanceFetch = +new Date(); 473 | } 474 | 475 | async _fetchUtxoBatch(addresses) { 476 | const api = new Frisbee({ 477 | baseURI: 'https://blockchain.info', 478 | }); 479 | 480 | addresses = addresses.join('|'); 481 | let utxos = []; 482 | 483 | let response; 484 | let uri; 485 | try { 486 | uri = 'https://blockchain.info' + '/unspent?active=' + addresses + '&limit=1000'; 487 | response = await api.get('/unspent?active=' + addresses + '&limit=1000'); 488 | // this endpoint does not support offset of some kind o_O 489 | // so doing only one call 490 | let json = response.body; 491 | if (typeof json === 'undefined' || typeof json.unspent_outputs === 'undefined') { 492 | throw new Error('Could not fetch UTXO from API ' + response.err); 493 | } 494 | 495 | for (let unspent of json.unspent_outputs) { 496 | // a lil transform for signer module 497 | unspent.txid = unspent.tx_hash_big_endian; 498 | unspent.vout = unspent.tx_output_n; 499 | unspent.amount = unspent.value; 500 | 501 | unspent.address = bitcoin.address.fromOutputScript(Buffer.from(unspent.script, 'hex')); 502 | utxos.push(unspent); 503 | } 504 | } catch (err) { 505 | console.warn(err, { uri }); 506 | } 507 | 508 | return utxos; 509 | } 510 | 511 | /** 512 | * @inheritDoc 513 | */ 514 | async fetchUtxo() { 515 | if (this.usedAddresses.length === 0) { 516 | // just for any case, refresh balance (it refreshes internal `this.usedAddresses`) 517 | await this.fetchBalance(); 518 | } 519 | 520 | this.utxo = []; 521 | let addresses = this.usedAddresses; 522 | addresses.push(this._getExternalAddressByIndex(this.next_free_address_index)); 523 | addresses.push(this._getInternalAddressByIndex(this.next_free_change_address_index)); 524 | 525 | let duplicateUtxos = {}; 526 | 527 | let batch = []; 528 | for (let addr of addresses) { 529 | batch.push(addr); 530 | if (batch.length >= 75) { 531 | let utxos = await this._fetchUtxoBatch(batch); 532 | for (let utxo of utxos) { 533 | let key = utxo.txid + utxo.vout; 534 | if (!duplicateUtxos[key]) { 535 | this.utxo.push(utxo); 536 | duplicateUtxos[key] = 1; 537 | } 538 | } 539 | batch = []; 540 | } 541 | } 542 | 543 | // final batch 544 | if (batch.length > 0) { 545 | let utxos = await this._fetchUtxoBatch(batch); 546 | for (let utxo of utxos) { 547 | let key = utxo.txid + utxo.vout; 548 | if (!duplicateUtxos[key]) { 549 | this.utxo.push(utxo); 550 | duplicateUtxos[key] = 1; 551 | } 552 | } 553 | } 554 | } 555 | 556 | weOwnAddress(addr) { 557 | let hashmap = {}; 558 | for (let a of this.usedAddresses) { 559 | hashmap[a] = 1; 560 | } 561 | 562 | return hashmap[addr] === 1; 563 | } 564 | 565 | _getDerivationPathByAddress(address) { 566 | throw new Error('Not implemented'); 567 | } 568 | 569 | _getNodePubkeyByIndex(address) { 570 | throw new Error('Not implemented'); 571 | } 572 | } 573 | -------------------------------------------------------------------------------- /lightning-custodian-wallet.js: -------------------------------------------------------------------------------- 1 | import { LegacyWallet } from './legacy-wallet'; 2 | import Frisbee from 'frisbee'; 3 | import bolt11 from 'bolt11'; 4 | import { BitcoinUnit, Chain } from '../models/bitcoinUnits'; 5 | 6 | export class LightningCustodianWallet extends LegacyWallet { 7 | static type = 'lightningCustodianWallet'; 8 | static typeReadable = 'Lightning'; 9 | static defaultBaseUri = 'https://lndhub.herokuapp.com/'; 10 | constructor(props) { 11 | super(props); 12 | this.setBaseURI(); // no args to init with default value 13 | this.init(); 14 | this.refresh_token = ''; 15 | this.access_token = ''; 16 | this._refresh_token_created_ts = 0; 17 | this._access_token_created_ts = 0; 18 | this.refill_addressess = []; 19 | this.pending_transactions_raw = []; 20 | this.user_invoices_raw = []; 21 | this.info_raw = false; 22 | this.preferredBalanceUnit = BitcoinUnit.SATS; 23 | this.chain = Chain.OFFCHAIN; 24 | } 25 | 26 | /** 27 | * requires calling init() after setting 28 | * 29 | * @param URI 30 | */ 31 | setBaseURI(URI) { 32 | if (URI) { 33 | this.baseURI = URI; 34 | } else { 35 | this.baseURI = LightningCustodianWallet.defaultBaseUri; 36 | } 37 | } 38 | 39 | getBaseURI() { 40 | return this.baseURI; 41 | } 42 | 43 | allowSend() { 44 | return true; 45 | } 46 | 47 | getAddress() { 48 | if (this.refill_addressess.length > 0) { 49 | return this.refill_addressess[0]; 50 | } else { 51 | return undefined; 52 | } 53 | } 54 | 55 | getSecret() { 56 | if (this.baseURI === LightningCustodianWallet.defaultBaseUri) { 57 | return this.secret; 58 | } 59 | return this.secret + '@' + this.baseURI; 60 | } 61 | 62 | timeToRefreshBalance() { 63 | return (+new Date() - this._lastBalanceFetch) / 1000 > 300; // 5 min 64 | } 65 | 66 | timeToRefreshTransaction() { 67 | return (+new Date() - this._lastTxFetch) / 1000 > 300; // 5 min 68 | } 69 | 70 | static fromJson(param) { 71 | let obj = super.fromJson(param); 72 | obj.init(); 73 | return obj; 74 | } 75 | 76 | init() { 77 | this._api = new Frisbee({ 78 | baseURI: this.baseURI, 79 | }); 80 | } 81 | 82 | accessTokenExpired() { 83 | return (+new Date() - this._access_token_created_ts) / 1000 >= 3600 * 2; // 2h 84 | } 85 | 86 | refreshTokenExpired() { 87 | return (+new Date() - this._refresh_token_created_ts) / 1000 >= 3600 * 24 * 7; // 7d 88 | } 89 | 90 | generate() { 91 | // nop 92 | } 93 | 94 | async createAccount(isTest) { 95 | let response = await this._api.post('/create', { 96 | body: { partnerid: 'bluewallet', accounttype: (isTest && 'test') || 'common' }, 97 | headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' }, 98 | }); 99 | let json = response.body; 100 | if (typeof json === 'undefined') { 101 | throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); 102 | } 103 | 104 | if (json && json.error) { 105 | throw new Error('API error: ' + (json.message ? json.message : json.error) + ' (code ' + json.code + ')'); 106 | } 107 | 108 | if (!json.login || !json.password) { 109 | throw new Error('API unexpected response: ' + JSON.stringify(response.body)); 110 | } 111 | 112 | this.secret = 'lndhub://' + json.login + ':' + json.password; 113 | } 114 | 115 | async payInvoice(invoice, freeAmount = 0) { 116 | let response = await this._api.post('/payinvoice', { 117 | body: { invoice: invoice, amount: freeAmount }, 118 | headers: { 119 | 'Access-Control-Allow-Origin': '*', 120 | 'Content-Type': 'application/json', 121 | Authorization: 'Bearer' + ' ' + this.access_token, 122 | }, 123 | }); 124 | 125 | if (response.originalResponse && typeof response.originalResponse === 'string') { 126 | try { 127 | response.originalResponse = JSON.parse(response.originalResponse); 128 | } catch (_) {} 129 | } 130 | 131 | if (response.originalResponse && response.originalResponse.status && response.originalResponse.status === 503) { 132 | throw new Error('Payment is in transit'); 133 | } 134 | 135 | let json = response.body; 136 | if (typeof json === 'undefined') { 137 | throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.originalResponse)); 138 | } 139 | 140 | if (json && json.error) { 141 | throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); 142 | } 143 | 144 | this.last_paid_invoice_result = json; 145 | } 146 | 147 | /** 148 | * Returns list of LND invoices created by user 149 | * 150 | * @return {Promise.} 151 | */ 152 | async getUserInvoices(limit = false) { 153 | let limitString = ''; 154 | if (limit) limitString = '?limit=' + parseInt(limit); 155 | let response = await this._api.get('/getuserinvoices' + limitString, { 156 | headers: { 157 | 'Access-Control-Allow-Origin': '*', 158 | 'Content-Type': 'application/json', 159 | Authorization: 'Bearer' + ' ' + this.access_token, 160 | }, 161 | }); 162 | let json = response.body; 163 | if (typeof json === 'undefined') { 164 | throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.originalResponse)); 165 | } 166 | 167 | if (json && json.error) { 168 | throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); 169 | } 170 | 171 | if (limit) { 172 | // need to merge existing invoices with the ones that arrived 173 | // but the ones received later should overwrite older ones 174 | 175 | for (let oldInvoice of this.user_invoices_raw) { 176 | // iterate all OLD invoices 177 | let found = false; 178 | for (let newInvoice of json) { 179 | // iterate all NEW invoices 180 | if (newInvoice.payment_request === oldInvoice.payment_request) found = true; 181 | } 182 | 183 | if (!found) { 184 | // if old invoice is not found in NEW array, we simply add it: 185 | json.push(oldInvoice); 186 | } 187 | } 188 | } 189 | 190 | this.user_invoices_raw = json.sort(function(a, b) { 191 | return a.timestamp - b.timestamp; 192 | }); 193 | 194 | return this.user_invoices_raw; 195 | } 196 | 197 | /** 198 | * Basically the same as this.getUserInvoices() but saves invoices list 199 | * to internal variable 200 | * 201 | * @returns {Promise} 202 | */ 203 | async fetchUserInvoices() { 204 | await this.getUserInvoices(); 205 | } 206 | 207 | isInvoiceGeneratedByWallet(paymentRequest) { 208 | return this.user_invoices_raw.some(invoice => invoice.payment_request === paymentRequest); 209 | } 210 | 211 | async addInvoice(amt, memo) { 212 | let response = await this._api.post('/addinvoice', { 213 | body: { amt: amt + '', memo: memo }, 214 | headers: { 215 | 'Access-Control-Allow-Origin': '*', 216 | 'Content-Type': 'application/json', 217 | Authorization: 'Bearer' + ' ' + this.access_token, 218 | }, 219 | }); 220 | let json = response.body; 221 | if (typeof json === 'undefined') { 222 | throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.originalResponse)); 223 | } 224 | 225 | if (json && json.error) { 226 | throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); 227 | } 228 | 229 | if (!json.r_hash || !json.pay_req) { 230 | throw new Error('API unexpected response: ' + JSON.stringify(response.body)); 231 | } 232 | 233 | return json.pay_req; 234 | } 235 | 236 | async checkRouteInvoice(invoice) { 237 | let response = await this._api.get('/checkrouteinvoice?invoice=' + invoice, { 238 | headers: { 239 | 'Access-Control-Allow-Origin': '*', 240 | 'Content-Type': 'application/json', 241 | Authorization: 'Bearer' + ' ' + this.access_token, 242 | }, 243 | }); 244 | 245 | let json = response.body; 246 | if (typeof json === 'undefined') { 247 | throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); 248 | } 249 | 250 | if (json && json.error) { 251 | throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); 252 | } 253 | } 254 | 255 | /** 256 | * Uses login & pass stored in `this.secret` to authorize 257 | * and set internal `access_token` & `refresh_token` 258 | * 259 | * @return {Promise.} 260 | */ 261 | async authorize() { 262 | let login, password; 263 | if (this.secret.indexOf('blitzhub://') !== -1) { 264 | login = this.secret.replace('blitzhub://', '').split(':')[0]; 265 | password = this.secret.replace('blitzhub://', '').split(':')[1]; 266 | } else { 267 | login = this.secret.replace('lndhub://', '').split(':')[0]; 268 | password = this.secret.replace('lndhub://', '').split(':')[1]; 269 | } 270 | let response = await this._api.post('/auth?type=auth', { 271 | body: { login: login, password: password }, 272 | headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' }, 273 | }); 274 | 275 | let json = response.body; 276 | if (typeof json === 'undefined') { 277 | throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); 278 | } 279 | 280 | if (json && json.error) { 281 | throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); 282 | } 283 | 284 | if (!json.access_token || !json.refresh_token) { 285 | throw new Error('API unexpected response: ' + JSON.stringify(response.body)); 286 | } 287 | 288 | this.refresh_token = json.refresh_token; 289 | this.access_token = json.access_token; 290 | this._refresh_token_created_ts = +new Date(); 291 | this._access_token_created_ts = +new Date(); 292 | } 293 | 294 | async checkLogin() { 295 | if (this.accessTokenExpired() && this.refreshTokenExpired()) { 296 | // all tokens expired, only option is to login with login and password 297 | return this.authorize(); 298 | } 299 | 300 | if (this.accessTokenExpired()) { 301 | // only access token expired, so only refreshing it 302 | let refreshedOk = true; 303 | try { 304 | await this.refreshAcessToken(); 305 | } catch (Err) { 306 | refreshedOk = false; 307 | } 308 | 309 | if (!refreshedOk) { 310 | // something went wrong, lets try to login regularly 311 | return this.authorize(); 312 | } 313 | } 314 | } 315 | 316 | async refreshAcessToken() { 317 | let response = await this._api.post('/auth?type=refresh_token', { 318 | body: { refresh_token: this.refresh_token }, 319 | headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' }, 320 | }); 321 | 322 | let json = response.body; 323 | if (typeof json === 'undefined') { 324 | throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); 325 | } 326 | 327 | if (json && json.error) { 328 | throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); 329 | } 330 | 331 | if (!json.access_token || !json.refresh_token) { 332 | throw new Error('API unexpected response: ' + JSON.stringify(response.body)); 333 | } 334 | 335 | this.refresh_token = json.refresh_token; 336 | this.access_token = json.access_token; 337 | this._refresh_token_created_ts = +new Date(); 338 | this._access_token_created_ts = +new Date(); 339 | } 340 | 341 | async fetchBtcAddress() { 342 | let response = await this._api.get('/getbtc', { 343 | headers: { 344 | 'Access-Control-Allow-Origin': '*', 345 | 'Content-Type': 'application/json', 346 | Authorization: 'Bearer' + ' ' + this.access_token, 347 | }, 348 | }); 349 | 350 | let json = response.body; 351 | if (typeof json === 'undefined') { 352 | throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); 353 | } 354 | 355 | if (json && json.error) { 356 | throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); 357 | } 358 | 359 | this.refill_addressess = []; 360 | 361 | for (let arr of json) { 362 | this.refill_addressess.push(arr.address); 363 | } 364 | } 365 | 366 | async getAddressAsync() { 367 | return this.fetchBtcAddress(); 368 | } 369 | 370 | async allowOnchainAddress() { 371 | if (this.getAddress() !== undefined) { 372 | return true; 373 | } else { 374 | await this.fetchBtcAddress(); 375 | return this.getAddress() !== undefined; 376 | } 377 | } 378 | 379 | getTransactions() { 380 | let txs = []; 381 | this.pending_transactions_raw = this.pending_transactions_raw || []; 382 | this.user_invoices_raw = this.user_invoices_raw || []; 383 | this.transactions_raw = this.transactions_raw || []; 384 | txs = txs.concat(this.pending_transactions_raw.slice(), this.transactions_raw.slice().reverse(), this.user_invoices_raw.slice()); // slice so array is cloned 385 | // transforming to how wallets/list screen expects it 386 | for (let tx of txs) { 387 | tx.fromWallet = this.getSecret(); 388 | if (tx.amount) { 389 | // pending tx 390 | tx.amt = tx.amount * -100000000; 391 | tx.fee = 0; 392 | tx.timestamp = tx.time; 393 | tx.memo = 'On-chain transaction'; 394 | } 395 | 396 | if (typeof tx.amt !== 'undefined' && typeof tx.fee !== 'undefined') { 397 | // lnd tx outgoing 398 | tx.value = parseInt((tx.amt * 1 + tx.fee * 1) * -1); 399 | } 400 | 401 | if (tx.type === 'paid_invoice') { 402 | tx.memo = tx.memo || 'Lightning payment'; 403 | if (tx.value > 0) tx.value = tx.value * -1; // value already includes fee in it (see lndhub) 404 | // outer code expects spending transactions to of negative value 405 | } 406 | 407 | if (tx.type === 'bitcoind_tx') { 408 | tx.memo = 'On-chain transaction'; 409 | } 410 | 411 | if (tx.type === 'user_invoice') { 412 | // incoming ln tx 413 | tx.value = parseInt(tx.amt); 414 | tx.memo = tx.description || 'Lightning invoice'; 415 | } 416 | 417 | tx.received = new Date(tx.timestamp * 1000).toString(); 418 | } 419 | return txs.sort(function(a, b) { 420 | return b.timestamp - a.timestamp; 421 | }); 422 | } 423 | 424 | async fetchPendingTransactions() { 425 | let response = await this._api.get('/getpending', { 426 | headers: { 427 | 'Access-Control-Allow-Origin': '*', 428 | 'Content-Type': 'application/json', 429 | Authorization: 'Bearer' + ' ' + this.access_token, 430 | }, 431 | }); 432 | 433 | let json = response.body; 434 | if (typeof json === 'undefined') { 435 | throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response)); 436 | } 437 | 438 | if (json && json.error) { 439 | throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); 440 | } 441 | 442 | this.pending_transactions_raw = json; 443 | } 444 | 445 | async fetchTransactions() { 446 | // TODO: iterate over all available pages 447 | const limit = 10; 448 | let queryRes = ''; 449 | let offset = 0; 450 | queryRes += '?limit=' + limit; 451 | queryRes += '&offset=' + offset; 452 | 453 | let response = await this._api.get('/gettxs' + queryRes, { 454 | headers: { 455 | 'Access-Control-Allow-Origin': '*', 456 | 'Content-Type': 'application/json', 457 | Authorization: 'Bearer' + ' ' + this.access_token, 458 | }, 459 | }); 460 | 461 | let json = response.body; 462 | if (typeof json === 'undefined') { 463 | throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); 464 | } 465 | 466 | if (json && json.error) { 467 | throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); 468 | } 469 | 470 | if (!Array.isArray(json)) { 471 | throw new Error('API unexpected response: ' + JSON.stringify(response.body)); 472 | } 473 | 474 | this._lastTxFetch = +new Date(); 475 | this.transactions_raw = json; 476 | } 477 | 478 | getBalance() { 479 | return this.balance; 480 | } 481 | 482 | async fetchBalance(noRetry) { 483 | await this.checkLogin(); 484 | 485 | let response = await this._api.get('/balance', { 486 | headers: { 487 | 'Access-Control-Allow-Origin': '*', 488 | 'Content-Type': 'application/json', 489 | Authorization: 'Bearer' + ' ' + this.access_token, 490 | }, 491 | }); 492 | 493 | let json = response.body; 494 | if (typeof json === 'undefined') { 495 | throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); 496 | } 497 | 498 | if (json && json.error) { 499 | if (json.code * 1 === 1 && !noRetry) { 500 | await this.authorize(); 501 | return this.fetchBalance(true); 502 | } 503 | throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); 504 | } 505 | 506 | if (!json.BTC || typeof json.BTC.AvailableBalance === 'undefined') { 507 | throw new Error('API unexpected response: ' + JSON.stringify(response.body)); 508 | } 509 | 510 | this.balance_raw = json; 511 | this.balance = json.BTC.AvailableBalance; 512 | this._lastBalanceFetch = +new Date(); 513 | } 514 | 515 | /** 516 | * Example return: 517 | * { destination: '03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f', 518 | * payment_hash: 'faf996300a468b668c58ca0702a12096475a0dd2c3dde8e812f954463966bcf4', 519 | * num_satoshis: '100', 520 | * timestamp: '1535116657', 521 | * expiry: '3600', 522 | * description: 'hundredSatoshis blitzhub', 523 | * description_hash: '', 524 | * fallback_addr: '', 525 | * cltv_expiry: '10', 526 | * route_hints: [] } 527 | * 528 | * @param invoice BOLT invoice string 529 | * @return {Promise.} 530 | */ 531 | decodeInvoice(invoice) { 532 | let { payeeNodeKey, tags, satoshis, millisatoshis, timestamp } = bolt11.decode(invoice); 533 | 534 | let decoded = { 535 | destination: payeeNodeKey, 536 | num_satoshis: satoshis ? satoshis.toString() : '0', 537 | num_millisatoshis: millisatoshis ? millisatoshis.toString() : '0', 538 | timestamp: timestamp.toString(), 539 | fallback_addr: '', 540 | route_hints: [], 541 | }; 542 | 543 | for (let i = 0; i < tags.length; i++) { 544 | let { tagName, data } = tags[i]; 545 | switch (tagName) { 546 | case 'payment_hash': 547 | decoded.payment_hash = data; 548 | break; 549 | case 'purpose_commit_hash': 550 | decoded.description_hash = data; 551 | break; 552 | case 'min_final_cltv_expiry': 553 | decoded.cltv_expiry = data.toString(); 554 | break; 555 | case 'expire_time': 556 | decoded.expiry = data.toString(); 557 | break; 558 | case 'description': 559 | decoded.description = data; 560 | break; 561 | } 562 | } 563 | 564 | if (!decoded.expiry) decoded.expiry = '3600'; // default 565 | 566 | if (parseInt(decoded.num_satoshis) === 0 && decoded.num_millisatoshis > 0) { 567 | decoded.num_satoshis = (decoded.num_millisatoshis / 1000).toString(); 568 | } 569 | 570 | return (this.decoded_invoice_raw = decoded); 571 | } 572 | 573 | async fetchInfo() { 574 | let response = await this._api.get('/getinfo', { 575 | headers: { 576 | 'Access-Control-Allow-Origin': '*', 577 | 'Content-Type': 'application/json', 578 | Authorization: 'Bearer' + ' ' + this.access_token, 579 | }, 580 | }); 581 | 582 | let json = response.body; 583 | if (typeof json === 'undefined') { 584 | throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); 585 | } 586 | 587 | if (json && json.error) { 588 | throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); 589 | } 590 | 591 | if (!json.identity_pubkey) { 592 | throw new Error('API unexpected response: ' + JSON.stringify(response.body)); 593 | } 594 | this.info_raw = json; 595 | } 596 | 597 | static async isValidNodeAddress(address) { 598 | let apiCall = new Frisbee({ 599 | baseURI: address, 600 | }); 601 | let response = await apiCall.get('/getinfo', { 602 | headers: { 603 | 'Access-Control-Allow-Origin': '*', 604 | 'Content-Type': 'application/json', 605 | }, 606 | }); 607 | let json = response.body; 608 | if (typeof json === 'undefined') { 609 | throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); 610 | } 611 | 612 | if (json && json.code && json.code !== 1) { 613 | throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); 614 | } 615 | return true; 616 | } 617 | 618 | allowReceive() { 619 | return true; 620 | } 621 | 622 | /** 623 | * Example return: 624 | * { destination: '03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f', 625 | * payment_hash: 'faf996300a468b668c58ca0702a12096475a0dd2c3dde8e812f954463966bcf4', 626 | * num_satoshis: '100', 627 | * timestamp: '1535116657', 628 | * expiry: '3600', 629 | * description: 'hundredSatoshis blitzhub', 630 | * description_hash: '', 631 | * fallback_addr: '', 632 | * cltv_expiry: '10', 633 | * route_hints: [] } 634 | * 635 | * @param invoice BOLT invoice string 636 | * @return {Promise.} 637 | */ 638 | async decodeInvoiceRemote(invoice) { 639 | await this.checkLogin(); 640 | 641 | let response = await this._api.get('/decodeinvoice?invoice=' + invoice, { 642 | headers: { 643 | 'Access-Control-Allow-Origin': '*', 644 | 'Content-Type': 'application/json', 645 | Authorization: 'Bearer' + ' ' + this.access_token, 646 | }, 647 | }); 648 | 649 | let json = response.body; 650 | if (typeof json === 'undefined') { 651 | throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); 652 | } 653 | 654 | if (json && json.error) { 655 | throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); 656 | } 657 | 658 | if (!json.payment_hash) { 659 | throw new Error('API unexpected response: ' + JSON.stringify(response.body)); 660 | } 661 | 662 | return (this.decoded_invoice_raw = json); 663 | } 664 | } 665 | 666 | /* 667 | 668 | 669 | 670 | pending tx: 671 | 672 | [ { amount: 0.00078061, 673 | account: '521172', 674 | address: '3F9seBGCJZQ4WJJHwGhrxeGXCGbrm5SNpF', 675 | category: 'receive', 676 | confirmations: 0, 677 | blockhash: '', 678 | blockindex: 0, 679 | blocktime: 0, 680 | txid: '28a74277e47c2d772ee8a40464209c90dce084f3b5de38a2f41b14c79e3bfc62', 681 | walletconflicts: [], 682 | time: 1535024434, 683 | timereceived: 1535024434 } ] 684 | 685 | 686 | tx: 687 | 688 | [ { amount: 0.00078061, 689 | account: '521172', 690 | address: '3F9seBGCJZQ4WJJHwGhrxeGXCGbrm5SNpF', 691 | category: 'receive', 692 | confirmations: 5, 693 | blockhash: '0000000000000000000edf18e9ece18e449c6d8eed1f729946b3531c32ee9f57', 694 | blockindex: 693, 695 | blocktime: 1535024914, 696 | txid: '28a74277e47c2d772ee8a40464209c90dce084f3b5de38a2f41b14c79e3bfc62', 697 | walletconflicts: [], 698 | time: 1535024434, 699 | timereceived: 1535024434 } ] 700 | 701 | */ 702 | -------------------------------------------------------------------------------- /abstract-hd-electrum-wallet.js: -------------------------------------------------------------------------------- 1 | import { NativeModules } from 'react-native'; 2 | import bip39 from 'bip39'; 3 | import BigNumber from 'bignumber.js'; 4 | import b58 from 'bs58check'; 5 | import { AbstractHDWallet } from './abstract-hd-wallet'; 6 | const bitcoin = require('bitcoinjs-lib'); 7 | const BlueElectrum = require('../BlueElectrum'); 8 | const HDNode = require('bip32'); 9 | const coinSelectAccumulative = require('coinselect/accumulative'); 10 | const coinSelectSplit = require('coinselect/split'); 11 | 12 | const { RNRandomBytes } = NativeModules; 13 | 14 | export class AbstractHDElectrumWallet extends AbstractHDWallet { 15 | static type = 'abstract'; 16 | static typeReadable = 'abstract'; 17 | static defaultRBFSequence = 2147483648; // 1 << 31, minimum for replaceable transactions as per BIP68 18 | static finalRBFSequence = 4294967295; // 0xFFFFFFFF 19 | 20 | constructor() { 21 | super(); 22 | this._balances_by_external_index = {}; // 0 => { c: 0, u: 0 } // confirmed/unconfirmed 23 | this._balances_by_internal_index = {}; 24 | 25 | this._txs_by_external_index = {}; 26 | this._txs_by_internal_index = {}; 27 | 28 | this._utxo = []; 29 | } 30 | 31 | /** 32 | * @inheritDoc 33 | */ 34 | getBalance() { 35 | let ret = 0; 36 | for (let bal of Object.values(this._balances_by_external_index)) { 37 | ret += bal.c; 38 | } 39 | for (let bal of Object.values(this._balances_by_internal_index)) { 40 | ret += bal.c; 41 | } 42 | return ret + (this.getUnconfirmedBalance() < 0 ? this.getUnconfirmedBalance() : 0); 43 | } 44 | 45 | /** 46 | * @inheritDoc 47 | */ 48 | timeToRefreshTransaction() { 49 | for (let tx of this.getTransactions()) { 50 | if (tx.confirmations < 7) return true; 51 | } 52 | return false; 53 | } 54 | 55 | getUnconfirmedBalance() { 56 | let ret = 0; 57 | for (let bal of Object.values(this._balances_by_external_index)) { 58 | ret += bal.u; 59 | } 60 | for (let bal of Object.values(this._balances_by_internal_index)) { 61 | ret += bal.u; 62 | } 63 | return ret; 64 | } 65 | 66 | async generate() { 67 | let that = this; 68 | return new Promise(function(resolve) { 69 | if (typeof RNRandomBytes === 'undefined') { 70 | // CLI/CI environment 71 | // crypto should be provided globally by test launcher 72 | return crypto.randomBytes(32, (err, buf) => { // eslint-disable-line 73 | if (err) throw err; 74 | that.secret = bip39.entropyToMnemonic(buf.toString('hex')); 75 | resolve(); 76 | }); 77 | } 78 | 79 | // RN environment 80 | RNRandomBytes.randomBytes(32, (err, bytes) => { 81 | if (err) throw new Error(err); 82 | let b = Buffer.from(bytes, 'base64').toString('hex'); 83 | that.secret = bip39.entropyToMnemonic(b); 84 | resolve(); 85 | }); 86 | }); 87 | } 88 | 89 | _getExternalWIFByIndex(index) { 90 | return this._getWIFByIndex(false, index); 91 | } 92 | 93 | _getInternalWIFByIndex(index) { 94 | return this._getWIFByIndex(true, index); 95 | } 96 | 97 | /** 98 | * Get internal/external WIF by wallet index 99 | * @param {Boolean} internal 100 | * @param {Number} index 101 | * @returns {string|false} Either string WIF or FALSE if error happened 102 | * @private 103 | */ 104 | _getWIFByIndex(internal, index) { 105 | if (!this.secret) return false; 106 | const mnemonic = this.secret; 107 | const seed = bip39.mnemonicToSeed(mnemonic); 108 | const root = HDNode.fromSeed(seed); 109 | const path = `m/84'/0'/0'/${internal ? 1 : 0}/${index}`; 110 | const child = root.derivePath(path); 111 | 112 | return child.toWIF(); 113 | } 114 | 115 | _getNodeAddressByIndex(node, index) { 116 | index = index * 1; // cast to int 117 | if (node === 0) { 118 | if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit 119 | } 120 | 121 | if (node === 1) { 122 | if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit 123 | } 124 | 125 | if (node === 0 && !this._node0) { 126 | const xpub = this.constructor._zpubToXpub(this.getXpub()); 127 | const hdNode = HDNode.fromBase58(xpub); 128 | this._node0 = hdNode.derive(node); 129 | } 130 | 131 | if (node === 1 && !this._node1) { 132 | const xpub = this.constructor._zpubToXpub(this.getXpub()); 133 | const hdNode = HDNode.fromBase58(xpub); 134 | this._node1 = hdNode.derive(node); 135 | } 136 | 137 | let address; 138 | if (node === 0) { 139 | address = this.constructor._nodeToBech32SegwitAddress(this._node0.derive(index)); 140 | } 141 | 142 | if (node === 1) { 143 | address = this.constructor._nodeToBech32SegwitAddress(this._node1.derive(index)); 144 | } 145 | 146 | if (node === 0) { 147 | return (this.external_addresses_cache[index] = address); 148 | } 149 | 150 | if (node === 1) { 151 | return (this.internal_addresses_cache[index] = address); 152 | } 153 | } 154 | 155 | _getNodePubkeyByIndex(node, index) { 156 | index = index * 1; // cast to int 157 | 158 | if (node === 0 && !this._node0) { 159 | const xpub = this.constructor._zpubToXpub(this.getXpub()); 160 | const hdNode = HDNode.fromBase58(xpub); 161 | this._node0 = hdNode.derive(node); 162 | } 163 | 164 | if (node === 1 && !this._node1) { 165 | const xpub = this.constructor._zpubToXpub(this.getXpub()); 166 | const hdNode = HDNode.fromBase58(xpub); 167 | this._node1 = hdNode.derive(node); 168 | } 169 | 170 | if (node === 0) { 171 | return this._node0.derive(index).publicKey; 172 | } 173 | 174 | if (node === 1) { 175 | return this._node1.derive(index).publicKey; 176 | } 177 | } 178 | 179 | _getExternalAddressByIndex(index) { 180 | return this._getNodeAddressByIndex(0, index); 181 | } 182 | 183 | _getInternalAddressByIndex(index) { 184 | return this._getNodeAddressByIndex(1, index); 185 | } 186 | 187 | /** 188 | * Returning zpub actually, not xpub. Keeping same method name 189 | * for compatibility. 190 | * 191 | * @return {String} zpub 192 | */ 193 | getXpub() { 194 | if (this._xpub) { 195 | return this._xpub; // cache hit 196 | } 197 | // first, getting xpub 198 | const mnemonic = this.secret; 199 | const seed = bip39.mnemonicToSeed(mnemonic); 200 | const root = HDNode.fromSeed(seed); 201 | 202 | const path = "m/84'/0'/0'"; 203 | const child = root.derivePath(path).neutered(); 204 | const xpub = child.toBase58(); 205 | 206 | // bitcoinjs does not support zpub yet, so we just convert it from xpub 207 | let data = b58.decode(xpub); 208 | data = data.slice(4); 209 | data = Buffer.concat([Buffer.from('04b24746', 'hex'), data]); 210 | this._xpub = b58.encode(data); 211 | 212 | return this._xpub; 213 | } 214 | 215 | /** 216 | * @inheritDoc 217 | */ 218 | async fetchTransactions() { 219 | // if txs are absent for some internal address in hierarchy - this is a sign 220 | // we should fetch txs for that address 221 | // OR if some address has unconfirmed balance - should fetch it's txs 222 | // OR some tx for address is unconfirmed 223 | // OR some tx has < 7 confirmations 224 | 225 | // fetching transactions in batch: first, getting batch history for all addresses, 226 | // then batch fetching all involved txids 227 | // finally, batch fetching txids of all inputs (needed to see amounts & addresses of those inputs) 228 | // then we combine it all together 229 | 230 | let addresses2fetch = []; 231 | 232 | for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { 233 | // external addresses first 234 | let hasUnconfirmed = false; 235 | this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; 236 | for (let tx of this._txs_by_external_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7; 237 | 238 | if (hasUnconfirmed || this._txs_by_external_index[c].length === 0 || this._balances_by_external_index[c].u !== 0) { 239 | addresses2fetch.push(this._getExternalAddressByIndex(c)); 240 | } 241 | } 242 | 243 | for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { 244 | // next, internal addresses 245 | let hasUnconfirmed = false; 246 | this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; 247 | for (let tx of this._txs_by_internal_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7; 248 | 249 | if (hasUnconfirmed || this._txs_by_internal_index[c].length === 0 || this._balances_by_internal_index[c].u !== 0) { 250 | addresses2fetch.push(this._getInternalAddressByIndex(c)); 251 | } 252 | } 253 | 254 | // first: batch fetch for all addresses histories 255 | let histories = await BlueElectrum.multiGetHistoryByAddress(addresses2fetch); 256 | let txs = {}; 257 | for (let history of Object.values(histories)) { 258 | for (let tx of history) { 259 | txs[tx.tx_hash] = tx; 260 | } 261 | } 262 | 263 | // next, batch fetching each txid we got 264 | let txdatas = await BlueElectrum.multiGetTransactionByTxid(Object.keys(txs)); 265 | 266 | // now, tricky part. we collect all transactions from inputs (vin), and batch fetch them too. 267 | // then we combine all this data (we need inputs to see source addresses and amounts) 268 | let vinTxids = []; 269 | for (let txdata of Object.values(txdatas)) { 270 | for (let vin of txdata.vin) { 271 | vinTxids.push(vin.txid); 272 | } 273 | } 274 | let vintxdatas = await BlueElectrum.multiGetTransactionByTxid(vinTxids); 275 | 276 | // fetched all transactions from our inputs. now we need to combine it. 277 | // iterating all _our_ transactions: 278 | for (let txid of Object.keys(txdatas)) { 279 | // iterating all inputs our our single transaction: 280 | for (let inpNum = 0; inpNum < txdatas[txid].vin.length; inpNum++) { 281 | let inpTxid = txdatas[txid].vin[inpNum].txid; 282 | let inpVout = txdatas[txid].vin[inpNum].vout; 283 | // got txid and output number of _previous_ transaction we shoud look into 284 | if (vintxdatas[inpTxid] && vintxdatas[inpTxid].vout[inpVout]) { 285 | // extracting amount & addresses from previous output and adding it to _our_ input: 286 | txdatas[txid].vin[inpNum].addresses = vintxdatas[inpTxid].vout[inpVout].scriptPubKey.addresses; 287 | txdatas[txid].vin[inpNum].value = vintxdatas[inpTxid].vout[inpVout].value; 288 | } 289 | } 290 | } 291 | 292 | // now purge all unconfirmed txs from internal hashmaps, since some may be evicted from mempool because they became invalid 293 | // or replaced. hashmaps are going to be re-populated anyways, since we fetched TXs for addresses with unconfirmed TXs 294 | for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { 295 | this._txs_by_external_index[c] = this._txs_by_external_index[c].filter(tx => !!tx.confirmations); 296 | } 297 | for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { 298 | this._txs_by_internal_index[c] = this._txs_by_internal_index[c].filter(tx => !!tx.confirmations); 299 | } 300 | 301 | // now, we need to put transactions in all relevant `cells` of internal hashmaps: this._txs_by_internal_index && this._txs_by_external_index 302 | 303 | for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { 304 | for (let tx of Object.values(txdatas)) { 305 | for (let vin of tx.vin) { 306 | if (vin.addresses && vin.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) { 307 | // this TX is related to our address 308 | this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; 309 | let clonedTx = Object.assign({}, tx); 310 | clonedTx.inputs = tx.vin.slice(0); 311 | clonedTx.outputs = tx.vout.slice(0); 312 | delete clonedTx.vin; 313 | delete clonedTx.vout; 314 | 315 | // trying to replace tx if it exists already (because it has lower confirmations, for example) 316 | let replaced = false; 317 | for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) { 318 | if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) { 319 | replaced = true; 320 | this._txs_by_external_index[c][cc] = clonedTx; 321 | } 322 | } 323 | if (!replaced) this._txs_by_external_index[c].push(clonedTx); 324 | } 325 | } 326 | for (let vout of tx.vout) { 327 | if (vout.scriptPubKey.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) { 328 | // this TX is related to our address 329 | this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; 330 | let clonedTx = Object.assign({}, tx); 331 | clonedTx.inputs = tx.vin.slice(0); 332 | clonedTx.outputs = tx.vout.slice(0); 333 | delete clonedTx.vin; 334 | delete clonedTx.vout; 335 | 336 | // trying to replace tx if it exists already (because it has lower confirmations, for example) 337 | let replaced = false; 338 | for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) { 339 | if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) { 340 | replaced = true; 341 | this._txs_by_external_index[c][cc] = clonedTx; 342 | } 343 | } 344 | if (!replaced) this._txs_by_external_index[c].push(clonedTx); 345 | } 346 | } 347 | } 348 | } 349 | 350 | for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { 351 | for (let tx of Object.values(txdatas)) { 352 | for (let vin of tx.vin) { 353 | if (vin.addresses && vin.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) { 354 | // this TX is related to our address 355 | this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; 356 | let clonedTx = Object.assign({}, tx); 357 | clonedTx.inputs = tx.vin.slice(0); 358 | clonedTx.outputs = tx.vout.slice(0); 359 | delete clonedTx.vin; 360 | delete clonedTx.vout; 361 | 362 | // trying to replace tx if it exists already (because it has lower confirmations, for example) 363 | let replaced = false; 364 | for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) { 365 | if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) { 366 | replaced = true; 367 | this._txs_by_internal_index[c][cc] = clonedTx; 368 | } 369 | } 370 | if (!replaced) this._txs_by_internal_index[c].push(clonedTx); 371 | } 372 | } 373 | for (let vout of tx.vout) { 374 | if (vout.scriptPubKey.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) { 375 | // this TX is related to our address 376 | this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; 377 | let clonedTx = Object.assign({}, tx); 378 | clonedTx.inputs = tx.vin.slice(0); 379 | clonedTx.outputs = tx.vout.slice(0); 380 | delete clonedTx.vin; 381 | delete clonedTx.vout; 382 | 383 | // trying to replace tx if it exists already (because it has lower confirmations, for example) 384 | let replaced = false; 385 | for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) { 386 | if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) { 387 | replaced = true; 388 | this._txs_by_internal_index[c][cc] = clonedTx; 389 | } 390 | } 391 | if (!replaced) this._txs_by_internal_index[c].push(clonedTx); 392 | } 393 | } 394 | } 395 | } 396 | 397 | this._lastTxFetch = +new Date(); 398 | } 399 | 400 | getTransactions() { 401 | let txs = []; 402 | 403 | for (let addressTxs of Object.values(this._txs_by_external_index)) { 404 | txs = txs.concat(addressTxs); 405 | } 406 | for (let addressTxs of Object.values(this._txs_by_internal_index)) { 407 | txs = txs.concat(addressTxs); 408 | } 409 | 410 | let ret = []; 411 | for (let tx of txs) { 412 | tx.received = tx.blocktime * 1000; 413 | if (!tx.blocktime) tx.received = +new Date() - 30 * 1000; // unconfirmed 414 | tx.confirmations = tx.confirmations || 0; // unconfirmed 415 | tx.hash = tx.txid; 416 | tx.value = 0; 417 | 418 | for (let vin of tx.inputs) { 419 | // if input (spending) goes from our address - we are loosing! 420 | if ((vin.address && this.weOwnAddress(vin.address)) || (vin.addresses && vin.addresses[0] && this.weOwnAddress(vin.addresses[0]))) { 421 | tx.value -= new BigNumber(vin.value).multipliedBy(100000000).toNumber(); 422 | } 423 | } 424 | 425 | for (let vout of tx.outputs) { 426 | // when output goes to our address - this means we are gaining! 427 | if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses[0] && this.weOwnAddress(vout.scriptPubKey.addresses[0])) { 428 | tx.value += new BigNumber(vout.value).multipliedBy(100000000).toNumber(); 429 | } 430 | } 431 | ret.push(tx); 432 | } 433 | 434 | // now, deduplication: 435 | let usedTxIds = {}; 436 | let ret2 = []; 437 | for (let tx of ret) { 438 | if (!usedTxIds[tx.txid]) ret2.push(tx); 439 | usedTxIds[tx.txid] = 1; 440 | } 441 | 442 | return ret2.sort(function(a, b) { 443 | return b.received - a.received; 444 | }); 445 | } 446 | 447 | async _binarySearchIterationForInternalAddress(index) { 448 | const gerenateChunkAddresses = chunkNum => { 449 | let ret = []; 450 | for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) { 451 | ret.push(this._getInternalAddressByIndex(c)); 452 | } 453 | return ret; 454 | }; 455 | 456 | let lastChunkWithUsedAddressesNum = null; 457 | let lastHistoriesWithUsedAddresses = null; 458 | for (let c = 0; c < Math.round(index / this.gap_limit); c++) { 459 | let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c)); 460 | if (this.constructor._getTransactionsFromHistories(histories).length > 0) { 461 | // in this particular chunk we have used addresses 462 | lastChunkWithUsedAddressesNum = c; 463 | lastHistoriesWithUsedAddresses = histories; 464 | } else { 465 | // empty chunk. no sense searching more chunks 466 | break; 467 | } 468 | } 469 | 470 | let lastUsedIndex = 0; 471 | 472 | if (lastHistoriesWithUsedAddresses) { 473 | // now searching for last used address in batch lastChunkWithUsedAddressesNum 474 | for ( 475 | let c = lastChunkWithUsedAddressesNum * this.gap_limit; 476 | c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit; 477 | c++ 478 | ) { 479 | let address = this._getInternalAddressByIndex(c); 480 | if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) { 481 | lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued 482 | } 483 | } 484 | } 485 | 486 | return lastUsedIndex; 487 | } 488 | 489 | async _binarySearchIterationForExternalAddress(index) { 490 | const gerenateChunkAddresses = chunkNum => { 491 | let ret = []; 492 | for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) { 493 | ret.push(this._getExternalAddressByIndex(c)); 494 | } 495 | return ret; 496 | }; 497 | 498 | let lastChunkWithUsedAddressesNum = null; 499 | let lastHistoriesWithUsedAddresses = null; 500 | for (let c = 0; c < Math.round(index / this.gap_limit); c++) { 501 | let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c)); 502 | if (this.constructor._getTransactionsFromHistories(histories).length > 0) { 503 | // in this particular chunk we have used addresses 504 | lastChunkWithUsedAddressesNum = c; 505 | lastHistoriesWithUsedAddresses = histories; 506 | } else { 507 | // empty chunk. no sense searching more chunks 508 | break; 509 | } 510 | } 511 | 512 | let lastUsedIndex = 0; 513 | 514 | if (lastHistoriesWithUsedAddresses) { 515 | // now searching for last used address in batch lastChunkWithUsedAddressesNum 516 | for ( 517 | let c = lastChunkWithUsedAddressesNum * this.gap_limit; 518 | c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit; 519 | c++ 520 | ) { 521 | let address = this._getExternalAddressByIndex(c); 522 | if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) { 523 | lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued 524 | } 525 | } 526 | } 527 | 528 | return lastUsedIndex; 529 | } 530 | 531 | async fetchBalance() { 532 | try { 533 | if (this.next_free_change_address_index === 0 && this.next_free_address_index === 0) { 534 | // doing binary search for last used address: 535 | this.next_free_change_address_index = await this._binarySearchIterationForInternalAddress(1000); 536 | this.next_free_address_index = await this._binarySearchIterationForExternalAddress(1000); 537 | } // end rescanning fresh wallet 538 | 539 | // finally fetching balance 540 | await this._fetchBalance(); 541 | } catch (err) { 542 | console.warn(err); 543 | } 544 | } 545 | 546 | async _fetchBalance() { 547 | // probing future addressess in hierarchy whether they have any transactions, in case 548 | // our 'next free addr' pointers are lagging behind 549 | let tryAgain = false; 550 | let txs = await BlueElectrum.getTransactionsByAddress( 551 | this._getExternalAddressByIndex(this.next_free_address_index + this.gap_limit - 1), 552 | ); 553 | if (txs.length > 0) { 554 | // whoa, someone uses our wallet outside! better catch up 555 | this.next_free_address_index += this.gap_limit; 556 | tryAgain = true; 557 | } 558 | 559 | txs = await BlueElectrum.getTransactionsByAddress( 560 | this._getInternalAddressByIndex(this.next_free_change_address_index + this.gap_limit - 1), 561 | ); 562 | if (txs.length > 0) { 563 | this.next_free_change_address_index += this.gap_limit; 564 | tryAgain = true; 565 | } 566 | 567 | // FIXME: refactor me ^^^ can be batched in single call. plus not just couple of addresses, but all between [ next_free .. (next_free + gap_limit) ] 568 | 569 | if (tryAgain) return this._fetchBalance(); 570 | 571 | // next, business as usuall. fetch balances 572 | 573 | let addresses2fetch = []; 574 | 575 | // generating all involved addresses. 576 | // basically, refetch all from index zero to maximum. doesnt matter 577 | // since we batch them 100 per call 578 | 579 | // external 580 | for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { 581 | addresses2fetch.push(this._getExternalAddressByIndex(c)); 582 | } 583 | 584 | // internal 585 | for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { 586 | addresses2fetch.push(this._getInternalAddressByIndex(c)); 587 | } 588 | 589 | let balances = await BlueElectrum.multiGetBalanceByAddress(addresses2fetch); 590 | 591 | // converting to a more compact internal format 592 | for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { 593 | let addr = this._getExternalAddressByIndex(c); 594 | if (balances.addresses[addr]) { 595 | // first, if balances differ from what we store - we delete transactions for that 596 | // address so next fetchTransactions() will refetch everything 597 | if (this._balances_by_external_index[c]) { 598 | if ( 599 | this._balances_by_external_index[c].c !== balances.addresses[addr].confirmed || 600 | this._balances_by_external_index[c].u !== balances.addresses[addr].unconfirmed 601 | ) { 602 | delete this._txs_by_external_index[c]; 603 | } 604 | } 605 | // update local representation of balances on that address: 606 | this._balances_by_external_index[c] = { 607 | c: balances.addresses[addr].confirmed, 608 | u: balances.addresses[addr].unconfirmed, 609 | }; 610 | } 611 | } 612 | for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { 613 | let addr = this._getInternalAddressByIndex(c); 614 | if (balances.addresses[addr]) { 615 | // first, if balances differ from what we store - we delete transactions for that 616 | // address so next fetchTransactions() will refetch everything 617 | if (this._balances_by_internal_index[c]) { 618 | if ( 619 | this._balances_by_internal_index[c].c !== balances.addresses[addr].confirmed || 620 | this._balances_by_internal_index[c].u !== balances.addresses[addr].unconfirmed 621 | ) { 622 | delete this._txs_by_internal_index[c]; 623 | } 624 | } 625 | // update local representation of balances on that address: 626 | this._balances_by_internal_index[c] = { 627 | c: balances.addresses[addr].confirmed, 628 | u: balances.addresses[addr].unconfirmed, 629 | }; 630 | } 631 | } 632 | 633 | this._lastBalanceFetch = +new Date(); 634 | } 635 | 636 | async fetchUtxo() { 637 | // considering only confirmed balance 638 | let addressess = []; 639 | 640 | for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { 641 | if (this._balances_by_external_index[c] && this._balances_by_external_index[c].c && this._balances_by_external_index[c].c > 0) { 642 | addressess.push(this._getExternalAddressByIndex(c)); 643 | } 644 | } 645 | 646 | for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { 647 | if (this._balances_by_internal_index[c] && this._balances_by_internal_index[c].c && this._balances_by_internal_index[c].c > 0) { 648 | addressess.push(this._getInternalAddressByIndex(c)); 649 | } 650 | } 651 | 652 | this._utxo = []; 653 | for (let arr of Object.values(await BlueElectrum.multiGetUtxoByAddress(addressess))) { 654 | this._utxo = this._utxo.concat(arr); 655 | } 656 | 657 | // backward compatibility TODO: remove when we make sure `.utxo` is not used 658 | this.utxo = this._utxo; 659 | // this belongs in `.getUtxo()` 660 | for (let u of this.utxo) { 661 | u.txid = u.txId; 662 | u.amount = u.value; 663 | u.wif = this._getWifForAddress(u.address); 664 | u.confirmations = u.height ? 1 : 0; 665 | } 666 | } 667 | 668 | getUtxo() { 669 | return this._utxo; 670 | } 671 | 672 | _getDerivationPathByAddress(address) { 673 | const path = "m/84'/0'/0'"; 674 | for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { 675 | if (this._getExternalAddressByIndex(c) === address) return path + '/0/' + c; 676 | } 677 | for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { 678 | if (this._getInternalAddressByIndex(c) === address) return path + '/1/' + c; 679 | } 680 | 681 | return false; 682 | } 683 | 684 | _getPubkeyByAddress(address) { 685 | for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { 686 | if (this._getExternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(0, c); 687 | } 688 | for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { 689 | if (this._getInternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(1, c); 690 | } 691 | 692 | return false; 693 | } 694 | 695 | weOwnAddress(address) { 696 | for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { 697 | if (this._getExternalAddressByIndex(c) === address) return true; 698 | } 699 | for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { 700 | if (this._getInternalAddressByIndex(c) === address) return true; 701 | } 702 | return false; 703 | } 704 | 705 | /** 706 | * @deprecated 707 | */ 708 | createTx(utxos, amount, fee, address) { 709 | throw new Error('Deprecated'); 710 | } 711 | 712 | /** 713 | * 714 | * @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String}>} List of spendable utxos 715 | * @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate) 716 | * @param feeRate {Number} satoshi per byte 717 | * @param changeAddress {String} Excessive coins will go back to that address 718 | * @param sequence {Number} Used in RBF 719 | * @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case 720 | * @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}} 721 | */ 722 | createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false) { 723 | if (!changeAddress) throw new Error('No change address provided'); 724 | sequence = sequence || AbstractHDElectrumWallet.defaultRBFSequence; 725 | 726 | let algo = coinSelectAccumulative; 727 | if (targets.length === 1 && targets[0] && !targets[0].value) { 728 | // we want to send MAX 729 | algo = coinSelectSplit; 730 | } 731 | 732 | let { inputs, outputs, fee } = algo(utxos, targets, feeRate); 733 | 734 | // .inputs and .outputs will be undefined if no solution was found 735 | if (!inputs || !outputs) { 736 | throw new Error('Not enough balance. Try sending smaller amount'); 737 | } 738 | 739 | let psbt = new bitcoin.Psbt(); 740 | 741 | let c = 0; 742 | let keypairs = {}; 743 | let values = {}; 744 | 745 | inputs.forEach(input => { 746 | let keyPair; 747 | if (!skipSigning) { 748 | // skiping signing related stuff 749 | keyPair = bitcoin.ECPair.fromWIF(this._getWifForAddress(input.address)); 750 | keypairs[c] = keyPair; 751 | } 752 | values[c] = input.value; 753 | c++; 754 | if (!skipSigning) { 755 | // skiping signing related stuff 756 | if (!input.address || !this._getWifForAddress(input.address)) throw new Error('Internal error: no address or WIF to sign input'); 757 | } 758 | let pubkey = this._getPubkeyByAddress(input.address); 759 | let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]); 760 | // this is not correct fingerprint, as we dont know real fingerprint - we got zpub with 84/0, but fingerpting 761 | // should be from root. basically, fingerprint should be provided from outside by user when importing zpub 762 | let path = this._getDerivationPathByAddress(input.address); 763 | const p2wpkh = bitcoin.payments.p2wpkh({ pubkey }); 764 | psbt.addInput({ 765 | hash: input.txId, 766 | index: input.vout, 767 | sequence, 768 | bip32Derivation: [ 769 | { 770 | masterFingerprint, 771 | path, 772 | pubkey, 773 | }, 774 | ], 775 | witnessUtxo: { 776 | script: p2wpkh.output, 777 | value: input.value, 778 | }, 779 | }); 780 | }); 781 | 782 | outputs.forEach(output => { 783 | // if output has no address - this is change output 784 | let change = false; 785 | if (!output.address) { 786 | change = true; 787 | output.address = changeAddress; 788 | } 789 | 790 | let path = this._getDerivationPathByAddress(output.address); 791 | let pubkey = this._getPubkeyByAddress(output.address); 792 | let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]); 793 | // this is not correct fingerprint, as we dont know realfingerprint - we got zpub with 84/0, but fingerpting 794 | // should be from root. basically, fingerprint should be provided from outside by user when importing zpub 795 | 796 | let outputData = { 797 | address: output.address, 798 | value: output.value, 799 | }; 800 | 801 | if (change) { 802 | outputData['bip32Derivation'] = [ 803 | { 804 | masterFingerprint, 805 | path, 806 | pubkey, 807 | }, 808 | ]; 809 | } 810 | 811 | psbt.addOutput(outputData); 812 | }); 813 | 814 | if (!skipSigning) { 815 | // skiping signing related stuff 816 | for (let cc = 0; cc < c; cc++) { 817 | psbt.signInput(cc, keypairs[cc]); 818 | } 819 | } 820 | 821 | let tx; 822 | if (!skipSigning) { 823 | tx = psbt.finalizeAllInputs().extractTransaction(); 824 | } 825 | return { tx, inputs, outputs, fee, psbt }; 826 | } 827 | 828 | /** 829 | * Combines 2 PSBTs into final transaction from which you can 830 | * get HEX and broadcast 831 | * 832 | * @param base64one {string} 833 | * @param base64two {string} 834 | * @returns {Transaction} 835 | */ 836 | combinePsbt(base64one, base64two) { 837 | const final1 = bitcoin.Psbt.fromBase64(base64one); 838 | const final2 = bitcoin.Psbt.fromBase64(base64two); 839 | final1.combine(final2); 840 | return final1.finalizeAllInputs().extractTransaction(); 841 | } 842 | 843 | /** 844 | * Creates Segwit Bech32 Bitcoin address 845 | * 846 | * @param hdNode 847 | * @returns {String} 848 | */ 849 | static _nodeToBech32SegwitAddress(hdNode) { 850 | return bitcoin.payments.p2wpkh({ 851 | pubkey: hdNode.publicKey, 852 | }).address; 853 | } 854 | 855 | /** 856 | * Converts zpub to xpub 857 | * 858 | * @param {String} zpub 859 | * @returns {String} xpub 860 | */ 861 | static _zpubToXpub(zpub) { 862 | let data = b58.decode(zpub); 863 | data = data.slice(4); 864 | data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]); 865 | 866 | return b58.encode(data); 867 | } 868 | 869 | static _getTransactionsFromHistories(histories) { 870 | let txs = []; 871 | for (let history of Object.values(histories)) { 872 | for (let tx of history) { 873 | txs.push(tx); 874 | } 875 | } 876 | return txs; 877 | } 878 | 879 | /** 880 | * Broadcast txhex. Can throw an exception if failed 881 | * 882 | * @param {String} txhex 883 | * @returns {Promise} 884 | */ 885 | async broadcastTx(txhex) { 886 | let broadcast = await BlueElectrum.broadcastV2(txhex); 887 | console.log({ broadcast }); 888 | if (broadcast.indexOf('successfully') !== -1) return true; 889 | return broadcast.length === 64; // this means return string is txid (precise length), so it was broadcasted ok 890 | } 891 | } 892 | --------------------------------------------------------------------------------