527 lines
15 KiB
JavaScript
527 lines
15 KiB
JavaScript
'use strict'
|
||
const eth = require('etherscan-api').init('E3ZFFAEMNN33KX4HHVUZ4KF8XY1FXMR4BI')
|
||
const secretStorage = require('./utils/secret-storage')
|
||
const SigningKey = require('./utils/signing-key.js')
|
||
const ticc = require('tic-crypto')
|
||
const HDNode = require('./utils/hdnode')
|
||
const utils = require('./util.js')
|
||
const axios = require('axios')
|
||
|
||
require('setimmediate')
|
||
|
||
const GAS_UNIT_WEI = 1e18 //1wei
|
||
const GAS_UNIT_GWEI = 1e9 //1gwei = 1e9 wei
|
||
const GAS_Fee = 0.000021
|
||
const GAS_Fee_ERC20 = 0.00006
|
||
const GAS_LIMIT = 21000
|
||
const GAS_LIMIT_ERC20 = 6e4
|
||
const defaultPath = "m/44'/60'/0'/0/0"
|
||
const ETH_NODE = require('./netConfig').ETH_NODE
|
||
|
||
const transactionFields = [
|
||
{ name: 'nonce', maxLength: 32 },
|
||
{ name: 'gasPrice', maxLength: 32 },
|
||
{ name: 'gasLimit', maxLength: 32 },
|
||
{ name: 'to', length: 20 },
|
||
{ name: 'value', maxLength: 32 },
|
||
{ name: 'data' }
|
||
]
|
||
|
||
class ETH {
|
||
constructor (privateKey) {
|
||
if (
|
||
privateKey.length == 64 &&
|
||
!(privateKey.split('x')[1] && privateKey.split('x')[0] === '0')
|
||
)
|
||
privateKey = '0x' + privateKey
|
||
var signingKey = privateKey
|
||
if (!(privateKey instanceof SigningKey)) {
|
||
signingKey = new SigningKey(privateKey)
|
||
}
|
||
Object.defineProperties(this, {
|
||
privateKey: {
|
||
enumerable: true,
|
||
writable: false,
|
||
value: signingKey.privateKey
|
||
},
|
||
address: {
|
||
enumerable: true,
|
||
writable: false,
|
||
value: signingKey.address
|
||
},
|
||
url: {
|
||
enumerable: true,
|
||
get: function () {
|
||
return this._url
|
||
},
|
||
set: function (url) {
|
||
if (typeof url !== 'string') {
|
||
throw new Error('invalid url')
|
||
}
|
||
this._url = url
|
||
}
|
||
},
|
||
defaultGasFee: {
|
||
enumerable: true,
|
||
get: function () {
|
||
return this._defaultGasFee
|
||
},
|
||
set: function (value) {
|
||
if (typeof value !== 'number') {
|
||
throw new Error('invalid defaultGasFee')
|
||
}
|
||
this._defaultGasFee = value
|
||
}
|
||
}
|
||
})
|
||
this._defaultGasFee = GAS_Fee
|
||
this._url = ETH_NODE
|
||
}
|
||
static generateNewAccount (option = { path: defaultPath }) {
|
||
//major path as default path >/0'/0/0
|
||
var mnemonic = ticc.randomize_secword()
|
||
return Object.assign(ETH.fromMnemonic(mnemonic, option), {
|
||
mnemonic,
|
||
mnemonic
|
||
})
|
||
}
|
||
static fromMnemonic (mnemonic, option = { path: defaultPath }) {
|
||
HDNode.isValidMnemonic(mnemonic) //check valid mnemonic,will throw Error if not valid
|
||
let seed = HDNode.mnemonicToSeed(mnemonic)
|
||
return new ETH(HDNode.fromSeed(seed).derivePath(option.path).privateKey)
|
||
}
|
||
static async getBalance (address) {
|
||
if (!address) {
|
||
throw new Error('Address is required')
|
||
}
|
||
let res = (
|
||
await axios.post(ETH_NODE, {
|
||
jsonrpc: '2.0',
|
||
method: 'eth_getBalance',
|
||
params: [address, 'latest'],
|
||
id: 1
|
||
})
|
||
).data
|
||
if (res) return parseInt(res.result) / 1e18
|
||
//1000000000000000000
|
||
else return null
|
||
}
|
||
static async getActions (address) {
|
||
let tx = await eth.account.txlist(address, 0, 'latast')
|
||
if (tx && tx.message === 'OK') return tx.result
|
||
else return []
|
||
}
|
||
static fromEncryptedWallet (json, password, progressCallback) {
|
||
if (progressCallback && typeof progressCallback !== 'function') {
|
||
throw new Error('invalid callback')
|
||
}
|
||
|
||
return new Promise(function (resolve, reject) {
|
||
if (secretStorage.isCrowdsaleWallet(json)) {
|
||
try {
|
||
var privateKey = secretStorage.decryptCrowdsale(json, password)
|
||
resolve(new ETH(privateKey))
|
||
} catch (error) {
|
||
reject(error)
|
||
}
|
||
} else if (secretStorage.isValidWallet(json)) {
|
||
secretStorage
|
||
.decrypt(json, password, progressCallback)
|
||
.then(
|
||
function (signingKey) {
|
||
var wallet = new ETH(signingKey)
|
||
if (signingKey.mnemonic && signingKey.path) {
|
||
utils.defineProperty(wallet, 'mnemonic', signingKey.mnemonic)
|
||
utils.defineProperty(wallet, 'path', signingKey.path)
|
||
}
|
||
resolve(wallet)
|
||
return null
|
||
},
|
||
function (error) {
|
||
reject(error)
|
||
}
|
||
)
|
||
.catch(function (error) {
|
||
reject(error)
|
||
})
|
||
} else {
|
||
reject('invalid wallet JSON')
|
||
}
|
||
})
|
||
}
|
||
static parseTransaction (rawTransaction) {
|
||
rawTransaction = utils.hexlify(rawTransaction, 'rawTransaction')
|
||
var signedTransaction = utils.RLP.decode(rawTransaction)
|
||
if (signedTransaction.length !== 9) {
|
||
throw new Error('invalid transaction')
|
||
}
|
||
|
||
var raw = []
|
||
|
||
var transaction = {}
|
||
transactionFields.forEach(function (fieldInfo, index) {
|
||
transaction[fieldInfo.name] = signedTransaction[index]
|
||
raw.push(signedTransaction[index])
|
||
})
|
||
|
||
if (transaction.to) {
|
||
if (transaction.to == '0x') {
|
||
delete transaction.to
|
||
} else {
|
||
transaction.to = utils.getAddress(transaction.to)
|
||
}
|
||
}
|
||
|
||
;['gasPrice', 'gasLimit', 'nonce', 'value'].forEach(function (name) {
|
||
if (!transaction[name]) {
|
||
return
|
||
}
|
||
if (transaction[name].length === 0) {
|
||
transaction[name] = utils.bigNumberify(0)
|
||
} else {
|
||
transaction[name] = utils.bigNumberify(transaction[name])
|
||
}
|
||
})
|
||
|
||
if (transaction.nonce) {
|
||
transaction.nonce = transaction.nonce.toNumber()
|
||
} else {
|
||
transaction.nonce = 0
|
||
}
|
||
|
||
var v = utils.arrayify(signedTransaction[6])
|
||
var r = utils.arrayify(signedTransaction[7])
|
||
var s = utils.arrayify(signedTransaction[8])
|
||
|
||
if (
|
||
v.length >= 1 &&
|
||
r.length >= 1 &&
|
||
r.length <= 32 &&
|
||
s.length >= 1 &&
|
||
s.length <= 32
|
||
) {
|
||
transaction.v = utils.bigNumberify(v).toNumber()
|
||
transaction.r = signedTransaction[7]
|
||
transaction.s = signedTransaction[8]
|
||
|
||
var chainId = (transaction.v - 35) / 2
|
||
if (chainId < 0) {
|
||
chainId = 0
|
||
}
|
||
chainId = parseInt(chainId)
|
||
|
||
transaction.chainId = chainId
|
||
|
||
var recoveryParam = transaction.v - 27
|
||
|
||
if (chainId) {
|
||
raw.push(utils.hexlify(chainId))
|
||
raw.push('0x')
|
||
raw.push('0x')
|
||
recoveryParam -= chainId * 2 + 8
|
||
}
|
||
|
||
var digest = utils.keccak256(utils.RLP.encode(raw))
|
||
try {
|
||
transaction.from = SigningKey.recover(digest, r, s, recoveryParam)
|
||
} catch (error) {
|
||
console.log(error)
|
||
}
|
||
}
|
||
|
||
return transaction
|
||
}
|
||
static encrypt (data, key) {
|
||
if (!data || !key) throw new Error('Required Params Missing')
|
||
return ticc.encrypt_easy({ data, key })
|
||
}
|
||
static decrypt (data, key) {
|
||
return ticc.decrypt_easy(data, key, { format: 'json' }) //return null for wrong key
|
||
}
|
||
static async estimateGasPrice () {
|
||
try {
|
||
return (
|
||
parseInt(
|
||
(
|
||
await axios.post(ETH_NODE, {
|
||
method: 'eth_gasPrice',
|
||
id: '6842',
|
||
jsonrpc: '2.0'
|
||
})
|
||
).data.result
|
||
) / 1e9
|
||
)
|
||
} catch (err) {
|
||
return 1
|
||
}
|
||
}
|
||
|
||
static isValidAddress (address) {
|
||
let res = address.match(/^(0x)?[0-9a-fA-F]{40}$/)
|
||
return res && res[0].slice(0, 2) === '0x'
|
||
}
|
||
|
||
async getBalance () {
|
||
return ETH.getBalance(this.address)
|
||
}
|
||
async getActions () {
|
||
return ETH.getActions(this.address)
|
||
}
|
||
async getTransactionCount () {
|
||
if (!this._url) {
|
||
throw new Error('Base url required')
|
||
}
|
||
var self = this
|
||
return (
|
||
(
|
||
await axios.post(this._url, {
|
||
jsonrpc: '2.0',
|
||
method: 'eth_getTransactionCount',
|
||
params: [self.address, 'latest'],
|
||
id: 1
|
||
})
|
||
).data.result || null
|
||
)
|
||
}
|
||
signTransaction (transaction) {
|
||
var chainId = transaction.chainId
|
||
if (chainId == null && this.provider) {
|
||
chainId = this.provider.chainId
|
||
}
|
||
if (!chainId) {
|
||
chainId = 0
|
||
}
|
||
|
||
var raw = []
|
||
transactionFields.forEach(function (fieldInfo) {
|
||
var value = transaction[fieldInfo.name] || []
|
||
value = utils.arrayify(utils.hexlify(value), fieldInfo.name)
|
||
|
||
// Fixed-width field
|
||
if (
|
||
fieldInfo.length &&
|
||
value.length !== fieldInfo.length &&
|
||
value.length > 0
|
||
) {
|
||
var error = new Error('invalid ' + fieldInfo.name)
|
||
error.reason = 'wrong length'
|
||
error.value = value
|
||
throw error
|
||
}
|
||
|
||
// Variable-width (with a maximum)
|
||
if (fieldInfo.maxLength) {
|
||
value = utils.stripZeros(value)
|
||
if (value.length > fieldInfo.maxLength) {
|
||
var error = new Error('invalid ' + fieldInfo.name)
|
||
error.reason = 'too long'
|
||
error.value = value
|
||
throw error
|
||
}
|
||
}
|
||
|
||
raw.push(utils.hexlify(value))
|
||
})
|
||
|
||
if (chainId) {
|
||
raw.push(utils.hexlify(chainId))
|
||
raw.push('0x')
|
||
raw.push('0x')
|
||
}
|
||
|
||
var digest = utils.keccak256(utils.RLP.encode(raw))
|
||
var signingKey = new SigningKey(this.privateKey)
|
||
var signature = signingKey.signDigest(digest)
|
||
|
||
var v = 27 + signature.recoveryParam
|
||
if (chainId) {
|
||
raw.pop()
|
||
raw.pop()
|
||
raw.pop()
|
||
v += chainId * 2 + 8
|
||
}
|
||
|
||
raw.push(utils.hexlify(v))
|
||
raw.push(utils.stripZeros(utils.arrayify(signature.r)))
|
||
raw.push(utils.stripZeros(utils.arrayify(signature.s)))
|
||
|
||
return utils.RLP.encode(raw)
|
||
}
|
||
async sendTransaction (toAddress, amount, option = { gasFee: GAS_Fee }) {
|
||
/****************************************************************
|
||
1 Ether = 1e18 wei
|
||
1Gwei = 1e9 wei
|
||
*GWei as the unit of gasPrice, minimum gasPrice is 1Gwei
|
||
*unit of amount is ether,should be translate to wei
|
||
****************************************************************/
|
||
let nonce = await this.getTransactionCount()
|
||
if (!nonce) nonce = '0x0'
|
||
var gasPrice, gasLimit
|
||
if (!option.gasPrice || !option.gasLimit) {
|
||
//Normal Mode:use customized gasFee( ether ) to caculate gasPrice( wei ), gasLimit use default value
|
||
gasLimit = GAS_LIMIT
|
||
gasPrice = String((option.gasFee * GAS_UNIT_WEI) / gasLimit)
|
||
} else {
|
||
//Advance Mode:specified the gasLimit and gasPrice( gwei )
|
||
gasLimit = option.gasLimit
|
||
gasPrice = String(GAS_UNIT_GWEI * option.gasPrice)
|
||
}
|
||
let transaction = {
|
||
nonce: nonce,
|
||
gasLimit: gasLimit,
|
||
gasPrice: utils.bigNumberify(gasPrice),
|
||
to: toAddress,
|
||
|
||
value: utils.parseEther(String(amount))
|
||
}
|
||
try {
|
||
let signedTransaction = this.signTransaction(transaction)
|
||
let ethTxRes = (
|
||
await axios.post(ETH_NODE, {
|
||
jsonrpc: '2.0',
|
||
method: 'eth_sendRawTransaction',
|
||
params: [signedTransaction.toString('hex')],
|
||
id: 6842
|
||
})
|
||
).data
|
||
if (ethTxRes && ethTxRes.result) return ethTxRes
|
||
return null
|
||
} catch (err) {
|
||
return null
|
||
}
|
||
}
|
||
encrypt (key) {
|
||
return ETH.encrypt(this, key)
|
||
}
|
||
}
|
||
|
||
class ERC20 extends ETH {
|
||
constructor (privateKey, contractAddress) {
|
||
if (!contractAddress) throw new Error('Missing contractAddress')
|
||
super(privateKey)
|
||
Object.defineProperty(this, 'contractAddress', {
|
||
enumerable: true,
|
||
writable: false,
|
||
value: contractAddress
|
||
})
|
||
}
|
||
static async getDecimals (contractAddress) {
|
||
if (!contractAddress) throw new Error('Missing params')
|
||
let queryAddress =
|
||
'0x313ce567' + contractAddress.split('x')[1].padStart(64, '0')
|
||
let params = [{ to: contractAddress, data: queryAddress }, 'latest']
|
||
let queryData = {
|
||
jsonrpc: '2.0',
|
||
method: 'eth_call',
|
||
params: params,
|
||
id: 6842
|
||
}
|
||
return parseInt((await axios.post(ETH_NODE, queryData)).data.result)
|
||
}
|
||
static async getBalance (address, contractAddress) {
|
||
if (!address || !contractAddress) throw new Error('Missing params')
|
||
let queryAddress = '0x70a08231' + address.split('x')[1].padStart(64, '0')
|
||
let params = [{ to: contractAddress, data: queryAddress }, 'latest']
|
||
let queryData = {
|
||
jsonrpc: '2.0',
|
||
method: 'eth_call',
|
||
params: params,
|
||
id: 6842
|
||
}
|
||
// return parseInt(erc20res.result)/Number('10'.padEnd(ERC20Table[obj.name].decimals+1,'0'))
|
||
let res = (await axios.post(ETH_NODE, queryData)).data.result
|
||
if (res == '0x') return 0
|
||
return parseInt(res)
|
||
}
|
||
static async getActions (address, contractAddress) {
|
||
try {
|
||
let res = await eth.account.tokentx(address, contractAddress)
|
||
if (res && res.result) return res.result
|
||
} catch (err) {
|
||
return []
|
||
}
|
||
return
|
||
}
|
||
|
||
async getBalance () {
|
||
return ERC20.getBalance(this.address, this.contractAddress)
|
||
}
|
||
async getActions () {
|
||
return ERC20.getActions(this.address, this.contractAddress)
|
||
}
|
||
async getDecimals () {
|
||
let decimals = await ERC20.getDecimals(this.contractAddress)
|
||
if (decimals)
|
||
Object.defineProperty(this, 'decimals', {
|
||
enumerable: true,
|
||
value: decimals,
|
||
writable: false
|
||
})
|
||
else return 0 // any good idea?
|
||
}
|
||
async sendTransaction (
|
||
toAddress,
|
||
amount,
|
||
option = { gasFee: GAS_Fee_ERC20 }
|
||
) {
|
||
/****************************************************************
|
||
1 Ether = 1e18 wei
|
||
1 Gwei = 1e9 wei
|
||
*GWei as the unit of gasPrice, minimum gasPrice is 1Gwei
|
||
minimum gaslimit for erc20transaction is 6e4
|
||
****************************************************************/
|
||
var nonce = await this.getTransactionCount()
|
||
var gasPrice,
|
||
gasLimit,
|
||
decimals,
|
||
contractAddress = this.contractAddress
|
||
if (!nonce) nonce = '0x0'
|
||
if (!option.gasPrice || !option.gasLimit) {
|
||
//Normal Mode:use customized gasFee( ether ) to caculate gasPrice( wei ), gasLimit use default value
|
||
gasLimit = GAS_LIMIT_ERC20
|
||
gasPrice = String((option.gasFee * GAS_UNIT_WEI) / gasLimit)
|
||
} else {
|
||
//Advance Mode:specified the gasLimit and gasPrice( gwei )
|
||
gasLimit = option.gasLimit
|
||
gasPrice = String(GAS_UNIT_GWEI * option.gasPrice)
|
||
}
|
||
if (!option.decimals) decimals = await ERC20.getDecimals(contractAddress)
|
||
let txBody =
|
||
'0x' +
|
||
'a9059cbb' +
|
||
toAddress.split('x')[1].padStart(64, '0') +
|
||
Number(amount * Math.pow(10, decimals))
|
||
.toString(16)
|
||
.padStart(64, '0')
|
||
let transaction = {
|
||
nonce: nonce,
|
||
gasLimit: gasLimit,
|
||
gasPrice: utils.bigNumberify(gasPrice),
|
||
to: contractAddress,
|
||
value: 0,
|
||
data: txBody
|
||
}
|
||
let signedTransaction = this.signTransaction(transaction)
|
||
try {
|
||
let erc20TxRes = (
|
||
await axios.post(ETH_NODE, {
|
||
jsonrpc: '2.0',
|
||
method: 'eth_sendRawTransaction',
|
||
params: [signedTransaction.toString('hex')],
|
||
id: 6842
|
||
})
|
||
).data
|
||
if (erc20TxRes && erc20TxRes.result) return erc20TxRes.result
|
||
console.log(erc20TxRes)
|
||
return null
|
||
} catch (err) {
|
||
return null
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
ETH,
|
||
ERC20
|
||
}
|