本文介绍了 Metacube 中使用的交易批处理程序,用于即时发送玩家赚取的 NFT。它解释了批处理程序基于参与者的可扩展架构,并提供了 Go 中的详细实现。
所有代码片段都可以在关联的 GitHub 存储库中找到。
巴彻由两位主要演员组成:
这种参与者分离可以实现可扩展且高效的批处理程序。构建器在发送者发送交易时准备交易,从而实现连续且高效的交易流。
以下实现特定于 Go,但这些概念可以轻松适应其他语言,因为功能保持不变。
此外,请注意,此实现特定于从同一合约发送 NFT。然而,本文后面提到了一种更通用的方法。
最后,代码基于Nethermind开发的starknet.go库。
让我们从 Batcher 本身开始:
type Batcher struct { accnt *account.Account contractAddress *felt.Felt maxSize int inChan <-chan []string failChan chan<- []string }
账户(accnt)是持有NFT的账户,它将用于签署转移NFT的交易。这些 NFT 是同一合约的一部分,因此有合约地址字段。 maxSize 字段是批次的最大大小,inChan 是将事务发送到 Batcher 的通道。 failChan 用于发回发送失败的交易。
请注意,在这个实现中,后面所说的交易数据([]string)是一个由两个元素组成的数组:接收者地址和 NFT ID。
Batcher 同时运行 Builder 和 Sender Actor:
type TxnDataPair struct { Txn rpc.BroadcastInvokev1Txn Data [][]string } func (b *Batcher) Run() { txnDataPairChan := make(chan TxnDataPair) go b.runBuildActor(txnDataPairChan) go b.runSendActor(txnDataPairChan) }
定义的通道 txnDataPairChan 将交易数据对从 Builder 发送到 Sender。每个交易数据对都包含批量交易,每个交易的数据都嵌入其中。每笔交易的数据都与批量交易一起发送,以便失败的交易可以被发送回实例化 Batcher 的实体。
让我们分析一下Build actor。请注意,为了更好的可读性,代码已被简化(完整代码):
type Batcher struct { accnt *account.Account contractAddress *felt.Felt maxSize int inChan <-chan []string failChan chan<- []string }
runBuildActor 函数是 Builder actor 的事件循环。它等待事务发送到批处理程序,并在批处理已满或达到超时时构建批处理事务。然后批量交易被发送到 Sender actor。
现在让我们分析一下Sender actor。请注意,为了更好的可读性,代码已被简化(完整代码):
type TxnDataPair struct { Txn rpc.BroadcastInvokev1Txn Data [][]string } func (b *Batcher) Run() { txnDataPairChan := make(chan TxnDataPair) go b.runBuildActor(txnDataPairChan) go b.runSendActor(txnDataPairChan) }
runSendActor 函数是发送者 Actor 的事件循环。它等待 Builder 发送批量交易,对它们进行签名,将它们发送到 Starknet 网络,并监控它们的状态。
关于费用估算的说明:可以在发送之前估算批量交易的费用成本。交易签名后可添加以下代码:
// This function builds a function call from the transaction data. func (b *Batcher) buildFunctionCall(data []string) (*rpc.FunctionCall, error) { // Parse the recipient address toAddressInFelt, err := utils.HexToFelt(data[0]) if err != nil { ... } // Parse the NFT ID nftID, err := strconv.Atoi(data[1]) if err != nil { ... } // The entry point is a standard ERC721 function // https://docs.openzeppelin.com/contracts-cairo/0.20.0/erc721 return &rpc.FunctionCall{ ContractAddress: b.contractAddress, EntryPointSelector: utils.GetSelectorFromNameFelt( "safe_transfer_from", ), Calldata: []*felt.Felt{ b.accnt.AccountAddress, // from toAddressInFelt, // to new(felt.Felt).SetUint64(uint64(nftID)), // NFT ID new(felt.Felt).SetUint64(0), // data -> None new(felt.Felt).SetUint64(0), // extra data -> None }, }, nil } // This function builds the batch transaction from the function calls. func (b *Batcher) buildBatchTransaction(functionCalls []rpc.FunctionCall) (rpc.BroadcastInvokev1Txn, error) { // Format the calldata (i.e., the function calls) calldata, err := b.accnt.FmtCalldata(functionCalls) if err != nil { ... } return rpc.BroadcastInvokev1Txn{ InvokeTxnV1: rpc.InvokeTxnV1{ MaxFee: new(felt.Felt).SetUint64(MAX_FEE), Version: rpc.TransactionV1, Nonce: new(felt.Felt).SetUint64(0), // Will be set by the send actor Type: rpc.TransactionType_Invoke, SenderAddress: b.accnt.AccountAddress, Calldata: calldata, }, }, nil } // Actual Build actor event loop func (b *Batcher) runBuildActor(txnDataPairChan chan<- TxnDataPair) { size := 0 functionCalls := make([]rpc.FunctionCall, 0, b.maxSize) currentData := make([][]string, 0, b.maxSize) for { // Boolean to trigger the batch building trigger := false select { // Receive new transaction data case data, ok := <-b.inChan: if !ok { ... } functionCall, err := b.buildFunctionCall(data) if err != nil { ... } functionCalls = append(functionCalls, *functionCall) size++ currentData = append(currentData, data) if size >= b.maxSize { // The batch is full, trigger the building trigger = true } // We don't want a smaller batch to wait indefinitely to be full, so we set a timeout to trigger the building even if the batch is not full case <-time.After(WAITING_TIME): if size > 0 { trigger = true } } if trigger { builtTxn, err := b.buildBatchTransaction(functionCalls) if err != nil { ... } else { // Send the batch transaction to the Sender txnDataPairChan <- TxnDataPair{ Txn: builtTxn, Data: currentData, } } // Reset variables size = 0 functionCalls = make([]rpc.FunctionCall, 0, b.maxSize) currentData = make([][]string, 0, b.maxSize) } } }
这可能有助于确保在发送交易之前费用不会太高。如果估计费用高于预期,则可能还需要重新调整交易的最大费用字段。但请注意,当交易发生任何更改时,必须重新签名!
但是请注意,如果交易吞吐量相当高,您在估算费用时可能会遇到一些问题。这是因为当给定的交易刚刚被批准时,更新帐户的随机数会有一点延迟。因此,在估计下一笔交易的费用时,它可能会失败,认为随机数仍然是前一笔交易。因此,如果您仍然想估算费用,那么您可能需要在每笔交易之间提供一些睡眠以避免此类问题。
所提供的批处理程序专门用于从同一合约发送 NFT。然而,该架构可以轻松适应发送任何类型的交易。
首先,发送到 Batcher 的交易数据必须更加通用,因此包含更多信息。它们必须包含合约地址、入口点选择器和调用数据。然后必须调整 buildFunctionCall 函数来解析此信息。
还可以更进一步,将发件人帐户设为通用。这将需要更多的重构,因为必须针对每个发送者帐户对交易进行批处理。然而,它是可行的,并且可以实现更通用的批处理机。
但是,请记住,过早的优化是万恶之源。因此,如果您只需要发送 NFT 或特定代币(例如 ETH 或 STRK),那么提供的批处理程序就足够了。
存储库代码可以用作 CLI 工具来批量发送一堆 NFT。该工具易于使用,阅读本文后您应该能够根据您的需要进行调整。请参阅 README 了解更多信息。
我希望这篇文章可以帮助您更好地了解Metacube如何向其玩家发送NFT。批处理程序是一个关键的基础设施组件,我们很高兴与社区分享它。如果您有任何问题或反馈,请随时发表评论或与我联系。感谢您的阅读!
以上是Starknet 交易批量处理程序的详细内容。更多信息请关注PHP中文网其他相关文章!