src/wallet/localwallet.js
import Wallet from './wallet';
import { Wallet as EthersWallet } from '@ethersproject/wallet';
import { SigningException, UserRejectedSigningException, WalletNotFoundException, WalletLockedException, NotInBrowserException} from "../exceptions";
/**
* libsimba-js Local Wallet implementation
* Stores the wallet as encrypted json within the browsers localstorage
* Wraps the [ethersjs]{@link https://docs.ethers.io/ethers.js/html/} library.
*/
export default class LocalWallet extends Wallet {
/**
* Use a wallet stored in the browsers local storage
* @param {function} [signingConfirmation] - - an optional callback for requesting user permission to sign a
* transaction. Should resolve a promise with true for accept, and false (or reject) for reject.
*/
constructor(signingConfirmation) {
super(signingConfirmation);
if(typeof window === 'undefined') {
throw new NotInBrowserException("LocalWallet can only be used in a browser!");
}
this.window = window;
}
/**
* @override
* Unlock the wallet
* @param {string} passkey - The pass key to unlock the wallet
* @param {function} [progressCB] - A callback, accepting a number between 0-1, indicating decryption progress
* @returns {Promise} - Returns a promise resolving when the wallet is unlocked
*/
unlockWallet(passkey, progressCB){
return EthersWallet.fromEncryptedJson(this.window.localStorage.getItem('localwallet'), passkey, progressCB)
.then(wallet=>this.wallet = wallet);
}
/**
* @override
* Generate a wallet
* @param {string} passkey - The pass key to lock the wallet
* @param {function} [progressCB] - A callback, accepting a number between 0-1, indicating decryption progress
* @returns {Promise} - Returns a promise resolving when the wallet is created
*/
generateWallet(passkey, progressCB){
this.wallet = EthersWallet.createRandom();
return this.wallet.encrypt(passkey, progressCB).then((json)=>{
this.window.localStorage.setItem('localwallet', json);
return true;
});
}
/**
* Generate a wallet from an existing private key
* @param {string} key - The existing private key
* @param {string} passkey - The pass key to lock the wallet
* @param {function} [progressCB] - A callback, accepting a number between 0-1, indicating decryption progress
* @returns {Promise} - Returns a promise resolving when the wallet is created
*/
generateWalletFromPrivateKey(key, passkey, progressCB){
this.wallet = new EthersWallet(key);
return this.wallet.encrypt(passkey, progressCB).then((json)=>{
this.window.localStorage.setItem('localwallet', json);
return true;
});
}
/**
* Generate a wallet from a mnemonic
* @param {string} mnemonic - The mnemonic
* @param {string} passkey - The pass key to lock the wallet
* @param {function} [progressCB] - A callback, accepting a number between 0-1, indicating decryption progress
* @returns {Promise} - Returns a promise resolving when the wallet is created
*/
generateWalletFromMnemonic(mnemonic, passkey, progressCB){
this.wallet = EthersWallet.fromMnemonic(mnemonic);
return this.wallet.encrypt(passkey, progressCB).then((json)=>{
this.window.localStorage.setItem('localwallet', json);
return true;
});
}
/**
* Generate a wallet from a encrypted json (see
* [ethers docs]{@link https://docs.ethers.io/ethers.js/html/api-wallet.html?highlight=fromencryptedjson})
* @param {string} json - The json
* @param {string} passkey - The pass key to lock the wallet
* @param {function} [progressCB] - A callback, accepting a number between 0-1, indicating decryption progress
* @returns {Promise} - Returns a promise resolving when the wallet is created
*/
generateWalletFromEncryptedJson(json, passkey, progressCB){
this.wallet = EthersWallet.fromEncryptedJson(mnemonic, passkey);
return this.wallet.encrypt(passkey, progressCB).then((json)=>{
this.window.localStorage.setItem('localwallet', json);
return true;
});
}
/**
* @override
* Delete the wallet
*/
deleteWallet(){
this.window.localStorage.removeItem('localwallet');
}
/**
* @override
* Check if a wallet exists
* @return {boolean} - does the wallet exist
*/
walletExists(){
return !!this.window.localStorage.getItem('localwallet');
}
/**
* The mnemonic phrase for this wallet, or null if the mnemonic is unknown.
* @return {string} - The mnemonic phrase for this wallet, or null if the mnemonic is unknown.
*/
getMnemonic(){
if(!this.walletExists()){
throw new WalletNotFoundException("No wallet generated!")
}
if(!this.wallet) {
throw new WalletLockedException("Wallet not unlocked!");
}
return this.wallet.mnemonic;
}
/**
*
* @param {string} passkey - Passkey to encrypt the wallet
* @param {function} [progressCB] - An optional callback to monitor progress of encryption, calls with a value between 0-1
* @returns {Promise<string>} A promise that resolves with the JSON wallet
*/
getEncryptedJson(passkey, progressCB){
if(!this.walletExists()){
throw new WalletNotFoundException("No wallet generated!")
}
if(!this.wallet) {
throw new WalletLockedException("Wallet not unlocked!");
}
return this.wallet.encrypt(passkey, progressCB);
}
/**
* @protected
* @override
* Sign a transaction payload
* @param {Object} payload - The transaction to sign
* @returns {Promise<string>} - Returns a promise resolving to the signed transaction
*/
sign(payload) {
if(!this.walletExists()){
throw new WalletNotFoundException("No wallet generated!")
}
if(!this.wallet) {
throw new WalletLockedException("Wallet not unlocked!");
}
return this.signingConfirmation().then(allow=>{
if(allow){
const cleanedPayload = this.cleanPayload(payload);
return this.wallet.signTransaction(cleanedPayload)
.catch(error=>{throw new SigningException("Failed to sign transaction", error)});
}
throw new UserRejectedSigningException("User rejected signing");
});
}
/**
* @private
* Clean the payload before signing
* @param {Object} payload - The transaction to clean
* @returns {string} - The cleaned transaction
*/
cleanPayload(payload){
const allowedKeys = [ 'to','nonce','gasLimit','gasPrice', 'data','value','chainId'];
let cleanedPayload = {};
Object.keys(payload).forEach(key=>{
if(allowedKeys.indexOf(key) >= 0){
cleanedPayload[key] = payload[key];
if((typeof payload[key] === 'string' || payload[key] instanceof String) &&
payload[key].startsWith('0x') &&
payload[key].length % 2 !== 0){
cleanedPayload[key] = payload[key].replace('0x', '0x0');
console.log(`Bad Hex - txn.${key} = ${payload[key]}, reformatted to ${cleanedPayload[key]}`);
}
}
});
return cleanedPayload;
}
/**
* @override
* Get the wallets address
* @returns {Promise<string>} - Returns a promise resolving to the wallets address
*/
getAddress(){
return this.wallet.getAddress();
}
}