import { Injectable } from '@angular/core' ; import { HttpClient } from '@angular/common/http' ; import 'rxjs/add/operator/catch' ; import 'rxjs/add/operator/toPromise' ; import { Storage } from '@ionic/storage' ; import { TranslateService } from '@ngx-translate/core' ; import { AlertController , App , Events , LoadingController , Platform , ToastController } from 'ionic-angular' ; import { FingerprintAIO } from '@ionic-native/fingerprint-aio' ; import { InAppBrowser } from '@ionic-native/in-app-browser' ; import { LocalNotifications } from '@ionic-native/local-notifications' ; import { SplashScreen } from '@ionic-native/splash-screen' ; import 'rxjs/add/operator/timeout' ; import QRCode from 'qrcode' ; import * as bitcoincash from 'bsv' ; import * as bitcoincash_Mnemonic from 'bsv/mnemonic' ; import * as bitcoincash_Message from 'bsv/message' ; import * as bchaddr from 'bchaddrjs' ; import * as bs58check from 'bs58check' ; import io from 'socket.io-client' ; import * as crypto from 'crypto-browserify' ; import { Buffer } from 'buffer/' ; import * as BN from 'bn.js' ; enum EState { CLOSED = 1 , OFFLINE , CONNECTING , CONNECTED , SYNCING , SYNCED } interface IUnits { [ key ] : IUnit ; } interface IUnit { rate : number ; dp : number ; } interface IAddresses { receive : string [ ] ; change : string [ ] ; } interface IUtxo { txid : string ; vout : number ; address ? : string ; path ? : [ number , number ] ; scriptPubKey : string ; satoshis : number ; } interface ITxRecord { txid : string ; height : number ; timestamp : number ; friendlyTimestamp : number ; delta : number ; remark : string ; seen : boolean ; } interface IWallet { handle : string ; name : string ; protection : string ; keys : { encMnemonic : string ; compliant : boolean ; xpub : string ; } ; addresses : IAddresses ; cache : { receiveAddress : string ; changeAddress : string ; utxos : IUtxo [ ] ; history : ITxRecord [ ] ; } ; } interface IPreference { contacts : string [ ] ; defaultWallet : string ; showBalance : boolean ; unitIndex : number ; cryptoUnit : string ; currency : string ; addressFormat : string ; lastAnnouncement : string ; } interface IPaymentRef { paymail : string ; reference : string ; outputs ? : IOutput [ ] ; expires ? : number ; } interface IStorage { version : string ; wallets : IWallet [ ] ; preference : IPreference ; paymentRefs : IPaymentRef [ ] ; } interface IRecoveryInfo { mnemonic ? : string ; path ? : string ; passphrase ? : string ; xprv ? : string ; } interface IInput { txid : string ; vout : number ; path ? : [ number , number ] ; script : string ; satoshis : number ; } interface IOutput { path ? : [ number , number ] ; address ? : string ; script ? : string ; satoshis : number ; } interface IRequest { outputs : IOutput [ ] ; r ? : string ; label ? : string ; message ? : string ; purpose ? : string ; isPaymail : boolean ; isBIP270 : boolean ; } interface IWifInfo { address : string ; balance : number ; utxos : IUtxo [ ] ; } interface ITransaction { satoshis : number ; fee : number ; hex : string ; inputs : IInput [ ] ; outputs : IOutput [ ] ; } interface IBIP270Request { outputs : IOutput [ ] ; creationTimestamp : number ; expirationTimestamp : number ; memo : string ; paymentUrl : string ; merchantData : string ; r : string ; bip270 : boolean ; } @ Injectable ( ) export class Wallet { public readonly DUMMY_KEY : string = 'well... at least better than plain text ¯\\_(ツ)_/¯' ; public readonly WALLET_KEY : string = '_wallet' ; public readonly UNITS : IUnits = { 'BSV' : { rate : 1 , dp : 8 } , 'BITS' : { rate : 1e6 , dp : 2 } , 'SATS' : { rate : 1e8 , dp : 0 } } ; public readonly ANNOUNCEMENT_URL : string = 'https://simply.cash/announcement.json' ; public readonly WS_URL : string = 'https://ws.simply.cash:3000' ; public readonly VERSION : string = '0.0.99' ; public readonly supportedAddressFormats : ReadonlyArray < string > = [ 'legacy' , 'cashaddr' ] ; public readonly supportedProtections : ReadonlyArray < string > = [ 'OFF' , 'PIN' , 'FINGERPRINT' ] ; public isPaused : boolean = false ; public socket : any ; public socketRequestId : number = 0 ; public state : EState = EState . CLOSED ; public syncAddressesLock : Promise < void > ; public syncTaskId : number = 0 ; public pendingAddresses : string [ ] = [ ] ; public notificationId : number = 0 ; public currentWallet : IWallet ; public stored : IStorage ; getDefaultPreference ( ) { return { contacts : [ 'tips@simply.cash' ] , defaultWallet : '' , showBalance : true , unitIndex : 0 , cryptoUnit : 'BSV' , currency : 'USD' , addressFormat : 'legacy' , lastAnnouncement : '' } ; } constructor ( public alertCtrl , public app , public events , public http , public faio , public iab , public loadingCtrl , public localNotifications , public platform , public splashScreen , public storage , public toastCtrl , public translate ) { this . platform . pause . subscribe ( ( ) => { this . isPaused = true ; } ) ; this . platform . resume . subscribe ( ( ) => { this . isPaused = false ; if ( this . isOffline ( ) ) { this . showAnnouncement ( ) ; this . tryToConnectAndSync ( ) ; } } ) ; } async authorize ( ) { await this . delay ( 0 ) ; let p = this . getPreferredProtection ( ) ; if ( p === 'OFF' ) { return this . getRecoveryString ( ) ; } else if ( p === 'FINGERPRINT' ) { if ( await this . canUseFingerprint ( ) ) { return await this . authorizeFingerprint ( ) ; } else { await this . fingerprintNAPrompt ( ) ; throw new Error ( 'cancelled' ) ; } } else if ( p === 'PIN' ) { return await this . authorizePIN ( ) ; } } getSupportedProtections ( ) { return this . supportedProtections . slice ( ) ; } getPreferredProtection ( ) { if ( this . currentWallet . protection === 'password' ) { return 'PIN' ; } if ( this . currentWallet . protection === 'fingerprint' ) { return 'FINGERPRINT' ; } return 'OFF' ; } async setPreferredProtection ( p , m ) { if ( p === 'PIN' ) { let pw = await this . newPIN ( ) ; let encrypted = this . _encryptText ( m , pw ) ; this . currentWallet . keys . encMnemonic = encrypted ; this . currentWallet . protection = 'password' ; } else if ( p === 'FINGERPRINT' ) { if ( ! await this . canUseFingerprint ( ) ) { await this . fingerprintNAPrompt ( ) ; throw new Error ( 'auth unavailable' ) ; } let encrypted = this . _encryptText ( m , this . DUMMY_KEY ) ; this . currentWallet . keys . encMnemonic = encrypted ; this . currentWallet . protection = 'fingerprint' ; } else if ( p === 'OFF' ) { let encrypted = this . _encryptText ( m , this . DUMMY_KEY ) ; this . currentWallet . keys . encMnemonic = encrypted ; this . currentWallet . protection = 'off' ; } await this . updateStorage ( ) ; } newPIN ( ) { return new Promise < string > ( ( resolve , reject ) => { let pinAlert = this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'PIN' ) , inputs : [ { name : 'pin1' , type : 'password' , placeholder : this . translate . instant ( 'ENTER_PIN' ) } , { name : 'pin2' , type : 'password' , placeholder : this . translate . instant ( 'CONFIRM_PIN' ) } ] , buttons : [ { text : this . translate . instant ( 'CANCEL' ) , handler : data => { pinAlert . dismiss ( ) . then ( ( ) => { reject ( new Error ( 'cancelled' ) ) ; } ) ; return false ; } } , { text : this . translate . instant ( 'OK' ) , handler : data => { if ( data . pin1 . length > 0 && data . pin1 === data . pin2 ) { pinAlert . dismiss ( ) . then ( ( ) => { resolve ( data . pin1 ) ; } ) ; } else { this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'ERROR' ) , message : this . translate . instant ( 'ERR_INCORRECT_PIN' ) , buttons : [ this . translate . instant ( 'OK' ) ] } ) . present ( ) ; } return false ; } } ] } ) ; pinAlert . present ( ) ; } ) ; } authorizePIN ( ) { return new Promise < string > ( ( resolve , reject ) => { let pinAlert = this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'PIN' ) , inputs : [ { name : 'pin' , type : 'password' , placeholder : this . translate . instant ( 'ENTER_PIN' ) } ] , buttons : [ { text : this . translate . instant ( 'CANCEL' ) , handler : data => { pinAlert . dismiss ( ) . then ( ( ) => { reject ( new Error ( 'cancelled' ) ) ; } ) ; return false ; } } , { text : this . translate . instant ( 'OK' ) , handler : data => { let m ; try { m = this . getRecoveryString ( data . pin ) ; pinAlert . dismiss ( ) . then ( ( ) => { resolve ( m ) ; } ) ; } catch ( err ) { this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'ERROR' ) , message : this . translate . instant ( 'ERR_INCORRECT_PIN' ) , buttons : [ this . translate . instant ( 'OK' ) ] } ) . present ( ) ; } return false ; } } ] } ) ; pinAlert . present ( ) ; } ) ; } async canUseFingerprint ( ) { try { if ( this . platform . is ( 'cordova' ) && ( await this . faio . isAvailable ( ) ) . match ( /^(finger|face)$/gi ) ) { return true ; } else { return false ; } } catch ( err ) { return false ; } } fingerprintNAPrompt ( ) { return new Promise < void > ( ( resolve , reject ) => { let naAlert = this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'ERROR' ) , message : this . translate . instant ( 'ERR_AUTH_UNAVAILABLE' ) , buttons : [ { text : this . translate . instant ( 'OK' ) , handler : data => { naAlert . dismiss ( ) . then ( ( ) => { resolve ( ) ; } ) ; return false ; } } ] } ) ; naAlert . present ( ) ; } ) ; } authorizeFingerprint ( ) { return this . faio . show ( { clientId : 'wallet owner' , clientSecret : 'cash.simply.wallet.dummy.password' , disableBackup : false , localizedFallbackTitle : 'PIN' , } ) . then ( ( ) => { return this . getRecoveryString ( ) ; } ) . catch ( ( err ) => { throw new Error ( 'cancelled' ) ; } ) ; } getShowBalance ( ) { return this . stored . preference . showBalance ; } async toggleShowBalance ( ) { this . stored . preference . showBalance = ! this . stored . preference . showBalance ; await this . updateStorage ( ) ; } getSupportedCryptoUnits ( ) { return Object . keys ( this . UNITS ) . slice ( 0 , 3 ) ; } getSupportedCurrencies ( ) { return Object . keys ( this . UNITS ) . slice ( 3 ) ; } getPreferredCryptoUnit ( ) { return this . stored . preference . cryptoUnit ; } async setPreferredCryptoUnit ( sym ) { this . stored . preference . cryptoUnit = sym ; this . events . publish ( 'wallet:preferredcryptounit' , sym ) ; this . events . publish ( 'wallet:preferredunit' , this . getPreferredUnit ( ) ) ; await this . updateStorage ( ) ; } getPreferredCurrency ( ) { return this . stored . preference . currency ; } async setPreferredCurrency ( sym ) { this . stored . preference . currency = sym ; this . events . publish ( 'wallet:preferredcurrency' , sym ) ; this . events . publish ( 'wallet:preferredunit' , this . getPreferredUnit ( ) ) ; await this . updateStorage ( ) ; } getPreferredUnit ( ) { return this . getUnits ( ) [ this . stored . preference . unitIndex ] ; } async changePreferredUnit ( ) { let units = this . getUnits ( ) ; this . stored . preference . unitIndex = ( this . stored . preference . unitIndex + 1 ) % units . length ; let punit = units [ this . stored . preference . unitIndex ] ; this . events . publish ( 'wallet:preferredunit' , punit ) ; await this . updateStorage ( ) ; } getUnits ( ) { return [ this . stored . preference . cryptoUnit , this . stored . preference . currency ] ; } convertUnit ( from , to , amountStr , comma ? ) { let amount = parseFloat ( amountStr ) ; if ( isNaN ( amount ) ) { return undefined ; } let fromUnit = this . UNITS [ from ] ; let toUnit = this . UNITS [ to ] ; if ( ! ( fromUnit && toUnit && fromUnit . rate && toUnit . rate ) ) { return undefined ; } let result = ( amount / fromUnit . rate * toUnit . rate ) . toFixed ( toUnit . dp ) ; if ( comma ) { let p = result . split ( '.' ) ; p [ 0 ] = p [ 0 ] . split ( '' ) . reverse ( ) . join ( '' ) . match ( /\d{1,3}-?/g ) . join ( ',' ) . split ( '' ) . reverse ( ) . join ( '' ) ; result = p . join ( '.' ) ; } return result ; } getPreferredAddressFormat ( ) { return this . stored . preference . addressFormat ; } async setPreferredAddressFormat ( af ) { if ( af !== 'cashaddr' && af !== 'legacy' ) { return Promise . resolve ( ) ; } this . stored . preference . addressFormat = af ; this . events . publish ( 'wallet:preferredaddressformat' , af ) ; await this . updateStorage ( ) ; } getAddressFormat ( address ) { try { let format = bchaddr . detectAddressFormat ( address ) ; if ( format === bchaddr . Format . Legacy ) { return 'legacy' ; } if ( format === bchaddr . Format . Cashaddr ) { return 'cashaddr' ; } } catch ( err ) { } return undefined ; } convertAddress ( from , to , address ) { from = from || this . getAddressFormat ( address ) ; if ( ! address || this . supportedAddressFormats . indexOf ( from ) === - 1 || this . supportedAddressFormats . indexOf ( to ) === - 1 ) { return undefined ; } if ( from === 'cashaddr' && address . indexOf ( 'bitcoincash:' ) !== 0 ) { address = 'bitcoincash:' + address ; } if ( to === 'legacy' ) { return bchaddr . toLegacyAddress ( address ) ; } if ( to === 'cashaddr' ) { return bchaddr . toCashAddress ( address ) . slice ( 12 ) ; } } async updateStorage ( obj ? ) { let value = obj || this . stored ; await this . storage . set ( this . WALLET_KEY , value ) ; return value ; } changeState ( s ) { console . log ( 'state: ' + s ) ; this . state = s ; if ( s === EState . CLOSED ) { this . events . publish ( 'wallet:closed' ) ; } else if ( s === EState . OFFLINE ) { this . events . publish ( 'wallet:offline' ) ; this . events . publish ( 'wallet:update' ) ; } else if ( s === EState . SYNCED ) { this . events . publish ( 'wallet:synced' ) ; this . events . publish ( 'wallet:update' ) ; } } isClosed ( ) { return this . state === EState . CLOSED ; } isOffline ( ) { return this . state === EState . OFFLINE ; } isConnecting ( ) { return this . state === EState . CONNECTING ; } isOnline ( ) { return this . state >= EState . CONNECTED ; } isConnected ( ) { return this . state === EState . CONNECTED ; } isSyncing ( ) { return this . state === EState . SYNCING ; } isSynced ( ) { return this . state === EState . SYNCED ; } isWatchOnly ( ) { return typeof this . currentWallet . keys . encMnemonic === 'undefined' ; } closeWallet ( ) { if ( ! this . isOffline ( ) && ! this . isClosed ( ) ) { this . socket . off ( 'disconnect' ) ; this . socket . close ( ) ; this . changeState ( EState . CLOSED ) ; } } async createStorage ( ) { let obj = { version : this . VERSION , wallets : [ ] , preference : this . getDefaultPreference ( ) , paymentRefs : [ ] } ; let value = await this . updateStorage ( obj ) ; console . log ( 'successfully created new storage' ) ; return value ; } nextWalletName ( ) { let prefix = this . translate . instant ( 'WALLET' ) + ' ' ; let i ; if ( this . stored && this . stored . wallets ) { let names = this . getAllWalletNames ( ) ; i = this . stored . wallets . length ; do { i ++ ; } while ( names . indexOf ( prefix + i ) !== - 1 ) ; } else { i = 1 ; } return prefix + i ; } getCurrentWalletName ( ) { return this . currentWallet . name ; } getAllWalletNames ( ) { return this . stored . wallets . map ( w => w . name ) ; } async createWallet ( mnemonicOrXprvOrXpub ? , path ? , passphrase ? , name ? , compliant ? ) { compliant = compliant === false ? false : true ; let encrypted ; let xpub ; let addresses ; if ( mnemonicOrXprvOrXpub && mnemonicOrXprvOrXpub . match ( /^xpub/g ) ) { encrypted = undefined ; xpub = mnemonicOrXprvOrXpub ; addresses = this . generateAddressesFromPublicKey ( new bitcoincash . HDPublicKey ( xpub ) ) ; } else { let m = this . makeRecoveryString ( mnemonicOrXprvOrXpub , path , passphrase ) ; let hdPrivateKey = this . getHDPrivateKeyFromRecoveryString ( m , compliant ) ; let hdPublicKey = hdPrivateKey . hdPublicKey ; encrypted = this . _encryptText ( m , this . DUMMY_KEY ) ; xpub = hdPublicKey . toString ( ) ; addresses = this . generateAddressesFromPrivateKey ( hdPrivateKey ) ; } let wallet = { handle : '' , name : name || this . nextWalletName ( ) , protection : 'off' , keys : { encMnemonic : encrypted , compliant : compliant , xpub : xpub , } , addresses : addresses , cache : { receiveAddress : addresses . receive [ 0 ] , changeAddress : addresses . change [ 0 ] , utxos : [ ] , history : [ ] } } ; this . stored . wallets . push ( wallet ) ; this . stored . preference . defaultWallet = wallet . name ; await this . updateStorage ( ) ; } async recoverWalletFromMnemonicOrXprvOrXpub ( mnemonicOrXprvOrXpub , path , passphrase , name , compliant ) { this . closeWallet ( ) ; await this . createWallet ( mnemonicOrXprvOrXpub , path , passphrase , name , compliant ) ; await this . startWallet ( ) ; } async switchWallet ( name ) { if ( ! this . stored . wallets . find ( w => w . name === name ) ) { throw new Error ( 'no such wallet' ) ; } this . closeWallet ( ) ; this . stored . preference . defaultWallet = name ; await this . updateStorage ( ) ; await this . startWallet ( ) ; } async renameWallet ( oldName , newName ) { let w = this . stored . wallets . find ( w => w . name === oldName ) ; if ( ! w ) { throw new Error ( 'no such wallet' ) ; } w . name = newName ; if ( this . stored . preference . defaultWallet === oldName ) { this . stored . preference . defaultWallet = newName ; } await this . updateStorage ( ) ; } async deleteWallet ( name ) { let i = this . stored . wallets . findIndex ( w => w . name === name ) ; if ( i === - 1 ) { throw new Error ( 'no such wallet' ) ; } this . closeWallet ( ) ; let names = this . getAllWalletNames ( ) . sort ( ) ; let deleted = this . stored . wallets . splice ( i , 1 ) [ 0 ] ; let j = names . indexOf ( deleted . name ) ; let next = names [ j + 1 ] || names [ j - 1 ] ; if ( next ) { await this . switchWallet ( next ) ; } else { let loader = this . loadingCtrl . create ( ) ; await loader . present ( ) ; try { await this . updateStorage ( ) ; await this . startWallet ( ) ; await loader . dismiss ( ) ; } catch ( err ) { await loader . dismiss ( ) ; throw err ; } } } async startWallet ( ) { await this . loadWalletFromStorage ( ) ; this . deleteExpiredPaymentRefs ( ) ; this . showAnnouncement ( ) ; this . tryToConnectAndSync ( ) ; } async loadWalletFromStorage ( ) { let value = await this . storage . get ( this . WALLET_KEY ) ; if ( ! value ) { console . log ( 'no stored object, will create a new one' ) ; this . stored = await this . createStorage ( ) ; } else { console . log ( 'stored object found' ) ; let willUpdate = false ; if ( ! value . hasOwnProperty ( 'version' ) ) { if ( value . keys . hasOwnProperty ( 'mnemonic' ) && ! value . keys . hasOwnProperty ( 'encMnemonic' ) ) { let encrypted = this . _encryptText ( value . keys . mnemonic , this . DUMMY_KEY ) ; delete value . keys . mnemonic ; value . keys . encMnemonic = encrypted ; willUpdate = true ; } if ( value . preference . hasOwnProperty ( 'pin' ) ) { if ( typeof value . preference . pin === 'undefined' || value . preference . pin === null ) { value . preference . password = false ; } else { let decrypted = this . _decryptText ( value . keys . encMnemonic , this . DUMMY_KEY ) ; let encrypted = this . _encryptText ( decrypted , value . preference . pin ) ; value . keys . encMnemonic = encrypted ; value . preference . password = true ; } delete value . preference . pin ; willUpdate = true ; } if ( value . preference . hasOwnProperty ( 'pinHash' ) ) { if ( typeof value . preference . pinHash === 'undefined' || value . preference . pinHash === null ) { value . preference . password = false ; } else { this . splashScreen . hide ( ) ; let pw = await new Promise < string > ( ( resolve , reject ) => { let pinAlert = this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'PIN' ) , inputs : [ { name : 'pin' , type : 'password' , placeholder : this . translate . instant ( 'ENTER_PIN' ) } ] , buttons : [ { text : this . translate . instant ( 'OK' ) , handler : data => { let p = value . preference . pinHash . split ( ':' ) ; let salt = Buffer . from ( p [ 0 ] , 'hex' ) ; let hash = crypto . createHash ( 'sha256' ) . update ( salt ) . update ( data . pin , 'utf8' ) . digest ( 'hex' ) ; let r = p [ 0 ] + ':' + hash ; if ( r === value . preference . pinHash ) { pinAlert . dismiss ( ) . then ( ( ) => { resolve ( data . pin ) ; } ) ; } else { this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'ERROR' ) , message : this . translate . instant ( 'ERR_INCORRECT_PIN' ) , buttons : [ this . translate . instant ( 'OK' ) ] } ) . present ( ) ; } return false ; } } ] } ) ; pinAlert . present ( ) ; } ) ; let decrypted = this . _decryptText ( value . keys . encMnemonic , this . DUMMY_KEY ) ; let encrypted = this . _encryptText ( decrypted , pw ) ; value . keys . encMnemonic = encrypted ; value . preference . password = true ; } delete value . preference . pinHash ; willUpdate = true ; } if ( value . preference . hasOwnProperty ( 'chain' ) ) { delete value . preference . chain ; willUpdate = true ; } if ( value . preference . cryptoUnit === 'BCH' ) { value . preference . cryptoUnit = 'BSV' ; willUpdate = true ; } if ( value . preference . addressFormat !== 'legacy' ) { value . preference . addressFormat = 'legacy' ; willUpdate = true ; } value . wallets = [ { name : this . translate . instant ( 'WALLET' ) + ' 1' , protection : value . preference . password ? 'password' : value . preference . fingerprint ? 'fingerprint' : 'off' , keys : value . keys , addresses : value . addresses , cache : value . cache } ] ; value . preference . defaultWallet = value . wallets [ 0 ] . name ; delete value . keys ; delete value . addresses ; delete value . cache ; delete value . preference . password ; delete value . preference . fingerprint ; value . version = '0.0.61' ; willUpdate = true ; } let ver = value . version . split ( '.' ) . map ( v => parseInt ( v ) ) ; if ( ver [ 0 ] === 0 && ver [ 1 ] === 0 && ver [ 2 ] < 80 ) { value . wallets . forEach ( ( wallet ) => { wallet . keys . compliant = false ; } ) ; willUpdate = true ; } if ( ver [ 0 ] === 0 && ver [ 1 ] === 0 && ver [ 2 ] < 89 ) { value . wallets . forEach ( ( wallet ) => { wallet . handle = '' ; } ) ; willUpdate = true ; } if ( ver [ 0 ] === 0 && ver [ 1 ] === 0 && ver [ 2 ] < 98 ) { value . paymentRefs = [ ] ; willUpdate = true ; } let defaultPreference = this . getDefaultPreference ( ) ; Object . keys ( defaultPreference ) . forEach ( ( k ) => { if ( typeof value . preference [ k ] === 'undefined' ) { value . preference [ k ] = defaultPreference [ k ] ; willUpdate = true ; } } ) ; if ( value . version !== this . VERSION ) { value . version = this . VERSION ; willUpdate = true ; } if ( willUpdate ) { value = await this . updateStorage ( value ) ; } this . stored = value ; } if ( this . stored . wallets . length === 0 ) { console . log ( 'no wallet, will create a new one' ) ; await this . createWallet ( ) ; } this . currentWallet = this . stored . wallets . find ( w => w . name === this . stored . preference . defaultWallet ) ; if ( ! this . currentWallet ) { this . currentWallet = this . stored . wallets [ 0 ] ; this . stored . preference . defaultWallet = this . currentWallet . name ; await this . updateStorage ( ) ; } this . syncAddressesLock = Promise . resolve ( ) ; this . changeState ( EState . OFFLINE ) ; console . log ( 'default wallet loaded' ) ; } async showAnnouncement ( isInitiatedByUser ? ) { let ann ; try { let o = await this . http . get ( this . ANNOUNCEMENT_URL ) . toPromise ( ) ; let v = o [ this . VERSION ] || o [ 'default' ] ; ann = v [ ( window as any ) . translationLanguage ] || v [ 'en' ] ; } catch ( err ) { console . log ( err ) ; if ( isInitiatedByUser ) { await this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'ERROR' ) , buttons : [ this . translate . instant ( 'OK' ) ] } ) . present ( ) ; } return ; } if ( isInitiatedByUser ) { if ( ! ann ) { await this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'NO_ANNOUNCEMENT' ) , buttons : [ this . translate . instant ( 'OK' ) ] } ) . present ( ) ; return ; } } else if ( ! ann || this . stored . preference . lastAnnouncement === ann ) { return ; } let buttons = [ ] ; if ( ! isInitiatedByUser ) { buttons . push ( { text : this . translate . instant ( 'DO_NOT_SHOW_AGAIN' ) , handler : ( ) => { this . stored . preference . lastAnnouncement = ann ; this . updateStorage ( ) ; } } ) ; } buttons . push ( { text : this . translate . instant ( 'OK' ) } ) ; let annAlert = this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'ANNOUNCEMENT' ) , message : ann , buttons : buttons } ) ; if ( isInitiatedByUser || this . app . getRootNav ( ) . getActive ( ) . component . pageName === 'HomePage' && ! this . app . _appRoot . _overlayPortal . getActive ( ) ) { await annAlert . present ( ) ; } } tryToConnectAndSync ( ) { let socket = io ( this . WS_URL , { autoConnect : false , reconnection : true , reconnectionAttempts : 1 , timeout : 10000 , transports : [ 'polling' , 'websocket' ] } ) ; this . socket = socket ; socket . on ( 'connect' , async ( ) => { try { let challenge = await this . apiWS ( 'challengev2' , { version : this . VERSION } , true ) ; let challengeBuffer = Buffer . from ( challenge . nonce , 'hex' ) ; let response = Buffer . alloc ( 4 ) ; let x = '' ; let i = 0 ; while ( i < 4294967296 ) { response . writeUInt32LE ( i ) ; x = crypto . createHash ( 'sha256' ) . update ( challengeBuffer ) . update ( response ) . digest ( 'hex' ) . slice ( 0 , challenge . target . length ) ; if ( x <= challenge . target ) { break ; } if ( ++ i % 100 === 0 ) { await this . delay ( 0 ) ; if ( socket !== this . socket ) { throw new Error ( 'obsolete' ) ; } } } await this . apiWS ( 'startwallet' , { response : response . toString ( 'hex' ) , xpub : this . currentWallet . keys . xpub } , true ) ; if ( socket !== this . socket ) { throw new Error ( 'obsolete' ) ; } this . changeState ( EState . CONNECTED ) ; await Promise . all ( [ this . refreshHandle ( ) , this . apiWS ( 'price.subscribe' ) . then ( ( price ) => { this . updatePrice ( price ) ; } ) , this . apiWS ( 'address.subscribe' ) , this . syncEverything ( true ) ] ) ; } catch ( err ) { console . log ( err ) ; socket . close ( ) ; if ( socket !== this . socket ) { return ; } this . changeState ( EState . OFFLINE ) ; if ( err . message === 'update' ) { this . showUpdateAlert ( ) ; } } } ) ; let timer ; socket . on ( 'notification' , ( data ) => { if ( data . method === 'address.subscribe' ) { window . clearTimeout ( timer ) ; if ( this . pendingAddresses . indexOf ( data . params [ 0 ] ) === - 1 ) { this . pendingAddresses . push ( data . params [ 0 ] ) ; } timer = window . setTimeout ( ( ) => { if ( this . isSyncing ( ) || ! this . isOnline ( ) || this . pendingAddresses . length === 0 ) { return ; } this . syncEverything ( ) . catch ( ( err ) => { console . log ( err ) ; } ) ; } , 100 ) ; } else if ( data . method === 'price.subscribe' ) { this . updatePrice ( data . params ) ; } } ) ; socket . on ( 'disconnect' , ( reason ) => { if ( reason === 'io server disconnect' || this . isPaused ) { socket . close ( ) ; if ( socket !== this . socket ) { return ; } this . changeState ( EState . OFFLINE ) ; } } ) ; socket . on ( 'reconnect_attempt' , ( n ) => { if ( socket !== this . socket ) { return ; } this . changeState ( EState . CONNECTING ) ; } ) ; socket . on ( 'reconnect_failed' , ( ) => { socket . close ( ) ; if ( socket !== this . socket ) { return ; } this . changeState ( EState . OFFLINE ) ; } ) ; this . changeState ( EState . CONNECTING ) ; socket . open ( ) ; } async syncEverything ( fullSync ? ) { let currentWallet = this . currentWallet ; this . changeState ( EState . SYNCING ) ; let syncTaskId = this . syncTaskId ++ ; let targetAddresses = fullSync ? undefined : this . pendingAddresses . slice ( ) ; let minHeight = 0 ; try { minHeight = Math . max ( 0 , currentWallet . cache . history . find ( h => h . height > 0 ) . height - 200 ) ; } catch ( err ) { console . log ( err ) ; } this . pendingAddresses . length = 0 ; let results = await Promise . all ( [ this . apiWS ( 'finaladdresspair' ) . then ( result => this . syncAddresses ( currentWallet , result ) ) , this . apiWS ( 'unusedreceiveaddress' ) , this . apiWS ( 'unusedchangeaddress' ) , this . apiWS ( 'utxos' , { addresses : targetAddresses } ) , this . apiWS ( 'history' , { addresses : targetAddresses , minHeight : minHeight } ) ] ) ; if ( currentWallet !== this . currentWallet || syncTaskId !== this . syncTaskId - 1 ) { return ; } if ( ! this . isMyReceiveAddress ( results [ 1 ] ) || ! this . isMyChangeAddress ( results [ 2 ] ) ) { throw new Error ( 'invalid address' ) ; } let allTxids = this . getAllTxids ( ) ; let newTxs = results [ 4 ] . filter ( tx => allTxids . indexOf ( tx . txid ) === - 1 ) ; let oldTxs = results [ 4 ] . filter ( tx => allTxids . indexOf ( tx . txid ) !== - 1 ) ; let newTxids = newTxs . map ( tx => tx . txid ) ; window . setTimeout ( ( ) => { newTxids . forEach ( ( txid ) => { this . events . publish ( `tx:${ txid }` ) ; } ) ; } , 0 ) ; let skipNotification = ( allTxids . length === 0 && results [ 4 ] . length > 1 ) || this . app . getRootNav ( ) . getActive ( ) . component . pageName === 'HistoryPage' ; if ( ! skipNotification ) { let _newTxs = newTxs ; let lastConfirmed = currentWallet . cache . history . find ( tx => typeof tx . timestamp !== 'undefined' ) ; if ( typeof lastConfirmed !== 'undefined' ) { _newTxs = _newTxs . filter ( tx => typeof tx . timestamp === 'undefined' || tx . timestamp >= lastConfirmed . timestamp ) ; } let unit = this . getPreferredUnit ( ) ; let isCordova = this . platform . is ( 'cordova' ) ; _newTxs . forEach ( ( tx ) => { if ( tx . delta <= 0 ) { return ; } let receivedAmount = this . convertUnit ( 'SATS' , unit , tx . delta . toString ( ) , true ) || '' ; if ( unit === 'BSV' && receivedAmount . match ( /^\d+\.\d+$/g ) ) { receivedAmount = receivedAmount . replace ( /\.?0+$/ , '' ) ; } let msg = `${ this . translate . instant ( 'RECEIVED' ) } ${ receivedAmount } ${ unit }` ; if ( isCordova ) { this . localNotifications . schedule ( { id : this . notificationId ++ , text : msg , data : { page : 'HistoryPage' , navParams : { } } , foreground : true } ) ; } else { this . toastCtrl . create ( { message : msg , position : 'bottom' , duration : 3000 } ) . present ( ) ; } } ) ; } let currentHistory = currentWallet . cache . history ; let unseenTxids = this . getUnseenTxids ( ) . concat ( newTxids ) ; let currentTimestamp = Math . floor ( new Date ( ) . getTime ( ) / 1000 ) ; oldTxs . forEach ( ( otx ) => { let tx = currentHistory . find ( tx => tx . txid === otx . txid ) ; let newValue = { txid : otx . txid , height : otx . height , timestamp : otx . timestamp , friendlyTimestamp : tx . friendlyTimestamp || ( otx . timestamp ? Math . min ( otx . timestamp , currentTimestamp ) : currentTimestamp ) , delta : otx . delta , remark : tx . remark , seen : unseenTxids . indexOf ( otx . txid ) === - 1 } ; Object . assign ( tx , newValue ) ; } ) ; currentHistory . splice . apply ( currentHistory , ( [ 0 , 0 ] as any [ ] ) . concat ( newTxs . map ( ntx => { return { txid : ntx . txid , height : ntx . height , timestamp : ntx . timestamp , friendlyTimestamp : ntx . timestamp ? Math . min ( ntx . timestamp , currentTimestamp ) : currentTimestamp , delta : ntx . delta , remark : undefined , seen : false } ; } ) ) ) ; currentHistory . forEach ( ( h , i ) => { ( h as any ) . tempIndex = i ; } ) ; currentHistory . sort ( ( a , b ) => { let v = b . friendlyTimestamp - a . friendlyTimestamp ; if ( v !== 0 ) { return v ; } return ( a as any ) . tempIndex - ( b as any ) . tempIndex ; } ) ; currentHistory . forEach ( ( h ) => { delete ( h as any ) . tempIndex ; } ) ; let newUtxos ; if ( fullSync ) { newUtxos = results [ 3 ] . map ( ( obj ) => { return { txid : obj . txid , vout : obj . vout , address : obj . address , path : this . getAddressTypeAndIndex ( obj . address ) , scriptPubKey : obj . scriptPubKey , satoshis : obj . satoshis } ; } ) . filter ( utxo => typeof utxo . path !== 'undefined' ) ; } else { let futxos = results [ 3 ] . filter ( ( nutxo ) => { return this . currentWallet . cache . utxos . findIndex ( outxo => outxo . txid === nutxo . txid && outxo . vout === nutxo . vout ) === - 1 ; } ) . map ( ( utxo ) => { return { txid : utxo . txid , vout : utxo . vout , address : utxo . address , path : this . getAddressTypeAndIndex ( utxo . address ) , scriptPubKey : utxo . scriptPubKey , satoshis : utxo . satoshis } ; } ) . filter ( utxo => typeof utxo . path !== 'undefined' ) ; let rutxos = this . currentWallet . cache . utxos . filter ( outxo => targetAddresses . indexOf ( outxo . address ) === - 1 || results [ 3 ] . findIndex ( nutxo => outxo . txid === nutxo . txid && outxo . vout === nutxo . vout ) !== - 1 ) ; newUtxos = rutxos . concat ( futxos ) ; } this . currentWallet . cache = { receiveAddress : results [ 1 ] , changeAddress : results [ 2 ] , utxos : newUtxos , history : currentHistory } ; await this . updateStorage ( ) ; if ( ! this . isSyncing ( ) || currentWallet !== this . currentWallet || syncTaskId !== this . syncTaskId - 1 ) { return ; } if ( this . pendingAddresses . length > 0 ) { return await this . syncEverything ( ) ; } this . changeState ( EState . SYNCED ) ; } async syncAddresses ( currentWallet , pair ) { let previousLock = this . syncAddressesLock ; let release ; this . syncAddressesLock = new Promise ( ( resolve , reject ) => { release = resolve ; } ) ; await previousLock ; let result ; let error ; try { result = await this . _syncAddresses ( currentWallet , pair ) ; } catch ( err ) { error = err ; } release ( ) ; if ( error ) { throw error ; } return result ; } async _syncAddresses ( currentWallet , pair ) { let newAddresses = { receive : [ ] , change : [ ] } ; currentWallet . addresses . receive . length = Math . min ( pair . receive + 1 , currentWallet . addresses . receive . length ) ; currentWallet . addresses . change . length = Math . min ( pair . change + 1 , currentWallet . addresses . change . length ) ; if ( pair . receive === currentWallet . addresses . receive . length - 1 && pair . change === currentWallet . addresses . change . length - 1 ) { return newAddresses ; } let hdPublicKey = this . getHDPublicKey ( currentWallet ) ; let d = [ hdPublicKey . deriveChild ( 0 ) , hdPublicKey . deriveChild ( 1 ) ] ; let loader ; if ( pair . receive - currentWallet . addresses . receive . length + pair . change - currentWallet . addresses . change . length > 10 ) { loader = this . loadingCtrl . create ( { content : this . translate . instant ( 'PLEASE_WAIT' ) } ) ; await loader . present ( ) ; } for ( let i = currentWallet . addresses . receive . length ; i <= pair . receive ; i ++ ) { await this . delay ( 0 ) ; newAddresses . receive . push ( d [ 0 ] . deriveChild ( i ) . publicKey . toAddress ( ) . toString ( ) ) ; } for ( let i = currentWallet . addresses . change . length ; i <= pair . change ; i ++ ) { await this . delay ( 0 ) ; newAddresses . change . push ( d [ 1 ] . deriveChild ( i ) . publicKey . toAddress ( ) . toString ( ) ) ; } if ( loader ) { await loader . dismiss ( ) ; } Array . prototype . push . apply ( currentWallet . addresses . receive , newAddresses . receive ) ; Array . prototype . push . apply ( currentWallet . addresses . change , newAddresses . change ) ; return newAddresses ; } updatePrice ( price ) { for ( let k in price ) { if ( this . UNITS [ k ] ) { this . UNITS [ k ] . rate = price [ k ] ; } else { this . UNITS [ k ] = { rate : price [ k ] , dp : 2 } ; } } this . events . publish ( 'wallet:price' ) ; } subscribeUpdate ( callback ) { if ( this . state >= EState . OFFLINE ) { callback ( ) ; } this . events . subscribe ( 'wallet:update' , callback ) ; console . log ( 'subscribeUpdate' ) ; } unsubscribeUpdate ( callback ) { this . events . unsubscribe ( 'wallet:update' , callback ) ; console . log ( 'unsubscribeUpdate' ) ; } subscribePrice ( callback ) { this . events . subscribe ( 'wallet:price' , callback ) ; console . log ( 'subscribePrice' ) ; } unsubscribePrice ( callback ) { this . events . unsubscribe ( 'wallet:price' , callback ) ; console . log ( 'unsubscribePrice' ) ; } subscribePreferredUnit ( callback ) { this . events . subscribe ( 'wallet:preferredunit' , callback ) ; console . log ( 'subscribePreferredUnit' ) ; } unsubscribePreferredUnit ( callback ) { this . events . unsubscribe ( 'wallet:preferredunit' , callback ) ; console . log ( 'unsubscribePreferredUnit' ) ; } subscribeTx ( txid , callback ) { this . events . subscribe ( `tx:${ txid }` , callback ) ; console . log ( 'subscribeTx' ) ; } unsubscribeTx ( txid , callback ) { this . events . unsubscribe ( `tx:${ txid }` , callback ) ; console . log ( 'unsubscribeTx' ) ; } delay ( ms ) { return new Promise < void > ( ( resolve , reject ) => { window . setTimeout ( ( ) => { resolve ( ) ; } , ms ) ; } ) ; } createMnemonic ( ) { return new bitcoincash_Mnemonic ( crypto . randomBytes ( 16 ) ) . phrase ; } generateAddressesFromPrivateKey ( hdPrivateKey ) { let d = [ hdPrivateKey . deriveChild ( 0 ) , hdPrivateKey . deriveChild ( 1 ) ] ; let addresses = { receive : [ ] , change : [ ] } ; for ( let i = 0 ; i < 20 ; i ++ ) { addresses . receive [ i ] = d [ 0 ] . deriveChild ( i ) . privateKey . toAddress ( ) . toString ( ) ; addresses . change [ i ] = d [ 1 ] . deriveChild ( i ) . privateKey . toAddress ( ) . toString ( ) ; } return addresses ; } generateAddressesFromPublicKey ( hdPublicKey ) { let d = [ hdPublicKey . deriveChild ( 0 ) , hdPublicKey . deriveChild ( 1 ) ] ; let addresses = { receive : [ ] , change : [ ] } ; for ( let i = 0 ; i < 20 ; i ++ ) { addresses . receive [ i ] = d [ 0 ] . deriveChild ( i ) . publicKey . toAddress ( ) . toString ( ) ; addresses . change [ i ] = d [ 1 ] . deriveChild ( i ) . publicKey . toAddress ( ) . toString ( ) ; } return addresses ; } getHDPrivateKeyFromRecoveryString ( m , compliant ? ) { let o = this . parseRecoveryString ( m ) ; if ( o . xprv ) { return new bitcoincash . HDPrivateKey ( o . xprv ) ; } compliant = typeof compliant === 'boolean' ? compliant : this . isBIP32Compliant ( ) ; if ( compliant ) { console . log ( 'derive compliant' ) ; return new bitcoincash_Mnemonic ( o . mnemonic ) . toHDPrivateKey ( o . passphrase ) . deriveChild ( o . path ) ; } else { console . log ( 'derive non-compliant' ) ; return new bitcoincash_Mnemonic ( o . mnemonic ) . toHDPrivateKey ( o . passphrase ) . deriveNonCompliantChild ( o . path ) ; } } makeRecoveryString ( mnemonicOrXprv ? , path ? , passphrase ? ) { if ( mnemonicOrXprv && mnemonicOrXprv . match ( /^xprv[\d\w]+$/g ) ) { return mnemonicOrXprv ; } mnemonicOrXprv = mnemonicOrXprv || this . createMnemonic ( ) ; path = path || "m/44'/145'/0'" ; passphrase = passphrase || '' ; return mnemonicOrXprv + ':' + path + ':' + passphrase ; } parseRecoveryString ( m ) { if ( m . match ( /^xprv[\d\w]+$/g ) ) { return { xprv : m } ; } let a = m . split ( ':' ) ; return { mnemonic : a [ 0 ] , path : a [ 1 ] || "m/44'/145'/0'" , passphrase : a . slice ( 2 ) . join ( ':' ) || undefined } ; } formatMnemonic ( s ) { s = s . trim ( ) ; s = s . replace ( /\s+/g , ' ' ) ; if ( s . indexOf ( ' ' ) === - 1 ) { s = s . split ( '' ) . join ( ' ' ) ; } s = s . toLowerCase ( ) ; return s ; } autocorrectMnemonic ( m ) { let prefixes ; try { prefixes = m . split ( ' ' ) . map ( w => new RegExp ( '^' + w . slice ( 0 , 4 ) ) ) ; } catch ( err ) { return m ; } let result ; for ( let lang in bitcoincash_Mnemonic . Words ) { let matches = [ ] ; for ( let p of prefixes ) { let match ; for ( let w of bitcoincash_Mnemonic . Words [ lang ] ) { if ( w . match ( p ) ) { match = w ; break ; } } if ( match ) { matches . push ( match ) ; } else { matches . length = 0 ; break ; } } if ( matches . length > 0 ) { result = matches . join ( ' ' ) ; break ; } } return result || m ; } validateMnemonic ( m ) { try { return bitcoincash_Mnemonic . isValid ( m ) ; } catch ( err ) { return false ; } } validateXprv ( xprv ) { if ( ! xprv ) { return false ; } try { new bitcoincash . HDPrivateKey ( xprv ) ; return true ; } catch ( err ) { return false ; } } validateXpub ( xpub ) { try { new bitcoincash . HDPublicKey ( xpub ) ; return true ; } catch ( err ) { return false ; } } validateWIF ( wif ) { if ( wif . toLowerCase ( ) . match ( /^[a-f0-9]+$/g ) ) { return false ; } try { new bitcoincash . PrivateKey ( wif ) ; return true ; } catch ( err ) { return false ; } } validateEncryptedWIF ( wif ) { if ( ! wif . match ( /^6P[1-9A-NP-Za-km-z]{56}$/g ) ) { return false ; } try { bs58check . decode ( wif ) ; return true ; } catch ( err ) { return false ; } } isMyReceiveAddress ( address ) { return typeof this . getAddressTypeAndIndex ( address , 0 ) !== 'undefined' ; } isMyChangeAddress ( address ) { return typeof this . getAddressTypeAndIndex ( address , 1 ) !== 'undefined' ; } scriptFromAddress ( address ) { try { let a = new bitcoincash . Address ( address ) ; if ( a . isPayToPublicKeyHash ( ) ) { return bitcoincash . Script . buildPublicKeyHashOut ( a ) . toHex ( ) ; } else if ( a . isPayToScriptHash ( ) ) { return bitcoincash . Script . buildScriptHashOut ( a ) . toHex ( ) ; } else { throw new Error ( 'invalid address' ) ; } } catch ( err ) { console . log ( err ) ; throw new Error ( 'invalid address' ) ; } } getAddressTypeAndIndex ( address , type ? ) { let ra = this . currentWallet . addresses . receive ; let ca = this . currentWallet . addresses . change ; let lenr = typeof type === 'undefined' || type === 0 ? ra . length : 0 ; let lenc = typeof type === 'undefined' || type === 1 ? ca . length : 0 ; let len = Math . min ( lenr , lenc ) ; for ( let i = 1 ; i <= len ; i ++ ) { if ( address === ra [ lenr - i ] ) { return [ 0 , lenr - i ] ; } else if ( address === ca [ lenc - i ] ) { return [ 1 , lenc - i ] ; } } if ( lenr > len ) { for ( let i = lenr - len - 1 ; i >= 0 ; i -- ) { if ( address === ra [ i ] ) { return [ 0 , i ] ; } } } else if ( lenc > len ) { for ( let i = lenc - len - 1 ; i >= 0 ; i -- ) { if ( address === ca [ i ] ) { return [ 1 , i ] ; } } } else { return undefined ; } } getAllTxids ( ) { return this . currentWallet . cache . history . map ( obj => obj . txid ) ; } getUnseenTxids ( ) { return this . currentWallet . cache . history . filter ( obj => ! obj . seen ) . map ( obj => obj . txid ) ; } async seeTheUnseen ( ) { let touch = false ; this . currentWallet . cache . history . forEach ( ( obj ) => { if ( ! touch && ! obj . seen ) { touch = true ; } obj . seen = true ; } ) ; if ( touch ) { await this . updateStorage ( ) ; } } promptForTxRemark ( txid ) { let r = this . currentWallet . cache . history . find ( tx => tx . txid === txid ) ; if ( ! r ) { return Promise . reject ( new Error ( 'cancelled' ) ) ; } return new Promise ( ( resolve , reject ) => { let remarkAlert = this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'REMARK' ) , inputs : [ { name : 'remark' , value : r . remark } ] , buttons : [ { role : 'cancel' , text : this . translate . instant ( 'CANCEL' ) , handler : data => { remarkAlert . dismiss ( ) . then ( ( ) => { reject ( new Error ( 'cancelled' ) ) ; } ) ; return false ; } } , { text : this . translate . instant ( 'OK' ) , handler : data => { let remark = data . remark . trim ( ) ; if ( r . remark === remark || ! r . remark && ! remark ) { remarkAlert . dismiss ( ) . then ( ( ) => { reject ( new Error ( 'cancelled' ) ) ; } ) ; } else { remarkAlert . dismiss ( ) . then ( ( ) => { r . remark = remark || undefined ; return this . updateStorage ( ) ; } ) . then ( ( ) => { resolve ( r . remark ) ; } ) ; } return false ; } } ] } ) ; remarkAlert . present ( ) ; } ) ; } getHDPublicKey ( currentWallet ? ) { currentWallet = currentWallet || this . currentWallet ; return new bitcoincash . HDPublicKey ( currentWallet . keys . xpub ) ; } getXpub ( ) { return this . currentWallet . keys . xpub ; } getRecoveryString ( password ? ) { let decrypted = this . _decryptText ( this . currentWallet . keys . encMnemonic , password || this . DUMMY_KEY ) ; let o = this . parseRecoveryString ( decrypted ) ; if ( o . mnemonic && this . validateMnemonic ( o . mnemonic ) || o . xprv && this . validateXprv ( o . xprv ) ) { return decrypted ; } else { throw new Error ( 'invalid password' ) ; } } isBIP32Compliant ( ) { return this . currentWallet . keys . compliant ; } _encryptText ( text , password ) { let cipher = crypto . createCipher ( 'aes192' , password ) ; let encrypted = cipher . update ( text , 'utf8' , 'hex' ) ; encrypted += cipher . final ( 'hex' ) ; return encrypted ; } _decryptText ( cipherText , password ) { let decipher = crypto . createDecipher ( 'aes192' , password ) ; let decrypted = decipher . update ( cipherText , 'hex' , 'utf8' ) ; decrypted += decipher . final ( 'utf8' ) ; return decrypted ; } getPaymentRequestURL ( address , sat ? ) { let af = this . getAddressFormat ( address ) ; if ( ! af ) { throw new Error ( 'invalid address' ) ; } if ( sat > 0 ) { return 'bitcoin:' + address + '?sv&amount=' + this . convertUnit ( 'SATS' , 'BSV' , sat . toString ( ) ) . replace ( /\.?0+$/ , '' ) ; } else { return address ; } } getQR ( text ) { return QRCode . toDataURL ( text , { margin : 1 , errorCorrectionLevel : 'L' } ) ; } getQRs ( text , prefix ) { let segLength = Math . ceil ( text . length / Math . ceil ( text . length / 1100 ) ) ; let regex = new RegExp ( '.{1,' + segLength + '}' , 'gi' ) ; let segments = text . match ( regex ) ; let last = segments . length - 1 ; let qrStrings = segments . map ( ( s , i ) => [ prefix , i , last , s ] . join ( ' ' ) ) ; return Promise . all ( qrStrings . map ( q => this . getQR ( q ) ) ) ; } getRequestFromURL ( text ) { let isPaymail = false ; let isBIP270 = false ; let outputs ; let params = { } ; let address ; let satoshis ; let r ; let label ; let message ; let purpose ; if ( text . match ( /^bitcoin:/gi ) ) { text = text . slice ( 8 ) ; } else if ( text . match ( /^bitcoincash:/gi ) ) { text = text . slice ( 12 ) ; } else if ( text . match ( /^bitcoin[-_]cash:/gi ) ) { text = text . slice ( 13 ) ; } else if ( text . match ( /^bitcoinsv:/gi ) ) { text = text . slice ( 10 ) ; } else if ( text . match ( /^bitcoin[-_]sv:/gi ) ) { text = text . slice ( 11 ) ; } else if ( text . match ( /^payto:/gi ) ) { text = text . slice ( 6 ) ; isPaymail = true ; } else if ( text . match ( /^pay:/gi ) ) { text = text . slice ( 4 ) ; } else { return ; } let path ; let i = text . indexOf ( '?' ) ; if ( i === - 1 ) { path = text ; } else { path = text . slice ( 0 , i ) ; } if ( typeof this . getAddressFormat ( path ) !== 'undefined' ) { address = path ; } else if ( isPaymail ) { if ( ! this . validatePaymail ( path ) ) { return ; } address = path . trim ( ) . toLowerCase ( ) ; } let kvs = text . slice ( i + 1 ) . split ( '&' ) ; let j = kvs . findIndex ( ( kv ) => { return kv . match ( /^req-.*$/gi ) && ! kv . match ( /^req-sv(=.*)?$/gi ) ; } ) ; if ( j !== - 1 ) { return ; } kvs . forEach ( ( kv ) => { let s = kv . split ( '=' ) ; params [ s [ 0 ] ] = s . slice ( 1 ) . join ( '=' ) ; } ) ; if ( typeof params . r !== 'undefined' ) { let decoded = decodeURIComponent ( params . r ) ; if ( decoded . match ( /^https:\/\/.+$/g ) ) { r = decoded ; isBIP270 = true ; } } if ( typeof address === 'undefined' && ! isBIP270 ) { return ; } if ( typeof params . amount !== 'undefined' ) { if ( isPaymail ) { let amount = parseInt ( params . amount ) ; if ( amount > 0 ) { satoshis = amount ; } } else { let amount = parseFloat ( params . amount ) ; if ( amount > 0 ) { satoshis = parseFloat ( this . convertUnit ( 'BSV' , 'SATS' , amount . toString ( ) ) ) ; } } } if ( typeof params . label !== 'undefined' ) { label = decodeURIComponent ( params . label ) ; } if ( typeof params . message !== 'undefined' ) { message = decodeURIComponent ( params . message ) ; } if ( typeof params . purpose !== 'undefined' ) { purpose = decodeURIComponent ( params . purpose ) ; } if ( ! outputs ) { outputs = ! address ? [ ] : [ { address : address , satoshis : satoshis } ] ; } return { outputs : outputs , r : r , label : label , message : message , purpose : purpose , isPaymail : isPaymail , isBIP270 : isBIP270 , } ; } async getInfoFromWIF ( wif ) { let sk = new bitcoincash . PrivateKey ( wif ) ; let address = sk . toAddress ( ) . toString ( ) ; let result = await this . apiWS ( 'utxos' , { address : address } ) ; let utxos = result . map ( ( obj ) => { return { txid : obj . txid , vout : obj . vout , address : obj . address , scriptPubKey : obj . scriptPubKey , satoshis : obj . satoshis } ; } ) ; return { address : address , balance : utxos . length > 0 ? utxos . map ( ( curr ) => curr . satoshis ) . reduce ( ( acc , curr ) => acc + curr ) : 0 , utxos : utxos } ; } getReceiveAddressCount ( ) { return this . currentWallet . addresses . receive . length ; } getChangeAddressCount ( ) { return this . currentWallet . addresses . change . length ; } getAllReceiveAddresses ( ) { return this . currentWallet . addresses . receive . slice ( ) ; } getAllChangeAddresses ( ) { return this . currentWallet . addresses . change . slice ( ) ; } getCacheReceiveAddress ( ) { return this . currentWallet . cache . receiveAddress ; } getCacheChangeAddress ( ) { return this . currentWallet . cache . changeAddress ; } getCacheBalance ( ) { let balance = 0 ; this . currentWallet . cache . utxos . forEach ( ( utxo ) => { balance += utxo . satoshis ; } ) ; return balance ; } getCacheHistory ( ) { return this . currentWallet . cache . history . slice ( ) ; } getCacheUtxos ( ) { return this . currentWallet . cache . utxos . slice ( ) ; } getIdentityPrivateKey ( m ) { let hdPrivateKey = this . getHDPrivateKeyFromRecoveryString ( m ) ; return hdPrivateKey . deriveChild ( 2 ) . deriveChild ( 0 ) . privateKey ; } getIdentityPublicKey ( ) { let hdPublicKey = this . getHDPublicKey ( ) ; return hdPublicKey . deriveChild ( 2 ) . deriveChild ( 0 ) . publicKey ; } getPrivateKeys ( paths , m ) { let _paths = [ ] ; paths . forEach ( ( p ) => { let i = _paths . findIndex ( _p => _p [ 0 ] === p [ 0 ] && _p [ 1 ] === p [ 1 ] ) ; if ( i === - 1 ) { _paths . push ( p ) ; } } ) ; let hdPrivateKey = this . getHDPrivateKeyFromRecoveryString ( m ) ; let d = [ hdPrivateKey . deriveChild ( 0 ) , hdPrivateKey . deriveChild ( 1 ) ] ; return _paths . map ( p => d [ p [ 0 ] ] . deriveChild ( p [ 1 ] ) . privateKey ) ; } getWIF ( path , m ) { return this . getPrivateKeys ( [ path ] , m ) [ 0 ] . toWIF ( ) ; } getXprv ( m ) { return this . getHDPrivateKeyFromRecoveryString ( m ) . toString ( ) ; } getTxidFromHex ( hex ) { return bitcoincash . crypto . Hash . sha256 ( bitcoincash . crypto . Hash . sha256 ( Buffer . from ( hex , 'hex' ) ) ) . reverse ( ) . toString ( 'hex' ) ; } async makeSignedTx ( outputs , drain , m ) { let au = this . getCacheUtxos ( ) ; let ak = this . getPrivateKeys ( au . map ( u => u . path ) , m ) ; return await this . _makeTx ( outputs , drain , au , ak ) ; } async makeUnsignedTx ( outputs , drain ) { let au = this . getCacheUtxos ( ) ; return await this . _makeTx ( outputs , drain , au , [ ] ) ; } async _makeTx ( outputs , drain , availableUtxos , availableKeys ) { let satoshis = drain ? undefined : outputs . map ( output => output . satoshis ) . reduce ( ( acc , curr ) => acc + curr ) ; if ( availableUtxos . length === 0 ) { throw new Error ( 'not enough fund' ) ; } let availableAmount = drain ? availableUtxos . map ( ( curr ) => curr . satoshis ) . reduce ( ( acc , curr ) => acc + curr ) : 0 ; let agedAmount = 0 ; let agedUtxos = [ ] ; let recentUtxos = [ ] ; if ( ! drain ) { let limits = [ Math . max ( 0 , this . getReceiveAddressCount ( ) - 20 - 100 ) , Math . max ( 0 , this . getChangeAddressCount ( ) - 20 - 100 ) ] ; availableUtxos . forEach ( ( u ) => { if ( u . path [ 1 ] < limits [ u . path [ 0 ] ] && agedUtxos . length < 10 ) { agedUtxos . push ( u ) ; agedAmount += u . satoshis ; } else { recentUtxos . push ( u ) ; } } ) ; } let acc ; let utxos ; let toAmount ; let changeAmount ; let changeOutputScript = this . scriptFromAddress ( this . currentWallet . cache . changeAddress ) ; let changeOutputIndex = Math . floor ( Math . random ( ) * ( outputs . length + 1 ) ) ; let _outputs ; let hex_tentative ; let fee_tentative = 0 ; let fee_required ; while ( true ) { if ( drain ) { acc = availableAmount ; utxos = availableUtxos ; toAmount = acc - fee_tentative ; if ( toAmount < 546 ) { throw new Error ( 'not enough fund' ) ; } changeAmount = 0 ; _outputs = [ { script : outputs [ 0 ] . script , satoshis : toAmount } ] ; } else { let i = 0 ; acc = agedAmount ; utxos = agedUtxos . slice ( ) ; while ( true ) { let excess = acc - satoshis - fee_tentative ; if ( excess >= 0 && excess <= 34 || excess >= 546 || i >= recentUtxos . length ) { break ; } let utxo = recentUtxos [ i ] ; acc += utxo . satoshis ; utxos . push ( utxo ) ; i ++ ; } toAmount = satoshis ; if ( acc < toAmount + fee_tentative ) { throw new Error ( 'not enough fund' ) ; } changeAmount = acc - toAmount - fee_tentative ; _outputs = outputs . slice ( ) ; if ( changeAmount < 546 ) { changeAmount = 0 ; } else { _outputs . splice ( changeOutputIndex , 0 , { script : changeOutputScript , satoshis : changeAmount } ) ; } } if ( availableKeys . length > 0 ) { let ustx = new bitcoincash . Transaction ( ) . from ( utxos . map ( utxo => new bitcoincash . Transaction . UnspentOutput ( utxo ) ) ) ; _outputs . forEach ( ( o ) => { ustx . addOutput ( new bitcoincash . Transaction . Output ( o ) ) ; } ) ; hex_tentative = await this . signTx ( ustx , availableKeys ) ; fee_required = Math . ceil ( hex_tentative . length / 4 ) ; } else { let f1 = 149 * utxos . length ; let f2 = _outputs . map ( ( o ) => { let len = o . script . length / 2 ; let f = len + 8 + 1 ; if ( len <= 75 ) { return f ; } if ( len <= 256 ) { return f + 1 ; } if ( len <= 65536 ) { return f + 2 ; } return f + 4 ; } ) . reduce ( ( a , c ) => a + c ) ; fee_required = Math . ceil ( ( f1 + f2 + 10 ) / 2 ) ; } if ( fee_tentative >= fee_required ) { break ; } else { fee_tentative = fee_required ; } } let txOutputs = _outputs ; let changeOutput = txOutputs . find ( o => o . script === changeOutputScript ) ; if ( changeOutput ) { changeOutput . path = this . getAddressTypeAndIndex ( this . currentWallet . cache . changeAddress , 1 ) ; } let txInputs = utxos . map ( u => { return { txid : u . txid , vout : u . vout , path : u . path , script : u . scriptPubKey , satoshis : u . satoshis } ; } ) ; let fee_final = acc - toAmount - changeAmount ; if ( fee_final > 100000 ) { await this . confirmFee ( fee_final ) ; } return { satoshis : toAmount , fee : fee_final , hex : hex_tentative , inputs : txInputs , outputs : txOutputs } ; } async confirmFee ( fee ) { let unit = this . getPreferredUnit ( ) ; let message = `${ this . convertUnit ( 'SATS' , unit , fee . toString ( ) , true ) || '--' } ${ unit }` ; return new Promise < void > ( ( resolve , reject ) => { let feeAlert = this . alertCtrl . create ( { enableBackdropDismiss : true , title : this . translate . instant ( 'FEE' ) , message : message , buttons : [ { role : 'cancel' , text : this . translate . instant ( 'CANCEL' ) , handler : ( ) => { feeAlert . dismiss ( ) . then ( ( ) => { reject ( new Error ( 'cancelled' ) ) ; } ) ; return false ; } } , { text : this . translate . instant ( 'OK' ) , handler : ( ) => { feeAlert . dismiss ( ) . then ( ( ) => { resolve ( ) ; } ) ; return false ; } } ] } ) ; feeAlert . present ( ) ; } ) ; } async makeSweepTx ( wif , info ? ) { let keys = [ new bitcoincash . PrivateKey ( wif ) ] ; if ( ! info ) { info = await this . getInfoFromWIF ( wif ) ; } let output = { script : this . scriptFromAddress ( this . currentWallet . cache . receiveAddress ) , satoshis : 0 } ; return await this . _makeTx ( [ output ] , true , info . utxos , keys ) ; } async signTx ( ustx , keys ) { if ( ! ustx . hasAllUtxoInfo ( ) ) { throw new Error ( 'invalid utxo' ) ; } let n = 0 ; for ( let privKey of keys ) { privKey = new bitcoincash . PrivateKey ( privKey ) ; let sigtype = bitcoincash . crypto . Signature . SIGHASH_ALL | bitcoincash . crypto . Signature . SIGHASH_FORKID ; let transaction = ustx ; let hashData = bitcoincash . crypto . Hash . sha256ripemd160 ( privKey . publicKey . toBuffer ( ) ) ; for ( let index = 0 ; index < transaction . inputs . length ; index ++ ) { let sigs = transaction . inputs [ index ] . getSignatures ( transaction , privKey , index , sigtype , hashData ) ; for ( let signature of sigs ) { transaction . applySignature ( signature ) ; if ( n === 9 ) { n = 0 ; await this . delay ( 0 ) ; } else { n ++ ; } } } } return ustx . serialize ( { disableDustOutputs : true , disableSmallFees : true } ) ; } async signPreparedTx ( tx , m ) { let ustx = new bitcoincash . Transaction ( ) . from ( tx . inputs . map ( utxo => new bitcoincash . Transaction . UnspentOutput ( utxo ) ) ) ; tx . outputs . forEach ( ( output ) => { ustx . addOutput ( new bitcoincash . Transaction . Output ( output ) ) ; } ) ; let ak = this . getPrivateKeys ( tx . inputs . map ( u => u . path ) , m ) ; tx . hex = await this . signTx ( ustx , ak ) ; return tx ; } validatePreparedTx ( tx ) { let toAmount = 0 ; let hdPublicKey = this . getHDPublicKey ( ) ; let d = [ hdPublicKey . deriveChild ( 0 ) , hdPublicKey . deriveChild ( 1 ) ] ; tx . inputs . forEach ( i => { let address = d [ i . path [ 0 ] ] . deriveChild ( i . path [ 1 ] ) . publicKey . toAddress ( ) . toString ( ) ; if ( this . scriptFromAddress ( address ) !== i . script ) { throw new Error ( 'invalid' ) ; } } ) ; tx . outputs . forEach ( o => { if ( o . path ) { let address = d [ o . path [ 0 ] ] . deriveChild ( o . path [ 1 ] ) . publicKey . toAddress ( ) . toString ( ) ; if ( this . scriptFromAddress ( address ) !== o . script ) { throw new Error ( 'invalid' ) ; } } else { toAmount += o . satoshis ; } } ) ; if ( tx . satoshis !== toAmount ) { throw new Error ( 'invalid' ) ; } } getRecipientsFromTx ( tx ) { return tx . outputs . filter ( o => typeof o . path === 'undefined' ) . map ( o => { let a = ( new bitcoincash . Script ( o . script ) ) . toAddress ( ) ; return a ? a . toString ( ) : 'unknown' ; } ) ; } async _broadcastTx ( signedTx , paymentRefs , metadata ) { let txid = await this . apiWS ( 'broadcast' , { tx : signedTx , refs : paymentRefs , meta : metadata } ) ; return txid ; } async broadcastTx ( hex , loader ? , paymentRefs ? , metadata ? ) { if ( ! loader ) { loader = this . loadingCtrl . create ( { content : this . translate . instant ( 'BROADCASTING' ) + '...' } ) ; await loader . present ( ) ; } else { loader . setContent ( this . translate . instant ( 'BROADCASTING' ) + '...' ) ; } try { let deleteRefs = false ; if ( ! paymentRefs ) { if ( this . stored . paymentRefs . length > 0 ) { paymentRefs = this . findPaymentRefs ( hex ) ; if ( paymentRefs . length > 0 ) { deleteRefs = true ; } } else { paymentRefs = [ ] ; } } let _paymentRefs = paymentRefs . map ( r => ( { paymail : r . paymail , reference : r . reference } ) ) ; metadata = metadata || { } ; await this . _broadcastTx ( hex , _paymentRefs , metadata ) ; if ( deleteRefs ) { await this . deletePaymentRefs ( paymentRefs ) ; } await loader . dismiss ( ) ; return true ; } catch ( err ) { await loader . dismiss ( ) ; console . log ( err ) ; let message ; if ( err . message === 'not connected' ) { message = this . translate . instant ( 'ERR_NOT_CONNECTED' ) ; } else if ( err . message === 'timeout' ) { message = this . translate . instant ( 'ERR_TIMEOUT' ) ; } else { message = this . translate . instant ( 'ERR_INVALID_TX' ) ; } await this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'ERROR' ) , message : message , buttons : [ this . translate . instant ( 'OK' ) ] } ) . present ( ) ; return false ; } } async getRequestFromMerchant ( r ) { let paymentRequest = await this . http . get ( r , { headers : { 'Accept' : 'application/bitcoinsv-paymentrequest, application/json' } , responseType : 'json' } ) . toPromise ( ) ; if ( typeof paymentRequest . network !== 'string' || ! Array . isArray ( paymentRequest . outputs ) || paymentRequest . outputs . length === 0 || ! Number . isInteger ( paymentRequest . creationTimestamp ) || ! Number . isInteger ( paymentRequest . expirationTimestamp ) && typeof paymentRequest . expirationTimestamp !== 'undefined' || typeof paymentRequest . memo !== 'string' && typeof paymentRequest . memo !== 'undefined' || typeof paymentRequest . paymentUrl !== 'string' || typeof paymentRequest . merchantData !== 'string' && typeof paymentRequest . merchantData !== 'undefined' ) { throw new Error ( 'invalid payment request' ) ; } if ( ! paymentRequest . network . match ( /^bitcoin([-_]?sv)?$/g ) ) { throw new Error ( 'unsupported network' ) ; } if ( Number . isInteger ( paymentRequest . expirationTimestamp ) && new Date ( ) . getTime ( ) > paymentRequest . expirationTimestamp * 1000 ) { throw new Error ( 'expired' ) ; } let outputs = paymentRequest . outputs . map ( ( output ) => { if ( ! Number . isInteger ( output . amount ) || typeof output . script != 'string' || ! output . script . match ( /^([0-9a-fA-F]{2})+$/g ) || typeof output . description !== 'string' && typeof output . description !== 'undefined' ) { throw new Error ( 'invalid payment request' ) ; } return { satoshis : output . amount , script : output . script . toLowerCase ( ) } ; } ) ; return { outputs , creationTimestamp : paymentRequest . creationTimestamp , expirationTimestamp : paymentRequest . expirationTimestamp , memo : paymentRequest . memo , paymentUrl : paymentRequest . paymentUrl , merchantData : paymentRequest . merchantData , r , bip270 : true } ; } async sendPaymentToMerchant ( url , tx , merchantData ) { let payment = { merchantData : merchantData , transaction : tx , refundTo : this . getHandle ( ) ? this . getPaymail ( ) : undefined , memo : undefined } ; let paymentACK = await this . http . post ( url , payment , { headers : { 'Content-Type' : 'application/bitcoinsv-payment' , 'Accept' : 'application/bitcoinsv-paymentack, application/json' } , responseType : 'json' } ) . toPromise ( ) ; if ( Number . isInteger ( paymentACK . error ) && paymentACK . error > 0 ) { throw new Error ( "payment error" ) ; } return paymentACK . memo ; } showUpdateAlert ( ) { if ( ! this . platform . is ( 'cordova' ) ) { return ; } let updateAlert = this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'PLEASE_UPDATE' ) , buttons : [ { text : this . translate . instant ( 'CANCEL' ) } , { text : this . translate . instant ( 'UPDATE' ) , handler : ( ) => { let url ; if ( this . platform . is ( 'android' ) ) { url = 'https://play.google.com/store/apps/details?id=cash.simply.wallet' ; } else { url = 'https://itunes.apple.com/app/id1398370340' ; } this . iab . create ( url , '_system' ) ; } } ] } ) ; updateAlert . present ( ) ; } getContacts ( ) { return this . stored . preference . contacts . slice ( ) ; } async addContact ( address ) { address = address . trim ( ) . toLowerCase ( ) ; let contacts = this . stored . preference . contacts ; if ( contacts . indexOf ( address ) !== - 1 ) { return ; } contacts . push ( address ) ; contacts . sort ( ) ; await this . updateStorage ( ) ; } async removeContact ( address ) { address = address . trim ( ) . toLowerCase ( ) ; let contacts = this . stored . preference . contacts ; let i = contacts . indexOf ( address ) ; if ( i === - 1 ) { return ; } contacts . splice ( i , 1 ) ; await this . updateStorage ( ) ; } validatePaymail ( text ) { return text . match ( /^[^@:]+@[^@]+\.[^@]+$/g ) ? true : false ; } async savePaymentRefs ( paymentRefs ) { paymentRefs . forEach ( paymentRef => { paymentRef . expires = Date . now ( ) + 3600 * 1000 ; } ) ; this . stored . paymentRefs . unshift ( ... paymentRefs ) ; await this . updateStorage ( ) ; } async deletePaymentRefs ( targetPaymentRefs ) { this . stored . paymentRefs = this . stored . paymentRefs . filter ( existing => { return targetPaymentRefs . every ( target => target !== existing ) ; } ) ; await this . updateStorage ( ) ; } deleteExpiredPaymentRefs ( ) { let now = Date . now ( ) ; this . stored . paymentRefs = this . stored . paymentRefs . filter ( paymentRef => paymentRef . expires > now ) ; } findPaymentRefs ( hex ) { let tx = new bitcoincash . Transaction ( hex ) ; let targets = tx . outputs . map ( o => o . toObject ( ) ) ; let matched = [ ] ; let found = this . stored . paymentRefs . filter ( paymentRef => { return paymentRef . outputs . every ( output => { return targets . some ( target => { if ( target . script === output . script && target . satoshis === output . satoshis && matched . indexOf ( target ) === - 1 ) { matched . push ( target ) ; return true ; } else { return false ; } } ) ; } ) ; } ) ; return found ; } signMessage ( message , signingKey ) { return bitcoincash_Message . sign ( message , signingKey ) ; } async lookupPaymail ( paymail , amount , senderPaymail , signingKey ) { if ( ! senderPaymail ) { senderPaymail = 'someone@simply.cash' ; signingKey = new bitcoincash . PrivateKey ( 'KyWGFe4Xy2eotn8uVmA3YMVp3HsGdzvqpteZAt7Ax7WQf1SFRREN' ) ; } let dt = JSON . parse ( JSON . stringify ( { 'now' : new Date ( ) } ) ) . now ; let hashed = bitcoincash . crypto . Hash . sha256 ( Buffer . from ( senderPaymail + amount + dt + '' ) ) . toString ( 'hex' ) ; let signature = bitcoincash_Message . sign ( hashed , signingKey ) ; let result = await this . apiWS ( 'paymail.destination' , { paymail : paymail , senderRequest : { senderHandle : senderPaymail , senderPaymail : senderPaymail , dt : dt , amount : amount , signature : signature } } ) ; if ( amount !== result . outputs . map ( o => o . satoshis ) . reduce ( ( a , b ) => a + b , 0 ) ) { throw new Error ( 'invalid amount' ) ; } let outputs = result . outputs . map ( o => ( { script : o . script , satoshis : o . satoshis } ) ) ; let reference = result . reference ; return { paymail : paymail , reference : reference , outputs : outputs } ; } getHandle ( ) { return this . currentWallet . handle ; } getPaymail ( ) { return this . currentWallet . handle + '@simply.cash' ; } async refreshHandle ( ) { let currentWallet = this . currentWallet ; let currentHandle = this . getHandle ( ) ; let latestHandle = currentHandle ; let public_key = this . getIdentityPublicKey ( ) . toString ( ) ; try { latestHandle = await this . apiWS ( 'handle.get' , { public_key : public_key } ) ; } catch ( err ) { console . log ( err ) ; return ; } if ( this . currentWallet !== currentWallet ) { return ; } if ( latestHandle !== currentHandle ) { this . currentWallet . handle = latestHandle ; await this . updateStorage ( ) ; } } h_handleIsValid ( handle ) { if ( handle . match ( /^[a-z0-9_]{1,30}$/g ) ) { return true ; } else { this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'ERROR' ) , message : this . translate . instant ( 'ERR_INVALID_HANDLE' ) , buttons : [ this . translate . instant ( 'OK' ) ] } ) . present ( ) ; return false ; } } async h_handleIsAvailable ( handle ) { try { let result = await this . apiWS ( 'handle.is_available' , { handle : handle } ) ; if ( result === 'true' ) { return true ; } this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'ERROR' ) , message : this . translate . instant ( 'ERR_HANDLE_NOT_AVAILABLE' ) , buttons : [ this . translate . instant ( 'OK' ) ] } ) . present ( ) ; return false ; } catch ( err ) { console . log ( err ) ; this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'ERROR' ) , buttons : [ this . translate . instant ( 'OK' ) ] } ) . present ( ) ; return false ; } } async h_doPOW ( handle , public_key , _threshold ) { let threshold = Buffer . from ( _threshold , 'hex' ) ; let byteLength = threshold . length + 2 ; let buf = Buffer . concat ( [ Buffer . from ( handle , 'utf8' ) , Buffer . from ( public_key , 'hex' ) , Buffer . alloc ( byteLength ) ] ) ; let offset = buf . length - byteLength ; let i = 0 ; let valid = false ; while ( ! valid ) { if ( i % 10000 === 9999 ) { await this . delay ( 0 ) ; } buf . writeUIntBE ( i , offset , byteLength ) ; valid = bitcoincash . crypto . Hash . sha256 ( buf ) . slice ( 0 , threshold . length ) . compare ( threshold ) <= 0 ; i ++ ; } return buf . slice ( - byteLength ) . toString ( 'hex' ) ; } async h_register ( handle , privateKey ) { let loader = this . loadingCtrl . create ( { content : this . translate . instant ( 'PLEASE_WAIT' ) } ) ; await loader . present ( ) ; try { let threshold = await this . apiWS ( 'handle.pow_threshold' ) ; let public_key = privateKey . publicKey . toString ( ) ; let address = this . getCacheReceiveAddress ( ) ; let pow = await this . h_doPOW ( handle , public_key , threshold ) ; let timestamp = Math . floor ( new Date ( ) . getTime ( ) / 1000 ) ; let message = [ 'register' , handle , public_key , address , timestamp ] . join ( ':' ) ; let signature = bitcoincash_Message . sign ( message , privateKey ) ; await this . apiWS ( 'handle.register' , { handle : handle , public_key : public_key , address : address , pow : pow , timestamp : timestamp , signature : signature } ) ; await this . refreshHandle ( ) ; await loader . dismiss ( ) ; } catch ( err ) { console . log ( err ) ; await this . refreshHandle ( ) ; await loader . dismiss ( ) ; this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'ERROR' ) , buttons : [ this . translate . instant ( 'OK' ) ] } ) . present ( ) ; } } async promptForHandle ( ) { let privateKey ; try { let m = await this . authorize ( ) ; privateKey = this . getIdentityPrivateKey ( m ) ; } catch ( err ) { if ( err . message !== 'cancelled' ) { console . log ( err ) ; } return ; } return new Promise < void > ( ( resolve , reject ) => { let freeze = false ; let handleAlert = this . alertCtrl . create ( { enableBackdropDismiss : false , cssClass : 'promptForHandleCSSClass' , title : this . translate . instant ( 'ENTER_HANDLE' ) , inputs : [ { name : 'handle' , type : 'password' , placeholder : 'a-z 0-9 _' } ] , buttons : [ { role : 'cancel' , text : this . translate . instant ( 'CANCEL' ) , handler : data => { if ( freeze ) { return false ; } handleAlert . dismiss ( ) . then ( ( ) => { resolve ( ) ; } ) ; return false ; } } , { text : this . translate . instant ( 'OK' ) , handler : data => { if ( freeze ) { return false ; } let handle = data . handle . trim ( ) . toLowerCase ( ) ; if ( ! handle || ! this . h_handleIsValid ( handle ) ) { return false ; } freeze = true ; this . h_handleIsAvailable ( handle ) . then ( ( isAvailable ) => { freeze = false ; if ( ! isAvailable ) { return ; } handleAlert . dismiss ( ) . then ( ( ) => { return this . h_register ( handle , privateKey ) ; } ) . then ( ( ) => { resolve ( ) ; } ) ; } ) ; return false ; } } ] } ) ; handleAlert . present ( ) . then ( ( ) => { Array . from ( window . document . querySelectorAll ( '.promptForHandleCSSClass input' ) ) . forEach ( ( el ) => { el . setAttribute ( 'type' , 'text' ) ; el . setAttribute ( 'autocomplete' , 'off' ) ; el . setAttribute ( 'autocorrect' , 'off' ) ; } ) ; } ) ; } ) . catch ( ( err ) => { console . log ( err ) ; } ) ; } promptForRecovery ( autoFill ? , compliant ? ) { compliant = compliant === false ? false : true ; return new Promise < void > ( ( resolve , reject ) => { let recoverAlert = this . alertCtrl . create ( { enableBackdropDismiss : false , cssClass : 'promptForRecoveryCSSClass' , title : this . translate . instant ( compliant ? 'RECOVER_WALLET' : 'RECOVER_NON_COMPLIANT_WALLET' ) , inputs : [ { name : 'name' , placeholder : this . nextWalletName ( ) } , { name : 'mnemonicOrXprvOrXpub' , type : 'password' , value : autoFill || '' , placeholder : this . translate . instant ( compliant ? 'RECOVERY_PHRASE_OR_XPRV' : 'RECOVERY_PHRASE' ) } , { name : 'path' , type : 'password' , placeholder : "m/44'/145'/0'" } , { name : 'passphrase' , type : 'password' , placeholder : this . translate . instant ( 'RECOVERY_PASSPHRASE' ) } ] , buttons : [ { role : 'cancel' , text : this . translate . instant ( 'CANCEL' ) , handler : data => { recoverAlert . dismiss ( ) . then ( ( ) => { resolve ( ) ; } ) ; return false ; } } , { text : this . translate . instant ( 'OK' ) , handler : data => { if ( data . name && ! this . r_nameIsValid ( data . name ) || data . path && ! this . r_pathIsValid ( data . path ) ) { return false ; } if ( compliant ) { if ( data . mnemonicOrXprvOrXpub ) { let validated = this . r_validatedMnemonicOrXprvOrXpub ( data . mnemonicOrXprvOrXpub ) ; if ( ! validated ) { return false ; } data . mnemonicOrXprvOrXpub = validated ; } } else { let validated = this . r_validatedMnemonicOnly ( data . mnemonicOrXprvOrXpub ) ; if ( ! validated ) { return false ; } data . mnemonicOrXprvOrXpub = validated ; } recoverAlert . dismiss ( ) . then ( ( ) => { return this . r_recover ( data . mnemonicOrXprvOrXpub , data . path , data . passphrase , data . name , compliant ) ; } ) . catch ( ( err ) => { console . log ( err ) ; } ) . then ( ( ) => { resolve ( ) ; } ) ; return false ; } } ] } ) ; recoverAlert . present ( ) . then ( ( ) => { Array . from ( window . document . querySelectorAll ( '.promptForRecoveryCSSClass input' ) ) . forEach ( ( el ) => { el . setAttribute ( 'type' , 'text' ) ; el . setAttribute ( 'autocomplete' , 'off' ) ; el . setAttribute ( 'autocorrect' , 'off' ) ; } ) ; window . setTimeout ( ( ) => { ( window . document . querySelector ( '.promptForRecoveryCSSClass input' ) as any ) . blur ( ) ; } , 0 ) ; } ) ; } ) . catch ( ( err ) => { console . log ( err ) ; } ) ; } r_nameIsValid ( name ) { if ( this . getAllWalletNames ( ) . indexOf ( name ) !== - 1 ) { this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'ERROR' ) , message : this . translate . instant ( 'ERR_INVALID_WALLET_NAME' ) , buttons : [ this . translate . instant ( 'OK' ) ] } ) . present ( ) ; return false ; } else { return true ; } } r_validatedMnemonicOnly ( m ) { let errMessage ; m = this . formatMnemonic ( m ) ; if ( ! this . validateMnemonic ( m ) ) { m = this . autocorrectMnemonic ( m ) ; if ( ! this . validateMnemonic ( m ) ) { errMessage = this . translate . instant ( 'ERR_INVALID_RECOVERY_PHRASE' ) ; } } if ( errMessage ) { this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'ERROR' ) , message : errMessage , buttons : [ this . translate . instant ( 'OK' ) ] } ) . present ( ) ; } else { return m ; } } r_validatedMnemonicOrXprvOrXpub ( m ) { let errMessage ; m = m . trim ( ) ; if ( m . match ( /^xpub.+$/gi ) ) { if ( ! this . validateXpub ( m ) ) { errMessage = this . translate . instant ( 'ERR_INVALID_XPUB' ) ; } } else if ( m . match ( /^xprv.+$/gi ) ) { if ( ! this . validateXprv ( m ) ) { errMessage = this . translate . instant ( 'ERR_INVALID_XPRV' ) ; } } else if ( m . match ( /^[0-1]+$/g ) ) { if ( m . length < 128 ) { errMessage = this . translate . instant ( 'ERR_NOT_ENOUGH_ENTROPY' ) + ' ' + m . length + '/128' ; } else { m = this . r_entropyToMnemonic ( m , 2 ) ; } } else if ( m . match ( /^[1-6]+$/g ) ) { if ( m . length < 50 ) { errMessage = this . translate . instant ( 'ERR_NOT_ENOUGH_ENTROPY' ) + ' ' + m . length + '/50' ; } else { m = m . split ( '' ) . map ( s => s === '6' ? '0' : s ) . join ( '' ) ; m = this . r_entropyToMnemonic ( m , 6 ) ; } } else { m = this . formatMnemonic ( m ) ; if ( ! this . validateMnemonic ( m ) ) { m = this . autocorrectMnemonic ( m ) ; if ( ! this . validateMnemonic ( m ) ) { errMessage = this . translate . instant ( 'ERR_INVALID_RECOVERY_PHRASE' ) ; } } } if ( errMessage ) { this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'ERROR' ) , message : errMessage , buttons : [ this . translate . instant ( 'OK' ) ] } ) . present ( ) ; } else { return m ; } } r_entropyToMnemonic ( entropy , base ) { let length = Math . floor ( entropy . length * Math . log2 ( base ) ) ; length = ( length - length % 32 ) / 8 ; let buf = new BN ( entropy , base ) . toArrayLike ( Buffer , 'be' , length + 4 ) . slice ( - length ) ; return new bitcoincash_Mnemonic ( buf ) . phrase ; } r_pathIsValid ( path ) { path = path . trim ( ) . replace ( /[‘’]/g , "'" ) ; if ( ! path . match ( /^m(\/\d+'?)*$/g ) ) { this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'ERROR' ) , message : this . translate . instant ( 'ERR_INVALID_DERIVATION_PATH' ) , buttons : [ this . translate . instant ( 'OK' ) ] } ) . present ( ) ; return false ; } else { return true ; } } async r_recover ( mnemonicOrXprvOrXpub , path , passphrase , name , compliant ) { path = path ? path . trim ( ) . replace ( /[‘’]/g , "'" ) : undefined ; passphrase = passphrase || undefined ; name = name || undefined ; let translations ; if ( mnemonicOrXprvOrXpub ) { translations = [ 'RECOVERING' , 'RECOVER_SUCCESS' , 'RECOVER_FAILED' ] ; } else { translations = [ 'CREATING' , 'CREATE_SUCCESS' , 'CREATE_FAILED' ] ; } let error ; let loader = this . loadingCtrl . create ( { content : this . translate . instant ( translations [ 0 ] ) + '...' } ) ; await loader . present ( ) ; try { await this . recoverWalletFromMnemonicOrXprvOrXpub ( mnemonicOrXprvOrXpub , path , passphrase , name , compliant ) ; } catch ( err ) { console . log ( err ) ; error = err ; } await loader . dismiss ( ) ; if ( ! error ) { await this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'SUCCESS' ) , message : this . translate . instant ( translations [ 1 ] ) , buttons : [ this . translate . instant ( 'OK' ) ] } ) . present ( ) ; } else { await this . alertCtrl . create ( { enableBackdropDismiss : false , title : this . translate . instant ( 'ERROR' ) , message : this . translate . instant ( translations [ 2 ] ) , buttons : [ this . translate . instant ( 'OK' ) ] } ) . present ( ) ; } } apiWS ( method , params ? , force ? , timeout ? ) { let socket = this . socket ; if ( typeof method === 'undefined' ) { return Promise . reject ( new Error ( 'no method' ) ) ; } if ( typeof params === 'undefined' ) { params = { } ; } if ( typeof force === 'undefined' ) { force = false ; } if ( typeof timeout === 'undefined' ) { timeout = 30000 ; } if ( ! force && ! this . isOnline ( ) ) { return Promise . reject ( new Error ( 'not connected' ) ) ; } return new Promise < any > ( ( resolve , reject ) => { let id = this . socketRequestId ++ ; let cb ; let timer ; cb = ( data ) => { if ( data . id !== id ) { return ; } window . clearTimeout ( timer ) ; socket . off ( 'response' , cb ) ; if ( typeof data . result !== 'undefined' ) { resolve ( data . result ) ; } else { try { reject ( new Error ( data . error . message ) ) ; } catch ( err ) { reject ( new Error ( 'unknown error' ) ) ; } } } ; socket . on ( 'response' , cb ) ; timer = window . setTimeout ( ( ) => { socket . off ( 'response' , cb ) ; reject ( new Error ( 'timeout' ) ) ; } , timeout ) ; socket . emit ( 'request' , { id : id , method : method , params : params } ) ; } ) ; } }