caver-js
Version:
caver-js is a JavaScript API library that allows developers to interact with a Kaia node
1,342 lines (1,144 loc) • 78 kB
JavaScript
/*
Modifications copyright 2018 The caver-js Authors
This file is part of web3.js.
web3.js 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.
web3.js 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 web3.js. If not, see <http://www.gnu.org/licenses/>.
This file is derived from web3.js/packages/web3-eth-accounts/src/index.js (2019/06/12).
Modified and improved for the caver-js development.
*/
/**
* @file accounts.js
* @author Fabian Vogelsteller <fabian@ethereum.org>
* @date 2017
*/
const _ = require('lodash')
const Promise = require('any-promise')
// account, hash, rlp, nat, bytes library will be used from 'eth-lib' temporarily.
const AccountLib = require('eth-lib/lib/account')
const Hash = require('eth-lib/lib/hash')
const RLP = require('eth-lib/lib/rlp')
const Nat = require('eth-lib/lib/nat')
const Bytes = require('eth-lib/lib/bytes')
const cryp = typeof global === 'undefined' ? require('crypto-browserify') : require('crypto')
const uuid = require('uuid')
const elliptic = require('elliptic')
const scrypt = require('scrypt-js')
const utils = require('../../../caver-utils')
const helpers = require('../../../caver-core-helpers')
const Method = require('../../../caver-core-method')
const core = require('../../../caver-core')
const {
encodeRLPByTxType,
makeRawTransaction,
getSenderTxHash,
decodeFromRawTransaction,
splitFeePayer,
extractSignatures,
} = require('./makeRawTransaction')
const secp256k1 = new elliptic.ec('secp256k1')
const AccountKeyPublic = require('./accountKey/accountKeyPublic')
const AccountKeyMultiSig = require('./accountKey/accountKeyMultiSig')
const AccountKeyRoleBased = require('./accountKey/accountKeyRoleBased')
const { AccountKeyEnum } = require('./accountKey/accountKeyEnum')
const Account = require('./account/account')
const AccountForUpdate = require('./account/accountForUpdate')
const { rpc } = require('../../../caver-rtm')
const isNot = function(value) {
return _.isUndefined(value) || _.isNull(value)
}
function coverInitialTxValue(tx) {
if (typeof tx !== 'object') throw new Error('Invalid transaction')
if (!tx.senderRawTransaction && (!tx.type || tx.type === 'LEGACY' || tx.type.includes('SMART_CONTRACT_DEPLOY'))) {
tx.to = tx.to || '0x'
tx.data = utils.addHexPrefix(tx.data || '0x')
}
tx.chainId = utils.numberToHex(tx.chainId)
return tx
}
/**
* resolveArgsForSignTransaction parse arguments for signTransaction.
*
* @method resolveArgsForSignTransaction
* @param {Object} args Parameters of signTransaction.
* @return {Object}
*/
function resolveArgsForSignTransaction(args) {
if (args.length === 0 || args.length > 3) {
throw new Error('Invalid parameter: The number of parameters is invalid.')
}
// privateKey and callback are optional parameter
// "args.length === 2" means that user sent parameter privateKey or callback
const tx = args[0]
let privateKey
let callback
if (!tx || (!_.isObject(tx) && !_.isString(tx))) {
throw new Error('Invalid parameter: The transaction must be defined as an object or RLP encoded string')
}
if (args.length === 2) {
if (_.isFunction(args[1])) {
callback = args[1]
} else {
privateKey = args[1]
}
} else if (args.length === 3) {
if (args[1] && typeof args[1] !== 'string' && !_.isArray(args[1])) {
throw new Error('Invalid parameter: The parameter for the private key is invalid')
}
privateKey = args[1]
callback = args[2]
}
// For handling when callback is undefined.
callback = callback || function() {}
return { tx, privateKey, callback }
}
/**
* resolveArgsForFeePayerSignTransaction parse arguments for feePayerSignTransaction.
*
* @method resolveArgsForFeePayerSignTransaction
* @param {Object} args Parameters of feePayerSignTransaction.
* @return {Object}
*/
function resolveArgsForFeePayerSignTransaction(args) {
if (args.length === 0 || args.length > 4) {
throw new Error('Invalid parameter: The number of parameters is invalid.')
}
// privateKey and callback are optional parameter
// "args.length === 3" means that user sent parameter privateKey or callback
const tx = args[0]
const feePayer = args[1]
let privateKey
let callback
if (!tx || (!_.isObject(tx) && !_.isString(tx))) {
throw new Error('Invalid parameter: The transaction must be defined as an object or RLP encoded string')
}
if (!utils.isAddress(feePayer)) {
throw new Error(`Invalid fee payer address : ${feePayer}`)
}
if (args.length === 3) {
if (_.isFunction(args[2])) {
callback = args[2]
} else {
privateKey = args[2]
}
} else if (args.length === 4) {
if (args[2] && typeof args[2] !== 'string' && !_.isArray(args[2])) {
throw new Error('Invalid parameter: The parameter for the private key is invalid')
}
privateKey = args[2]
callback = args[3]
}
// For handling when callback is undefined.
callback = callback || function() {}
return { tx, privateKey, feePayer, callback }
}
/**
* resolveArgsForSignTransactionWithHash parse arguments for signTransactionWithHash.
*
* @method resolveArgsForSignTransactionWithHash
* @param {Object} args Parameters of signTransactionWithHash.
* @return {Object}
*/
function resolveArgsForSignTransactionWithHash(args) {
if (args.length < 2 || args.length > 4) {
throw new Error('Invalid parameter: The number of parameters is invalid.')
}
const hash = args[0]
const privateKeys = args[1]
let chainId
let callback
if (!hash) {
throw new Error('Invalid parameter: The hash of transaction must be defined as a parameter.')
}
if (!utils.isValidHashStrict(hash)) {
throw new Error('Invalid parameter: The hash of transaction must be 0x-hex prefixed string format.')
}
if (!privateKeys || (!Array.isArray(privateKeys) && !_.isString(privateKeys))) {
throw new Error(`Invalid parameter: The private key should be a private key string or an array of private keys.`)
}
if (args.length === 3) {
if (_.isFunction(args[2])) {
callback = args[2]
} else {
chainId = args[2]
}
} else if (args.length === 4) {
if (args[2] && !_.isString(args[2]) && !_.isNumber(args[2])) {
throw new Error('Invalid parameter: The parameter for the chain id is invalid.')
}
chainId = args[2]
callback = args[3]
}
// For handling when callback is undefined.
callback = callback || function() {}
return { hash, privateKeys, chainId, callback }
}
function encryptKey(privateKey, password, options) {
const encryptedArray = []
if (!privateKey) return encryptedArray
const privateKeyArray = _.isArray(privateKey) ? privateKey : [privateKey]
for (let i = 0; i < privateKeyArray.length; i++) {
const salt = options.salt || cryp.randomBytes(32)
const iv = options.iv || cryp.randomBytes(16)
let derivedKey
const kdf = options.kdf || 'scrypt'
const kdfparams = {
dklen: options.dklen || 32,
salt: salt.toString('hex'),
}
/**
* Supported kdf modules are the following:
* 1) pbkdf2
* 2) scrypt - default
*/
if (kdf === 'pbkdf2') {
kdfparams.c = options.c || 262144
kdfparams.prf = 'hmac-sha256'
derivedKey = cryp.pbkdf2Sync(Buffer.from(password), Buffer.from(kdfparams.salt, 'hex'), kdfparams.c, kdfparams.dklen, 'sha256')
} else if (kdf === 'scrypt') {
// FIXME: support progress reporting callback
kdfparams.n = options.n || 4096 // 2048 4096 8192 16384
kdfparams.r = options.r || 8
kdfparams.p = options.p || 1
derivedKey = scrypt.syncScrypt(
Buffer.from(password),
Buffer.from(kdfparams.salt, 'hex'),
kdfparams.n,
kdfparams.r,
kdfparams.p,
kdfparams.dklen
)
} else {
throw new Error('Unsupported kdf')
}
const cipher = cryp.createCipheriv(options.cipher || 'aes-128-ctr', derivedKey.slice(0, 16), iv)
if (!cipher) {
throw new Error('Unsupported cipher')
}
const ciphertext = Buffer.from([...cipher.update(Buffer.from(privateKeyArray[i].replace('0x', ''), 'hex')), ...cipher.final()])
const mac = utils.sha3(Buffer.from([...derivedKey.slice(16, 32), ...ciphertext])).replace('0x', '')
encryptedArray.push({
ciphertext: ciphertext.toString('hex'),
cipherparams: {
iv: iv.toString('hex'),
},
cipher: options.cipher || 'aes-128-ctr',
kdf,
kdfparams,
mac: mac.toString('hex'),
})
}
return encryptedArray
}
const Accounts = function Accounts(...args) {
const _this = this
// sets _requestmanager
core.packageInit(this, args)
// remove unecessary core functions
delete this.BatchRequest
delete this.extend
const _klaytnCall = [rpc.getChainId, rpc.getGasPrice, rpc.getTransactionCount, rpc.getHeader]
// attach methods to this._klaytnCall
this._klaytnCall = {}
_.each(_klaytnCall, function(method) {
method = new Method(method)
method.attachToObject(_this._klaytnCall)
method.setRequestManager(_this._requestManager)
})
this.wallet = new Wallet(this)
}
Accounts.prototype._addAccountFunctions = function(account) {
const _this = this
// add sign functions
account.signTransaction = function signTransaction(tx, callback) {
const roleKey = _this._getRoleKey(tx, account)
return _this.signTransaction(tx, roleKey, callback)
}
account.feePayerSignTransaction = function feePayerSignTransaction(tx, callback) {
return _this.feePayerSignTransaction(tx, account.address, account.feePayerKey, callback)
}
account.sign = function sign(data) {
return _this.sign(data, account.privateKey)
}
account.encrypt = function encrypt(password, options = {}) {
options.address = account.address
return _this.encrypt(account.keys, password, options)
}
account.getKlaytnWalletKey = function getKlaytnWalletKey() {
return genKlaytnWalletKeyStringFromAccount(account)
}
return account
}
/**
* _determineAddress determines the priority of the parameters entered and returns the address that should be used for the account.
*
* @method _determineAddress
* @param {Object} legacyAccount Account with a legacy account key extracted from private key to be used for address determination.
* @param {String} addressFromKey Address extracted from key.
* @param {String} userInputAddress Address passed as parameter by user.
* @return {String}
*/
Accounts.prototype._determineAddress = function _determineAddress(legacyAccount, addressFromKey, userInputAddress) {
if (userInputAddress) {
if (addressFromKey && addressFromKey !== userInputAddress) {
throw new Error('The address extracted from the private key does not match the address received as the input value.')
}
if (!utils.isAddress(userInputAddress)) {
throw new Error('The address received as the input value is invalid.')
}
return userInputAddress
}
if (addressFromKey) {
if (!utils.isAddress(addressFromKey)) {
throw new Error('The address extracted from the private key is invalid.')
}
// If userInputAddress is undefined and address which is came from private is existed, set address in account.
return addressFromKey
}
return legacyAccount.address
}
/**
* _getRoleKey returns a key that matches the role that should be used according to the transaction.
*
* @method _getRoleKey
* @param {Object} tx transaction object to be sign.
* @param {Object} account Account to be used for signing.
* @return {String|Array}
*/
Accounts.prototype._getRoleKey = function _getRoleKey(tx, account) {
let key
if (!account) {
throw new Error('The account to be used for signing is not defined.')
}
if (tx.senderRawTransaction && tx.feePayer) {
key = account.feePayerKey
} else if (tx.type && tx.type.includes('ACCOUNT_UPDATE')) {
key = account.updateKey
} else {
key = account.transactionKey
}
if (!key) {
throw new Error('The key corresponding to the role used for signing is not defined.')
}
return key
}
/**
* _suggestGasPrice suggests a gas price.
* This function will be used to set gasPrice field if that is omitted.
* Before common architecture does not support newly added transaction types.
*
* @method _suggestGasPrice
* @return {string}
*/
Accounts.prototype._suggestGasPrice = async function _suggestGasPrice() {
const gasPrice = await this._klaytnCall.getGasPrice()
return gasPrice
}
/**
* create function creates random account with entropy.
*
* @method create
* @param {Object} entropy A random string to increase entropy.
* @return {Object}
*/
Accounts.prototype.create = function create(entropy) {
return this._addAccountFunctions(Account.fromObject(AccountLib.create(entropy || utils.randomHex(32))))
}
/**
* createAccountKey creates AccountKeyPublic, AccountKeyMultiSig or AccountKeyRoleBased instance with parameter.
*
* @method createAccountKey
* @param {String|Array|Object} accountKey Parameters to be used when creating the AccountKey.
* @return {Object}
*/
Accounts.prototype.createAccountKey = function createAccountKey(accountKey) {
if (Account.isAccountKey(accountKey)) accountKey = accountKey.keys
if (_.isString(accountKey)) {
accountKey = this.createAccountKeyPublic(accountKey)
} else if (_.isArray(accountKey)) {
accountKey = this.createAccountKeyMultiSig(accountKey)
} else if (_.isObject(accountKey)) {
accountKey = this.createAccountKeyRoleBased(accountKey)
} else {
throw new Error(`Invalid accountKey type: ${typeof accountKey}`)
}
return accountKey
}
/**
* createAccountKeyPublic creates AccountKeyPublic with a string of private key.
*
* @method createAccountKeyPublic
* @param {String} privateKey Private key string that will be used to create AccountKeyPublic.
* @return {Object}
*/
Accounts.prototype.createAccountKeyPublic = function createAccountKeyPublic(privateKey) {
if (privateKey instanceof AccountKeyPublic) return privateKey
if (!_.isString(privateKey)) {
throw new Error('Creating a AccountKeyPublic requires a private key string.')
}
const parsed = utils.parsePrivateKey(privateKey)
privateKey = parsed.privateKey
if (!utils.isValidPrivateKey(privateKey)) {
throw new Error(`Failed to create AccountKeyPublic. Invalid private key : ${privateKey}`)
}
return new AccountKeyPublic(privateKey)
}
/**
* createAccountKeyMultiSig creates AccountKeyMultiSig with an array of private keys.
*
* @method createAccountKeyMultiSig
* @param {Array} privateKeys An Array of private key strings that will be used to create AccountKeyMultiSig.
* @return {Object}
*/
Accounts.prototype.createAccountKeyMultiSig = function createAccountKeyMultiSig(privateKeys) {
if (privateKeys instanceof AccountKeyMultiSig) return privateKeys
if (!_.isArray(privateKeys)) {
throw new Error('Creating a AccountKeyMultiSig requires an array of private key string.')
}
for (let i = 0; i < privateKeys.length; i++) {
const parsed = utils.parsePrivateKey(privateKeys[i])
const p = parsed.privateKey
if (!utils.isValidPrivateKey(p)) {
throw new Error(`Failed to create AccountKeyMultiSig. Invalid private key : ${p}`)
}
}
return new AccountKeyMultiSig(privateKeys)
}
/**
* createAccountKeyRoleBased creates AccountKeyRoleBased with an object of key.
*
* @method createAccountKeyRoleBased
* @param {Object} keyObject Object that defines key for each role to use when creating AccountKeyRoleBased.
* @return {Object}
*/
Accounts.prototype.createAccountKeyRoleBased = function createAccountKeyRoleBased(keyObject) {
if (keyObject instanceof AccountKeyRoleBased) return keyObject
if (!_.isObject(keyObject) || _.isArray(keyObject)) {
throw new Error('Creating a AccountKeyRoleBased requires an object.')
}
return new AccountKeyRoleBased(keyObject)
}
/**
* accountKeyToPublicKey creates public key format with AccountKey.
*
* @method accountKeyToPublicKey
* @param {Object} accountKey AccountKey instance for which you want to generate a public key format.
* @return {String|Array|Object}
*/
Accounts.prototype.accountKeyToPublicKey = function accountKeyToPublicKey(accountKey) {
accountKey = this.createAccountKey(accountKey)
return accountKey.toPublicKey(this.privateKeyToPublicKey)
}
/**
* createWithAccountKey creates Account instance with AccountKey.
*
* @method createWithAccountKey
* @param {String} address The address of account.
* @param {String|Array|Object} accountKey The accountKey of account.
* @return {Object}
*/
Accounts.prototype.createWithAccountKey = function createWithAccountKey(address, accountKey) {
const account = new Account(address, this.createAccountKey(accountKey))
return this._addAccountFunctions(account)
}
/**
* createWithAccountKeyPublic create an account with AccountKeyPublic.
*
* @method createWithAccountKeyPublic
* @param {String} address An address of account.
* @param {String|Object} key Key of account.
* @return {Object}
*/
Accounts.prototype.createWithAccountKeyPublic = function createWithAccountKeyPublic(address, key) {
if (!Account.isAccountKey(key)) key = this.createAccountKeyPublic(key)
if (key.type !== AccountKeyEnum.ACCOUNT_KEY_PUBLIC) {
throw new Error(`Failed to create account with AccountKeyPublic. Invalid account key : ${key.type}`)
}
const account = new Account(address, key)
return this._addAccountFunctions(account)
}
/**
* createWithAccountKeyMultiSig create an account with AccountKeyMultiSig.
*
* @method createWithAccountKeyMultiSig
* @param {String} address An address of account.
* @param {String|Object} keys Key of account.
* @return {Object}
*/
Accounts.prototype.createWithAccountKeyMultiSig = function createWithAccountKeyMultiSig(address, keys) {
if (!Account.isAccountKey(keys)) keys = this.createAccountKeyMultiSig(keys)
if (keys.type !== AccountKeyEnum.ACCOUNT_KEY_MULTISIG) {
throw new Error(`Failed to create account with AccountKeyMultiSig. Invalid account key : ${keys.type}`)
}
const account = new Account(address, keys)
return this._addAccountFunctions(account)
}
/**
* createWithAccountKeyRoleBased create an account with AccountKeyRoleBased.
*
* @method createWithAccountKeyRoleBased
* @param {String} address An address of account.
* @param {String|Object} keyObject Key of account.
* @return {Object}
*/
Accounts.prototype.createWithAccountKeyRoleBased = function createWithAccountKeyRoleBased(address, keyObject) {
if (!Account.isAccountKey(keyObject)) {
keyObject = this.createAccountKeyRoleBased(keyObject)
}
if (keyObject.type !== AccountKeyEnum.ACCOUNT_KEY_ROLEBASED) {
throw new Error(`Failed to create account with AccountKeyRoleBased. Invalid account key : ${keyObject.type}`)
}
const account = new Account(address, keyObject)
return this._addAccountFunctions(account)
}
/**
* privateKeyToAccount creates and returns an Account through the input passed as parameters.
*
* @method privateKeyToAccount
* @param {String} key The key parameter can be either normal private key or KlaytnWalletKey format.
* @param {String} userInputAddress The address entered by the user for use in creating an account.
* @return {Object}
*/
Accounts.prototype.privateKeyToAccount = function privateKeyToAccount(key, userInputAddress) {
const { legacyAccount: account, klaytnWalletKeyAddress } = this.getLegacyAccount(key)
account.address = this._determineAddress(account, klaytnWalletKeyAddress, userInputAddress)
account.address = account.address.toLowerCase()
account.address = utils.addHexPrefix(account.address)
return account
}
/**
* createAccountForUpdate creates an AccountForUpdate instance.
* The AccountForUpdate returned as a result of this function contains only the address and public key used to update the account.
*
* @method createAccountForUpdate
* @param {String} address The address value of AccountForUpdate, a structure that contains data for updating an account.
* @param {String|Array|Object} accountKey Private key or AccountKey to update account.
* @param {Object} options Options to use for setting threshold and weight for multiSig.
* @return {Object}
*/
Accounts.prototype.createAccountForUpdate = function createAccountForUpdate(address, accountKey, options) {
let legacyOrFail
// Logic for handling cases where legacyKey or failKey is set inside AccountKeyRoleBased object.
if (!_.isArray(accountKey) && _.isObject(accountKey)) {
legacyOrFail = {}
Object.keys(accountKey).map(role => {
if (accountKey[role] === 'legacyKey' || accountKey[role] === 'failKey') {
legacyOrFail[role] = accountKey[role]
delete accountKey[role]
}
})
if (Object.keys(accountKey).length === 0) {
return new AccountForUpdate(address, legacyOrFail, options)
}
}
const publicKey = this.accountKeyToPublicKey(accountKey)
if (legacyOrFail !== undefined) {
Object.assign(publicKey, legacyOrFail)
}
return new AccountForUpdate(address, publicKey, options)
}
/**
* createAccountForUpdateWithPublicKey creates AccountForUpdate instance with public key format.
*
* @method createAccountForUpdateWithPublicKey
* @param {String} address The address value of AccountForUpdate, a structure that contains data for updating an account.
* @param {String|Array|Object} keyForUpdate Public key to update.
* @param {Object} options Options to use for setting threshold and weight for multiSig.
* @return {Object}
*/
Accounts.prototype.createAccountForUpdateWithPublicKey = function createAccountForUpdateWithPublicKey(address, keyForUpdate, options) {
return new AccountForUpdate(address, keyForUpdate, options)
}
/**
* createAccountForUpdateWithLegacyKey creates AccountForUpdate instance with legacyKey.
*
* @method createAccountForUpdateWithLegacyKey
* @param {String} address The address of account to update with the legacy key.
* @return {Object}
*/
Accounts.prototype.createAccountForUpdateWithLegacyKey = function createAccountForUpdateWithLegacyKey(address) {
return new AccountForUpdate(address, 'legacyKey')
}
/**
* createAccountForUpdateWithFailKey creates AccountForUpdate instance with failKey.
*
* @method createAccountForUpdateWithFailKey
* @param {String} address The address of account to update with the fail key.
* @return {Object}
*/
Accounts.prototype.createAccountForUpdateWithFailKey = function createAccountForUpdateWithFailKey(address) {
return new AccountForUpdate(address, 'failKey')
}
/**
* isDecoupled determines whether or not it is decoupled based on the input value.
*
* @method isDecoupled
* @param {String} key The key parameter can be either normal private key or KlaytnWalletKey format.
* @param {String} userInputAddress The address to use when determining whether it is decoupled.
* @return {Boolean}
*/
Accounts.prototype.isDecoupled = function isDecoupled(key, userInputAddress) {
const { legacyAccount, klaytnWalletKeyAddress } = this.getLegacyAccount(key)
const actualAddress = this._determineAddress(legacyAccount, klaytnWalletKeyAddress, userInputAddress)
return legacyAccount.address.toLowerCase() !== actualAddress.toLowerCase()
}
/**
* getLegacyAccount extracts the private key from the input key and returns an account with the corresponding legacy account key.
* If the input key is KlaytnWalletKey format, it returns klaytnWalletKeyAddress, which is the address extracted from KlaytnWalletKey.
*
* @method getLegacyAccount
* @param {String} key The key parameter can be either normal private key or KlaytnWalletKey format.
* @return {Object}
*/
Accounts.prototype.getLegacyAccount = function getLegacyAccount(key) {
const parsed = utils.parsePrivateKey(key)
if (!utils.isValidPrivateKey(parsed.privateKey)) {
throw new Error('Invalid private key')
}
const privateKey = utils.addHexPrefix(parsed.privateKey)
const account = this._addAccountFunctions(Account.fromObject(AccountLib.fromPrivate(privateKey)))
return { legacyAccount: account, klaytnWalletKeyAddress: parsed.address }
}
/**
* signTransaction signs to transaction with private key.
* If there are signatures(feePayerSignatures if the fee payer signs) in tx entered as a parameter,
* the signatures(feePayerSignatures if the fee payer signs) are appended.
*
* @method signTransaction
* @param {String|Object} tx The transaction to sign.
* @param {String|Array} privateKey The private key to use for signing.
* @param {Function} callback The callback function to call.
* @return {Object}
*/
Accounts.prototype.signTransaction = function signTransaction() {
const _this = this
let isLegacy = false
let isFeePayer = false
let existedSenderSignatures = []
let existedFeePayerSignatures = []
let result
let tx
let privateKey
let callback
const handleError = e => {
e = e instanceof Error ? e : new Error(e)
if (callback) callback(e)
return Promise.reject(e)
}
try {
const resolved = resolveArgsForSignTransaction(arguments)
tx = resolved.tx
privateKey = resolved.privateKey
callback = resolved.callback
} catch (e) {
return handleError(e)
}
// If the user signs an RLP encoded transaction, tx is of type string.
if (_.isString(tx)) {
tx = decodeFromRawTransaction(tx)
}
// Validate tx object
const error = helpers.validateFunction.validateParams(tx)
if (error) return handleError(error)
if (tx.senderRawTransaction) {
if (tx.feePayerSignatures) {
existedFeePayerSignatures = existedFeePayerSignatures.concat(tx.feePayerSignatures)
}
try {
// Decode senderRawTransaction to get signatures of fee payer
const { senderRawTransaction, feePayer, feePayerSignatures } = splitFeePayer(tx.senderRawTransaction)
// feePayer !== '0x' means that in senderRawTransaction there are feePayerSignatures
if (feePayer !== '0x' && feePayer !== '0x0000000000000000000000000000000000000000') {
// The feePayer inside the tx object does not match the feePayer information contained in the senderRawTransaction.
if (feePayer.toLowerCase() !== tx.feePayer.toLowerCase()) {
return handleError(
`Invalid feePayer: The fee payer(${feePayer}) included in the transaction does not match the fee payer(${tx.feePayer}) you want to sign.`
)
}
existedFeePayerSignatures = existedFeePayerSignatures.concat(feePayerSignatures)
}
tx.senderRawTransaction = senderRawTransaction
isFeePayer = true
} catch (e) {
return handleError(e)
}
} else {
isLegacy = !!(tx.type === undefined || tx.type === 'LEGACY')
if (tx.signatures) {
// if there is existed signatures or feePayerSignatures, those should be preserved.
if (isLegacy) {
return handleError('Legacy transaction cannot be signed with multiple keys.')
}
existedSenderSignatures = existedSenderSignatures.concat(tx.signatures)
}
}
// When privateKey is undefined, find Account from Wallet.
if (privateKey === undefined) {
try {
const account = this.wallet.getAccount(isFeePayer ? tx.feePayer : tx.from)
if (!account) {
return handleError(
'Failed to find get private key to sign. The account you want to use for signing must exist in caver.klay.accounts.wallet or you must pass the private key as a parameter.'
)
}
privateKey = this._getRoleKey(tx, account)
} catch (e) {
return handleError(e)
}
}
const privateKeys = _.isArray(privateKey) ? privateKey : [privateKey]
try {
for (let i = 0; i < privateKeys.length; i++) {
const parsed = utils.parsePrivateKey(privateKeys[i])
privateKeys[i] = parsed.privateKey
privateKeys[i] = utils.addHexPrefix(privateKeys[i])
if (!utils.isValidPrivateKey(privateKeys[i])) {
return handleError('Invalid private key')
}
}
} catch (e) {
return handleError(e)
}
// Attempting to sign with a decoupled account into a legacy type transaction should be rejected.
if (isLegacy) {
if (privateKeys.length > 1) {
return handleError('Legacy transaction cannot signed with multiple keys')
}
if (_this.isDecoupled(privateKeys[0], tx.from)) {
return handleError('A legacy transaction must be with a legacy account key')
}
}
function signed(txObject) {
try {
// Guarantee all property in transaction is hex.
txObject = helpers.formatters.inputCallFormatter(txObject)
const transaction = coverInitialTxValue(txObject)
const rlpEncoded = encodeRLPByTxType(transaction)
const messageHash = Hash.keccak256(rlpEncoded)
const sigs = isFeePayer ? existedFeePayerSignatures : existedSenderSignatures
for (const p of privateKeys) {
const signature = AccountLib.makeSigner(Nat.toNumber(transaction.chainId || '0x1') * 2 + 35)(messageHash, p)
const [v, r, s] = AccountLib.decodeSignature(signature).map(sig => utils.makeEven(utils.trimLeadingZero(sig)))
sigs.push([v, r, s])
}
// makeRawTransaction will return signatures and feePayerSignatures with duplicates removed.
const { rawTransaction, signatures, feePayerSignatures } = makeRawTransaction(rlpEncoded, sigs, transaction)
result = {
messageHash,
v: sigs[0][0],
r: sigs[0][1],
s: sigs[0][2],
rawTransaction,
txHash: Hash.keccak256(rawTransaction),
senderTxHash: getSenderTxHash(rawTransaction),
}
if (isFeePayer) {
result.feePayerSignatures = feePayerSignatures
} else {
result.signatures = signatures
}
} catch (e) {
callback(e)
return Promise.reject(e)
}
callback(null, result)
return result
}
if (tx.nonce !== undefined && tx.chainId !== undefined && tx.gasPrice !== undefined) {
return Promise.resolve(signed(tx))
}
// When the feePayer signs a transaction, required information is only chainId.
if (isFeePayer) {
return Promise.all([isNot(tx.chainId) ? _this._klaytnCall.getChainId() : tx.chainId]).then(function(args) {
if (isNot(args[0])) {
throw new Error(`"chainId" couldn't be fetched: ${JSON.stringify(args)}`)
}
return signed(_.extend(tx, { chainId: args[0] }))
})
}
// Otherwise, get the missing info from the Klaytn Node
return Promise.all([
isNot(tx.chainId) ? _this._klaytnCall.getChainId() : tx.chainId,
isNot(tx.gasPrice) ? _this._suggestGasPrice() : tx.gasPrice,
isNot(tx.nonce) ? _this._klaytnCall.getTransactionCount(tx.from, 'pending') : tx.nonce,
]).then(function(args) {
if (isNot(args[0]) || isNot(args[1]) || isNot(args[2])) {
throw new Error(`One of the values "chainId", "gasPrice", or "nonce" couldn't be fetched: ${JSON.stringify(args)}`)
}
return signed(
_.extend(tx, {
chainId: args[0],
gasPrice: args[1],
nonce: args[2],
})
)
})
}
/**
* feePayerSignTransaction calls signTransaction, creating a format for feePayer to sign the transaction.
* If there are feePayerSignatures in tx entered as a parameter, the signatures for fee payer are appended.
*
* @method feePayerSignTransaction
* @param {Object|String} tx The transaction to sign.
* @param {String} feePayer The address of fee payer.
* @param {String|Array} privateKey The private key to use for signing.
* @param {Function} callback The callback function to call.
* @return {Object}
*/
Accounts.prototype.feePayerSignTransaction = function feePayerSignTransaction() {
const _this = this
let tx
let feePayer
let privateKey
let callback
const handleError = e => {
e = e instanceof Error ? e : new Error(e)
if (callback) callback(e)
return Promise.reject(e)
}
try {
const resolved = resolveArgsForFeePayerSignTransaction(arguments)
tx = resolved.tx
feePayer = resolved.feePayer
privateKey = resolved.privateKey
callback = resolved.callback
} catch (e) {
return handleError(e)
}
if (_.isString(tx)) {
return this.signTransaction({ senderRawTransaction: tx, feePayer }, privateKey, callback)
}
if (!tx.feePayer || tx.feePayer === '0x' || tx.feePayer === '0x0000000000000000000000000000000000000000') {
tx.feePayer = feePayer
}
if (!tx.senderRawTransaction) {
if (!tx.type || !tx.type.includes('FEE_DELEGATED')) {
return handleError(`Failed to sign transaction with fee payer: invalid transaction type(${tx.type ? tx.type : 'LEGACY'})`)
}
}
const e = helpers.validateFunction.validateParams(tx)
if (e) {
return handleError(e)
}
if (tx.feePayer.toLowerCase() !== feePayer.toLowerCase()) {
return handleError('Invalid parameter: The address of fee payer does not match.')
}
if (tx.senderRawTransaction) {
return this.signTransaction(tx, privateKey, callback)
}
return Promise.all([
isNot(tx.chainId) ? _this._klaytnCall.getChainId() : tx.chainId,
isNot(tx.gasPrice) ? _this._klaytnCall.getGasPrice() : tx.gasPrice,
isNot(tx.nonce) ? _this._klaytnCall.getTransactionCount(tx.from, 'pending') : tx.nonce,
]).then(function(args) {
const chainId = args[0]
const gasPrice = args[1]
const nonce = args[2]
if (isNot(chainId) || isNot(gasPrice) || isNot(nonce)) {
throw new Error(`One of the values "chainId", "gasPrice", or "nonce" couldn't be fetched: ${JSON.stringify(args)}`)
}
let transaction = _.extend(tx, { chainId, gasPrice, nonce })
transaction = helpers.formatters.inputCallFormatter(transaction)
transaction = coverInitialTxValue(transaction)
const rlpEncoded = encodeRLPByTxType(transaction)
const sig = transaction.signatures ? transaction.signatures : [['0x01', '0x', '0x']]
const { rawTransaction } = makeRawTransaction(rlpEncoded, sig, transaction)
return _this.signTransaction({ senderRawTransaction: rawTransaction, feePayer, chainId }, privateKey, callback)
})
}
/**
* signTransactionWithHash signs to transaction hash with private key(s).
*
* @method signTransactionWithHash
* @param {String} hash The hash of transaction to sign.
* @param {String|Array} privateKeys The private key(s) to use for signing.
* @param {String|Number} chainId The chain id of the network.
* @param {Function} callback The callback function to call.
* @return {Array}
*/
Accounts.prototype.signTransactionWithHash = function signTransactionWithHash() {
const _this = this
let hash
let privateKeys
let chainId
let callback
const handleError = e => {
e = e instanceof Error ? e : new Error(e)
if (callback) callback(e)
return Promise.reject(e)
}
try {
const resolved = resolveArgsForSignTransactionWithHash(arguments)
hash = resolved.hash
chainId = resolved.chainId
privateKeys = resolved.privateKeys
callback = resolved.callback
} catch (e) {
return handleError(e)
}
privateKeys = Array.isArray(privateKeys) ? privateKeys : [privateKeys]
function signWithHash(transactionHash, prvKeys, chain, callbackFunc) {
const result = []
chain = utils.numberToHex(chain)
try {
for (const privateKey of prvKeys) {
const p = utils.addHexPrefix(utils.parsePrivateKey(privateKey).privateKey)
if (!utils.isValidPrivateKey(p)) {
return handleError(`Failed to sign transaction with hash: Invalid private key ${privateKey}`)
}
const signature = AccountLib.makeSigner(Nat.toNumber(chain || '0x1') * 2 + 35)(transactionHash, p)
const [v, r, s] = AccountLib.decodeSignature(signature).map(sig => utils.makeEven(utils.trimLeadingZero(sig)))
result.push(utils.transformSignaturesToObject([v, r, s]))
}
} catch (e) {
callbackFunc(e)
return Promise.reject(e)
}
callbackFunc(null, result)
return result
}
return Promise.resolve(isNot(chainId) ? _this._klaytnCall.getChainId() : chainId).then(id => {
return signWithHash(hash, privateKeys, id, callback)
})
}
/**
* getRawTransactionWithSignatures returns object which contains rawTransaction.
*
* @method getRawTransactionWithSignatures
* @param {Object} tx The transaction object which contains signatures or feePayerSignatures.
* @param {Function} callback The callback function to call.
* @return {Object}
*/
Accounts.prototype.getRawTransactionWithSignatures = function getRawTransactionWithSignatures(tx, callback) {
const _this = this
let result
callback = callback || function() {}
const handleError = e => {
e = e instanceof Error ? e : new Error(e)
if (callback) callback(e)
return Promise.reject(e)
}
if (!tx || !_.isObject(tx)) {
return handleError('Invalid parameter: The transaction must be defined as an object')
}
if (!tx.signatures && !tx.feePayerSignatures) {
return handleError('There are no signatures or feePayerSignatures defined in the transaction object.')
}
const error = helpers.validateFunction.validateParams(tx)
if (error) return handleError(error)
if (tx.senderRawTransaction) {
tx.feePayerSignatures = tx.feePayerSignatures || [['0x01', '0x', '0x']]
const decoded = decodeFromRawTransaction(tx.senderRawTransaction)
// feePayer !== '0x' means that in senderRawTransaction there are feePayerSignatures
if (
decoded.feePayer !== '0x' &&
decoded.feePayer !== '0x0000000000000000000000000000000000000000' &&
!utils.isEmptySig(decoded.feePayerSignatures)
) {
if (decoded.feePayer.toLowerCase() !== tx.feePayer.toLowerCase()) {
return handleError('Invalid feePayer')
}
tx.feePayerSignatures = tx.feePayerSignatures.concat(decoded.feePayerSignatures)
}
decoded.feePayer = tx.feePayer
decoded.feePayerSignatures = tx.feePayerSignatures
if (tx.signatures) {
decoded.signatures = decoded.signatures.concat(tx.signatures)
}
tx = decoded
}
function signed(txObject) {
try {
// Guarantee all property in transaction is hex.
txObject = helpers.formatters.inputCallFormatter(txObject)
const transaction = coverInitialTxValue(txObject)
const rlpEncoded = encodeRLPByTxType(transaction)
let sigs = transaction.signatures ? transaction.signatures : ['0x01', '0x', '0x']
if (!_.isArray(sigs[0])) sigs = [sigs]
const { rawTransaction, signatures, feePayerSignatures } = makeRawTransaction(rlpEncoded, sigs, transaction)
result = {
rawTransaction,
txHash: Hash.keccak256(rawTransaction),
senderTxHash: getSenderTxHash(rawTransaction),
}
if (signatures && !utils.isEmptySig(signatures)) {
result.signatures = signatures
}
if (feePayerSignatures && !utils.isEmptySig(feePayerSignatures)) {
result.feePayerSignatures = feePayerSignatures
}
} catch (e) {
callback(e)
return Promise.reject(e)
}
callback(null, result)
return result
}
if (tx.nonce !== undefined && tx.chainId !== undefined && tx.gasPrice !== undefined) {
return Promise.resolve(signed(tx))
}
// Otherwise, get the missing info from the Klaytn Node
return Promise.all([
isNot(tx.chainId) ? _this._klaytnCall.getChainId() : tx.chainId,
isNot(tx.gasPrice) ? _this._klaytnCall.getGasPrice() : tx.gasPrice,
isNot(tx.nonce) ? _this._klaytnCall.getTransactionCount(tx.from, 'pending') : tx.nonce,
]).then(function(args) {
if (isNot(args[0]) || isNot(args[1]) || isNot(args[2])) {
throw new Error(`One of the values "chainId", "gasPrice", or "nonce" couldn't be fetched: ${JSON.stringify(args)}`)
}
return signed(
_.extend(tx, {
chainId: args[0],
gasPrice: args[1],
nonce: args[2],
})
)
})
}
/**
* combineSignatures combines RLP encoded raw transaction strings.
* combineSignatures compares transaction before combining, and if values in field are not same, this throws error.
* The comparison allows that the address of the fee payer is '0x0000000000000000000000000000000000000000'(default value) for some transactions while the other transactions have a specific fee payer. This is for the use case that some transactions do not have the fee payer's information.
* In this case, feePayer field doesn't have to be compared with other transaction.
*
* @method combineSignatures
* @param {Array} rawTransactions The array of raw transaction string to combine.
* @param {Function} callback The callback function to call.
* @return {Object}
*/
Accounts.prototype.combineSignatures = function combineSignatures(rawTransactions, callback) {
let decodedTx
let senders = []
let feePayers = []
let feePayer
callback = callback || function() {}
const handleError = e => {
e = e instanceof Error ? e : new Error(e)
if (callback) callback(e)
return Promise.reject(e)
}
if (!_.isArray(rawTransactions)) {
return handleError('The parameter of the combineSignatures function must be an array of RLP encoded transaction strings.')
}
for (const raw of rawTransactions) {
const { senderSignatures, feePayerSignatures, decodedTransaction } = extractSignatures(raw)
senders = senders.concat(senderSignatures)
feePayers = feePayers.concat(feePayerSignatures)
if (decodedTx) {
let isSame = true
const keys = Object.keys(decodedTx)
for (const key of keys) {
if (
key === 'v' ||
key === 'r' ||
key === 's' ||
key === 'signatures' ||
key === 'payerV' ||
key === 'payerR' ||
key === 'payerS' ||
key === 'feePayerSignatures'
) {
continue
}
// feePayer field can be '0x' or '0x0000000000000000000000000000000000000000' when after sender signs to trasnaction.
// For handling this, if feePayer is '0x' or '0x0000000000000000000000000000000000000000', don't compare with other transaction
if (key === 'feePayer') {
if (decodedTransaction[key] === '0x' || decodedTransaction[key] === '0x0000000000000000000000000000000000000000') {
continue
} else {
// set feePayer letiable with valid feePayer address(not '0x' and '0x0000000000000000000000000000000000000000')
feePayer = decodedTransaction[key]
if (decodedTx[key] === '0x' || decodedTx[key] === '0x0000000000000000000000000000000000000000') {
// set feePayer field to decodedTx for comparing feePayer address with other transactions
decodedTx[key] = decodedTransaction[key]
}
}
}
if (decodedTransaction[key] === undefined || decodedTx[key] !== decodedTransaction[key]) {
isSame = false
break
}
}
if (!isSame) {
return handleError('Failed to combineSignatures: Signatures that sign to different transaction cannot be combined.')
}
} else {
decodedTx = decodedTransaction
}
}
const parsedTxObject = decodeFromRawTransaction(rawTransactions[0])
parsedTxObject.signatures = senders
if (feePayer) {
parsedTxObject.feePayer = feePayer
if (feePayers.length > 0) {
parsedTxObject.feePayerSignatures = feePayers
}
}
return this.getRawTransactionWithSignatures(parsedTxObject, callback)
}
/**
* cav.klay.accounts.recoverTransaction('0xf86180808401ef364594f0109fc8df283027b6285cc889f5aa624eac1f5580801ca031573280d608f75137e33fc14655f097867d691d5c4c44ebe5ae186070ac3d5ea0524410802cdc025034daefcdfa08e7d2ee3f0b9d9ae184b2001fe0aff07603d9');
* > "0xF0109fC8DF283027b6285cc889F5aA624EaC1F55"
*/
Accounts.prototype.recoverTransaction = function recoverTransaction(rawTx) {
if (utils.getTxTypeStringFromRawTransaction(rawTx) !== undefined) {
throw new Error('recoverTransaction only supports transactions of type "LEGACY".')
}
const values = RLP.decode(rawTx)
// If the leading zero is trimmed, it will be filled with a valid length of '0'.
const arr = values.slice(7, 9).map(sig => {
sig = sig.replace('0x', '')
while (sig.length < 64) {
sig = `0${sig}`
}
return `0x${sig}`
})
arr.unshift(values[6])
const signature = AccountLib.encodeSignature(arr)
const recovery = Bytes.toNumber(values[6])
const extraData = recovery < 35 ? [] : [Bytes.fromNumber((recovery - 35) >> 1), '0x', '0x']
const signingData = values.slice(0, 6).concat(extraData)
const signingDataHex = RLP.encode(signingData)
return AccountLib.recover(Hash.keccak256(signingDataHex), signature)
}
/**
* Hashes the given message to be passed cav.klay.accounts.recover() function.
* The data will be UTF-8 HEX decoded and enveloped as follows:
* "\x19Klaytn Signed Message:\n" + message.length + message and hashed using keccak256.
*
* cav.klay.accounts.hashMessage("Hello World")
* > "0xa1de988600a42c4b4ab089b619297c17d53cffae5d5120d82d8a92d0bb3b78f2"
* // the below results in the same hash
* cav.klay.accounts.hashMessage(caver.utils.utf8ToHex("Hello World"))
* > "0xa1de988600a42c4b4ab089b619297c17d53cffae5d5120d82d8a92d0bb3b78f2"
*/
Accounts.prototype.hashMessage = function hashMessage(data) {
const message = utils.isHexStrict(data) ? utils.hexToBytes(data) : data
const messageBuffer = Buffer.from(message)
const preamble = `\x19Klaytn Signed Message:\n${message.length}`
const preambleBuffer = Buffer.from(preamble)
// klayMessage is concatenated buffer (preambleBuffer + messageBuffer)
const klayMessage = Buffer.concat([preambleBuffer, messageBuffer])
// Finally, run keccak256 on klayMessage.
return Hash.keccak256(klayMessage)
}
/**
* Signs arbitrary data.
* This data is before UTF-8 HEX decoded and enveloped as follows:
* "\x19Klaytn Signed Message:\n" + message.length + message.
*
* cav.klay.accounts.sign('Some data', '0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318');
* > {
* message: 'Some data',
* messageHash: '0x1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655',
* v: '0x1c',
* r: '0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd',
* s: '0x6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029',
* signature: '0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c