以太坊开发(二十九)[升级版]使用Web3.js+Node.js封装成接口以供钱包管理/查询/转账

96
yuyangray
0.1 2018.10.29 16:31 字数 594

1. 前言

之前的文章介绍过使用Web3.js+Node.js封装成接口以供钱包管理/查询/转账。但是有一些问题:

  • 代币转账可能出现超时失败

  • 以太币/代币转账接口调用成功后,返回的交易hash,并不能保证交易已成功,还需要再次去查询交易状态

  • 以太币转账接口未判断转账者是否有足够的以太币。代币转账接口未判断转账者是否有足够的代币

  • 转账前没有预估gas

  • 以太币转账,之前必须传入带小数位的金额。例如转账1ether,传入的参数为1000000000000000000

  • 代币转账,之前必须先去查询该代币小数位,然后换算成带小数位的代币金额。例如转账1某token,传入的参数为1000000000000000000

  • 代币转账,之前必须将转账方法名,转账人地址,金额,转换为十六进制,再拼接为data

  • 根据passwordprivateKey返回keystore。之前必须将privateKey手动加上0x前缀再进行传参,否则将返回错误的keystore

针对这些问题,对代码进行了修改:

  • 修复了代币转账可能出现超时的问题

  • 调用转账接口后,只要返回交易hash,则此交易必定为success状态,不需要再去查询交易状态。耗时取决于当前以太坊网络状况,经测试在15-60秒左右

  • 自动预估gas,移除了gasLimit参数

  • 以太币转账。现在自动判断转账方是否有足够的以太币

  • 以太币转账。现在传入的以太币单位为ether不需要再加上18个0的小数位

  • 代币转账。现在自动判断转账方是否有足够的代币

  • 代币转账。不需要先去获取代币小数位,现在直接传入不带小数位的代币金额。例如,之前某个代币小数位为8,转账1token,则传入100000000。现在直接传入1

  • 根据passwordprivateKey返回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',
            })
        }
    })
})
以太坊开发