以太坊開(kāi)發(fā)(二十五)使用Node.js封裝并優(yōu)化以太幣和代幣轉(zhuǎn)賬

在之前的文章中以太坊開(kāi)發(fā)(二十三)使用Web3.js查詢以太幣和代幣余額以及轉(zhuǎn)賬,我們實(shí)現(xiàn)了使用web3.js查詢以太幣及代幣余額。這篇文章主要包含下面兩點(diǎn):

  1. 使用Node.js封裝成接口以供外部調(diào)用

  2. 優(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

備注

  1. 轉(zhuǎn)賬前會(huì)自動(dòng)判斷賬戶余額是否大于最高交易成本加上本次轉(zhuǎn)賬金額,如果賬戶余額不足以支持本次交易,頁(yè)面會(huì)提示余額不足

  2. 返回錯(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

備注

  1. 轉(zhuǎn)賬前會(huì)自動(dòng)判斷賬戶余額是否大于最高交易成本,如果賬戶余額不足以支持本次交易,頁(yè)面會(huì)提示余額不足

  2. 返回錯(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)于gasLimitgasPrice,建議再看下這篇文章
以太坊轉(zhuǎn)帳費(fèi)用相關(guān)設(shè)置:Gas Price&Gas Limit

總結(jié)下關(guān)鍵點(diǎn)就是:

  1. 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)。)

  2. 更關(guān)鍵的是gas Limit燃料供給上限,這個(gè)數(shù)值一定要設(shè)置的高一些,而且多出來(lái)的部分會(huì)退回的。但是如果你的余額本身較少,gasLimit設(shè)置高了會(huì)導(dǎo)致gasLimit * gasPrice大于你的余額,從而報(bào)錯(cuò)。

  3. 交易發(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)為99000weigasPrice為必傳參數(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)出的代幣余額是否足夠

原創(chuàng)內(nèi)容,轉(zhuǎn)載請(qǐng)注明出處,謝謝!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容