2018-06-17 23:32:57 +03:00
|
|
|
|
2018-09-24 22:55:17 +03:00
|
|
|
import { Zero } from '../constants';
|
|
|
|
|
|
|
|
import * as errors from '../errors';
|
|
|
|
|
2018-07-31 01:59:52 +03:00
|
|
|
import { recoverAddress } from './secp256k1';
|
|
|
|
|
2018-06-17 23:32:57 +03:00
|
|
|
import { getAddress } from './address';
|
2018-08-03 04:34:10 +03:00
|
|
|
import { BigNumber, bigNumberify } from './bignumber';
|
2018-07-16 10:27:49 +03:00
|
|
|
import { arrayify, hexlify, hexZeroPad, splitSignature, stripZeros, } from './bytes';
|
2018-06-17 23:32:57 +03:00
|
|
|
import { keccak256 } from './keccak256';
|
2018-10-15 02:00:15 +03:00
|
|
|
import { checkProperties, resolveProperties, shallowCopy } from './properties';
|
2018-07-13 03:11:32 +03:00
|
|
|
|
2018-06-17 23:32:57 +03:00
|
|
|
import * as RLP from './rlp';
|
|
|
|
|
2018-06-26 04:02:20 +03:00
|
|
|
|
2018-07-31 01:59:52 +03:00
|
|
|
///////////////////////////////
|
|
|
|
// Imported Types
|
|
|
|
|
|
|
|
import { Arrayish, Signature } from './bytes';
|
|
|
|
import { BigNumberish } from './bignumber';
|
|
|
|
|
2018-10-15 02:00:15 +03:00
|
|
|
import { Provider } from '../providers/abstract-provider';
|
|
|
|
|
2018-07-31 01:59:52 +03:00
|
|
|
///////////////////////////////
|
|
|
|
// Exported Types
|
|
|
|
|
|
|
|
export type UnsignedTransaction = {
|
|
|
|
to?: string;
|
|
|
|
nonce?: number;
|
|
|
|
|
|
|
|
gasLimit?: BigNumberish;
|
|
|
|
gasPrice?: BigNumberish;
|
|
|
|
|
|
|
|
data?: Arrayish;
|
|
|
|
value?: BigNumberish;
|
|
|
|
chainId?: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface Transaction {
|
|
|
|
hash?: string;
|
|
|
|
|
|
|
|
to?: string;
|
|
|
|
from?: string;
|
|
|
|
nonce: number;
|
|
|
|
|
|
|
|
gasLimit: BigNumber;
|
|
|
|
gasPrice: BigNumber;
|
|
|
|
|
|
|
|
data: string;
|
|
|
|
value: BigNumber;
|
|
|
|
chainId: number;
|
|
|
|
|
|
|
|
r?: string;
|
|
|
|
s?: string;
|
|
|
|
v?: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////
|
2018-07-13 03:11:32 +03:00
|
|
|
|
2018-06-17 23:32:57 +03:00
|
|
|
function handleAddress(value: string): string {
|
|
|
|
if (value === '0x') { return null; }
|
|
|
|
return getAddress(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleNumber(value: string): BigNumber {
|
2018-08-03 04:34:10 +03:00
|
|
|
if (value === '0x') { return Zero; }
|
2018-06-17 23:32:57 +03:00
|
|
|
return bigNumberify(value);
|
|
|
|
}
|
|
|
|
|
2018-10-15 02:00:15 +03:00
|
|
|
const transactionFields = [
|
2018-06-23 03:30:50 +03:00
|
|
|
{ name: 'nonce', maxLength: 32 },
|
|
|
|
{ name: 'gasPrice', maxLength: 32 },
|
|
|
|
{ name: 'gasLimit', maxLength: 32 },
|
|
|
|
{ name: 'to', length: 20 },
|
|
|
|
{ name: 'value', maxLength: 32 },
|
|
|
|
{ name: 'data' },
|
2018-06-17 23:32:57 +03:00
|
|
|
];
|
|
|
|
|
2018-10-15 02:00:15 +03:00
|
|
|
const allowedTransactionKeys: { [ key: string ]: boolean } = {
|
|
|
|
chainId: true, data: true, gasLimit: true, gasPrice:true, nonce: true, to: true, value: true
|
|
|
|
}
|
|
|
|
|
2018-07-12 09:42:46 +03:00
|
|
|
export function serialize(transaction: UnsignedTransaction, signature?: Arrayish | Signature): string {
|
2018-10-15 02:00:15 +03:00
|
|
|
checkProperties(transaction, allowedTransactionKeys);
|
|
|
|
|
|
|
|
let raw: Array<string | Uint8Array> = [];
|
2018-06-17 23:32:57 +03:00
|
|
|
|
|
|
|
transactionFields.forEach(function(fieldInfo) {
|
2018-06-23 03:30:50 +03:00
|
|
|
let value = (<any>transaction)[fieldInfo.name] || ([]);
|
2018-06-17 23:32:57 +03:00
|
|
|
value = arrayify(hexlify(value));
|
|
|
|
|
|
|
|
// Fixed-width field
|
|
|
|
if (fieldInfo.length && value.length !== fieldInfo.length && value.length > 0) {
|
2018-06-26 04:02:20 +03:00
|
|
|
errors.throwError('invalid length for ' + fieldInfo.name, errors.INVALID_ARGUMENT, { arg: ('transaction' + fieldInfo.name), value: value });
|
2018-06-17 23:32:57 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Variable-width (with a maximum)
|
|
|
|
if (fieldInfo.maxLength) {
|
|
|
|
value = stripZeros(value);
|
|
|
|
if (value.length > fieldInfo.maxLength) {
|
2018-06-26 04:02:20 +03:00
|
|
|
errors.throwError('invalid length for ' + fieldInfo.name, errors.INVALID_ARGUMENT, { arg: ('transaction' + fieldInfo.name), value: value });
|
2018-06-17 23:32:57 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
raw.push(hexlify(value));
|
|
|
|
});
|
|
|
|
|
2018-07-12 09:42:46 +03:00
|
|
|
if (transaction.chainId != null && transaction.chainId !== 0) {
|
2018-06-26 04:02:20 +03:00
|
|
|
raw.push(hexlify(transaction.chainId));
|
2018-06-21 03:29:54 +03:00
|
|
|
raw.push('0x');
|
|
|
|
raw.push('0x');
|
|
|
|
}
|
|
|
|
|
2018-07-12 09:42:46 +03:00
|
|
|
let unsignedTransaction = RLP.encode(raw);
|
|
|
|
|
2018-06-26 04:02:20 +03:00
|
|
|
// Requesting an unsigned transation
|
2018-07-12 09:42:46 +03:00
|
|
|
if (!signature) {
|
|
|
|
return unsignedTransaction;
|
2018-06-17 23:32:57 +03:00
|
|
|
}
|
|
|
|
|
2018-07-12 09:42:46 +03:00
|
|
|
// The splitSignature will ensure the transaction has a recoveryParam in the
|
|
|
|
// case that the signTransaction function only adds a v.
|
2018-07-31 01:59:52 +03:00
|
|
|
let sig = splitSignature(signature);
|
2018-06-17 23:32:57 +03:00
|
|
|
|
2018-06-26 04:02:20 +03:00
|
|
|
// We pushed a chainId and null r, s on for hashing only; remove those
|
2018-10-15 02:00:15 +03:00
|
|
|
let v = 27 + sig.recoveryParam
|
2018-06-26 04:02:20 +03:00
|
|
|
if (raw.length === 9) {
|
2018-06-17 23:32:57 +03:00
|
|
|
raw.pop();
|
|
|
|
raw.pop();
|
|
|
|
raw.pop();
|
|
|
|
v += transaction.chainId * 2 + 8;
|
|
|
|
}
|
|
|
|
|
|
|
|
raw.push(hexlify(v));
|
2018-07-31 01:59:52 +03:00
|
|
|
raw.push(stripZeros(arrayify(sig.r)));
|
|
|
|
raw.push(stripZeros(arrayify(sig.s)));
|
2018-06-17 23:32:57 +03:00
|
|
|
|
|
|
|
return RLP.encode(raw);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function parse(rawTransaction: Arrayish): Transaction {
|
2018-06-26 04:02:20 +03:00
|
|
|
let transaction = RLP.decode(rawTransaction);
|
|
|
|
if (transaction.length !== 9 && transaction.length !== 6) {
|
|
|
|
errors.throwError('invalid raw transaction', errors.INVALID_ARGUMENT, { arg: 'rawTransactin', value: rawTransaction });
|
|
|
|
}
|
2018-06-17 23:32:57 +03:00
|
|
|
|
|
|
|
let tx: Transaction = {
|
2018-06-26 04:02:20 +03:00
|
|
|
nonce: handleNumber(transaction[0]).toNumber(),
|
|
|
|
gasPrice: handleNumber(transaction[1]),
|
|
|
|
gasLimit: handleNumber(transaction[2]),
|
|
|
|
to: handleAddress(transaction[3]),
|
|
|
|
value: handleNumber(transaction[4]),
|
|
|
|
data: transaction[5],
|
2018-06-17 23:32:57 +03:00
|
|
|
chainId: 0
|
|
|
|
};
|
|
|
|
|
2018-06-26 04:02:20 +03:00
|
|
|
// Legacy unsigned transaction
|
|
|
|
if (transaction.length === 6) { return tx; }
|
|
|
|
|
|
|
|
try {
|
|
|
|
tx.v = bigNumberify(transaction[6]).toNumber();
|
|
|
|
|
|
|
|
} catch (error) {
|
2018-12-27 23:48:38 +03:00
|
|
|
errors.info(error);
|
2018-06-26 04:02:20 +03:00
|
|
|
return tx;
|
|
|
|
}
|
|
|
|
|
|
|
|
tx.r = hexZeroPad(transaction[7], 32);
|
|
|
|
tx.s = hexZeroPad(transaction[8], 32);
|
2018-06-17 23:32:57 +03:00
|
|
|
|
2018-06-26 04:02:20 +03:00
|
|
|
if (bigNumberify(tx.r).isZero() && bigNumberify(tx.s).isZero()) {
|
|
|
|
// EIP-155 unsigned transaction
|
|
|
|
tx.chainId = tx.v;
|
|
|
|
tx.v = 0;
|
2018-06-17 23:32:57 +03:00
|
|
|
|
2018-06-26 04:02:20 +03:00
|
|
|
} else {
|
|
|
|
// Signed Tranasaction
|
2018-06-17 23:32:57 +03:00
|
|
|
|
2018-06-26 04:02:20 +03:00
|
|
|
tx.chainId = Math.floor((tx.v - 35) / 2);
|
|
|
|
if (tx.chainId < 0) { tx.chainId = 0; }
|
2018-06-17 23:32:57 +03:00
|
|
|
|
2018-10-15 02:00:15 +03:00
|
|
|
let recoveryParam = tx.v - 27;
|
2018-06-17 23:32:57 +03:00
|
|
|
|
2018-06-26 04:02:20 +03:00
|
|
|
let raw = transaction.slice(0, 6);
|
2018-06-17 23:32:57 +03:00
|
|
|
|
2018-06-26 04:02:20 +03:00
|
|
|
if (tx.chainId !== 0) {
|
|
|
|
raw.push(hexlify(tx.chainId));
|
2018-06-17 23:32:57 +03:00
|
|
|
raw.push('0x');
|
|
|
|
raw.push('0x');
|
2018-06-26 04:02:20 +03:00
|
|
|
recoveryParam -= tx.chainId * 2 + 8;
|
2018-06-17 23:32:57 +03:00
|
|
|
}
|
|
|
|
|
2018-10-15 02:00:15 +03:00
|
|
|
let digest = keccak256(RLP.encode(raw));
|
2018-06-17 23:32:57 +03:00
|
|
|
try {
|
2018-06-26 04:02:20 +03:00
|
|
|
tx.from = recoverAddress(digest, { r: hexlify(tx.r), s: hexlify(tx.s), recoveryParam: recoveryParam });
|
2018-06-17 23:32:57 +03:00
|
|
|
} catch (error) {
|
2018-12-27 23:48:38 +03:00
|
|
|
errors.info(error);
|
2018-06-17 23:32:57 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
tx.hash = keccak256(rawTransaction);
|
|
|
|
}
|
|
|
|
|
|
|
|
return tx;
|
|
|
|
}
|
2018-10-15 02:00:15 +03:00
|
|
|
|
|
|
|
export function populateTransaction(transaction: any, provider: Provider, from: string | Promise<string>): Promise<Transaction> {
|
|
|
|
|
|
|
|
if (!Provider.isProvider(provider)) {
|
|
|
|
errors.throwError('missing provider', errors.INVALID_ARGUMENT, {
|
|
|
|
argument: 'provider',
|
|
|
|
value: provider
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
checkProperties(transaction, allowedTransactionKeys);
|
|
|
|
|
|
|
|
let tx = shallowCopy(transaction);
|
|
|
|
|
|
|
|
if (tx.to != null) {
|
|
|
|
tx.to = provider.resolveName(tx.to);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tx.gasPrice == null) {
|
|
|
|
tx.gasPrice = provider.getGasPrice();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tx.nonce == null) {
|
|
|
|
tx.nonce = provider.getTransactionCount(from);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tx.gasLimit == null) {
|
|
|
|
let estimate = shallowCopy(tx);
|
|
|
|
estimate.from = from;
|
|
|
|
tx.gasLimit = provider.estimateGas(estimate);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tx.chainId == null) {
|
|
|
|
tx.chainId = provider.getNetwork().then((network) => network.chainId);
|
|
|
|
}
|
|
|
|
|
|
|
|
return resolveProperties(tx);
|
|
|
|
}
|