UNPKG

newpay-wallet-js

Version:

733 lines (638 loc) 30.5 kB
import iDB from "./idb-instance.js"; import idb_helper from "./idb-helper.js"; import {cloneDeep} from "lodash"; import PrivateKeyStore from "./PrivateKeyStore.js"; import {WalletTcomb} from "./tcomb_structs"; //import TransactionConfirmActions from "actions/TransactionConfirmActions"; import WalletUnlockActions from "./WalletUnlockActions.js"; //import PrivateKeyActions from "./PrivateKeyActions.js"; //import AccountActions from "./AccountActions.js"; import {ChainStore, PrivateKey, key, Aes} from "bitsharesjs/es"; import {Apis, ChainConfig} from "bitsharesjs-ws"; //import AddressIndex from "./AddressIndex.js"; import AccountRefsStore from "./AccountRefsStore.js"; import AccountStore from "./AccountStore.js"; let aes_private = null; let _passwordKey = null; // let transaction; let TRACE = false; let dictJson, AesWorker; if (false) { AesWorker = require("worker-loader?inline!workers/AesWorker"); dictJson = require("json-loader!common/dictionary_en.json"); } /** Represents a single wallet and related indexedDb database operations. */ class WalletDb{ constructor() { console.log("Walletdb constructor"); this.state = { wallet: null, saving_keys: false } // Confirm only works when there is a UI (this is for mocha unit tests) this.confirm_transactions = true ChainStore.subscribe(this.checkNextGeneratedKey.bind(this)) this.generateNextKey_pubcache = [] // WalletDb use to be a plan old javascript class (not an Alt store) so // for now many methods need to be exported... this.generatingKey = false; } static getInstance() { if (!WalletDb.instance) { WalletDb.instance = new WalletDb(); } return WalletDb.instance; } /** Discover derived keys that are not in this wallet */ checkNextGeneratedKey() { if( ! this.state.wallet) return; if( ! aes_private) return; // locked if( ! this.state.wallet.encrypted_brainkey) return; // no brainkey if(this.chainstore_account_ids_by_key === ChainStore.account_ids_by_key) return; // no change this.chainstore_account_ids_by_key = ChainStore.account_ids_by_key; // Helps to ensure we are looking at an un-used key try { this.generateNextKey( false /*save*/ ); } catch(e) { console.error(e); } } getWallet() { return this.state.wallet } onLock() { _passwordKey = null; aes_private = null; } isLocked() { return !(!!aes_private || !!_passwordKey); //return false; } decryptTcomb_PrivateKey(private_key_tcomb) { if( ! private_key_tcomb) return null if( this.isLocked() ){ throw new Error("wallet locked") } if (_passwordKey && _passwordKey[private_key_tcomb.pubkey]) { return _passwordKey[private_key_tcomb.pubkey]; } let private_key_hex = aes_private.decryptHex(private_key_tcomb.encrypted_key) return PrivateKey.fromBuffer(new Buffer(private_key_hex, 'hex')) } /** @return ecc/PrivateKey or null */ getPrivateKey(public_key) { if (_passwordKey) return _passwordKey[public_key]; if(! public_key) return null if(public_key.Q) public_key = public_key.toPublicKeyString() let private_key_tcomb = PrivateKeyStore.getTcomb_byPubkey(public_key) if(! private_key_tcomb) return null return this.decryptTcomb_PrivateKey(private_key_tcomb) } process_transaction(tr, signer_pubkeys, broadcast, extra_keys = []) { if(Apis.instance().chain_id !== this.state.wallet.chain_id) return Promise.reject("Mismatched chain_id; expecting " + this.state.wallet.chain_id + ", but got " + Apis.instance().chain_id) return WalletUnlockActions.unlock().then( () => { AccountStore.tryToSetCurrentAccount(); return Promise.all([ tr.set_required_fees(), tr.update_head_block() ]).then(()=> { let signer_pubkeys_added = {} if(signer_pubkeys) { // Balance claims are by address, only the private // key holder can know about these additional // potential keys. let pubkeys = PrivateKeyStore.getPubkeys_having_PrivateKey(signer_pubkeys) if( ! pubkeys.length) throw new Error("Missing signing key") for(let pubkey_string of pubkeys) { let private_key = this.getPrivateKey(pubkey_string) tr.add_signer(private_key, pubkey_string) signer_pubkeys_added[pubkey_string] = true } } return tr.get_potential_signatures().then( ({pubkeys, addys})=> { let my_pubkeys = PrivateKeyStore.getPubkeys_having_PrivateKey(pubkeys.concat(extra_keys), addys); console.log("pubkeys", pubkeys); //{//Testing only, don't send All public keys! // let pubkeys_all = PrivateKeyStore.getPubkeys() // All public keys // tr.get_required_signatures(pubkeys_all).then( required_pubkey_strings => // console.log('get_required_signatures all\t',required_pubkey_strings.sort(), pubkeys_all)) // tr.get_required_signatures(my_pubkeys).then( required_pubkey_strings => // console.log('get_required_signatures normal\t',required_pubkey_strings.sort(), pubkeys)) //} return tr.get_required_signatures(my_pubkeys).then( required_pubkeys => { for(let pubkey_string of required_pubkeys) { if(signer_pubkeys_added[pubkey_string]) continue let private_key = this.getPrivateKey(pubkey_string) if( ! private_key) // This should not happen, get_required_signatures will only // returned keys from my_pubkeys throw new Error("Missing signing key for " + pubkey_string) tr.add_signer(private_key, pubkey_string) } }) }).then(()=> { if(broadcast) { /* cyj delete 20171101 去掉web的界面刷新 if(this.confirm_transactions) { let p = new Promise((resolve, reject) => { TransactionConfirmActions.confirm(tr, resolve, reject) }) return p; } else */ let broadcast_timeout = setTimeout(() => { Promise.reject("Your transaction has expired without being confirmed, please try again later."); }, 15 * 2000); return tr.broadcast(() => { //广播成功 console.log("broadcast success"); }).then((res)=>{ console.log("tr end Info",res); clearTimeout(broadcast_timeout); let tr_res = {}; tr_res['block_num'] = res[0].block_num; tr_res['trx_num'] = res[0].trx_num; tr_res['txid'] = tr.id(); tr_res['fee'] = tr.serialize().operations[0][1].fee; return Promise.resolve(tr_res) }).catch( error => { clearTimeout(broadcast_timeout); // messages of length 1 are local exceptions (use the 1st line) // longer messages are remote API exceptions (use the 1st line) let splitError = error.message.split( "\n" ); let message = splitError[0]; //报错信息 return Promise.reject(message); }); } else return tr.serialize() }) }) }) } transaction_update() { let transaction = iDB.instance().db().transaction( ["wallet"], "readwrite" ) return transaction } transaction_update_keys() { let transaction = iDB.instance().db().transaction( ["wallet", "private_keys"], "readwrite" ) return transaction } getBrainKey() { let wallet = this.state.wallet if ( ! wallet.encrypted_brainkey) throw new Error("missing brainkey") if ( ! aes_private){ throw new Error("wallet locked") } let brainkey_plaintext = aes_private.decryptHexToText( wallet.encrypted_brainkey ) return brainkey_plaintext } getBrainKeyPrivate(brainkey_plaintext = this.getBrainKey()) { if( ! brainkey_plaintext) throw new Error("missing brainkey") return PrivateKey.fromSeed( key.normalize_brainKey(brainkey_plaintext) ) } onCreateWallet( password_plaintext, brainkey_plaintext, unlock = false, public_name = "default" ) { let walletCreateFct = (dictionary) => { //dictionary就是a-z的单词字典 return new Promise( (resolve, reject) => { if( typeof password_plaintext !== 'string') throw new Error("password string is required") let brainkey_backup_date if(brainkey_plaintext) { if(typeof brainkey_plaintext !== "string") throw new Error("Brainkey must be a string") if(brainkey_plaintext.trim() === "") throw new Error("Brainkey can not be an empty string") if(brainkey_plaintext.length < 50) throw new Error("Brainkey must be at least 50 characters long") // The user just provided the Brainkey so this avoids // bugging them to back it up again. brainkey_backup_date = new Date() } let password_aes = Aes.fromSeed( password_plaintext ) let encryption_buffer = key.get_random_key().toBuffer() // encryption_key is the global encryption key (does not change even if the passsword changes) let encryption_key = password_aes.encryptToHex( encryption_buffer ) // If unlocking, local_aes_private will become the global aes_private object let local_aes_private = Aes.fromSeed( encryption_buffer ) if( ! brainkey_plaintext) brainkey_plaintext = key.suggest_brain_key(dictionary.en) else brainkey_plaintext = key.normalize_brainKey(brainkey_plaintext) let brainkey_private = this.getBrainKeyPrivate( brainkey_plaintext ) let brainkey_pubkey = brainkey_private.toPublicKey().toPublicKeyString() let encrypted_brainkey = local_aes_private.encryptToHex( brainkey_plaintext ) let password_private = PrivateKey.fromSeed( password_plaintext ) let password_pubkey = password_private.toPublicKey().toPublicKeyString() let wallet = { public_name, password_pubkey, encryption_key, encrypted_brainkey, brainkey_pubkey, brainkey_sequence: 0, brainkey_backup_date, created: new Date(), last_modified: new Date(), chain_id: Apis.instance().chain_id } WalletTcomb(wallet) // validation let transaction = this.transaction_update() let add = idb_helper.add( transaction.objectStore("wallet"), wallet ) let end = idb_helper.on_transaction_end(transaction).then( () => { this.state.wallet = wallet if(unlock) { aes_private = local_aes_private; WalletUnlockActions.unlock(); } }) Promise.all([ add, end ]).then(() => { resolve(); }).catch(err => { reject(err); }) }) }; if (false) { //false return walletCreateFct(dictJson); } else { //let dictionaryPromise = brainkey_plaintext ? null : fetch(`${__BASE_URL__}dictionary.json`); let dictionaryPromise = brainkey_plaintext ? null : require("./lib/common/dictionary_en.json"); return Promise.all([ dictionaryPromise ]).then(res => { return brainkey_plaintext ? walletCreateFct(null) : walletCreateFct(res[0]); }).catch(err => { console.log("unable to fetch dictionary.json", err); return Promise.reject(err) }); } } generateKeyFromPassword(accountName, role, password) { let seed = accountName + role + password; let privKey = PrivateKey.fromSeed(seed); let pubKey = privKey.toPublicKey().toString(); return {privKey, pubKey}; } /** This also serves as 'unlock' */ validatePassword( password, unlock = false, account = null, roles = ["active", "owner", "memo"] ) { if (account) { let id = 0; function setKey(role, priv, pub) { if (!_passwordKey) _passwordKey = {}; _passwordKey[pub] = priv; id++; PrivateKeyStore.setPasswordLoginKey({ pubkey: pub, import_account_names: [account], encrypted_key: null, id, brainkey_sequence: null }); } /* Check if the user tried to login with a private key */ let fromWif; try { fromWif = PrivateKey.fromWif(password); } catch(err) { } let acc = ChainStore.getAccount(account, false); let key; if (fromWif) { key = {privKey: fromWif, pubKey: fromWif.toPublicKey().toString()}; } /* Test the pubkey for each role against either the wif key, or the password generated keys */ roles.forEach(role => { if (!fromWif) { key = this.generateKeyFromPassword(account, role, password); } let foundRole = false; if (acc) { if (role === "memo") { if (acc.getIn(["options", "memo_key"]) === key.pubKey) { setKey(role, key.privKey, key.pubKey); foundRole = true; } } else { acc.getIn([role, "key_auths"]).forEach(auth => { if (auth.get(0) === key.pubKey) { setKey(role, key.privKey, key.pubKey); foundRole = true; return false; } }); if (!foundRole) { let alsoCheckRole = role === "active" ? "owner" : "active"; acc.getIn([alsoCheckRole, "key_auths"]).forEach(auth => { if (auth.get(0) === key.pubKey) { setKey(alsoCheckRole, key.privKey, key.pubKey); foundRole = true; return false; } }); } } } }); return !!_passwordKey; } else { let wallet = this.state.wallet; try { let password_private = PrivateKey.fromSeed( password ); let password_pubkey = password_private.toPublicKey().toPublicKeyString(); if(wallet.password_pubkey !== password_pubkey) return false; if( unlock ) { let password_aes = Aes.fromSeed( password ); let encryption_plainbuffer = password_aes.decryptHexToBuffer( wallet.encryption_key ); aes_private = Aes.fromSeed( encryption_plainbuffer ); } return true; } catch(e) { console.error(e); return false; } } } /** This may lock the wallet unless <b>unlock</b> is used. */ changePassword( old_password, new_password, unlock = false ) { return new Promise( resolve => { let wallet = this.state.wallet if( ! this.validatePassword( old_password )) throw new Error("wrong password") let old_password_aes = Aes.fromSeed( old_password ) let new_password_aes = Aes.fromSeed( new_password ) if( ! wallet.encryption_key) // This change pre-dates the live chain.. throw new Error("This wallet does not support the change password feature.") let encryption_plainbuffer = old_password_aes.decryptHexToBuffer( wallet.encryption_key ) wallet.encryption_key = new_password_aes.encryptToHex( encryption_plainbuffer ) let new_password_private = PrivateKey.fromSeed( new_password ) wallet.password_pubkey = new_password_private.toPublicKey().toPublicKeyString() if( unlock ) { aes_private = Aes.fromSeed( encryption_plainbuffer ) } else { // new password, make sure the wallet gets locked aes_private = null } resolve( this.setWalletModified() ) }) } /** @throws "missing brainkey", "wallet locked" @return { private_key, sequence } */ generateNextKey(save = true) { if (this.generatingKey) return; this.generatingKey = true; let brainkey = this.getBrainKey() let wallet = this.state.wallet let sequence = Math.max(wallet.brainkey_sequence, 0); let used_sequence = null // Skip ahead in the sequence if any keys are found in use // Slowly look ahead (1 new key per block) to keep the wallet fast after unlocking this.brainkey_look_ahead = Math.min(10, (this.brainkey_look_ahead || 0) + 1) /* If sequence is 0 this is the first lookup, so check at least the first 10 positions */ const loopMax = !sequence ? Math.max(sequence + this.brainkey_look_ahead, 10) : sequence + this.brainkey_look_ahead; // console.log("generateNextKey, save:", save, "sequence:", sequence, "loopMax", loopMax, "brainkey_look_ahead:", this.brainkey_look_ahead); for (let i = sequence; i < loopMax; i++) { let private_key = key.get_brainPrivateKey( brainkey, i ) let pubkey = this.generateNextKey_pubcache[i] ? this.generateNextKey_pubcache[i] : this.generateNextKey_pubcache[i] = private_key.toPublicKey().toPublicKeyString() let next_key = ChainStore.getAccountRefsOfKey( pubkey ); // TODO if ( next_key === undefined ) return undefined /* If next_key exists, it means the generated private key controls an account, so we need to save it */ if(next_key && next_key.size) { used_sequence = i console.log("WARN: Private key sequence " + used_sequence + " in-use. " + "I am saving the private key and will go onto the next one.") this.saveKey( private_key, used_sequence ); // this.brainkey_look_ahead++; } } if(used_sequence !== null) { wallet.brainkey_sequence = used_sequence + 1 this._updateWallet() } sequence = Math.max(wallet.brainkey_sequence, 0); let private_key = key.get_brainPrivateKey( brainkey, sequence ) if( save && private_key ) { // save deterministic private keys ( the user can delete the brainkey ) // console.log("** saving a key and incrementing brainkey sequence **") this.saveKey( private_key, sequence ) //TODO .error( error => ErrorStore.onAdd( "wallet", "saveKey", error )) this.incrementBrainKeySequence() } this.generatingKey = false; return { private_key, sequence } } incrementBrainKeySequence( transaction ) { let wallet = this.state.wallet; // increment in RAM so this can't be out-of-sync wallet.brainkey_sequence ++; // update last modified return this._updateWallet( transaction ); //TODO .error( error => ErrorStore.onAdd( "wallet", "incrementBrainKeySequence", error )) } decrementBrainKeySequence() { let wallet = this.state.wallet; // increment in RAM so this can't be out-of-sync wallet.brainkey_sequence = Math.max(0, wallet.brainkey_sequence - 1); return this._updateWallet(); } resetBrainKeySequence() { let wallet = this.state.wallet // increment in RAM so this can't be out-of-sync wallet.brainkey_sequence = 0; console.log("reset sequence", wallet.brainkey_sequence); // update last modified return this._updateWallet() } /* cyj delete 20171024 //导入秘钥 importKeysWorker(private_key_objs) { return new Promise( (resolve, reject) => { let pubkeys = [] for(let private_key_obj of private_key_objs) pubkeys.push( private_key_obj.public_key_string ) let addyIndexPromise = AddressIndex.addAll(pubkeys) let private_plainhex_array = [] for(let private_key_obj of private_key_objs) { private_plainhex_array.push( private_key_obj.private_plainhex ); } if (!__ELECTRON__) { AesWorker = require("worker-loader!workers/AesWorker"); } let worker = new AesWorker worker.postMessage({ private_plainhex_array, key: aes_private.key, iv: aes_private.iv }) let _this = this this.setState({ saving_keys: true }) worker.onmessage = event => { try { console.log("Preparing for private keys save"); let private_cipherhex_array = event.data let enc_private_key_objs = [] for(let i = 0; i < private_key_objs.length; i++) { let private_key_obj = private_key_objs[i] let {import_account_names, public_key_string, private_plainhex} = private_key_obj let private_cipherhex = private_cipherhex_array[i] if( ! public_key_string) { // console.log('WARN: public key was not provided, this will incur slow performance') let private_key = PrivateKey.fromHex(private_plainhex) let public_key = private_key.toPublicKey() // S L O W public_key_string = public_key.toPublicKeyString() } else if(public_key_string.indexOf(ChainConfig.address_prefix) != 0) throw new Error("Public Key should start with " + ChainConfig.address_prefix) let private_key_object = { import_account_names, encrypted_key: private_cipherhex, pubkey: public_key_string // null brainkey_sequence } enc_private_key_objs.push(private_key_object) } console.log("Saving private keys", new Date().toString()); let transaction = _this.transaction_update_keys() let insertKeysPromise = idb_helper.on_transaction_end(transaction) try { let duplicate_count = PrivateKeyStore .addPrivateKeys_noindex(enc_private_key_objs, transaction ) if( private_key_objs.length != duplicate_count ) _this.setWalletModified(transaction) _this.setState({saving_keys: false}) resolve(Promise.all([ insertKeysPromise, addyIndexPromise ]).then( ()=> { console.log("Done saving keys", new Date().toString()) // return { duplicate_count } })) } catch(e) { transaction.abort() console.error(e) reject(e) } } catch( e ) { console.error('AesWorker.encrypt', e) }} }) } */ saveKeys(private_keys, transaction, public_key_string) { let promises = [] for(let private_key_record of private_keys) { promises.push( this.saveKey( private_key_record.private_key, private_key_record.sequence, null, //import_account_names public_key_string, transaction )) } return Promise.all(promises) } saveKey( private_key, brainkey_sequence, import_account_names, public_key_string, transaction = this.transaction_update_keys() ) { let private_cipherhex = aes_private.encryptToHex( private_key.toBuffer() ) let wallet = this.state.wallet if( ! public_key_string) { //S L O W // console.log('WARN: public key was not provided, this may incur slow performance') let public_key = private_key.toPublicKey() public_key_string = public_key.toPublicKeyString() } else if(public_key_string.indexOf(ChainConfig.address_prefix) != 0) throw new Error("Public Key should start with " + ChainConfig.address_prefix) let private_key_object = { import_account_names, encrypted_key: private_cipherhex, pubkey: public_key_string, brainkey_sequence } /* cyj delete 20171024 let p1 = PrivateKeyActions.addKey( private_key_object, transaction ).then((ret)=> { if(TRACE) console.log('... WalletDb.saveKey result',ret.result) return ret }) */ let p1 = Promise.all([PrivateKeyStore.onAddKey({private_key_object, transaction}), AccountRefsStore.onAddPrivateKey({private_key_object})]).then((ret)=> { if(TRACE) console.log('... WalletDb.saveKey result',ret[0].result) return ret[0]; }) // let p1 = PrivateKeyStore.onAddKey({private_key_object, transaction}).then((ret)=>{ // AccountRefsStore.onAddPrivateKey({private_key_object}); // return ret[0]; // }) return p1 } setWalletModified(transaction) { return this._updateWallet( transaction ) } setBackupDate() { let wallet = this.state.wallet wallet.backup_date = new Date() return this._updateWallet() } setBrainkeyBackupDate() { let wallet = this.state.wallet wallet.brainkey_backup_date = new Date() return this._updateWallet() } /** Saves wallet object to disk. Always updates the last_modified date. */ _updateWallet(transaction = this.transaction_update()) { let wallet = this.state.wallet if ( ! wallet) { reject("missing wallet") return } //DEBUG console.log('... wallet',wallet) let wallet_clone = cloneDeep( wallet ) wallet_clone.last_modified = new Date() WalletTcomb(wallet_clone) // validate let wallet_store = transaction.objectStore("wallet") let p = idb_helper.on_request_end( wallet_store.put(wallet_clone) ) let p2 = idb_helper.on_transaction_end( transaction ).then( () => { this.state.wallet = wallet_clone }) return Promise.all([p,p2]) } /** This method may be called again should the main database change */ loadDbData() { return idb_helper.cursor("wallet", cursor => { if( ! cursor) return false let wallet = cursor.value // Convert anything other than a string or number back into its proper type wallet.created = new Date(wallet.created) wallet.last_modified = new Date(wallet.last_modified) wallet.backup_date = wallet.backup_date ? new Date(wallet.backup_date):null wallet.brainkey_backup_date = wallet.brainkey_backup_date ? new Date(wallet.brainkey_backup_date):null try { WalletTcomb(wallet) } catch(e) { console.log("WalletDb format error", e); return Promise.reject(new Error("WalletDb format error "+e)); } this.state.wallet = wallet return false //stop iterating }); } } var WalletDbIns = WalletDb.getInstance(); export default WalletDbIns; function reject(error) { console.error( "----- WalletDb reject error -----", error) throw new Error(error) }