1. 前言
之前的文章介绍过使用Web3.js
+Node.js
封装成接口以供钱包管理/查询/转账。但是有一些问题:
代币转账可能出现超时失败
以太币/代币转账接口调用成功后,返回的交易
hash
,并不能保证交易已成功,还需要再次去查询交易状态以太币转账接口未判断转账者是否有足够的以太币。代币转账接口未判断转账者是否有足够的代币
转账前没有预估
gas
以太币转账,之前必须传入带小数位的金额。例如转账
1
ether,传入的参数为1000000000000000000
代币转账,之前必须先去查询该代币小数位,然后换算成带小数位的代币金额。例如转账
1
某token,传入的参数为1000000000000000000
代币转账,之前必须将转账方法名,转账人地址,金额,转换为十六进制,再拼接为
data
根据
password
和privateKey
返回keystore
。之前必须将privateKey
手动加上0x
前缀再进行传参,否则将返回错误的keystore
针对这些问题,对代码进行了修改:
修复了代币转账可能出现超时的问题
调用转账接口后,只要返回交易hash,则此交易必定为
success
状态,不需要再去查询交易状态。耗时取决于当前以太坊网络状况,经测试在15-60
秒左右自动预估
gas
,移除了gasLimit
参数以太币转账。现在自动判断转账方是否有足够的以太币
以太币转账。现在传入的以太币单位为
ether
,不需要
再加上18个0的小数位代币转账。现在自动判断转账方是否有足够的代币
代币转账。
不需要
先去获取代币小数位,现在直接传入不带
小数位的代币金额。例如,之前某个代币小数位为8
,转账1
token,则传入100000000
。现在直接传入1
根据
password
和privateKey
返回keystore
。现在直接传入不带0x
前缀的私钥
2. 关键代码
不明白可以看注释
// ======================Tools==========================
// =====================================================
// =====================================================
/**
* 创建钱包
*
* 参数:1.password: 钱包密码
*
* 返回:1.address: 账号地址
* 2.privateKey: 私钥
* 3.keystore: keystore
*
*/
router.get('/eth/account/createWallet', async(ctx, next) => {
if (typeof(ctx.request.query.password) == 'undefined' ||
ctx.request.query.password == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: password.',
})
return
}
try {
let account = web3.eth.accounts.create()
let response = web3.eth.accounts.encrypt(account.privateKey,
ctx.request.query.password)
ctx.body = await Promise.resolve({
code: 0,
address: account.address,
privateKey: account.privateKey.substr(2),
keystore: response,
message: 'Success',
})
} catch (error) {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: error.stack,
})
}
})
/**
* 根据password和keystore返回privateKey
*
* 参数:1.password: 钱包密码
* 2.keystore: keystore
*
* 返回:1.privateKey: 私钥
*
*/
router.get('/eth/account/getPrivateKey', async(ctx, next) => {
let { password, keystore } = ctx.request.query
if (typeof(password) == 'undefined' ||
password == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: password.',
})
return
}
if (typeof(keystore) == 'undefined' ||
keystore == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: keystore.',
})
return
}
try {
let response = web3.eth.accounts.decrypt(
keystore,
password)
ctx.body = await Promise.resolve({
code: 0,
data: response.privateKey.substr(2),
message: 'Success',
})
} catch (error) {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: error.stack,
})
}
})
/**
* 根据password和privateKey返回keystore
*
* 参数:1.password: 钱包密码
* 2.privateKey: 私钥
*
* 返回:1.keystore: keystore
*
*/
router.get('/eth/account/getKeystore', async(ctx, next) => {
let { password, privateKey } = ctx.request.query
if (typeof(password) == 'undefined' ||
password == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: password.',
})
return
}
if (typeof(privateKey) == 'undefined' ||
privateKey == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: privateKey.',
})
return
}
try {
let response = web3.eth.accounts.encrypt("0x" + privateKey,
password)
ctx.body = await Promise.resolve({
code: 0,
data: response,
message: 'Success',
})
} catch (error) {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: error.stack,
})
}
})
/**
* 获得当前最新区块高度
*
* 参数:无
*
* 返回: 1.data: 最新区块高度
*/
router.get('/eth/getBlockNumber', async(ctx, next) => {
try {
let response = await web3.eth.getBlockNumber()
ctx.body = await Promise.resolve({
code: 0,
data: response,
message: 'Success',
})
} catch (error) {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: error.stack,
})
}
})
/**
* 根据交易hash查询交易详情
*
* 参数:1.hash: 交易哈希
*
* 返回:1.data: 交易详情
*
*/
router.get('/eth/getTransaction', async(ctx, next) => {
if (typeof(ctx.request.query.hash) == 'undefined' ||
ctx.request.query.hash == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: hash.',
})
return
}
try {
let response = await web3.eth.getTransaction(ctx.request.query.hash)
ctx.body = await Promise.resolve({
code: 0,
data: response,
message: 'Success',
})
} catch (error) {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: error.stack,
})
}
})
/**
* 获取合约ABI
*
* 参数:1.contractAddress: 合约地址
*
* 返回:1.data: 合约ABI
*
*/
router.get('/token/getAbi', async(ctx, next) => {
if (typeof(ctx.request.query.contractAddress) == 'undefined' ||
ctx.request.query.contractAddress == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: contractAddress.',
})
return
}
try {
let response = await tools.getAbi(ctx.request.query.contractAddress)
ctx.body = await Promise.resolve({
code: 0,
data: eval(response.data.result),
message: 'Success',
})
} catch (error) {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: error.stack,
})
}
})
// ======================Ether==========================
// =====================================================
// =====================================================
/**
* ETH余额查询
*
* 参数:1.address: 查询地址
*
* 返回:1.data: ETH余额,单位为Ether
*
*/
router.get('/eth/getBalance', async(ctx, next) => {
if (typeof(ctx.request.query.address) == 'undefined' ||
ctx.request.query.address == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: address.',
})
return
}
try {
let balance = await web3.eth.getBalance(ctx.request.query.address)
ctx.body = await Promise.resolve({
code: 0,
data: web3.utils.fromWei(balance, 'ether'),
message: 'Success',
})
} catch (error) {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: error.stack,
})
}
})
/**
* ETH转账
*
* 1.自动检查必填参数是否传入,否则会提示某项参数缺失
* 2.gasPrice为可选参数。如果未传入,此接口默认取当前网络平均值。建议不传,使用默认值。
* gasPrice单位为Gwei
* 3.传入的ETH单位为Ether,此接口会自动进行转换为Wei。例如,转账1eth,
* 则传入1,而非1000000000000000000
* 4.此接口会检测转出方是否有足够ETH进行转账,否则会提示ETH不足
* 5.当接口返回交易hash时,此交易的状态即为success,不需要再通过查询获取交易状态
* 6.此接口会在交易状态为success时才返回交易hash,耗时取决于当前以太坊网络状况,
* 经测试在25-60秒左右。未出现过超时问题
*
*
* 参数:1.transferAddress: 转账方地址
* 2.receiverAddress: 接收方地址
* 3.privateKey: 私钥
* 4.num: eth数量
* 5.gasPrice: (可选)燃料费单价
*
* 返回:1.data: 交易成功的hash
*
*/
router.get('/eth/transfer', async(ctx, next) => {
let { transferAddress, receiverAddress, privateKey,
num, gasPrice } = ctx.request.query
if (typeof(transferAddress) == 'undefined' ||
transferAddress == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: transferAddress.',
})
return
}
if (typeof(receiverAddress) == 'undefined' ||
receiverAddress == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: receiverAddress.',
})
return
}
if (typeof(privateKey) == 'undefined' ||
privateKey == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: privateKey.',
})
return
}
if (typeof(num) == 'undefined' ||
num == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: num.',
})
return
}
// 如果没有传入gasPrice, 默认调用web3接口获取最近区块的gasPrice的平均值
if (typeof(gasPrice) == 'undefined' ||
gasPrice == '') {
gasPrice = await web3.eth.getGasPrice()
} else {
// 传值是传入的单位为gwei,需要转为wei
gasPrice = web3.utils.toWei(gasPrice, 'gwei')
}
let amount = await web3.utils.toWei(num)
console.log("transferAddress: " + transferAddress +
" receiverAddress: " + receiverAddress +
" privateKey: " + privateKey +
" num: " + num +
" gasPrice: " + gasPrice);
// 判断转账方是否有足够的以太币转账
let balance = await web3.eth.getBalance(transferAddress)
console.log("balance: " + balance)
console.log("amount: " + amount)
if(parseInt(balance) < parseInt(amount)){
console.log("balance < amount")
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Not enough ETH.',
})
return
}
let nonce = await web3.eth.getTransactionCount(transferAddress)
console.log("nonce: " + nonce)
var rawTx = {
from:transferAddress,
nonce: nonce,
gasPrice: gasPrice,
to: receiverAddress,
value: amount,
data: '0x00'
}
let gas = await web3.eth.estimateGas(rawTx)
rawTx.gas = gas
console.log("gas: " + gas)
var tx = new Tx(rawTx)
var _privateKey = new Buffer.from(privateKey, 'hex')
tx.sign(_privateKey)
var serializedTx = tx.serialize().toString('hex')
await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'), async function(err, data) {
console.log(err)
console.log(data)
if (err) {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Fail',
})
}
})
.then(async function(data) {
console.log(data)
if (data) {
ctx.body = await Promise.resolve({
code: 0,
data: data.transactionHash,
message: 'Success',
})
} else {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Fail',
})
}
})
})
// ======================Token==========================
// =====================================================
// =====================================================
/**
* 获得代币名称
*
* 参数:1.contractAddress: 代币合约地址
*
* 返回:1.data: 代币名称
*
*/
router.get('/token/getName', async(ctx, next) => {
let { contractAddress } = ctx.request.query
if (typeof(contractAddress) == 'undefined' ||
contractAddress == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: contractAddress.',
})
return
}
let abi = await tools.getAbi(contractAddress)
let contractAbi = eval(abi.data.result)
let contract = new web3.eth.Contract(contractAbi, contractAddress)
console.log("abi: " + abi +
"contractAbi: " + contractAbi +
" contract: " + contract);
try {
let response = await contract.methods.name().call()
ctx.body = await Promise.resolve({
code: 0,
data: response,
message: 'Success',
})
} catch (error) {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: error.stack,
})
}
})
/**
* 获得代币符号
*
* 参数:1.contractAddress: 代币合约地址
*
* 返回:1.data: 代币符号
*
*/
router.get('/token/getSymbol', async(ctx, next) => {
let { contractAddress } = ctx.request.query
if (typeof(contractAddress) == 'undefined' ||
contractAddress == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: contractAddress.',
})
return
}
let abi = await tools.getAbi(contractAddress)
let contractAbi = eval(abi.data.result)
let contract = new web3.eth.Contract(contractAbi, contractAddress)
console.log("abi: " + abi +
"contractAbi: " + contractAbi +
" contract: " + contract);
try {
let response = await contract.methods.symbol().call()
ctx.body = await Promise.resolve({
code: 0,
data: response,
message: 'Success',
})
} catch (error) {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: error.stack,
})
}
})
/**
* 获得代币小数位
*
* 参数:1.contractAddress: 代币合约地址
*
* 返回:1.data: 代币小数位
*
*/
router.get('/token/getDecimals', async(ctx, next) => {
let { contractAddress } = ctx.request.query
if (typeof(contractAddress) == 'undefined' ||
contractAddress == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: contractAddress.',
})
return
}
let abi = await tools.getAbi(contractAddress)
let contractAbi = eval(abi.data.result)
let contract = new web3.eth.Contract(contractAbi, contractAddress)
console.log("abi: " + abi +
"contractAbi: " + contractAbi +
" contract: " + contract);
try {
let response = await contract.methods.decimals().call()
ctx.body = await Promise.resolve({
code: 0,
data: response,
message: 'Success',
})
} catch (error) {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: error.stack,
})
}
})
/**
* token余额查询
*
* 参数:1.contractAddress: 代币合约地址
* 2.address: 查询地址
*
* 返回:1.data: 代币余额
*
*/
router.get('/token/getBalance', async(ctx, next) => {
let { contractAddress, address } = ctx.request.query
if (typeof(contractAddress) == 'undefined' ||
contractAddress == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: contractAddress.',
})
return
}
if (typeof(address) == 'undefined' ||
address == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: address.',
})
return
}
let abi = await tools.getAbi(contractAddress)
let contractAbi = eval(abi.data.result)
let contract = new web3.eth.Contract(contractAbi, contractAddress)
console.log("abi: " + abi +
"contractAbi: " + contractAbi +
" contract: " + contract);
let decimals = await contract.methods.decimals().call()
let symbol = await contract.methods.symbol().call()
console.log("decimals: " + decimals +
" symbol: " + symbol);
try {
let balance = await contract.methods.balanceOf(address).call()
ctx.body = await Promise.resolve({
code: 0,
data: balance / Math.pow(10, decimals),
message: 'Success',
})
} catch (error) {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: error.stack,
})
}
})
/**
* Token转账
*
* 1.自动检查必填参数是否传入,否则会提示某项参数缺失
* 2.gasPrice为可选参数。如果未传入,此接口默认取当前网络平均值。建议不传,使用默认值。
* gasPrice单位为Gwei
* 3.传入的代币金额为不带小数位的金额,此接口会自动进行转换。例如,转账1token,
* 则传入1,而非1000000000000000000
* 4.此接口会检测转出方是否有足够token进行转账,否则会提示token不足
* 5.当接口返回交易hash时,此交易的状态即为success,不需要再通过查询获取交易状态
* 6.此接口会在交易状态为success时才返回交易hash,耗时取决于当前以太坊网络状况,
* 经测试在25-60秒左右。未出现过超时问题
*
*
* 参数:1.contractAddress: 代币合约地址
* 2.transferAddress: 转账方地址
* 3.receiverAddress: 接收方地址
* 4.privateKey: 私钥
* 5.num: 代币数量
* 6.gasPrice: (可选)燃料费单价
*
* 返回:1.data: 交易成功的hash
*
*/
router.get('/token/transfer', async(ctx, next) => {
let { contractAddress, transferAddress, receiverAddress, privateKey,
num, gasPrice } = ctx.request.query
if (typeof(contractAddress) == 'undefined' ||
contractAddress == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: contractAddress.',
})
return
}
if (typeof(transferAddress) == 'undefined' ||
transferAddress == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: transferAddress.',
})
return
}
if (typeof(receiverAddress) == 'undefined' ||
receiverAddress == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: receiverAddress.',
})
return
}
if (typeof(privateKey) == 'undefined' ||
privateKey == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: privateKey.',
})
return
}
if (typeof(num) == 'undefined' ||
num == '') {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Missing parameter: num.',
})
return
}
// 如果没有传入gasPrice, 默认调用web3接口获取最近区块的gasPrice的平均值
if (typeof(gasPrice) == 'undefined' ||
gasPrice == '') {
gasPrice = await web3.eth.getGasPrice()
} else {
// 传值是传入的单位为gwei,需要转为wei
gasPrice = web3.utils.toWei(gasPrice, 'gwei')
}
console.log("contractAddress: " + contractAddress +
" transferAddress: " + transferAddress +
" receiverAddress: " + receiverAddress +
" privateKey: " + privateKey +
" num: " + num +
" gasPrice: " + gasPrice);
let abi = await tools.getAbi(contractAddress)
let contractAbi = eval(abi.data.result)
let contract = new web3.eth.Contract(contractAbi, contractAddress)
console.log("abi: " + abi +
"contractAbi: " + contractAbi +
" contract: " + contract);
let decimals = await contract.methods.decimals().call()
let amount = num * Math.pow(10, decimals)
let symbol = await contract.methods.symbol().call()
console.log("decimals: " + decimals +
" symbol: " + symbol);
let balance = await contract.methods.balanceOf(transferAddress).call()
console.log("balance: " + balance)
console.log("amount: " + amount)
console.log(parseInt(balance) < parseInt(amount))
if (parseInt(balance) < parseInt(amount)) {
console.log("balance < amount")
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Not enough ' + symbol + '.',
})
return
}
console.log("receiverAddress: " + receiverAddress)
console.log("amount: " + amount)
let tokenTransferData = await contract.methods.transfer(receiverAddress,
web3.utils.toHex(amount)).encodeABI()
console.log("tokenTransferData: " + tokenTransferData)
let nonce = await web3.eth.getTransactionCount(transferAddress)
console.log("nonce: " + nonce)
var rawTx = {
from: transferAddress,
nonce: nonce,
gasPrice: gasPrice,
to: contractAddress,
data: tokenTransferData
}
let gas = await web3.eth.estimateGas(rawTx)
rawTx.gas = gas
console.log("gas: " + gas)
var tx = new Tx(rawTx)
var _privateKey = new Buffer.from(privateKey, 'hex')
tx.sign(_privateKey)
var serializedTx = tx.serialize().toString('hex')
await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'), async function(err, data) {
console.log(err)
console.log(data)
if (err) {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Fail',
})
}
})
.then(async function(data) {
console.log(data)
if (data) {
ctx.body = await Promise.resolve({
code: 0,
data: data.transactionHash,
message: 'Success',
})
} else {
ctx.body = await Promise.resolve({
code: 1,
data: {},
message: 'Fail',
})
}
})
})