1.发现的问题
在做区块链项目的时候,一个核心的问题就是如何保证链上数据和数据库数据的数据一致性。比如一个订单,用户在购买后到发货。到底是先去链上 mint nft 给用户后再把订单状态改成已发货还是先改成已发货再去链上 mint nft 发送给用户?
2.分析问题
我们来分析下这两个情况
- 先 mint 后改订单状态,这会导致用户在链上已经拥有了 nft,但是数据库中的订单状态还是未发货,这个时候如果系统崩溃,那么订单状态就会一直停留再未发货,如果系统中有什么自动补发机制的会那么可能就会导致用户再收到一个 nft,存在重复发货的问题。
- 先改订单状态后 mint,这样就会导致用户在数据库中的订单状态已经是已发货了,但是链上还没有 mint nft,这个时候如果系统崩溃,那么用户会存在商品未到账的情况,运营压力会很大。
对于我遇到的情况来说,重复发货的问题是无法接受的,因为 nft 一旦发送到了用户手里。因为没有用户是私钥,我们是没办法操作用户手中的资产的,所以可以说资产一旦到了用户手里,是没办法回收的。所以我们的系统中选择的方式是先去修改订单状态后 mint nft。
3.解决办法
那么问题来了,方案2会导致运营压力过大的问题怎么解决呢?我的解决办法是这样的,先来看一段伪代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用了 sequlize
const transactionHash = await getTransactionHashBeforeSendingTransaction(); // 这个方法是用来获取交易的 transaction hash
// 修改订单状态的时候,我们把 transaction hash 也一起写入数据库
// 下面代码的sql 语句: update order set status = '发货中' and transactionHash = 'transactionHash' where id = 1 and status = '已付款';
await order.update({ status: '发货中', transactionHash }, { where: { id: 1, status: '已付款' } });
// (发货)给用户 mint nft
await contract.batchMintBox(owner, blindBoxTypes, assetIds);
// 下面代码的sql 语句: update order set status = '发货完成' where id = 1 and status = '发货中';
await order.update({ status: '发货完成' }, { where: { id: 1, status: '发货中' } });
首先我们先修改订单状态为发货中,然后去 mint nft,最后再修改订单状态为发货完成。这样就可以保证用户在数据库中的订单状态是发货完成的,如果系统崩溃,那么用户就会存在商品未到账的情况,但是我们可以通过定时任务去检查订单状态,如果发现有订单状态是发货中的,那么就去链上根据 transaction hash 查询一下这个订单是否已经 mint nft(transaction hash 是否在链上确认了),如果已经 mint nft,那么就把订单状态改成发货完成,如果没有 mint nft,那么就执行补发的操作。所以 transaction hash 就是我们解决这个问题的关键,用来判断链上是否已经 mint nft。
接下来我们来看一下如何在上链前确定一笔交易的 transaction hash。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// web3.js 实现方式
import Web3 from 'web3';
const web3 = new Web3(blockchainNetRpcUrl); // blockchainNetRpcUrl 是链的 rpc 地址
const account = web3.eth.accounts.privateKeyToAccount(privateKey); // privateKey 是账户私钥
web3.eth.accounts.wallet.add(account);
const contract = new web3.eth.Contract(contractAbi, contractAddress); // contractAbi 是合约的 abi
const contractMethod = contract.methods.batchMintBox(owner, blindBoxTypes, assetIds); // 具体的mint nft 的方法
const gasLimit = await contractMethod.estimateGas();
const data = contractMethod.encodeABI();
const txParams = {
nonce,
gasLimit: gasLimit.toNumber(),
gasPrice: new BigNumber(5).times(1000000000).toNumber(),
to: contractAddress,
data,
};
const signedTx = await web3.eth.accounts.signTransaction(txParams, admin.wallet.PrivateKey);
const transactionHash = signedTx.transactionHash; // 这个就是我们要的 transaction hash
const rawTransaction = signedTx.rawTransaction;
// 发送交易, 真正执行 mint nft 的方法
// sendSignedTransaction 会返回 TransactionReceipt 对象,解析这个对象可以获取到 transaction hash
// 对比上文的 transaction hash 你会发现, 解析的 transaction hash 和上文的 transaction hash 是一样的
const receipt = await web3.eth.sendSignedTransaction(rawTransaction);
assert.equal(receipt.transactionHash, transactionHash); // 两个 transaction hash 是一样的
1
2
3
4
5
6
7
// web3.js 确认transaction hash 是否在链上确认
const transaction = await web3.eth.getTransaction(hash);
if (transaction === null) {
console.log('交易未上链');
} else {
console.log('交易已上链');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ethers.js 实现方式
import { ethers } from 'ethers';
import { parse as TransactionParser } from '@ethersproject/transactions';
const getTransactionAdvance = async (originTransactionHash: ethers.PopulatedTransaction, wallet: ethers.Wallet) => {
const txPopulated = await wallet.populateTransaction(originTransactionHash);
const rawTransaction = await wallet.signTransaction(txPopulated);
const { hash: transactionHash } = TransactionParser(rawTransaction);
return { rawTransaction, transactionHash };
};
const provider = new ethers.providers.JsonRpcProvider(blockchainNetRpcUrl); // blockchainNetRpcUrl 是链的 rpc 地址
const wallet = new ethers.Wallet(privateKey, provider); // privateKey 是账户私钥
const contract = new ethers.Contract(contractAddress, contractAbi, wallet); // contractAbi 是合约的 abi
const nonce = await wallet.getTransactionCount();
const txOrigin = await contract.populateTransaction.batchMintBox(owner, blindBoxTypes, assetIds, { nonce }); // 调用具体的合约的方法
const { rawTransaction, transactionHash } = await getTransactionAdvance(txOrigin, wallet); // 获取 transaction hash
// 发送交易, 真正执行 mint nft 的方法, sendTransaction 会返回 TransactionResponse 对象
const tx = await this.provider.sendTransaction(rawTransaction);
await tx.wait(); // 等待交易确认
assert.equal(tx.hash, transactionHash); // 两个 transaction hash 是一样的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ethers.js 确认transaction hash 是否在链上确认
const isTransactionConfirmed = async (transactionHash: string, provider: ethers.providers.JsonRpcProvider): Promise<boolean> => {
const txReceipt = await provider.getTransactionReceipt(transactionHash);
if (txReceipt && txReceipt.blockNumber) {
return true;
} else {
return false;
}
};
const ethersProvider = new ethers.providers.JsonRpcProvider(blockchainNetRpcUrl); // blockchainNetRpcUrl 是链的 rpc 地址
const isConfirmed = await isTransactionConfirmed(transactionHash, ethersProvider);
if (!isConfirmed) {
console.log('交易未上链');
} else {
console.log('交易已上链');
}