After this PR merged, new transaction type will be supported.
Newly created transaction types are following:
- ValueTransfer type transaction
- AccountCreation type transaction
1. ValueTransfer type transaction
To send "ValueTransfer" type transaction, you should add type: 'VALUE_TRANSFER'
key-value pair to transaction object.
{
type: 'VALUE_TRANSFER',
from: '...',
to: '...',
...
}
The RLP encoding for this transaction is quite different from legacy transaction.
i) Value transfer transaction RLP encoding
RLP.encode([
VALUE_TRANFSER_TYPE_TAG, // '0x10'
[
Bytes.fromNat(transaction.nonce),
Bytes.fromNat(transaction.gasPrice),
Bytes.fromNat(transaction.gas),
transaction.to.toLowerCase(),
Bytes.fromNat(transaction.value),
transaction.from.toLowerCase(),
],
Bytes.fromNat(transaction.chainId || "0x1"),
"0x",
"0x"
])
ii) Legacy transaction RLP encoding
RLP.encode([
Bytes.fromNat(transaction.nonce),
Bytes.fromNat(transaction.gasPrice),
Bytes.fromNat(transaction.gas),
transaction.to.toLowerCase(),
Bytes.fromNat(transaction.value),
transaction.data,
Bytes.fromNat(transaction.chainId || "0x1"),
"0x",
"0x",
])
2. AccountCreation type transaction
To send "AccountCreation" type transaction, you should add
type: 'ACCOUNT_CREATION',
to: ..., (new address which would be newly created)
publicKey: ..., (optional)
humanReadable: ..., (optional)
key-value pairs to transaction object.
The RLP encoding for this transaction is quite different from legacy transaction.
i) Account creation transaction RLP encoding
RLP.encode([
ACCOUNT_CREATION_TYPE_TAG,
[
Bytes.fromNat(transaction.nonce),
Bytes.fromNat(transaction.gasPrice),
Bytes.fromNat(transaction.gas),
transaction.to.toLowerCase(),
Bytes.fromNat(transaction.value),
transaction.from.toLowerCase(),
],
Bytes.fromNat(transaction.transaction.humanReadable === true ? '0x1' : '0x0'),
accountKey,
Bytes.fromNat(transaction.chainId || "0x1"),
"0x",
"0x",
])
accountKey
is the value generated as following:
If there is no publicKey
field in the transaction object, the account key type is "ACCOUNT_NIL", on the other hand, "ACCOUNT_PUBLIC".
let accountKey
const xyPoints = transaction.publicKey && utils.xyPointFromPublicKey(transaction.publicKey)
// 1. Check Account key type
if (xyPoints !== undefined && xyPoints.length) { // ACCOUNT_KEY_PUBLIC_TAG
const [pubX, pubY] = xyPoints
accountKey = ACCOUNT_KEY_PUBLIC_TAG + RLP.encode([pubX, pubY]).slice(2)
} else { // ACCOUNT_KEY_NIL_TAG
accountKey = ACCOUNT_KEY_NIL_TAG
}
ii) Legacy transaction RLP encoding
RLP.encode([
Bytes.fromNat(transaction.nonce),
Bytes.fromNat(transaction.gasPrice),
Bytes.fromNat(transaction.gas),
transaction.to.toLowerCase(),
Bytes.fromNat(transaction.value),
transaction.data,
Bytes.fromNat(transaction.chainId || "0x1"),
"0x",
"0x",
])
"AccountCreation" type transaction has 4 cases:
-
has publicKey, humanReadable: true create human readable account and connect the given public key to the account. The address for this account is the value of
to
in transaction object. -
has publicKey, humanReadable: false create regular account and connect the given public key to the account. The address for this account is the value of
to
in transaction object.
// Possible, but useless cases. 3) hasn't publicKey, humanReadable: false create regular account without connecting public key to the account. If you don't have a private key for the account address, you can't withdraw balance from the account. However, if you have the private key for it, there is no reason to send "AccountCreation" type transaction.
- hasn't publicKey, humanReadable: true create human readable account without connecting public key to the account. Maybe this transaction is useless in many cases.
- Must have
from
field in transaction
i) There was no need to contain from
field to a transaction since it can be recovered from v
, r
, s
signatures.
However, after account type is newly created, there is no way to find address who signed this transaction by only recovering signatures. That's the reason why we must not omit from
field for sending transaction.
const fromOmittedTxObject = _.omit(tx, 'from')
So above line was removed.
ii) Should apply inputAddressFormatter
for from
field.
if (options.from) {
options.from = inputAddressFormatter(options.from)
}
So above line was created.
- Must support human-readable string
i) Should parse human-readable string to hex address.
'toshi'
utf8 string can be changed to hex string 0x746f736869
. However for the hex string to be valid address, it should be length of 20 bytes which can be generated by adding '0' padding to right. As a result, 'toshi'
can be changed to 0x746f736869000000000000000000000000000000
(20bytes).
const humanReadableStringToHexAddress = (humanReadableString) => {
const addressLength = 40 // 20 bytes
let hex = utf8ToHex(humanReadableString)
if (hex.length > 40 + 2) throw Error(`Invalid human readable account length! It should be less than 20 bytes: ${hex}`)
hex = rightPad(hex, addressLength)
return hex
}
So above line was created.
ii) Should be possible to add humanreadable account to accounts.wallet
instance.
caver.klay.accounts.wallet.add
function can have one more argument than before. Since human-readable address can't be achieved from private key from now on. So to add human-readable address with private key, You should use the API like below:
caver.klay.accounts.wallet.add(privateKey, 'toshi')
This will map 'toshi' address with the given private key.
Wallet.prototype.add = function (account, humanReadableString) {
// ...
if (humanReadableString) {
// utils.humanReadableStringToHexAddress('toshi') === '0x746f736869000000000000000000000000000000'
const humanReadableAddress = utils.humanReadableStringToHexAddress(humanReadableString)
account.address = humanReadableAddress
const accountAlreadyExists = !!this[humanReadableAddress]
if (accountAlreadyExists) return this[humanReadableAddress]
account.index = this._findSafeIndex()
this[account.index] = account
this[humanReadableString] = account // this['toshi']
this[humanReadableAddress] = account // this['0x746f736869000000000000000000000000000000']
this[humanReadableAddress.toLowerCase()] = account
this.length++
}
// ...
}
So above line was created.
iii) Removing, clearing wallet instance should consider human-readable address.
It was possible to remove a wallet instance by index or address by calling caver.klay.accounts.wallet.remove(index)
, caver.klay.accounts.wallet.clear()
. There were only 3 indexes for the wallet instance: index
, address
, address.toLowerCase()
. After human-readable address features added, we should have one more index humanReadableString
.
For example, caver.klay.accounts.wallet.add(privateKey, 'toshi')
should make an 4 indexes:
caver.klay.accounts.wallet[0]
caver.klay.accounts.wallet['0x746f736869000000000000000000000000000000']
caver.klay.accounts.wallet['0x746f736869000000000000000000000000000000'] // lowercase
caver.klay.accounts.wallet['toshi']
which means we should consider humanReadableString
index for removing and clearing also.
// humanreadable string
const humanReadableString = utils.hexToUtf8(account.address)
if (this[humanReadableString]) {
this[humanReadableString].privateKey = null
delete this[humanReadableString]
}
So above line was created.
iv) Validating for address should consider human-readable address.
var inputAddressFormatter = function (address) {
var iban = new utils.Iban(address);
if (iban.isValid() && iban.isDirect()) {
return iban.toAddress().toLowerCase();
} else if (utils.isAddress(address)) {
return '0x' + address.toLowerCase().replace('0x','');
} else if (utils.isHumanReadableString(address)) { // humanreadable address
return utils.humanReadableStringToHexAddress(address)
}
throw new Error('Provided address "'+ address +'" is invalid, the capitalization checksum test failed, or its an indrect IBAN address which can\'t be converted.');
};
So above line was created.