在之前的文章中以太坊開(kāi)發(fā)(二十三)使用Web3.js查詢以太幣和代幣余額以及轉(zhuǎn)賬,我們實(shí)現(xiàn)了使用web3.js查詢以太幣及代幣余額。這篇文章主要包含下面兩點(diǎn):
使用
Node.js封裝成接口以供外部調(diào)用優(yōu)化:判斷用戶余額是否足夠完成本次轉(zhuǎn)賬操作
1. 使用Node.js封裝成接口以供外部調(diào)用
因?yàn)槲覍?duì)Node.js也不是太熟悉,所以下面的代碼將就看下,但是可以正常使用。這里直接上部分代碼了,不明白的可以看注釋。
1.1 以太幣轉(zhuǎn)賬
先看下接口說(shuō)明:
簡(jiǎn)要描述:
- 以太幣轉(zhuǎn)賬
請(qǐng)求URL:
http://127.0.0.1:8084/eth/transfer
請(qǐng)求方式:
- GET
參數(shù):
| 參數(shù)名 | 必選 | 類型 | 說(shuō)明 |
|---|---|---|---|
| currentAccount | 是 | string | 轉(zhuǎn)賬人錢包地址 |
| to | 是 | string | 收款人錢包地址 |
| amount | 是 | string | 轉(zhuǎn)賬金額(單位:wei) |
| privateKey | 是 | string | 轉(zhuǎn)賬人錢包地址對(duì)應(yīng)私鑰 |
| gasPrice | 否 | string | 以太坊燃料費(fèi)價(jià)格(單位:Gwei) |
| gasLimit | 否 | string | 以太坊燃料供給上限(單位:Wei) ,默認(rèn)為26000 wei |
返回示例
{
"code": 10000,
"hash": "0x3aa7b47d69f38aa2e606c5b355c6c07e68d970cf5d891bbb6881011b5f2a4539"
"message": "ok"
}
返回參數(shù)說(shuō)明
| 參數(shù)名 | 類型 | 說(shuō)明 |
|---|---|---|
| hash | string | 交易hash |
備注
轉(zhuǎn)賬前會(huì)自動(dòng)判斷賬戶余額是否大于最高交易成本加上本次轉(zhuǎn)賬金額,如果賬戶余額不足以支持本次交易,頁(yè)面會(huì)提示余額不足
返回錯(cuò)誤碼20001即表示余額不足
代碼:
router.get('/eth/transfer', async(ctx, next) => {
if (!ctx.request.query.currentAccount) {
ctx.body = await Promise.resolve({
code: 20005,
data: {},
message: 'currentAccount 必須是一個(gè)字符串',
})
}
if (!ctx.request.query.gasLimit) {
gasLimit = '26000'
} else {
gasLimit = ctx.request.query.gasLimit
}
// 如果沒(méi)有傳入gasPrice, 默認(rèn)調(diào)用web3接口獲取最近區(qū)塊的gasPrice的平均值
if (!ctx.request.query.gasPrice) {
gasPrice = await web3.eth.getGasPrice();
} else {
// 傳值是傳入的單位為gwei,需要轉(zhuǎn)為wei
gasPrice = web3.utils.toWei(ctx.request.query.gasPrice, 'gwei')
}
if (!ctx.request.query.to) {
ctx.body = await Promise.resolve({
code: 20002,
data: {},
message: 'to 必須是一個(gè)字符串',
})
}
if (!ctx.request.query.privateKey) {
ctx.body = await Promise.resolve({
code: 20002,
data: {},
message: 'privateKey 必須是一個(gè)字符串',
})
}
if (!ctx.request.query.amount) {
ctx.body = await Promise.resolve({
code: 20002,
data: {},
message: 'amount 必須是一個(gè)數(shù)字',
})
}
// 計(jì)算最高交易成本
var fees = await getFees(gasLimit, gasPrice);
// 判斷如果最高交易成本加上轉(zhuǎn)賬金額大于余額,提示當(dāng)前余額不足
try {
var response = await web3.eth.getBalance(ctx.request.query.currentAccount)
if (parseInt(response) < (parseInt(ctx.request.query.amount) + parseInt(fees))) {
ctx.body = await Promise.resolve({
code: 20001,
data: {},
message: '當(dāng)前余額: ' + web3.utils.fromWei(response, 'ether') + ' 最高交易成本: ' +
web3.utils.fromWei(fees.toString(), 'ether') + ' 轉(zhuǎn)賬金額: ' +
web3.utils.fromWei(ctx.request.query.amount, 'ether') + ', 余額不足',
})
return;
}
} catch (error) {
ctx.body = await Promise.resolve({
code: 20000,
data: {},
message: error.stack,
})
}
var nonce = await web3.eth.getTransactionCount(ctx.request.query.currentAccount, web3.eth.defaultBlock.pending)
var txData = {
nonce: web3.utils.toHex(nonce++),
gasLimit: web3.utils.toHex(gasLimit),
gasPrice: web3.utils.toHex(gasPrice),
to: ctx.request.query.to,
from: ctx.request.query.currentAccount,
value: web3.utils.toHex(ctx.request.query.amount),
data: '',
}
var tx = new Tx(txData)
console.log(txData);
// privateKey 自定義
const privateKey = new Buffer.from(ctx.request.query.privateKey, 'hex')
tx.sign(privateKey)
var serializedTx = tx.serialize().toString('hex')
var hash = await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'));
ctx.body = await Promise.resolve({
code: 10000,
hash: hash.transactionHash,
message: 'ok',
})
})
1.2 代幣轉(zhuǎn)賬
接口說(shuō)明:
簡(jiǎn)要描述:
- 代幣轉(zhuǎn)賬
請(qǐng)求URL:
http://127.0.0.1:8084/token/transfer
請(qǐng)求方式:
- GET
參數(shù):
| 參數(shù)名 | 必選 | 類型 | 說(shuō)明 |
|---|---|---|---|
| contractAddress | 是 | string | 代幣合約地址 |
| currentAccount | 是 | string | 轉(zhuǎn)賬人錢包地址 |
| to | 是 | string | 收款人錢包地址 |
| amount | 是 | string | 轉(zhuǎn)賬金額。使用方需要先獲取代幣單位小數(shù)位,再乘以小數(shù)位 |
| privateKey | 是 | string | 轉(zhuǎn)賬人錢包地址對(duì)應(yīng)私鑰 |
| gasPrice | 否 | string | 以太坊燃料費(fèi)價(jià)格(單位:Gwei) |
| gasLimit | 否 | string | 以太坊字節(jié)燃料費(fèi)上限(單位:Wei) ,默認(rèn)為26000 wei |
返回示例
{
"code": 10000,
"hash": "0xff4a1ccb26cd8c24796ed68075f11934a2561438a218463f31f897d5fb650e7c",
"message": "ok"
}
返回參數(shù)說(shuō)明
| 參數(shù)名 | 類型 | 說(shuō)明 |
|---|---|---|
| hash | string | 交易hash |
備注
轉(zhuǎn)賬前會(huì)自動(dòng)判斷賬戶余額是否大于最高交易成本,如果賬戶余額不足以支持本次交易,頁(yè)面會(huì)提示余額不足
返回錯(cuò)誤碼20001即表示余額不足
代碼:
router.get('/token/transfer', async(ctx, next) => {
if (!ctx.request.query.contractAddress) {
ctx.body = await Promise.resolve({
code: 20004,
data: {},
message: 'contractAddress 必須是一個(gè)字符串',
})
}
if (!ctx.request.query.currentAccount) {
ctx.body = await Promise.resolve({
code: 20005,
data: {},
message: 'currentAccount 必須是一個(gè)字符串',
})
}
// 如果沒(méi)有傳入gasPrice, 默認(rèn)調(diào)用web3接口獲取最近區(qū)塊的gasPrice的平均值
if (!ctx.request.query.gasPrice) {
gasPrice = await web3.eth.getGasPrice();
} else {
// 傳值是傳入的單位為gwei,需要轉(zhuǎn)為wei
gasPrice = web3.utils.toWei(ctx.request.query.gasPrice, 'gwei')
}
if (!ctx.request.query.gasLimit) {
gasLimit = '26000'
} else {
gasLimit = ctx.request.query.gasLimit
}
if (!ctx.request.query.amount) {
ctx.body = await Promise.resolve({
code: 20002,
data: {},
message: 'amount 必須是一個(gè)字符串',
})
}
if (!ctx.request.query.to) {
ctx.body = await Promise.resolve({
code: 20002,
data: {},
message: 'to 必須是一個(gè)字符串',
})
}
if (!ctx.request.query.privateKey) {
ctx.body = await Promise.resolve({
code: 20002,
data: {},
message: 'privateKey 必須是一個(gè)字符串',
})
}
var fees = await getFees(gasLimit);
// 判斷如果最高交易成本大于余額,提示當(dāng)前余額不足
try {
var response = await web3.eth.getBalance(ctx.request.query.currentAccount)
if (parseInt(response) < parseInt(fees)) {
ctx.body = await Promise.resolve({
code: 20001,
data: {},
message: '當(dāng)前余額: ' + web3.utils.fromWei(response, 'ether') + ' 最高交易成本: ' +
web3.utils.fromWei(fees.toString(), 'ether') + ', 余額不足',
})
return;
}
} catch (error) {
ctx.body = await Promise.resolve({
code: 20000,
data: {},
message: error.stack,
})
}
var nonce = web3.eth.getTransactionCount(ctx.request.query.currentAccount, web3.eth.defaultBlock.pending);
//調(diào)用transfer
var txData = {
nonce: web3.utils.toHex(nonce++),
gasLimit: web3.utils.toHex(gasLimit),
gasPrice: web3.utils.toHex(gasPrice),
to: ctx.request.query.contractAddress,
from: ctx.request.query.currentAccount,
value: '0x00',
data: '0x' + 'a9059cbb' + '000000000000000000000000' +
ctx.request.query.to.substr(2) +
tools.addPreZero(web3.utils.toHex(ctx.request.query.amount).substr(2))
}
var tx = new Tx(txData)
const privateKey = new Buffer.from(ctx.request.query.privateKey, 'hex')
tx.sign(privateKey)
var serializedTx = tx.serialize().toString('hex')
var hash = await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'));
ctx.body = await Promise.resolve({
code: 10000,
hash: hash.transactionHash,
message: 'ok',
})
})
2. 優(yōu)化:判斷用戶余額是否足夠完成本次轉(zhuǎn)賬操作
關(guān)于gasLimit和gasPrice,建議再看下這篇文章
以太坊轉(zhuǎn)帳費(fèi)用相關(guān)設(shè)置:Gas Price&Gas Limit
總結(jié)下關(guān)鍵點(diǎn)就是:
gasPrice價(jià)格是浮動(dòng)的,由你來(lái)主動(dòng)出價(jià),但如果價(jià)格太低,礦工們就會(huì)拒絕幫你打包和轉(zhuǎn)發(fā)交易。但是如果設(shè)置太高,交易成本又會(huì)增加。這兩個(gè)數(shù)值如果設(shè)置錯(cuò)誤,你發(fā)出去的ETH,不但無(wú)法到達(dá)收款錢包,還會(huì)白白浪費(fèi)燃料費(fèi)。(無(wú)論交易是否成功,都會(huì)扣除燃料費(fèi)。)
更關(guān)鍵的是gas Limit燃料供給上限,這個(gè)數(shù)值一定要設(shè)置的高一些,而且多出來(lái)的部分會(huì)退回的。但是如果你的余額本身較少,gasLimit設(shè)置高了會(huì)導(dǎo)致gasLimit * gasPrice大于你的余額,從而報(bào)錯(cuò)。
交易發(fā)出后,會(huì)向全網(wǎng)廣播,途徑很多個(gè)礦工節(jié)點(diǎn),這些節(jié)點(diǎn)又會(huì)幫你轉(zhuǎn)發(fā)給下一個(gè)節(jié)點(diǎn),直到你的交易被礦工打包進(jìn)區(qū)塊中。每一次轉(zhuǎn)發(fā)都會(huì)消耗一部分Gas,如果被打包之前燃料耗盡,達(dá)到Gas Limit設(shè)置的上限,那這交易就一定會(huì)失敗。ETH會(huì)退回,但燃料費(fèi)gasPrice還是要扣除。
之前web3轉(zhuǎn)賬的代碼中,gasLimit為可選參數(shù),不傳的話默認(rèn)為99000wei。gasPrice為必傳參數(shù),單位為gwei。
這里需要優(yōu)化下。
2.1 gasPrice改為可選參數(shù)
首先gasPrice改為可選參數(shù)。因?yàn)橛脩艮D(zhuǎn)賬可能不關(guān)心gasPrice的具體值,只要能短時(shí)間內(nèi)轉(zhuǎn)賬成功就好。所以轉(zhuǎn)賬時(shí)gasPrice如果沒(méi)有傳,則調(diào)用web3.eth.getGasPrice()獲取gasPrice。
web3.eth.getGasPrice(),方法說(shuō)明http://web3js.readthedocs.io/en/1.0/web3-eth.html#getgasprice
Returns the current gas price oracle. The gas price is determined by the last few blocks median gas price.
意思是gasPrice按最近一些區(qū)塊的gasPrice取平均值。一般來(lái)說(shuō)和當(dāng)前gasPrice相近,可以保證gasPrice不會(huì)給的過(guò)低導(dǎo)致礦工拒絕打包你的交易,也不會(huì)過(guò)高導(dǎo)致交易成本過(guò)高。
2.2 gasLimit的默認(rèn)值
之前賬戶余額比較多的時(shí)候,進(jìn)行轉(zhuǎn)賬時(shí)都沒(méi)有問(wèn)題。而最近轉(zhuǎn)賬時(shí)總是提示Insufficient funds for gas * price + value。雖然賬戶余額很少,但是轉(zhuǎn)賬金額也很小,感覺(jué)應(yīng)該足夠本次轉(zhuǎn)賬,那到底問(wèn)題出在哪呢?
之前對(duì)gasLimit和gasPrice的理解不是很深,根據(jù)報(bào)錯(cuò)信心,所以又去查了一下資料。https://blog.csdn.net/wo541075754/article/details/79537043
再加上上面的文章總結(jié)的關(guān)鍵點(diǎn),可以發(fā)現(xiàn)原來(lái)是gasLimit默認(rèn)值設(shè)置太大導(dǎo)致。之前默認(rèn)值設(shè)為99000,在轉(zhuǎn)賬時(shí),以太坊會(huì)將gasLimit * gasPrice,再加上要轉(zhuǎn)賬的金額,對(duì)比賬戶余額。如果大于賬戶余額,則會(huì) 提示上面的錯(cuò)誤,表示最高交易成本加上轉(zhuǎn)賬金額大于賬戶余額,轉(zhuǎn)賬失敗。
那到底gaslimit設(shè)置多少合適呢?
web3沒(méi)有提供相應(yīng)的接口,于是我去看了下Imtoken轉(zhuǎn)賬時(shí)的gasLimit,它設(shè)置的值為25200,這里我將代碼中設(shè)置為26000。
這里我隱約想起之前看過(guò)一篇文章,介紹gasLimit的值是根據(jù)計(jì)算步驟決定的,如果調(diào)用的是某個(gè)智能合約的復(fù)雜方法,經(jīng)過(guò)的計(jì)算步驟越多,gasLimit越高。而這里由于只是簡(jiǎn)單的轉(zhuǎn)賬,所以gasLimit不需要設(shè)置太高。有了解這部分的同學(xué)可以回復(fù)討論下。
2.3 判斷余額是否足夠支持本次交易
現(xiàn)在我們提供一個(gè)計(jì)算最高交易成本的方法:
// 根據(jù)gasLimit和gasPrice計(jì)算最高交易成本
async function getFees(gasLimit, gasPrice) {
var fees = gasLimit * gasPrice;
return fees;
}
在轉(zhuǎn)賬前根據(jù)傳入或者默認(rèn)的值計(jì)算:
// 計(jì)算最高交易成本
var fees = await getFees(gasLimit, gasPrice);
如果是以太幣轉(zhuǎn)賬,判斷如果最高交易成本加上轉(zhuǎn)賬金額大于余額,提示當(dāng)前余額不足
如果是代幣轉(zhuǎn)賬,判斷如果最高交易成本大于余額,提示當(dāng)前余額不足。代幣的話還需要提前調(diào)用代幣余額,判斷要轉(zhuǎn)出的代幣余額是否足夠