diff --git a/examples/testNode.js b/examples/testNode.js index 8e53b73..494f157 100644 --- a/examples/testNode.js +++ b/examples/testNode.js @@ -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)); diff --git a/packages/harmony-account/src/account.ts b/packages/harmony-account/src/account.ts index f2cef35..a824324 100644 --- a/packages/harmony-account/src/account.ts +++ b/packages/harmony-account/src/account.ts @@ -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 diff --git a/packages/harmony-account/src/index.ts b/packages/harmony-account/src/index.ts index 73927b0..88db089 100644 --- a/packages/harmony-account/src/index.ts +++ b/packages/harmony-account/src/index.ts @@ -1,2 +1,4 @@ export * from './account'; export * from './wallet'; +export * from './types'; +export * from './utils'; diff --git a/packages/harmony-crypto/src/bytes.ts b/packages/harmony-crypto/src/bytes.ts index 5dec6e5..152f1b2 100644 --- a/packages/harmony-crypto/src/bytes.ts +++ b/packages/harmony-crypto/src/bytes.ts @@ -341,54 +341,55 @@ export function isSignature(value: any): value is Signature { } export function splitSignature(signature: Arrayish | Signature): Signature { - let v: number | undefined = 0; - let r = '0x'; - let s = '0x'; + if (signature !== undefined) { + let v = 0; + let r = '0x'; + let s = '0x'; + + if (isSignature(signature)) { + if (signature.v == null && signature.recoveryParam == null) { + errors.throwError( + 'at least on of recoveryParam or v must be specified', + errors.INVALID_ARGUMENT, + { argument: 'signature', value: signature }, + ); + } + r = hexZeroPad(signature.r, 32); + s = hexZeroPad(signature.s, 32); - if (isSignature(signature)) { - if (signature.v == null && signature.recoveryParam == null) { - errors.throwError( - 'at least on of recoveryParam or v must be specified', - errors.INVALID_ARGUMENT, - { argument: 'signature', value: signature }, - ); - } - r = hexZeroPad(signature.r, 32); - s = hexZeroPad(signature.s, 32); + v = signature.v || 0; + if (typeof v === 'string') { + v = parseInt(v, 16); + } - v = signature.v; - if (typeof v === 'string') { - v = parseInt(v, 16); - } + let recoveryParam = signature.recoveryParam || 0; + if (recoveryParam == null && signature.v != null) { + recoveryParam = 1 - (v % 2); + } + v = 27 + recoveryParam; + } else { + const bytes: Uint8Array = arrayify(signature) || new Uint8Array(); + if (bytes.length !== 65) { + throw new Error('invalid signature'); + } + r = hexlify(bytes.slice(0, 32)); + s = hexlify(bytes.slice(32, 64)); - let recoveryParam = signature.recoveryParam || 1; - if (recoveryParam == null && signature.v != null) { - recoveryParam = 1 - (typeof v !== 'number' ? 0 : v % 2); - } - v = 27 + recoveryParam; - } else { - const bytes: Uint8Array | null = arrayify(signature); - if (bytes === null) { - throw new Error('arrayify failed'); - } - if (bytes.length !== 65) { - throw new Error('invalid signature'); + v = bytes[64]; + if (v !== 27 && v !== 28) { + v = 27 + (v % 2); + } } - r = hexlify(bytes.slice(0, 32)); - s = hexlify(bytes.slice(32, 64)); - v = bytes[64]; - if (v !== 27 && v !== 28) { - v = 27 + (v % 2); - } + return { + r, + s, + recoveryParam: v - 27, + v, + }; + } else { + throw new Error('signature is not found'); } - - return { - r, - s, - recoveryParam: v - 27, - v, - }; } export function joinSignature(signature: Signature): string { diff --git a/packages/harmony-crypto/src/keyTool.ts b/packages/harmony-crypto/src/keyTool.ts index 45749fb..259baaa 100644 --- a/packages/harmony-crypto/src/keyTool.ts +++ b/packages/harmony-crypto/src/keyTool.ts @@ -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), + ); +} diff --git a/packages/harmony-transaction/src/transaction.ts b/packages/harmony-transaction/src/transaction.ts index 67bb711..8dc5a03 100644 --- a/packages/harmony-transaction/src/transaction.ts +++ b/packages/harmony-transaction/src/transaction.ts @@ -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); diff --git a/packages/harmony-transaction/src/types.ts b/packages/harmony-transaction/src/types.ts index 0e5c5d3..9843742 100644 --- a/packages/harmony-transaction/src/types.ts +++ b/packages/harmony-transaction/src/types.ts @@ -1,5 +1,6 @@ import { BN, Signature } from '@harmony/crypto'; export interface TxParams { + from: string; to: string; nonce: number | string; gasLimit: BN; diff --git a/packages/harmony-transaction/src/utils.ts b/packages/harmony-transaction/src/utils.ts index e69de29..c3d4269 100644 --- a/packages/harmony-transaction/src/utils.ts +++ b/packages/harmony-transaction/src/utils.ts @@ -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; +}; diff --git a/packages/harmony-utils/src/transformers.ts b/packages/harmony-utils/src/transformers.ts index 84df4f7..1369d88 100644 --- a/packages/harmony-utils/src/transformers.ts +++ b/packages/harmony-utils/src/transformers.ts @@ -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'); + } + } +} diff --git a/typings/elliptic.d.ts b/typings/elliptic.d.ts index ad5e127..2b83bdb 100644 --- a/typings/elliptic.d.ts +++ b/typings/elliptic.d.ts @@ -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;