UNPKG

@harmony-js/account

Version:

account and wallet for harmony

457 lines (436 loc) 13.7 kB
/** * @packageDocumentation * @module harmony-account */ import { bip39, hdkey, EncryptOptions, getAddress, generatePrivateKey } from '@harmony-js/crypto'; import { Messenger } from '@harmony-js/network'; import { isPrivateKey, isAddress, ChainType } from '@harmony-js/utils'; import { Transaction } from '@harmony-js/transaction'; import { StakingTransaction } from '@harmony-js/staking'; import { Account } from './account'; import { defaultMessenger } from './utils'; class Wallet { // static method generate Mnemonic static generateMnemonic(): string { return bip39.generateMnemonic(); } /** @hidden */ messenger: Messenger; /** @hidden */ protected defaultSigner?: string; /** * @hidden */ private accountMap: Map<string, Account> = new Map(); /** * get acounts addresses * * @return {string[]} accounts addresses * * @example * ```javascript * const wallet = new Wallet(customMessenger); * const key_1 = '45e497bd45a9049bcb649016594489ac67b9f052a6cdf5cb74ee2427a60bf25e'; * wallet.addByPrivateKey(key_1); * * console.log(wallet.accounts); * ``` */ get accounts(): string[] { return [...this.accountMap.keys()]; } /** * get the signer of the account, by default, using the first account * * @example * ```javascript * const wallet = new Wallet(customMessenger); * const key_1 = '45e497bd45a9049bcb649016594489ac67b9f052a6cdf5cb74ee2427a60bf25e'; * wallet.addByPrivateKey(key_1); * * console.log(wallet.signer) * ``` */ get signer(): Account | undefined { if (this.defaultSigner) { return this.getAccount(this.defaultSigner); } else if (!this.defaultSigner && this.accounts.length > 0) { this.setSigner(this.accounts[0]); return this.getAccount(this.accounts[0]); } else { return undefined; } } /** * @example * ``` * const { Wallet } = require('@harmony-js/account'); * const { HttpProvider, Messenger } = require('@harmony-js/network'); * const { ChainType, ChainID } = require('@harmony-js/utils'); * * // create a custom messenger * const customMessenger = new Messenger( * new HttpProvider('http://localhost:9500'), * ChainType.Harmony, // if you are connected to Harmony's blockchain * ChainID.HmyLocal, // check if the chainId is correct * ) * * const wallet = new Wallet(customMessenger); * ``` */ constructor(messenger: Messenger = defaultMessenger) { this.messenger = messenger; } /** * @function newMnemonic * @memberof Wallet * @return {string} Mnemonics */ newMnemonic(): string { return Wallet.generateMnemonic(); } /** * Add account using Mnemonic phrases * @param {string} phrase - Mnemonic phrase * @param {index} index - index to hdKey root * * @example * ```javascript * const mnemonic_1 = 'urge clog right example dish drill card maximum mix bachelor section select'; * const wallet = new Wallet(customMessenger); * wallet.addByMnemonic(mnemonic_1); * * console.log(wallet.accounts); * ``` */ addByMnemonic(phrase: string, index: number = 0) { if (!this.isValidMnemonic(phrase)) { throw new Error(`Invalid mnemonic phrase: ${phrase}`); } const seed = bip39.mnemonicToSeed(phrase); const hdKey = hdkey.fromMasterSeed(seed); // TODO:hdkey should apply to Harmony's settings const path = this.messenger.chainType === ChainType.Harmony ? '1023' : '60'; const childKey = hdKey.derive(`m/44'/${path}'/0'/0/${index}`); const privateKey = childKey.privateKey.toString('hex'); return this.addByPrivateKey(privateKey); } /** * Add an account using privateKey * * @param {string} privateKey - privateKey to add * @return {Account} return added Account * * @example * ```javascript * const wallet = new Wallet(customMessenger); * const key_1 = '45e497bd45a9049bcb649016594489ac67b9f052a6cdf5cb74ee2427a60bf25e'; * console.log(wallet.addByPrivateKey(key_1)); * ``` */ addByPrivateKey(privateKey: string): Account { try { const newAcc = Account.add(privateKey); newAcc.setMessenger(this.messenger); if (newAcc.address) { this.accountMap.set(newAcc.address, newAcc); if (!this.defaultSigner) { this.setSigner(newAcc.address); } return newAcc; } else { throw new Error('add account failed'); } } catch (error) { throw error; } } /** * Add an account using privateKey * @param {string} keyStore - keystore jsonString to add * @param {string} password - password to decrypt the file * @return {Account} return added Account */ async addByKeyStore(keyStore: string, password: string): Promise<Account> { try { const newAcc = new Account(undefined); const result = await newAcc.fromFile(keyStore, password); result.setMessenger(this.messenger); if (result.address) { this.accountMap.set(result.address, result); if (!this.defaultSigner) { this.setSigner(result.address); } return result; } else { throw new Error('add account failed'); } } catch (error) { throw error; } } /** * create a new account using Mnemonic * @return {Account} {description} * * @example * ```javascript * console.log(wallet.accounts); * wallet.createAccount(); * wallet.createAccount(); * * console.log(wallet.accounts); * ```` */ async createAccount(password?: string, options?: EncryptOptions): Promise<Account> { const prv = generatePrivateKey(); const acc = this.addByPrivateKey(prv); if (acc.address && password) { const encrypted = await this.encryptAccount(acc.address, password, options); return encrypted; } else if (acc.address && !password) { return acc; } else { throw new Error('create acount failed'); } } /** * To encrypt an account that lives in the wallet. * if encrypted, returns original one, if not found, throw error * @param {string} address - address in accounts * @param {string} password - string that used to encrypt * @param {EncryptOptions} options - encryption options * @return {Promise<Account>} * * @example * ```javascript * const key_1 = '45e497bd45a9049bcb649016594489ac67b9f052a6cdf5cb74ee2427a60bf25e'; * wallet.addByPrivateKey(key_1); * wallet.encryptAccount('one103q7qe5t2505lypvltkqtddaef5tzfxwsse4z7', '12345').then((value) => { * console.log(value); * }) * ``` */ async encryptAccount( address: string, password: string, options?: EncryptOptions, ): Promise<Account> { try { const foundAcc = this.getAccount(address); if (foundAcc && foundAcc.privateKey && isPrivateKey(foundAcc.privateKey)) { await foundAcc.toFile(password, options); return foundAcc; } else if (foundAcc && foundAcc.privateKey && !isPrivateKey(foundAcc.privateKey)) { return foundAcc; } else { throw new Error('encrypt account failed'); } } catch (error) { throw error; } } /** * To decrypt an account that lives in the wallet,if not encrypted, return original, * if not found, throw error * @param {string} address - address in accounts * @param {string} password - string that used to encrypt * @return {Promise<Account>} * * @example * ```javascript * const key_1 = '45e497bd45a9049bcb649016594489ac67b9f052a6cdf5cb74ee2427a60bf25e'; * wallet.addByPrivateKey(key_1); * wallet.encryptAccount('one103q7qe5t2505lypvltkqtddaef5tzfxwsse4z7', '12345') * .then(() => { * wallet.decryptAccount('one103q7qe5t2505lypvltkqtddaef5tzfxwsse4z7', '12345') * .then((value) =>{ * console.log(value); * }); * }); * ``` */ async decryptAccount(address: string, password: string): Promise<Account> { try { const foundAcc = this.getAccount(address); if (foundAcc && foundAcc.privateKey && !isPrivateKey(foundAcc.privateKey)) { await foundAcc.fromFile(foundAcc.privateKey, password); return foundAcc; } else if (foundAcc && foundAcc.privateKey && isPrivateKey(foundAcc.privateKey)) { foundAcc.encrypted = false; return foundAcc; } else { throw new Error('decrypt account failed'); } } catch (error) { throw error; } } /** * Get Account instance using address as param * @param {string} address - address hex * @return {Account} Account instance which lives in Wallet * * @example * ``` * const key_1 = '45e497bd45a9049bcb649016594489ac67b9f052a6cdf5cb74ee2427a60bf25e'; * wallet.addByPrivateKey(key_1); * console.log(wallet.getAccount('one103q7qe5t2505lypvltkqtddaef5tzfxwsse4z7')); * ``` */ getAccount(address: string): Account | undefined { return this.accountMap.get(getAddress(address).basicHex); } /** * @function removeAccount * @memberof Wallet * @description remove Account using address as param * @param {string} address: - address hex */ removeAccount(address: string): void { this.accountMap.delete(getAddress(address).basicHex); if (this.defaultSigner === address) { this.defaultSigner = undefined; } } /** * Set Customer Messenage * @param messenger * * @example * ```javascript * const customMessenger = new Messenger( * new HttpProvider('https://api.s0.b.hmny.io'), * ChainType.Harmony, // if you are connected to Harmony's blockchain * ChainID.HmyLocal, // check if the chainId is correct * ) * const wallet = new Wallet(); * wallet.setMessenger(customMessenger); * console.log(wallet.messenger); * ``` */ setMessenger(messenger: Messenger): void { this.messenger = messenger; } /** * Set signer * * @param address one of the address in the accounts */ setSigner(address: string): void { if (!isAddress(address) && !this.getAccount(address)) { throw new Error('could not set signer'); } this.defaultSigner = address; } async signTransaction( transaction: Transaction, account: Account | undefined = this.signer, // tslint:disable-next-line: no-unnecessary-initializer password: string | undefined = undefined, updateNonce: boolean = true, encodeMode: string = 'rlp', blockNumber: string = 'latest', ): Promise<Transaction> { const toSignWith = account || this.signer; if (!toSignWith) { throw new Error('no signer found or did not provide correct account'); } if (toSignWith instanceof Account && toSignWith.encrypted && toSignWith.address) { if (!password) { throw new Error('must provide password to further execution'); } try { const decrypted = await this.decryptAccount(toSignWith.address, password); const signed = await decrypted.signTransaction( transaction, updateNonce, encodeMode, blockNumber, ); await this.encryptAccount(toSignWith.address, password); return signed; } catch (error) { throw error; } } else if (toSignWith instanceof Account && !toSignWith.encrypted && toSignWith.address) { try { const signed = await toSignWith.signTransaction( transaction, updateNonce, encodeMode, blockNumber, ); return signed; } catch (error) { throw error; } } else { throw new Error('sign transaction failed'); } } async signStaking( staking: StakingTransaction, account: Account | undefined = this.signer, // tslint:disable-next-line: no-unnecessary-initializer password: string | undefined = undefined, updateNonce: boolean = true, encodeMode: string = 'rlp', blockNumber: string = 'latest', shardID: number = this.messenger.currentShard, ): Promise<StakingTransaction> { const toSignWith = account || this.signer; if (!toSignWith) { throw new Error('no signer found or did not provide correct account'); } if (toSignWith instanceof Account && toSignWith.encrypted && toSignWith.address) { if (!password) { throw new Error('must provide password to further execution'); } try { const decrypted = await this.decryptAccount(toSignWith.address, password); const signed = await decrypted.signStaking( staking, updateNonce, encodeMode, blockNumber, shardID, ); await this.encryptAccount(toSignWith.address, password); return signed; } catch (error) { throw error; } } else if (toSignWith instanceof Account && !toSignWith.encrypted && toSignWith.address) { try { const signed = await toSignWith.signStaking( staking, updateNonce, encodeMode, blockNumber, shardID, ); return signed; } catch (error) { throw error; } } else { throw new Error('sign transaction failed'); } } /** * @function isValidMnemonic * @memberof Wallet * @description check if Mnemonic is valid * @param {string} phrase - Mnemonic phrase * @return {boolean} * @ignore */ private isValidMnemonic(phrase: string): boolean { if (phrase.trim().split(/\s+/g).length < 12) { return false; } return bip39.validateMnemonic(phrase); } } export { Wallet };