Un système de traitement par lots des transactions Starknet
Résumé
Cet article présente le système de traitement par lots des transactions utilisé dans Metacube pour envoyer instantanément les NFT gagnés par les joueurs. Il explique l'architecture évolutive basée sur des acteurs du système et fournit une implémentation détaillée en Go. Tous les extraits de code sont disponibles dans le dépôt GitHub associé.
Cet article est basé sur l'article initialement publié sur dev.to par Bastien Faivre.
Architecture
Le Batcher est composé de deux acteurs principaux :
• Le Builder reçoit les transactions, les regroupe en une seule transaction multicall et l'envoie à l'acteur Sender.
• Le Sender finalise la transaction avec les champs appropriés (nonce, frais max, etc.), la signe, l'envoie au réseau Starknet et surveille son statut.
Cette séparation des acteurs permet un système évolutif et efficace. Le builder prépare les transactions pendant que le sender les envoie, permettant un flux de transactions continu et efficace.

Implémentation
L'implémentation suivante est spécifique à Go, mais les concepts peuvent facilement être adaptés à d'autres langages, car les fonctionnalités restent les mêmes. De plus, notez que cette implémentation est spécifique à l'envoi de NFT depuis le même contrat. Cependant, une approche plus générique est mentionnée plus loin dans l'article. Enfin, le code est basé sur la bibliothèque starknet.go développée par Nethermind.
Commençons par le `Batcher` lui-même :
type Batcher struct {
accnt *account.Account
contractAddress *felt.Felt
maxSize int
inChan <-chan []string
failChan chan<- []string
}Le Batcher exécute à la fois les acteurs `Builder` et `Sender` simultanément :
type TxnDataPair struct {
Txn rpc.BroadcastInvokev1Txn
Data [][]string
}
func (b *Batcher) Run() {
txnDataPairChan := make(chan TxnDataPair)
go b.runBuildActor(txnDataPairChan)
go b.runSendActor(txnDataPairChan)
}Builder
Analysons l'acteur Build. Notez que le code est simplifié pour une meilleure lisibilité (code complet disponible sur GitHub).
// 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 {
// ... error handling ...
}
// Parse the NFT ID
nftID, err := strconv.Atoi(data[1])
if err != nil {
// ... error handling ...
}
// 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 {
// ... error handling ...
}
return rpc.BroadcastInvokev1Txn{
InvokeTxnV1: rpc.InvokeTxnV1{
MaxFee: new(felt.Felt).SetUint64(MAX_FEE), // Define MAX_FEE appropriately
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)
var WAITING_TIME = 5 * time.Second // Example waiting time
for {
trigger := false
select {
case data, ok := <-b.inChan:
if !ok {
// Channel closed, handle shutdown
return
}
functionCall, err := b.buildFunctionCall(data)
if err != nil {
// Handle error, maybe send to failChan
continue
}
functionCalls = append(functionCalls, *functionCall)
size++
currentData = append(currentData, data)
if size >= b.maxSize {
trigger = true
}
case <-time.After(WAITING_TIME):
if size > 0 {
trigger = true
}
}
if trigger {
builtTxn, err := b.buildBatchTransaction(functionCalls)
if err != nil {
// Handle error, maybe send all in currentData to failChan
} else {
txnDataPairChan <- TxnDataPair{
Txn: builtTxn,
Data: currentData,
}
}
size = 0
functionCalls = make([]rpc.FunctionCall, 0, b.maxSize)
currentData = make([][]string, 0, b.maxSize)
}
}
}Sender
Analysons maintenant l'acteur Sender. Notez que le code est simplifié pour une meilleure lisibilité (code complet disponible sur GitHub).
// Actual Send actor event loop
func (b *Batcher) runSendActor(txnDataPairChan <-chan TxnDataPair) {
oldNonce := new(felt.Felt).SetUint64(0)
for {
txnDataPair, ok := <-txnDataPairChan
if !ok {
// Channel closed, handle shutdown
return
}
txn := txnDataPair.Txn
data := txnDataPair.Data // To send to failChan if needed
nonce, err := b.accnt.Nonce(
context.Background(),
rpc.BlockID{Tag: "latest"},
b.accnt.AccountAddress,
)
if err != nil {
// Handle error, send data to failChan
continue
}
if nonce.Cmp(oldNonce) <= 0 {
nonce.Add(oldNonce, new(felt.Felt).SetUint64(1))
}
txn.InvokeTxnV1.Nonce = nonce
err = b.accnt.SignInvokeTransaction(
context.Background(),
&txn.InvokeTxnV1,
)
if err != nil {
// Handle error, send data to failChan
continue
}
// Optional: Estimate fee
// fee, err := b.accnt.EstimateFee(...)
// if err != nil { ... }
// if fee > MAX_ACCEPTABLE_FEE { ... re-sign if MaxFee changed ... }
resp, err := b.accnt.SendTransaction(
context.Background(),
&txn,
)
if err != nil {
// Handle error, send data to failChan
continue
}
statusLoop:
for {
time.Sleep(time.Second * 5) // Wait before checking status
txStatus, err := b.accnt.GetTransactionStatus(
context.Background(),
resp.TransactionHash,
)
if err != nil {
// Log error, maybe retry or send to failChan after X retries
continue
}
switch txStatus.ExecutionStatus {
case rpc.TxnExecutionStatusSUCCEEDED:
oldNonce = nonce
// Successfully sent, log or notify
break statusLoop
case rpc.TxnExecutionStatusREVERTED:
oldNonce = nonce // Nonce is consumed even on revert
// Send data to failChan
break statusLoop
default: // PENDING, etc.
}
switch txStatus.FinalityStatus {
case rpc.TxnStatus_Received:
continue
case rpc.TxnStatus_Accepted_On_L2, rpc.TxnStatus_Accepted_On_L1:
oldNonce = nonce
// Finalized, log or notify
break statusLoop
case rpc.TxnStatus_Rejected:
// Send data to failChan (nonce might not be consumed, check Starknet docs)
break statusLoop
default:
}
}
}
}Vers un système générique
Le système présenté est spécifique à l'envoi de NFT depuis le même contrat. Cependant, l'architecture peut facilement être adaptée pour envoyer tout type de transaction.
Premièrement, les données de transaction envoyées au Batcher doivent être plus génériques et, par conséquent, contenir plus d'informations. Elles doivent contenir l'adresse du contrat, le sélecteur du point d'entrée et les données d'appel. La fonction `buildFunctionCall` doit ensuite être adaptée pour analyser ces informations.
On pourrait également aller plus loin en rendant le compte expéditeur générique. Cela nécessiterait plus de refactorisation, car les transactions doivent être regroupées par compte expéditeur. Cependant, c'est faisable et permettrait un système plus polyvalent.
Cependant, rappelez-vous que l'optimisation prématurée est la racine de tous les maux. Par conséquent, si vous devez simplement envoyer des NFT ou un token spécifique tel que ETH ou STRK, le système présenté est amplement suffisant.
Outil CLI
Le code du dépôt peut être utilisé comme outil CLI pour envoyer un lot de NFT par lots. L'outil est facile à utiliser, et vous devriez pouvoir l'adapter à vos besoins après avoir lu cet article. Veuillez consulter le README pour plus d'informations.
Conclusion
J'espère que cet article vous a aidé à mieux comprendre comment Metacube envoie des NFT à ses joueurs. Le système est un composant d'infrastructure clé, et nous sommes heureux de le partager avec la communauté. Si vous avez des questions ou des commentaires, n'hésitez pas à commenter ou à me contacter. Merci d'avoir lu !
Pour plus d'informations, consultez l'article original sur dev.to et le dépôt GitHub.