caver-js
Version:
caver-js is a JavaScript API library that allows developers to interact with a Kaia node
447 lines (395 loc) • 18.6 kB
JavaScript
/*
Copyright 2020 The caver-js Authors
This file is part of the caver-js library.
The caver-js library is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
The caver-js library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with the caver-js. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable no-unused-vars */
/* eslint-disable class-methods-use-this */
const _ = require('lodash')
const utils = require('../../../caver-utils')
const PrivateKey = require('./privateKey')
const { KEY_ROLE, MAXIMUM_KEY_NUM, isMultipleKeysFormat, isRoleBasedKeysFormat } = require('./keyringHelper')
const Account = require('../../../caver-account')
const { fillWeightedMultiSigOptionsForRoleBased } = require('../../../caver-account/src/accountKey/accountKeyHelper')
const { validateForSigning, validateIndexWithKeys, encryptKey, formatEncrypted } = require('./keyringHelper')
/**
* `RoleBasedKeyring` is a class that stores the address of the account and the private keys to be used for each role in the form of an array.
*
* `RoleBasedKeyring` defines keys which is implemented as a two-dimensional array (empty keys looks like `[ [], [], [] ]`) that can include multiple keys for each role.
* The first array element defines the private key(s) for `roleTransactionKey`, the second defines private key(s) for `roleAccountUpdateKey`, and the third defines the private key(s) for `roleFeePayerKey`.
*
* @class
* @hideconstructor
*/
class RoleBasedKeyring {
/**
* creates a RoleBasedKeyring.
* @param {string} address - The address of keyring.
* @param {Array.<Array<string>>|Array.<Array<PrivateKey>>} keys - The keys to use in RoleBasedKeyring.
*/
constructor(address, keys) {
this.address = address
this.keys = keys
}
/**
* @type {string}
*/
get type() {
return 'RoleBasedKeyring'
}
set type(t) {
throw new Error(`keyring type cannot be set.`)
}
/**
* @type {string}
*/
get address() {
return this._address
}
set address(addressInput) {
if (!utils.isAddress(addressInput)) throw new Error(`Invalid address : ${addressInput}`)
this._address = utils.addHexPrefix(addressInput).toLowerCase()
}
/**
* @type {Array.<Array.<PrivateKey>>}
*/
get keys() {
return this._keys
}
set keys(keyInput) {
if (keyInput === null) {
this._key = null
return
}
this._keys = formattingForKeyInKeyring(keyInput)
}
/**
* @type {Array.<PrivateKey>}
*/
get roleTransactionKey() {
return this.getKeyByRole(KEY_ROLE.roleTransactionKey)
}
/**
* @type {Array.<PrivateKey>}
*/
get roleAccountUpdateKey() {
return this.getKeyByRole(KEY_ROLE.roleAccountUpdateKey)
}
/**
* @type {Array.<PrivateKey>}
*/
get roleFeePayerKey() {
return this.getKeyByRole(KEY_ROLE.roleFeePayerKey)
}
/**
* Returns public key strings for each roles.
*
* @example
* const publicKeys = keyring.getPublicKey()
*
* @param {boolean} [compressed] Whether in compressed format or not.
* @return {Array.<Array<string>>} The public keys of the keyring.
*/
getPublicKey(compressed = false) {
const publicKeys = generateKeysFormat()
for (let i = 0; i < KEY_ROLE.roleLast; i++) {
for (const k of this._keys[i]) {
publicKeys[i].push(k.getPublicKey(compressed))
}
}
return publicKeys
}
/**
* Returns a copied RoleBasedKeyring instance.
*
* @example
* const copied = keyring.copy()
*
* @return {RoleBasedKeyring} A copied RoleBasedKeyring instance.
*/
copy() {
return new RoleBasedKeyring(this.address, this.keys)
}
/**
* Signs with transactionHash with the private keys and returns signatures.
*
* If you want to define an `index` when using RoleBasedKeyring, the `index` must be less than the length of the specific role key.
* And `RoleBasedKeyring` has the private key(s) defined by {@link KeyringFactory.role|caver.wallet.keyring.role}, so signs using the key(s) defined in the role.
*
* If the user has not defined an `index` parameter, `keyring.sign` signs transaction using all the private keys used by the role.
* If `index` is defined, the `keyring.sign` signs transaction using only one private key at the index.
* The role used in caver-js can be checked through {@link KeyringFactory.role|caver.wallet.keyring.role}.
*
* @param {string} transactionHash The hash of transaction.
* @param {string|number} chainId The chainId specific to the network.
* @param {number} role A number indicating the role of the key. You can use `caver.wallet.keyring.role`.
* @param {number} [index] The index of the key to be used. If index is undefined, all private keys in keyring will be used.
* @return {SignatureData|Array.<SignatureData>} A {@link SignatureData} when `index` is deinfed, otherwise an array of {@link SignatureData}.
*/
sign(transactionHash, chainId, role, index) {
validateForSigning(transactionHash, chainId)
const keys = this.getKeyByRole(role)
if (index !== undefined) {
validateIndexWithKeys(index, keys.length)
return keys[index].sign(transactionHash, chainId)
}
const signatures = []
for (const k of keys) {
signatures.push(k.sign(transactionHash, chainId))
}
return signatures
}
/**
* Signs with transactionHash with the private keys and returns signatures which V is 0 or 1 (parity of the y-value of a secp256k1 signature).
*
* If you want to define an `index` when using RoleBasedKeyring, the `index` must be less than the length of the specific role key.
* And `RoleBasedKeyring` has the private key(s) defined by {@link KeyringFactory.role|caver.wallet.keyring.role}, so signs using the key(s) defined in the role.
*
* If the user has not defined an `index` parameter, `keyring.sign` signs transaction using all the private keys used by the role.
* If `index` is defined, the `keyring.sign` signs transaction using only one private key at the index.
* The role used in caver-js can be checked through {@link KeyringFactory.role|caver.wallet.keyring.role}.
*
* @example
* const signed = keyring.ecsign('0xe9a11d9ef95fb437f75d07ce768d43e74f158dd54b106e7d3746ce29d545b550', caver.wallet.keyring.role.roleTransactionKey)
* const signed = keyring.ecsign('0xe9a11d9ef95fb437f75d07ce768d43e74f158dd54b106e7d3746ce29d545b550', caver.wallet.keyring.role.roleAccountUpdateKey, 1)
*
* @param {string} hash The hashed data to sign.
* @param {number} role A number indicating the role of the key. You can use `caver.wallet.keyring.role`.
* @param {number} [index] The index of the key to be used. If index is undefined, all private keys in keyring will be used.
* @return {SignatureData|Array.<SignatureData>} A {@link SignatureData} when `index` is deinfed, otherwise an array of {@link SignatureData}.
*/
ecsign(hash, role, index) {
if (!utils.isValidHashStrict(hash)) throw new Error(`Invalid hash: ${hash}`)
const keys = this.getKeyByRole(role)
if (index !== undefined) {
validateIndexWithKeys(index, keys.length)
return keys[index].ecsign(hash)
}
const signatures = []
for (const k of keys) {
signatures.push(k.ecsign(hash))
}
return signatures
}
/**
* Signs message with Klaytn-specific prefix.
*
* This calculates a Klaytn-specific signature with:
* `sign(keccak256("\x19Klaytn Signed Message:\n" + len(message) + message)))`.
*
* If you want to define an `index` when using RoleBasedKeyring, the `index` must be less than the length of the specific role key.
* And `RoleBasedKeyring` has the private key(s) defined by {@link KeyringFactory.role|caver.wallet.keyring.role}, so signs using the key(s) defined in the role.
*
* If the user has not provided the `index` parameter, `caver.wallet.signMessage` signs message using all the private keys used by the role.
* If the `index` parameter is given, `caver.wallet.signMessage` signs message using only one private key at the given index.
* The role used in caver-js can be found from {@link KeyringFactory.role|caver.wallet.keyring.role}.
*
* @example
* const signed = keyring.signMessage('message to sign', caver.wallet.keyring.role.roleTransactionKey)
*
* @param {string} message The message string to sign.
* @param {number} role A number indicating the role of the key. You can use {@link KeyringFactory.role|caver.wallet.keyring.role}.
* @param {number} [index] The index of the key to be used.
* @return {KeyringContainer.SignedMessage} An object that includes the result of signing.
*/
signMessage(message, role, index) {
if (role === undefined) throw new Error(`role should be defined for signMessage. Please use 'caver.wallet.keyring.role'.`)
const messageHash = utils.hashMessage(message)
const keys = this.getKeyByRole(role)
const signatures = []
if (index !== undefined) {
validateIndexWithKeys(index, keys.length)
signatures.push(keys[index].signMessage(messageHash))
} else {
for (const k of keys) {
signatures.push(k.signMessage(messageHash))
}
}
return {
messageHash,
signatures,
message,
}
}
/**
* Returns the private key(s) used by the role entered as a parameter.
*
* @example
* const key = keyring.getKeyByRole(caver.wallet.keyring.role.roleTransactionKey)
*
* @param {number} role A number indicating the role of the key. You can use {@link KeyringFactory.role|caver.wallet.keyring.role}.
* @return {Array.<PrivateKey>} An instance of PrivateKey.
*/
getKeyByRole(role) {
if (role === undefined) throw new Error(`role should be defined.`)
if (role >= KEY_ROLE.roleLast || role < 0) throw new Error(`Invalid role number: ${role}`)
let key = this._keys[role]
if (key.length === 0 && role > KEY_ROLE.roleTransactionKey) {
if (this._keys[KEY_ROLE.roleTransactionKey].length === 0) {
throw new Error(
`The key with ${KEY_ROLE[role]} role does not exist. The ${KEY_ROLE[0]} for the default role is also empty.`
)
}
key = this._keys[KEY_ROLE.roleTransactionKey]
}
return key
}
/**
* Returns the {@link Account} instance for updating the {@link Account.AccountKey|AccountKey} of the Klaytn accounts.
* The {@link Account} instance has an {@link Account.AccountKey|AccountKey} instance that can contain public key(s) inside, which will be sent to Klaytn Network and used for validating transactions.
* Please note that if you update the AccountKey of the Account stored in the Klaytn, the old private key(s) cannot be used anymore.
*
* `RoleBasedKeyring` returns an {@link Account} instance that includes the address in the keyring and an instance of {@link AccountKeyRoleBased}.
*
* @example
* const account = keyring.toAccount()
*
* @param {Array.<WeightedMultiSigOptions>} [options] An array of the `WeightedMultiSigOptions` instances containing information that should be defined when updating your existing account to the one with a number of private keys. The `RoleBasedKeyring` uses different private keys for each role, a `WeightedMultiSigOptions` instance must be defined for each role in an array. If multiple keys are used and `options` are not defined for specific role, defualt WeightedMultiSigOptions (`{ threshold: 1, weights: [1, 1...}}`) is used.
* @return {Account} An Account instance to be used when a user updates AccountKey for their account in the Klaytn. Note that if you want to replace the existing keyring (or the existing private key) with a new keyring (or a new private key) for your account, you must update your AccountKey by sending an Account Update transaction to Klaytn beforehand.
*/
toAccount(options) {
if (options !== undefined && !_.isArray(options))
throw new Error(`options for an account should define threshold and weight for each roles in an array format`)
const lengths = []
for (const k of this.keys) lengths.push(k.length)
options = fillWeightedMultiSigOptionsForRoleBased(lengths, options)
const publicKeysByRole = this.getPublicKey()
return Account.createWithAccountKeyRoleBased(this.address, publicKeysByRole, options)
}
/**
* Encrypts a keyring and returns a keystore v4 standard.
* For more information, please refer to {@link https://kips.klaytn.com/KIPs/kip-3|KIP-3}.
*
* `options` can include below:
* {
* salt: ...,
* iv: ...,
* kdf: ...,
* dklen: ...,
* c: ...,
* n: ...,
* r: ...,
* p: ...,
* cipher: ...,
* uuid: ...,
* cipher: ...,
* }
*
* @example
* const encrypted = keyring.encrypt('password')
*
* @param {string} password The password to be used for encryption. The encrypted key store can be decrypted with this password.
* @param {object} [options] The options parameter allows you to specify the values to use when using encrypt.
* @return {KeyringFactory.Keystore} The encrypted keystore v4.
*/
encrypt(password, options = {}) {
const keyring = []
for (let i = KEY_ROLE.roleTransactionKey; i < KEY_ROLE.roleLast; i++) {
const roledKey = this._keys[i]
keyring.push(encryptKey(roledKey, password, options))
}
return formatEncrypted(4, this._address, keyring, options)
}
/**
* Returns the {@link https://docs.klaytn.com/klaytn/design/accounts#klaytn-wallet-key-format|KlaytnWalletKey} string for the keyring.
* This function will throw error because `MultipleKeyring` uses more than one private key.
*
* @example
* const klaytnWalletKey = keyring.getKlaytnWalletKey()
*
* @ignore
* @return {string}
*/
getKlaytnWalletKey() {
throw new Error(`Not supported for this class.`)
}
/**
* Encrypts a keyring and returns a keystore v3 object.
*
* Note that {@link MultipleKeyring} and {@link RoleBasedKeyring} cannot use encryptV3.
* In this case, please use {@link MultipleKeyring#encrypt|keyring.encrypt} with a keystore V4 standard.
*
* @example
* const encrypted = keyring.encryptV3('password')
*
* @ignore
* @param {string} password The password to be used for keyring encryption. The encrypted key store can be decrypted with this password.
* @param {object} [options] The options to use when encrypt a keyring. See `keyring.encrypt` for more detail about options.
* @return {object}
*/
encryptV3(password, options) {
throw new Error(`Not supported for this class. Use 'keyring.encrypt(password)'.`)
}
/**
* Returns `true` if keyring has decoupled key.
*
* @example
* const isDecupled = keyring.isDecoupled()
*
* @return {boolean} `true` if keyring has decoupled key.
*/
isDecoupled() {
return true
}
}
module.exports = RoleBasedKeyring
/**
* Format the key parameters passed by the user to create a keyring instance into a two-dimensional array containing PrivateKey instances.
*
* The cases of the parameter that the user passes to the function is as follows, and this function formats it as a two-dimensional array.
* PrivateKey instance: PrivateKey{}
* single private key string: `0x{private key}`
* multiple private key strings: [`0x{private key}`, `0x{private key}`]
* multiple PrivateKey instances: [PrivateKey{}, PrivateKey{}]
* role-based private key strings: [[`0x{private key}`], [`0x{private key}`, `0x{private key}`], [`0x{private key}`]]
* role-based PrivateKey instances: [[PrivateKey{}], [PrivateKey{}, PrivateKey{}], [PrivateKey{}]]
*
* @param {string|PrivateKey|Array.<string|PrivateKey>|Array.<Array.<string|PrivateKey>>} keyInput The input parameter for key variable in Keyring.
* @return {Array.<Array.<PrivateKey>>}
*/
function formattingForKeyInKeyring(keyInput) {
if (keyInput === null) {
return keyInput
}
if (keyInput instanceof PrivateKey || _.isString(keyInput)) {
keyInput = [[keyInput], [], []]
} else if (isMultipleKeysFormat(keyInput)) {
// [`0x{private key}`, `0x{private key}`, `0x{private key}`]
keyInput = [keyInput, [], []]
} else if (!isRoleBasedKeysFormat(keyInput)) {
throw new Error(`Invalid format for key variable in keyring`)
}
const keys = generateKeysFormat()
for (let i = 0; i < KEY_ROLE.roleLast; i++) {
fillRoleKey(keys, i, keyInput[i])
}
return keys
}
function generateKeysFormat() {
return Array(KEY_ROLE.roleLast)
.fill(null)
.map(() => [])
}
function fillRoleKey(keys, role, keyToAdd) {
if (keyToAdd === undefined) return
keyToAdd = Array.isArray(keyToAdd) ? keyToAdd : [keyToAdd]
if (keyToAdd.length > MAXIMUM_KEY_NUM)
throw new Error(`The maximum number of private keys that can be used in keyring is ${MAXIMUM_KEY_NUM}.`)
if (role >= KEY_ROLE.roleLast)
throw new Error(
`Unsupported role number. The role number should be less than ${KEY_ROLE.roleLast}. Please use 'caver.wallet.keyring.role'`
)
for (const keyString of keyToAdd) {
const key = keyString instanceof PrivateKey ? keyString : new PrivateKey(keyString)
keys[role].push(key)
}
}