import { Keypair } from '../base'
import sjcl from 'sjcl-tokend'
import * as crypto from './crypto'
import isNil from 'lodash/isNil'
import isString from 'lodash/isString'
/**
* Manages user's key pair.
*
* @class
*/
export class Wallet {
/**
* Create a new instance from user's key pair.
*
* @constructor
*
* @param {Keypair|string} keypair User's key pair or a secret seed.
* @param {string} accountId User's account ID.
* @param {string} [walletId] Wallet ID.
*/
constructor (keypair, accountId, walletId) {
if (isNil(keypair)) {
throw new Error('No keypair provided.')
} else if (isString(keypair)) {
if (!Keypair.isValidSecretKey(keypair)) {
throw new Error('Invalid secret seed.')
}
keypair = Keypair.fromSecret(keypair)
} else if (!(keypair instanceof Keypair)) {
throw new Error('Invalid keypair. Expected a Keypair instance or a string seed.')
}
if (!Keypair.isValidPublicKey(accountId)) {
throw new Error('Invalid account ID.')
}
if (walletId && !isString(walletId)) {
throw new Error('Hex encoded wallet ID expected.')
}
this._keypair = keypair
this._accountId = accountId
this._id = walletId
}
/**
* Clone a wallet.
*
* @return {Wallet} Cloned wallet.
*/
clone () {
return new Wallet(
this._keypair,
this._accountId,
this._id
)
}
/**
* Generate a new wallet.
*
* @param {string} [accountId] User's account ID.
*
* @return {Wallet} The new wallet.
*/
static generate (accountId = null) {
let keypair = Keypair.random()
accountId = accountId || keypair.accountId()
return new Wallet(
keypair,
accountId
)
}
/**
* Decrypt a wallet obtained from a wallet server.
*
* @param {object} keychainData Encrypted wallet seed.
* @param {object} kdfParams Scrypt params used for encryption.
* @param {string} salt Salt used for encryption.
* @param {string} password User's password.
*/
static fromEncrypted (keychainData, kdfParams, salt, password) {
let rawMasterKey = crypto.calculateMasterKey(
salt,
password,
kdfParams
)
let rawWalletId = crypto.deriveWalletId(rawMasterKey)
let rawWalletKey = crypto.deriveWalletKey(rawMasterKey)
let rawDecrypted = crypto.decryptData(keychainData, rawWalletKey)
let decryptedKeychain = JSON.parse(rawDecrypted)
return new Wallet(
Keypair.fromSecret(decryptedKeychain.masterSeed),
decryptedKeychain.accountId,
sjcl.codec.base64.fromBits(rawWalletId)
)
}
/**
* Derive the wallet ID.
*
* @param {string} password
* @param {object} kdfParams
* @param {string} salt
*
* @return {string} Wallet ID.
*/
static deriveId (password, kdfParams, salt) {
let masterKey = crypto.calculateMasterKey(salt, password, kdfParams)
let walletId = crypto.deriveWalletId(masterKey)
return sjcl.codec.base64.fromBits(walletId)
}
/**
* Wallet ID.
*/
get id () {
if (!this._id) {
throw new Error('This wallet has no wallet ID yet.')
}
return this._id
}
/**
* Account ID.
*/
get accountId () {
return this._accountId
}
/**
* Email used for login.
*/
get email () {
return this._email
}
/**
* Secret seed.
*/
get secretSeed () {
return this._keypair.secret()
}
/**
* Get signing keypair.
*/
get keypair () {
return this._keypair
}
/**
* Encrypt wallet to securely store it.
*
* @param {object} kdfParams Scrypt params.
* @param {string} password User's password.
* @return {object} Encrypted keychain and metadata.
*/
encrypt (kdfParams, password) {
if (isNil(kdfParams)) {
throw new Error('KDF params required')
}
if (!isString(password) || password.length === 0) {
throw new TypeError('Password must be a non-empty string')
}
let salt = crypto.randomBytes(16).toString('base64')
let masterKey = crypto.calculateMasterKey(
salt,
password,
kdfParams
)
// Encrypt data
let walletKey = crypto.deriveWalletKey(masterKey)
let rawKeychainData = {
accountId: this.accountId,
masterSeed: this._keypair.secret()
}
let keychainData = crypto.encryptData(
JSON.stringify(rawKeychainData),
walletKey
)
// Derive wallet ID
let rawWalletId = crypto.deriveWalletId(masterKey)
this._id = sjcl.codec.base64.fromBits(rawWalletId)
return {
kdfParams: JSON.stringify(kdfParams),
id: this._id,
accountId: this.accountId,
salt,
keychainData
}
}
}