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

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

推荐阅读更多精彩内容