以太坊开发(二十五)使用Node.js封装并优化以太币和代币转账

在之前的文章中以太坊开发(二十三)使用Web3.js查询以太币和代币余额以及转账,我们实现了使用web3.js查询以太币及代币余额。这篇文章主要包含下面两点:

  1. 使用Node.js封装成接口以供外部调用

  2. 优化:判断用户余额是否足够完成本次转账操作

1. 使用Node.js封装成接口以供外部调用

因为我对Node.js也不是太熟悉,所以下面的代码将就看下,但是可以正常使用。这里直接上部分代码了,不明白的可以看注释。

1.1 以太币转账

先看下接口说明:

简要描述:

  • 以太币转账

请求URL:

  • http://127.0.0.1:8084/eth/transfer

请求方式:

  • GET

参数:

参数名 必选 类型 说明
currentAccount string 转账人钱包地址
to string 收款人钱包地址
amount string 转账金额(单位:wei)
privateKey string 转账人钱包地址对应私钥
gasPrice string 以太坊燃料费价格(单位:Gwei)
gasLimit string 以太坊燃料供给上限(单位:Wei) ,默认为26000 wei

返回示例

{
    "code": 10000,
    "hash": "0x3aa7b47d69f38aa2e606c5b355c6c07e68d970cf5d891bbb6881011b5f2a4539"
    "message": "ok"
}

返回参数说明

参数名 类型 说明
hash string 交易hash

备注

  1. 转账前会自动判断账户余额是否大于最高交易成本加上本次转账金额,如果账户余额不足以支持本次交易,页面会提示余额不足

  2. 返回错误码20001即表示余额不足

代码:

router.get('/eth/transfer', async(ctx, next) => {
    if (!ctx.request.query.currentAccount) {
        ctx.body = await Promise.resolve({
            code: 20005,
            data: {},
            message: 'currentAccount 必须是一个字符串',
        })
    }

    if (!ctx.request.query.gasLimit) {
        gasLimit = '26000'
    } else {
        gasLimit = ctx.request.query.gasLimit
    }

    // 如果没有传入gasPrice, 默认调用web3接口获取最近区块的gasPrice的平均值
    if (!ctx.request.query.gasPrice) {
        gasPrice = await web3.eth.getGasPrice();
    } else {
        // 传值是传入的单位为gwei,需要转为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 必须是一个字符串',
        })
    }
    if (!ctx.request.query.privateKey) {
        ctx.body = await Promise.resolve({
            code: 20002,
            data: {},
            message: 'privateKey 必须是一个字符串',
        })
    }
    if (!ctx.request.query.amount) {
        ctx.body = await Promise.resolve({
            code: 20002,
            data: {},
            message: 'amount 必须是一个数字',
        })
    }

    // 计算最高交易成本
    var fees = await getFees(gasLimit, gasPrice);

    // 判断如果最高交易成本加上转账金额大于余额,提示当前余额不足
    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: '当前余额: ' + web3.utils.fromWei(response, 'ether') + ' 最高交易成本: ' +
                    web3.utils.fromWei(fees.toString(), 'ether') + ' 转账金额: ' +
                    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 代币转账

接口说明:

简要描述:

  • 代币转账

请求URL:

  • http://127.0.0.1:8084/token/transfer

请求方式:

  • GET

参数:

参数名 必选 类型 说明
contractAddress string 代币合约地址
currentAccount string 转账人钱包地址
to string 收款人钱包地址
amount string 转账金额。使用方需要先获取代币单位小数位,再乘以小数位
privateKey string 转账人钱包地址对应私钥
gasPrice string 以太坊燃料费价格(单位:Gwei)
gasLimit string 以太坊字节燃料费上限(单位:Wei) ,默认为26000 wei

返回示例

 {
    "code": 10000,
    "hash": "0xff4a1ccb26cd8c24796ed68075f11934a2561438a218463f31f897d5fb650e7c",
    "message": "ok"
}

返回参数说明

参数名 类型 说明
hash string 交易hash

备注

  1. 转账前会自动判断账户余额是否大于最高交易成本,如果账户余额不足以支持本次交易,页面会提示余额不足

  2. 返回错误码20001即表示余额不足

代码:

router.get('/token/transfer', async(ctx, next) => {
    if (!ctx.request.query.contractAddress) {
        ctx.body = await Promise.resolve({
            code: 20004,
            data: {},
            message: 'contractAddress 必须是一个字符串',
        })
    }

    if (!ctx.request.query.currentAccount) {
        ctx.body = await Promise.resolve({
            code: 20005,
            data: {},
            message: 'currentAccount 必须是一个字符串',
        })
    }

    // 如果没有传入gasPrice, 默认调用web3接口获取最近区块的gasPrice的平均值
    if (!ctx.request.query.gasPrice) {
        gasPrice = await web3.eth.getGasPrice();
    } else {
        // 传值是传入的单位为gwei,需要转为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 必须是一个字符串',
        })
    }

    if (!ctx.request.query.to) {
        ctx.body = await Promise.resolve({
            code: 20002,
            data: {},
            message: 'to 必须是一个字符串',
        })
    }

    if (!ctx.request.query.privateKey) {
        ctx.body = await Promise.resolve({
            code: 20002,
            data: {},
            message: 'privateKey 必须是一个字符串',
        })
    }

    var fees = await getFees(gasLimit);

    // 判断如果最高交易成本大于余额,提示当前余额不足
    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: '当前余额: ' + 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);
    //调用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. 优化:判断用户余额是否足够完成本次转账操作

关于gasLimitgasPrice,建议再看下这篇文章
以太坊转帐费用相关设置:Gas Price&Gas Limit

总结下关键点就是:

  1. gasPrice价格是浮动的,由你来主动出价,但如果价格太低,矿工们就会拒绝帮你打包和转发交易。但是如果设置太高,交易成本又会增加。这两个数值如果设置错误,你发出去的ETH,不但无法到达收款钱包,还会白白浪费燃料费。(无论交易是否成功,都会扣除燃料费。)

  2. 更关键的是gas Limit燃料供给上限,这个数值一定要设置的高一些,而且多出来的部分会退回的。但是如果你的余额本身较少,gasLimit设置高了会导致gasLimit * gasPrice大于你的余额,从而报错。

  3. 交易发出后,会向全网广播,途径很多个矿工节点,这些节点又会帮你转发给下一个节点,直到你的交易被矿工打包进区块中。每一次转发都会消耗一部分Gas,如果被打包之前燃料耗尽,达到Gas Limit设置的上限,那这交易就一定会失败。ETH会退回,但燃料费gasPrice还是要扣除。

之前web3转账的代码中,gasLimit为可选参数,不传的话默认为99000weigasPrice为必传参数,单位为gwei

这里需要优化下。

2.1 gasPrice改为可选参数

首先gasPrice改为可选参数。因为用户转账可能不关心gasPrice的具体值,只要能短时间内转账成功就好。所以转账时gasPrice如果没有传,则调用web3.eth.getGasPrice()获取gasPrice

web3.eth.getGasPrice(),方法说明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按最近一些区块的gasPrice取平均值。一般来说和当前gasPrice相近,可以保证gasPrice不会给的过低导致矿工拒绝打包你的交易,也不会过高导致交易成本过高。

2.2 gasLimit的默认值

之前账户余额比较多的时候,进行转账时都没有问题。而最近转账时总是提示Insufficient funds for gas * price + value。虽然账户余额很少,但是转账金额也很小,感觉应该足够本次转账,那到底问题出在哪呢?

之前对gasLimit和gasPrice的理解不是很深,根据报错信心,所以又去查了一下资料。https://blog.csdn.net/wo541075754/article/details/79537043
再加上上面的文章总结的关键点,可以发现原来是gasLimit默认值设置太大导致。之前默认值设为99000,在转账时,以太坊会将gasLimit * gasPrice,再加上要转账的金额,对比账户余额。如果大于账户余额,则会 提示上面的错误,表示最高交易成本加上转账金额大于账户余额,转账失败。

那到底gaslimit设置多少合适呢?

web3没有提供相应的接口,于是我去看了下Imtoken转账时的gasLimit,它设置的值为25200,这里我将代码中设置为26000

这里我隐约想起之前看过一篇文章,介绍gasLimit的值是根据计算步骤决定的,如果调用的是某个智能合约的复杂方法,经过的计算步骤越多,gasLimit越高。而这里由于只是简单的转账,所以gasLimit不需要设置太高。有了解这部分的同学可以回复讨论下。

2.3 判断余额是否足够支持本次交易

现在我们提供一个计算最高交易成本的方法:

// 根据gasLimit和gasPrice计算最高交易成本
async function getFees(gasLimit, gasPrice) {
    var fees = gasLimit * gasPrice;
    return fees;
}

在转账前根据传入或者默认的值计算:

// 计算最高交易成本
var fees = await getFees(gasLimit, gasPrice);
  • 如果是以太币转账,判断如果最高交易成本加上转账金额大于余额,提示当前余额不足

  • 如果是代币转账,判断如果最高交易成本大于余额,提示当前余额不足。代币的话还需要提前调用代币余额,判断要转出的代币余额是否足够

原创内容,转载请注明出处,谢谢!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容