From b1ca749fb77fa162ac675cee589c7df3e2736766 Mon Sep 17 00:00:00 2001 From: neeboo Date: Thu, 11 Apr 2019 14:39:47 +0800 Subject: [PATCH] [WIP] update Transaction and Account.signTransaction --- examples/testNode.js | 40 ++++- packages/harmony-account/package.json | 4 +- packages/harmony-account/src/account.ts | 75 ++++++--- packages/harmony-account/src/utils.ts | 17 ++ packages/harmony-account/src/wallet.ts | 7 + packages/harmony-account/tsconfig.json | 4 +- packages/harmony-crypto/src/index.ts | 6 +- packages/harmony-network/src/rpc.ts | 1 + packages/harmony-transaction/src/index.ts | 1 + .../harmony-transaction/src/transaction.ts | 158 +++++++++++++++++- packages/harmony-transaction/src/types.ts | 13 ++ packages/harmony-transaction/src/utils.ts | 0 tsconfig.json | 3 +- 13 files changed, 299 insertions(+), 30 deletions(-) create mode 100644 packages/harmony-transaction/src/types.ts create mode 100644 packages/harmony-transaction/src/utils.ts diff --git a/examples/testNode.js b/examples/testNode.js index 470630b..8e53b73 100644 --- a/examples/testNode.js +++ b/examples/testNode.js @@ -1,8 +1,11 @@ const { Account, Wallet } = require('@harmony/account'); const { isAddress, isPrivateKey, numberToHex } = require('@harmony/utils'); const { HttpProvider, Messenger } = require('@harmony/network'); +const { Transaction } = require('@harmony/transaction'); +const { hexlify, BN } = require('@harmony/crypto'); -const wallet = new Wallet(); +const msgr = new Messenger(new HttpProvider('https://dev-api.zilliqa.com')); +const wallet = new Wallet(msgr); async function testEncrypt() { const mne = wallet.generateMnemonic(); @@ -27,7 +30,7 @@ async function testEncrypt() { console.log(`${encrypted.privateKey}`); } -testEncrypt(); +// testEncrypt(); async function testSign(prvKey, data) { const newAcc = wallet.addByPrivateKey(prvKey); @@ -36,7 +39,34 @@ async function testSign(prvKey, data) { console.log(result); } -testSign( - '0x0123456789012345678901234567890123456789012345678901234567890123', - '0x06', +// testSign( +// '0x0123456789012345678901234567890123456789012345678901234567890123', +// '0x06', +// ); + +const txn = new Transaction({ + to: '0xaa4cf82d745c3ead6c8275d782d6cb0a6beac617', + data: '0x', + gasLimit: '0xbfa8a6d03fa6', + gasPrice: '0x', + value: '0x7a92fecc67512737', + nonce: '0xf8', + chainId: 0, +}); + +const acc = wallet.addByPrivateKey( + '0xc3886f791236bf31fe8fd7522a7b12808700deb9c159826fc99236c74614118b', ); + +console.log(txn.getRLPUnsigned()[0]); + +const signed = wallet + .getAccount(acc.address) + .signTransaction(txn, false) + .then(console.log); + +// console.log(wallet.messenger); +// console.log(hexlify(0)); + +// 0xda8003049401234567890123456789012345678901234567890580; +// 0xda8003049401234567890123456789012345678901234567890580; diff --git a/packages/harmony-account/package.json b/packages/harmony-account/package.json index 0f64720..132d0cc 100644 --- a/packages/harmony-account/package.json +++ b/packages/harmony-account/package.json @@ -7,7 +7,7 @@ "browser": "dist/index.js", "module": "dist/index.esm.js", "jsnext:main": "dist/index.esm.js", - "typings":"dist/index.d.ts", + "typings": "dist/index.d.ts", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, @@ -15,6 +15,8 @@ "license": "ISC", "dependencies": { "@harmony/crypto": "^0.0.1", + "@harmony/network": "^0.0.1", + "@harmony/transaction": "^0.0.1", "@harmony/utils": "^0.0.1" } } diff --git a/packages/harmony-account/src/account.ts b/packages/harmony-account/src/account.ts index 04fd018..f2cef35 100644 --- a/packages/harmony-account/src/account.ts +++ b/packages/harmony-account/src/account.ts @@ -7,13 +7,14 @@ import { decrypt, EncryptOptions, Keystore, - sign, - isSignature, Signature, } from '@harmony/crypto'; -import { isPrivateKey, add0xToString } from '@harmony/utils'; +import { isPrivateKey, add0xToString, hexToNumber } from '@harmony/utils'; +import { Transaction } from '@harmony/transaction'; +import { Messenger, RPCMethod } from '@harmony/network'; import { Shards, ShardId } from './types'; +import { RLPSign } from './utils'; class Account { /** @@ -37,7 +38,10 @@ class Account { privateKey?: string; publicKey?: string; address?: string; + balance?: string = '0'; + nonce?: number = 0; shards: Shards = new Map().set('default', ''); + messenger?: Messenger; /** * @function checksumAddress checsumAddress getter @@ -55,7 +59,8 @@ class Account { return this.shards.size; } - constructor(key?: string) { + constructor(key?: string, messenger?: Messenger) { + this.messenger = messenger; if (!key) { this._new(); } else { @@ -108,9 +113,22 @@ class Account { * @function getBalance get Account's balance * @return {type} {description} */ - async getBalance(): Promise { - // console.log() - return ''; + async getBalance(): Promise { + if (this.messenger) { + const result = await this.messenger.send( + RPCMethod.GetBalance, + this.address, + ); + if (result.responseType === 'result') { + this.balance = hexToNumber(result.balance); + this.nonce = Number.parseInt(hexToNumber(result.nonce), 10); + } + } + return { + balance: this.balance, + nonce: this.nonce, + shards: this.shards, + }; } /** @@ -120,22 +138,39 @@ class Account { async updateShards(): Promise { return ''; } - /** - * @function sign - * @return {Promise} sign transaction - */ - async sign(message: string): Promise { - if (this.privateKey && isPrivateKey(this.privateKey)) { - const signature: Signature = await sign(message, this.privateKey); - if (isSignature(signature)) { - return signature; - } else { - throw new Error('Cannot sign'); - } + + async signTransaction( + transaction: Transaction, + updateNonce: boolean = true, + encodeMode: string = 'rlp', + ): Promise { + if (!this.privateKey || !isPrivateKey(this.privateKey)) { + throw new Error(`${this.privateKey} is not found or not correct`); + } + // let signed = ''; + if (updateNonce) { + const balanceObject: any = await this.getBalance(); + transaction.setParams({ + ...transaction.txParams, + nonce: balanceObject.nonce + 1, + }); + } + if (encodeMode === 'rlp') { + const [signature, txnHash]: [Signature, string] = RLPSign( + transaction, + this.privateKey, + ); + return transaction.map((obj: any) => { + return { ...obj, signature, txnHash }; + }); } else { - throw new Error('Privatekey not found or not correct'); + // TODO: if we use other encode method, eg. protobuf, we should implement this + return transaction; } } + setMessenger(messenger?: Messenger) { + this.messenger = messenger; + } /** * @function _new private method create Account * @return {Account} Account instance diff --git a/packages/harmony-account/src/utils.ts b/packages/harmony-account/src/utils.ts index e69de29..ad5e204 100644 --- a/packages/harmony-account/src/utils.ts +++ b/packages/harmony-account/src/utils.ts @@ -0,0 +1,17 @@ +import { Transaction, TxParams } from '@harmony/transaction'; +import { sign, keccak256, Signature } from '@harmony/crypto'; + +export const RLPSign = ( + transaction: Transaction, + prv: string, +): [Signature, string] => { + const [unsignedTxnHash, raw] = transaction.getRLPUnsigned(); + const regroup: TxParams = { + ...transaction.txParams, + unsignedTxnHash, + }; + transaction.setParams(regroup); + const signature = sign(keccak256(unsignedTxnHash), prv); + const signed = transaction.getRLPSigned(raw, signature); + return [signature, signed]; +}; diff --git a/packages/harmony-account/src/wallet.ts b/packages/harmony-account/src/wallet.ts index 5ced042..47ecba2 100644 --- a/packages/harmony-account/src/wallet.ts +++ b/packages/harmony-account/src/wallet.ts @@ -1,8 +1,10 @@ import { bip39, hdkey, EncryptOptions } from '@harmony/crypto'; +import { Messenger } from '@harmony/network'; import { isPrivateKey } from '@harmony/utils'; import { Account } from './account'; class Wallet { + messenger?: Messenger; /** * @memberof Wallet * @@ -15,6 +17,10 @@ class Wallet { get accounts(): string[] { return [...this.accountMap.keys()]; } + + constructor(messenger?: Messenger) { + this.messenger = messenger; + } /** * @function generateMnemonic * @memberof Wallet @@ -51,6 +57,7 @@ class Wallet { addByPrivateKey(privateKey: string): Account { try { const newAcc = Account.add(privateKey); + newAcc.setMessenger(this.messenger); if (newAcc.address) { this.accountMap.set(newAcc.address, newAcc); return newAcc; diff --git a/packages/harmony-account/tsconfig.json b/packages/harmony-account/tsconfig.json index f517c81..5e3d0f0 100644 --- a/packages/harmony-account/tsconfig.json +++ b/packages/harmony-account/tsconfig.json @@ -7,6 +7,8 @@ "include": ["src", "../../typings/**/*.d.ts"], "references": [ { "path": "../harmony-crypto" }, - { "path": "../harmony-utils" } + { "path": "../harmony-utils" }, + { "path": "../harmony-transaction" }, + { "path": "../harmony-network" } ] } diff --git a/packages/harmony-crypto/src/index.ts b/packages/harmony-crypto/src/index.ts index 04a5ef9..e156587 100644 --- a/packages/harmony-crypto/src/index.ts +++ b/packages/harmony-crypto/src/index.ts @@ -1,12 +1,16 @@ import hdkey from 'hdkey'; import bip39 from 'bip39'; +import BN from 'bn.js'; export * from './random'; export * from './keyTool'; export * from './keystore'; export * from './bytes'; +export * from './rlp'; +export * from './keccak256'; +export * from './errors'; // export types export * from './types'; -export { hdkey, bip39 }; +export { hdkey, bip39, BN }; diff --git a/packages/harmony-network/src/rpc.ts b/packages/harmony-network/src/rpc.ts index c49cc58..913fdc4 100644 --- a/packages/harmony-network/src/rpc.ts +++ b/packages/harmony-network/src/rpc.ts @@ -1,6 +1,7 @@ export const enum RPCMethod { // account related FetchBalance = 'FetchBalance', + GetBalance = 'GetBalance', // block info related GetLatestBlock = 'GetLatestBlock', GetBlock = 'GetBlock', diff --git a/packages/harmony-transaction/src/index.ts b/packages/harmony-transaction/src/index.ts index 7cd1838..376eb7a 100644 --- a/packages/harmony-transaction/src/index.ts +++ b/packages/harmony-transaction/src/index.ts @@ -1,2 +1,3 @@ export * from './factory'; export * from './transaction'; +export * from './types'; diff --git a/packages/harmony-transaction/src/transaction.ts b/packages/harmony-transaction/src/transaction.ts index 6c0e61c..67bb711 100644 --- a/packages/harmony-transaction/src/transaction.ts +++ b/packages/harmony-transaction/src/transaction.ts @@ -1,2 +1,158 @@ -class Transaction {} +import { + BN, + encode, + // toChecksumAddress, + arrayify, + hexlify, + stripZeros, + Signature, + splitSignature, +} from '@harmony/crypto'; +import { add0xToString } from '@harmony/utils'; +import { TxParams } from './types'; + +export const transactionFields = [ + { 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 }, +]; + +class Transaction { + // private hash?: string; + // private from?: string; + private nonce: number | string; + private to: string; + private gasLimit: BN; + private gasPrice: BN; + private data: string; + private value: BN; + private chainId: number; + private txnHash: string; + private unsignedTxnHash: string; + private signature: Signature; + // private r?: string; + // private s?: string; + // private v?: number; + // constructor + constructor(params?: TxParams) { + this.nonce = params ? params.nonce : 0; + this.gasPrice = params ? params.gasPrice : new BN(0); + this.gasLimit = params ? params.gasLimit : new BN(0); + this.to = params ? params.to : '0x'; + this.value = params ? params.value : new BN(0); + this.data = params ? params.data : '0x'; + this.chainId = params ? params.chainId : 0; + this.txnHash = params ? params.txnHash : '0x'; + this.unsignedTxnHash = params ? params.unsignedTxnHash : '0x'; + this.signature = params + ? params.signature + : { + r: '', + s: '', + recoveryParam: 0, + v: 0, + }; + } + + getRLPUnsigned(): [string, any[]] { + const raw: Array = []; + + transactionFields.forEach((field: any) => { + let value = (this.txParams)[field.name] || []; + value = arrayify( + hexlify( + field.transform === 'hex' + ? add0xToString(value.toString('hex')) + : value, + ), + ); + // Fixed-width field + if ( + field.fix === true && + field.length && + value.length !== field.length && + value.length > 0 + ) { + throw new Error(`invalid length for ${field.name}`); + } + + // Variable-width (with a maximum) + if (field.fix === false && field.length) { + value = stripZeros(value); + if (value.length > field.length) { + throw new Error(`invalid length for ${field.name}`); + } + } + + raw.push(hexlify(value)); + }); + + if (this.txParams.chainId != null && this.txParams.chainId !== 0) { + raw.push(hexlify(this.txParams.chainId)); + raw.push('0x'); + raw.push('0x'); + } + + return [encode(raw), raw]; + } + + getRLPSigned(raw: any[], signature: Signature): string { + const sig = splitSignature(signature); + let v = 27 + (sig.recoveryParam || 0); + if (raw.length === 9) { + raw.pop(); + raw.pop(); + raw.pop(); + v += this.chainId * 2 + 8; + } + + raw.push(hexlify(v)); + raw.push(stripZeros(arrayify(sig.r) || [])); + raw.push(stripZeros(arrayify(sig.s) || [])); + + return encode(raw); + } + get txParams(): TxParams { + return { + nonce: this.nonce || 0, + gasPrice: this.gasPrice || new BN(0), + gasLimit: this.gasLimit || new BN(0), + to: this.to || '0x', + value: this.value || new BN(0), + data: this.data || '0x', + chainId: this.chainId || 0, + txnHash: this.txnHash || '0x', + unsignedTxnHash: this.unsignedTxnHash || '0x', + signature: this.signature || '0x', + }; + } + setParams(params: TxParams) { + this.nonce = params ? params.nonce : 0; + this.gasPrice = params ? params.gasPrice : new BN(0); + this.gasLimit = params ? params.gasLimit : new BN(0); + this.to = params ? params.to : '0x'; + this.value = params ? params.value : new BN(0); + this.data = params ? params.data : '0x'; + this.chainId = params ? params.chainId : 0; + this.txnHash = params ? params.txnHash : '0x'; + this.unsignedTxnHash = params ? params.unsignedTxnHash : '0x'; + this.signature = params + ? params.signature + : { + r: '', + s: '', + recoveryParam: 0, + v: 0, + }; + } + + map(fn: any) { + const newParams = fn(this.txParams); + this.setParams(newParams); + return this; + } +} export { Transaction }; diff --git a/packages/harmony-transaction/src/types.ts b/packages/harmony-transaction/src/types.ts new file mode 100644 index 0000000..0e5c5d3 --- /dev/null +++ b/packages/harmony-transaction/src/types.ts @@ -0,0 +1,13 @@ +import { BN, Signature } from '@harmony/crypto'; +export interface TxParams { + to: string; + nonce: number | string; + gasLimit: BN; + gasPrice: BN; + data: string; + value: BN; + chainId: number; + txnHash: string; + unsignedTxnHash: string; + signature: Signature; +} diff --git a/packages/harmony-transaction/src/utils.ts b/packages/harmony-transaction/src/utils.ts new file mode 100644 index 0000000..e69de29 diff --git a/tsconfig.json b/tsconfig.json index 18f41fa..6597713 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ { "path": "packages/harmony-account" }, { "path": "packages/harmony-crypto" }, { "path": "packages/harmony-utils" }, - { "path": "packages/harmony-network" } + { "path": "packages/harmony-network" }, + { "path": "packages/harmony-transaction" } ] }