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

96
yuyangray
0.1 2018.07.09 15:31 字数 1599

在之前的文章中以太坊开发(二十三)使用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);
  • 如果是以太币转账,判断如果最高交易成本加上转账金额大于余额,提示当前余额不足

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

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

以太坊开发