[feat] added recover to Transaction

@types
neeboo 6 years ago
parent 75a238a425
commit cf7f7be197
  1. 57
      examples/testNode.js
  2. 3
      packages/harmony-account/src/account.ts
  3. 2
      packages/harmony-account/src/index.ts
  4. 17
      packages/harmony-crypto/src/bytes.ts
  5. 66
      packages/harmony-crypto/src/keyTool.ts
  6. 19
      packages/harmony-transaction/src/transaction.ts
  7. 1
      packages/harmony-transaction/src/types.ts
  8. 114
      packages/harmony-transaction/src/utils.ts
  9. 331
      packages/harmony-utils/src/transformers.ts
  10. 5
      typings/elliptic.d.ts

@ -1,8 +1,28 @@
const { Account, Wallet } = require('@harmony/account');
const { isAddress, isPrivateKey, numberToHex } = require('@harmony/utils');
const {
isAddress,
isPrivateKey,
numberToHex,
numToStr,
hexToNumber,
Unit,
strip0x,
} = require('@harmony/utils');
const { HttpProvider, Messenger } = require('@harmony/network');
const { Transaction } = require('@harmony/transaction');
const { hexlify, BN } = require('@harmony/crypto');
const {
arrayify,
hexlify,
BN,
encode,
decode,
getContractAddress,
recoverAddress,
recoverPublicKey,
keccak256,
hexZeroPad,
getAddressFromPublicKey,
} = require('@harmony/crypto');
const msgr = new Messenger(new HttpProvider('https://dev-api.zilliqa.com'));
const wallet = new Wallet(msgr);
@ -58,15 +78,44 @@ const acc = wallet.addByPrivateKey(
'0xc3886f791236bf31fe8fd7522a7b12808700deb9c159826fc99236c74614118b',
);
console.log(txn.getRLPUnsigned()[0]);
// console.log(txn.getRLPUnsigned()[0]);
const signed = wallet
.getAccount(acc.address)
.signTransaction(txn, false)
.then(console.log);
.then((tx) => {
const newTx = tx.recover(tx.unsignedTxnHash);
// console.log(newTx);
acc.signTransaction(newTx, false, 'rlp').then((signed) => {
console.log(signed);
});
});
// recoverAddress();
// console.log(wallet.messenger);
// console.log(hexlify(0));
// 0xda8003049401234567890123456789012345678901234567890580;
// 0xda8003049401234567890123456789012345678901234567890580;
/**
* { name: 'nonce', length: 32, fix: false },
{ name: 'gasPrice', length: 32, fix: false, transform: 'hex' },
{ name: 'gasLimit', length: 32, fix: false, transform: 'hex' },
{ name: 'to', length: 20, fix: true },
{ name: 'value', length: 32, fix: false, transform: 'hex' },
{ name: 'data', fix: false },
*/
// const [nonce, gasPrice, gasLimit, to, value, data] = decode(
// txn.getRLPUnsigned()[0],
// );
// console.log({
// nonce: new BN(strip0x(hexToNumber(nonce))).toNumber(),
// gasPrice: hexToNumber(gasPrice !== '0x' ? gasPrice : '0x00'),
// gasLimit: hexToNumber(gasLimit !== '0x' ? gasLimit : '0x00'),
// to: hexToNumber(to !== '0x' ? to : '0x00'),
// value: hexToNumber(value !== '0x' ? value : '0x00'),
// data: hexToNumber(data !== '0x' ? data : '0x00'),
// });
// console.log(getContractAddress(acc.publicKey, 248));

@ -152,6 +152,7 @@ class Account {
const balanceObject: any = await this.getBalance();
transaction.setParams({
...transaction.txParams,
from: this.address || '0x',
nonce: balanceObject.nonce + 1,
});
}
@ -161,7 +162,7 @@ class Account {
this.privateKey,
);
return transaction.map((obj: any) => {
return { ...obj, signature, txnHash };
return { ...obj, signature, txnHash, from: this.address };
});
} else {
// TODO: if we use other encode method, eg. protobuf, we should implement this

@ -1,2 +1,4 @@
export * from './account';
export * from './wallet';
export * from './types';
export * from './utils';

@ -341,7 +341,8 @@ export function isSignature(value: any): value is Signature {
}
export function splitSignature(signature: Arrayish | Signature): Signature {
let v: number | undefined = 0;
if (signature !== undefined) {
let v = 0;
let r = '0x';
let s = '0x';
@ -356,21 +357,18 @@ export function splitSignature(signature: Arrayish | Signature): Signature {
r = hexZeroPad(signature.r, 32);
s = hexZeroPad(signature.s, 32);
v = signature.v;
v = signature.v || 0;
if (typeof v === 'string') {
v = parseInt(v, 16);
}
let recoveryParam = signature.recoveryParam || 1;
let recoveryParam = signature.recoveryParam || 0;
if (recoveryParam == null && signature.v != null) {
recoveryParam = 1 - (typeof v !== 'number' ? 0 : v % 2);
recoveryParam = 1 - (v % 2);
}
v = 27 + recoveryParam;
} else {
const bytes: Uint8Array | null = arrayify(signature);
if (bytes === null) {
throw new Error('arrayify failed');
}
const bytes: Uint8Array = arrayify(signature) || new Uint8Array();
if (bytes.length !== 65) {
throw new Error('invalid signature');
}
@ -389,6 +387,9 @@ export function splitSignature(signature: Arrayish | Signature): Signature {
recoveryParam: v - 27,
v,
};
} else {
throw new Error('signature is not found');
}
}
export function joinSignature(signature: Signature): string {

@ -5,6 +5,7 @@ import * as errors from './errors';
import { keccak256 } from './keccak256';
import { randomBytes } from './random';
import { isPrivateKey, strip0x } from '@harmony/utils';
import { encode } from './rlp';
const secp256k1 = elliptic.ec('secp256k1');
@ -63,8 +64,8 @@ export const getPublic = (privateKey: string, compress?: boolean): string => {
*/
export const getAddressFromPublicKey = (publicKey: string): string => {
const ecKey = secp256k1.keyFromPublic(publicKey.slice(2), 'hex');
const publickHash = ecKey.getPublic(false, 'hex');
const address = '0x' + keccak256('0x' + publickHash.slice(2)).slice(-40);
const publicHash = ecKey.getPublic(false, 'hex');
const address = '0x' + keccak256('0x' + publicHash.slice(2)).slice(-40);
return address;
};
@ -110,12 +111,71 @@ export const sign = (
if (!isPrivateKey(privateKey)) {
throw new Error(`${privateKey} is not PrivateKey`);
}
const keyPair = secp256k1.keyFromPrivate(strip0x(privateKey), 'hex');
const signature = keyPair.sign(bytes.arrayify(digest), { canonical: true });
return {
const publicKey = '0x' + keyPair.getPublic(true, 'hex');
const result = {
recoveryParam: signature.recoveryParam,
r: bytes.hexZeroPad('0x' + signature.r.toString(16), 32),
s: bytes.hexZeroPad('0x' + signature.s.toString(16), 32),
v: 27 + signature.recoveryParam,
};
if (verifySignature(digest, result, publicKey)) {
return result;
} else {
throw new Error('signing process failed');
}
};
export function getContractAddress(from: string, nonce: number): string {
if (!from) {
throw new Error('missing from address');
}
const addr = keccak256(
encode([from, bytes.stripZeros(bytes.hexlify(nonce))]),
);
return '0x' + addr.substring(26);
}
export function verifySignature(
digest: bytes.Arrayish,
signature: bytes.Signature,
publicKey: string,
): boolean {
return recoverPublicKey(digest, signature) === publicKey;
}
export function recoverPublicKey(
digest: bytes.Arrayish | string,
signature: bytes.Signature | string,
): string {
const sig = bytes.splitSignature(signature);
const rs = { r: bytes.arrayify(sig.r), s: bytes.arrayify(sig.s) };
////
const recovered = secp256k1.recoverPubKey(
bytes.arrayify(digest),
rs,
sig.recoveryParam,
);
const key = recovered.encode('hex', false);
const ecKey = secp256k1.keyFromPublic(key, 'hex');
const publicKey = '0x' + ecKey.getPublic(true, 'hex');
///
return publicKey;
}
export function recoverAddress(
digest: bytes.Arrayish | string,
signature: bytes.Signature | string,
): string {
return getAddressFromPublicKey(
recoverPublicKey(bytes.arrayify(digest) || new Uint8Array(), signature),
);
}

@ -1,15 +1,19 @@
import {
BN,
encode,
// keccak256,
// decode,
// toChecksumAddress,
arrayify,
hexlify,
stripZeros,
Signature,
splitSignature,
// hexZeroPad,
} from '@harmony/crypto';
import { add0xToString } from '@harmony/utils';
import { TxParams } from './types';
import { recover } from './utils';
export const transactionFields = [
{ name: 'nonce', length: 32, fix: false },
@ -22,7 +26,7 @@ export const transactionFields = [
class Transaction {
// private hash?: string;
// private from?: string;
private from: string;
private nonce: number | string;
private to: string;
private gasLimit: BN;
@ -33,11 +37,10 @@ class Transaction {
private txnHash: string;
private unsignedTxnHash: string;
private signature: Signature;
// private r?: string;
// private s?: string;
// private v?: number;
// constructor
constructor(params?: TxParams) {
this.from = params ? params.from : '0x';
this.nonce = params ? params.nonce : 0;
this.gasPrice = params ? params.gasPrice : new BN(0);
this.gasLimit = params ? params.gasLimit : new BN(0);
@ -115,8 +118,15 @@ class Transaction {
return encode(raw);
}
recover(txnHash: string): Transaction {
this.setParams(recover(txnHash));
return this;
}
get txParams(): TxParams {
return {
from: this.from || '',
nonce: this.nonce || 0,
gasPrice: this.gasPrice || new BN(0),
gasLimit: this.gasLimit || new BN(0),
@ -130,6 +140,7 @@ class Transaction {
};
}
setParams(params: TxParams) {
this.from = params ? params.from : '0x';
this.nonce = params ? params.nonce : 0;
this.gasPrice = params ? params.gasPrice : new BN(0);
this.gasLimit = params ? params.gasLimit : new BN(0);

@ -1,5 +1,6 @@
import { BN, Signature } from '@harmony/crypto';
export interface TxParams {
from: string;
to: string;
nonce: number | string;
gasLimit: BN;

@ -0,0 +1,114 @@
import { hexToNumber, isHex, isAddress, strip0x } from '@harmony/utils';
import {
decode,
encode,
keccak256,
hexlify,
BN,
hexZeroPad,
recoverAddress,
} from '@harmony/crypto';
import { TxParams } from './types';
export const handleNumber = (value: string) => {
if (isHex(value) && value === '0x') {
return hexToNumber('0x00');
} else if (isHex(value) && value !== '0x') {
return hexToNumber(value);
} else {
return value;
}
};
export const handleAddress = (value: string): string => {
if (value === '0x') {
return '0x';
} else if (isAddress(value)) {
return value;
} else {
return '0x';
}
};
export const recover = (rawTransaction: string) => {
const transaction = decode(rawTransaction);
if (transaction.length !== 9 && transaction.length !== 6) {
throw new Error('invalid rawTransaction');
}
const tx: TxParams = {
from: '0x',
txnHash: '0x',
unsignedTxnHash: '0x',
nonce: new BN(strip0x(handleNumber(transaction[0]))).toNumber(),
gasPrice: new BN(strip0x(handleNumber(transaction[1]))),
gasLimit: new BN(strip0x(handleNumber(transaction[2]))),
to: handleAddress(transaction[3]),
value: new BN(strip0x(handleNumber(transaction[4]))),
data: transaction[5],
chainId: 0,
signature: {
r: '',
s: '',
recoveryParam: 0,
v: 0,
},
};
// Legacy unsigned transaction
if (transaction.length === 6) {
tx.unsignedTxnHash = rawTransaction;
return tx;
}
try {
tx.signature.v = new BN(strip0x(handleNumber(transaction[6]))).toNumber();
} catch (error) {
throw error;
}
tx.signature.r = hexZeroPad(transaction[7], 32);
tx.signature.s = hexZeroPad(transaction[8], 32);
if (
new BN(strip0x(handleNumber(tx.signature.r))).isZero() &&
new BN(strip0x(handleNumber(tx.signature.s))).isZero()
) {
// EIP-155 unsigned transaction
tx.chainId = tx.signature.v;
tx.signature.v = 0;
} else {
// Signed Tranasaction
tx.chainId = Math.floor((tx.signature.v - 35) / 2);
if (tx.chainId < 0) {
tx.chainId = 0;
}
let recoveryParam = tx.signature.v - 27;
const raw = transaction.slice(0, 6);
if (tx.chainId !== 0) {
raw.push(hexlify(tx.chainId));
raw.push('0x');
raw.push('0x');
recoveryParam -= tx.chainId * 2 + 8;
}
const digest = keccak256(encode(raw));
try {
tx.from = recoverAddress(digest, {
r: hexlify(tx.signature.r),
s: hexlify(tx.signature.s),
recoveryParam,
});
} catch (error) {
throw error;
}
tx.txnHash = keccak256(rawTransaction);
}
return tx;
};

@ -9,8 +9,30 @@ export const enum Units {
szabo = 'szabo',
finney = 'finney',
ether = 'ether',
Kether = 'Kether',
Mether = 'Mether',
Gether = 'Gether',
Tether = 'Tether',
}
export const unitMap = new Map([
[Units.wei, '1'],
[Units.kwei, '1000'], // 1e3 wei
[Units.Mwei, '1000000'], // 1e6 wei
[Units.Gwei, '1000000000'], // 1e9 wei
[Units.szabo, '1000000000000'], // 1e12 wei
[Units.finney, '1000000000000000'], // 1e15 wei
[Units.ether, '1000000000000000000'], // 1e18 wei
[Units.Kether, '1000000000000000000000'], // 1e21 wei
[Units.Mether, '1000000000000000000000000'], // 1e24 wei
[Units.Gether, '1000000000000000000000000000'], // 1e27 wei
[Units.Tether, '1000000000000000000000000000000'], // 1e30 wei
]);
const DEFAULT_OPTIONS = {
pad: false,
};
export const numberToString = (
obj: BN | number | string,
radix: number = 10,
@ -26,6 +48,25 @@ export const numberToString = (
}
};
export const numToStr = (input: any) => {
if (typeof input === 'string') {
if (!input.match(/^-?[0-9.]+$/)) {
throw new Error(
`while converting number to string, invalid number value '${input}', should be a number matching (^-?[0-9.]+).`,
);
}
return input;
} else if (typeof input === 'number') {
return String(input);
} else if (BN.isBN(input)) {
return input.toString(10);
}
throw new Error(
`while converting number to string, invalid number value '${input}' type ${typeof input}.`,
);
};
export const add0xToString = (obj: string): string => {
if (isString(obj) && !obj.startsWith('-')) {
return '0x' + obj.replace('0x', '');
@ -49,19 +90,297 @@ export const numberToHex = (obj: any): string => {
};
export const hexToNumber = (hex: string): string => {
if (isHex(hex)) {
if (isHex(hex) && hex[0] !== '-') {
return new BN(strip0x(hex), 'hex').toString();
} else if (isHex(hex) && hex[0] === '-') {
const result: BN = new BN(hex.substring(3), 16);
return result.mul(new BN(-1)).toString();
} else {
throw new Error(`${hex} is not hex number`);
}
};
export const toWei = (obj: any): string => {
return '';
export const toWei = (input: BN | string, unit: Units): BN => {
try {
let inputStr = numToStr(input);
const baseStr = unitMap.get(unit);
if (!baseStr) {
throw new Error(`No unit of type ${unit} exists.`);
}
const baseNumDecimals = baseStr.length - 1;
const base = new BN(baseStr, 10);
// Is it negative?
const isNegative = inputStr.substring(0, 1) === '-';
if (isNegative) {
inputStr = inputStr.substring(1);
}
if (inputStr === '.') {
throw new Error(`Cannot convert ${inputStr} to wei.`);
}
// Split it into a whole and fractional part
const comps = inputStr.split('.'); // eslint-disable-line
if (comps.length > 2) {
throw new Error(`Cannot convert ${inputStr} to wei.`);
}
let [whole, fraction] = comps;
if (!whole) {
whole = '0';
}
if (!fraction) {
fraction = '0';
}
if (fraction.length > baseNumDecimals) {
throw new Error(`Cannot convert ${inputStr} to Qa.`);
}
while (fraction.length < baseNumDecimals) {
fraction += '0';
}
const wholeBN = new BN(whole);
const fractionBN = new BN(fraction);
let wei = wholeBN.mul(base).add(fractionBN);
if (isNegative) {
wei = wei.neg();
}
return new BN(wei.toString(10), 10);
} catch (error) {
throw error;
}
};
export const fromWei = (obj: any): string => {
return '';
export const fromWei = (
wei: BN | string,
unit: Units,
options: any = DEFAULT_OPTIONS,
): string => {
try {
const weiBN: BN = !BN.isBN(wei) ? new BN(wei) : wei;
if (unit === 'wei') {
return weiBN.toString(10);
}
const baseStr = unitMap.get(unit);
if (!baseStr) {
throw new Error(`No unit of type ${unit} exists.`);
}
const base = new BN(baseStr, 10);
const baseNumDecimals = baseStr.length - 1;
let fraction = weiBN
.abs()
.mod(base)
.toString(10);
// prepend 0s to the fraction half
while (fraction.length < baseNumDecimals) {
fraction = `0${fraction}`;
}
if (!options.pad) {
/* eslint-disable prefer-destructuring */
const matchFraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/);
fraction = matchFraction ? matchFraction[1] : '0';
}
const whole = weiBN.div(base).toString(10);
return fraction === '0' ? `${whole}` : `${whole}.${fraction}`;
} catch (error) {
throw error;
}
};
export class Unit {}
export class Unit {
static from(str: BN | string) {
return new Unit(str);
}
static Wei(str: BN | string) {
return new Unit(str).asWei();
}
static Kwei(str: BN | string) {
return new Unit(str).asKwei();
}
static Mwei(str: BN | string) {
return new Unit(str).asMwei();
}
static Gwei(str: BN | string) {
return new Unit(str).asGwei();
}
static Szabo(str: BN | string) {
return new Unit(str).asSzabo();
}
static Finney(str: BN | string) {
return new Unit(str).asFinney();
}
static Ether(str: BN | string) {
return new Unit(str).asEther();
}
static Kether(str: BN | string) {
return new Unit(str).asKether();
}
static Mether(str: BN | string) {
return new Unit(str).asMether();
}
static Gether(str: BN | string) {
return new Unit(str).asGether();
}
static Tether(str: BN | string) {
return new Unit(str).asTether();
}
wei?: BN;
unit: BN | string;
constructor(str: BN | string) {
this.unit = str;
}
asWei() {
this.wei = new BN(this.unit);
return this;
}
asKwei() {
this.wei = toWei(this.unit, Units.kwei);
return this;
}
asMwei() {
this.wei = toWei(this.unit, Units.Mwei);
return this;
}
asGwei() {
this.wei = toWei(this.unit, Units.Gwei);
return this;
}
asSzabo() {
this.wei = toWei(this.unit, Units.szabo);
return this;
}
asFinney() {
this.wei = toWei(this.unit, Units.finney);
return this;
}
asEther() {
this.wei = toWei(this.unit, Units.ether);
return this;
}
asKether() {
this.wei = toWei(this.unit, Units.Kether);
return this;
}
asMether() {
this.wei = toWei(this.unit, Units.Mether);
return this;
}
asGether() {
this.wei = toWei(this.unit, Units.Gether);
return this;
}
asTether() {
this.wei = toWei(this.unit, Units.Gether);
return this;
}
toWei() {
return this.wei;
}
toKwei() {
if (this.wei) {
return fromWei(this.wei, Units.kwei);
} else {
throw new Error('error transforming');
}
}
toGwei() {
if (this.wei) {
return fromWei(this.wei, Units.Gwei);
} else {
throw new Error('error transforming');
}
}
toMwei() {
if (this.wei) {
return fromWei(this.wei, Units.Mwei);
} else {
throw new Error('error transforming');
}
}
toSzabo() {
if (this.wei) {
return fromWei(this.wei, Units.szabo);
} else {
throw new Error('error transforming');
}
}
tofinney() {
if (this.wei) {
return fromWei(this.wei, Units.finney);
} else {
throw new Error('error transforming');
}
}
toEther() {
if (this.wei) {
return fromWei(this.wei, Units.ether);
} else {
throw new Error('error transforming');
}
}
toKether() {
if (this.wei) {
return fromWei(this.wei, Units.Kether);
} else {
throw new Error('error transforming');
}
}
toMether() {
if (this.wei) {
return fromWei(this.wei, Units.Mether);
} else {
throw new Error('error transforming');
}
}
toGether() {
if (this.wei) {
return fromWei(this.wei, Units.Gether);
} else {
throw new Error('error transforming');
}
}
toTether() {
if (this.wei) {
return fromWei(this.wei, Units.Tether);
} else {
throw new Error('error transforming');
}
}
toWeiString() {
if (this.wei) {
return this.wei.toString();
} else {
throw new Error('error transforming');
}
}
toHex() {
if (this.wei) {
return numberToHex(this.wei);
} else {
throw new Error('error transforming');
}
}
}

@ -31,6 +31,11 @@ declare namespace Elliptic {
}
interface EC {
recoverPubKey(
arg0: Uint8Array | null,
rs: { r: Uint8Array | null; s: Uint8Array | null },
recoveryParam: number | undefined,
): any;
curve: Curve;
genKeyPair(opt?: GenKeyPairOpt): KeyPair;
keyFromPrivate(priv: string, enc: string): KeyPair;

Loading…
Cancel
Save