-
Notifications
You must be signed in to change notification settings - Fork 630
Expand file tree
/
Copy pathpending_transaction.go
More file actions
253 lines (228 loc) · 10.1 KB
/
Copy pathpending_transaction.go
File metadata and controls
253 lines (228 loc) · 10.1 KB
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
package orm
import (
"bytes"
"context"
"errors"
"fmt"
"time"
"github.com/scroll-tech/go-ethereum/common"
gethTypes "github.com/scroll-tech/go-ethereum/core/types"
"github.com/scroll-tech/go-ethereum/log"
"gorm.io/gorm"
"scroll-tech/common/types"
)
// SenderMeta holds the metadata for a transaction sender including the name, service, address and type.
type SenderMeta struct {
Name string
Service string
Address common.Address
Type types.SenderType
}
// PendingTransaction represents the structure of a transaction in the database.
type PendingTransaction struct {
db *gorm.DB `gorm:"column:-"`
ID uint `json:"id" gorm:"id;primaryKey"`
ContextID string `json:"context_id" gorm:"context_id"`
Hash string `json:"hash" gorm:"hash"`
ChainID uint64 `json:"chain_id" gorm:"chain_id"`
Type uint8 `json:"type" gorm:"type"`
GasTipCap uint64 `json:"gas_tip_cap" gorm:"gas_tip_cap"`
GasFeeCap uint64 `json:"gas_fee_cap" gorm:"gas_fee_cap"`
GasLimit uint64 `json:"gas_limit" gorm:"gas_limit"`
Nonce uint64 `json:"nonce" gorm:"nonce"`
SubmitBlockNumber uint64 `json:"submit_block_number" gorm:"submit_block_number"`
Status types.TxStatus `json:"status" gorm:"status"`
RLPEncoding []byte `json:"rlp_encoding" gorm:"rlp_encoding"`
SenderName string `json:"sender_name" gorm:"sender_name"`
SenderService string `json:"sender_service" gorm:"sender_service"`
SenderAddress string `json:"sender_address" gorm:"sender_address"`
SenderType types.SenderType `json:"sender_type" gorm:"sender_type"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"column:deleted_at"`
}
// TableName returns the table name for the Transaction model.
func (*PendingTransaction) TableName() string {
return "pending_transaction"
}
// NewPendingTransaction returns a new instance of PendingTransaction.
func NewPendingTransaction(db *gorm.DB) *PendingTransaction {
return &PendingTransaction{db: db}
}
// GetTxStatusByTxHash retrieves the status of a transaction by its hash.
func (o *PendingTransaction) GetTxStatusByTxHash(ctx context.Context, hash common.Hash) (types.TxStatus, error) {
var status types.TxStatus
db := o.db.WithContext(ctx)
db = db.Model(&PendingTransaction{})
db = db.Select("status")
db = db.Where("hash = ?", hash.String())
if err := db.Scan(&status).Error; err != nil {
return types.TxStatusUnknown, fmt.Errorf("failed to get tx status by hash, hash: %v, err: %w", hash, err)
}
return status, nil
}
// GetPendingOrReplacedTransactionsBySenderType retrieves pending or replaced transactions filtered by sender type, ordered by nonce, then gas_fee_cap (gas_price in legacy tx), and limited to a specified count.
func (o *PendingTransaction) GetPendingOrReplacedTransactionsBySenderType(ctx context.Context, senderType types.SenderType, limit int) ([]PendingTransaction, error) {
var transactions []PendingTransaction
db := o.db.WithContext(ctx)
db = db.Model(&PendingTransaction{})
db = db.Where("sender_type = ?", senderType)
db = db.Where("status = ? OR status = ?", types.TxStatusPending, types.TxStatusReplaced)
db = db.Order("nonce asc")
db = db.Order("gas_fee_cap asc")
db = db.Limit(limit)
if err := db.Find(&transactions).Error; err != nil {
return nil, fmt.Errorf("failed to get pending or replaced transactions by sender type, error: %w", err)
}
return transactions, nil
}
// GetCountPendingTransactionsBySenderType retrieves number of pending transactions filtered by sender type
func (o *PendingTransaction) GetCountPendingTransactionsBySenderType(ctx context.Context, senderType types.SenderType) (int64, error) {
var count int64
db := o.db.WithContext(ctx)
db = db.Model(&PendingTransaction{})
db = db.Where("sender_type = ?", senderType)
db = db.Where("status = ?", types.TxStatusPending)
if err := db.Count(&count).Error; err != nil {
return 0, fmt.Errorf("failed to count pending transactions by sender type, error: %w", err)
}
return count, nil
}
// GetConfirmedTransactionsBySenderType retrieves confirmed transactions filtered by sender type, limited to a specified count.
// for unit test
func (o *PendingTransaction) GetConfirmedTransactionsBySenderType(ctx context.Context, senderType types.SenderType, limit int) ([]PendingTransaction, error) {
var transactions []PendingTransaction
db := o.db.WithContext(ctx)
db = db.Model(&PendingTransaction{})
db = db.Where("sender_type = ?", senderType)
db = db.Where("status = ?", types.TxStatusConfirmed)
db = db.Limit(limit)
if err := db.Find(&transactions).Error; err != nil {
return nil, fmt.Errorf("failed to get confirmed transactions by sender type, error: %w", err)
}
return transactions, nil
}
// InsertPendingTransaction creates a new pending transaction record and stores it in the database.
func (o *PendingTransaction) InsertPendingTransaction(ctx context.Context, contextID string, senderMeta *SenderMeta, tx *gethTypes.Transaction, submitBlockNumber uint64, dbTX ...*gorm.DB) error {
rlp := new(bytes.Buffer)
if err := tx.EncodeRLP(rlp); err != nil {
return fmt.Errorf("failed to encode rlp, err: %w", err)
}
newTransaction := &PendingTransaction{
ContextID: contextID,
Hash: tx.Hash().String(),
Type: tx.Type(),
ChainID: tx.ChainId().Uint64(),
GasFeeCap: tx.GasFeeCap().Uint64(),
GasTipCap: tx.GasTipCap().Uint64(),
GasLimit: tx.Gas(),
Nonce: tx.Nonce(),
SubmitBlockNumber: submitBlockNumber,
Status: types.TxStatusPending,
RLPEncoding: rlp.Bytes(),
SenderName: senderMeta.Name,
SenderAddress: senderMeta.Address.String(),
SenderService: senderMeta.Service,
SenderType: senderMeta.Type,
}
db := o.db
if len(dbTX) > 0 && dbTX[0] != nil {
db = dbTX[0]
}
db = db.WithContext(ctx)
db = db.Model(&PendingTransaction{})
if err := db.Create(newTransaction).Error; err != nil {
return fmt.Errorf("failed to InsertTransaction, error: %w", err)
}
return nil
}
// DeleteTransactionByTxHash permanently deletes a transaction record from the database by transaction hash.
// Using permanent delete (Unscoped) instead of soft delete to prevent database bloat, as repeated SendTransaction failures
// could write a large number of transactions to the database.
func (o *PendingTransaction) DeleteTransactionByTxHash(ctx context.Context, hash common.Hash, dbTX ...*gorm.DB) error {
db := o.db
if len(dbTX) > 0 && dbTX[0] != nil {
db = dbTX[0]
}
db = db.WithContext(ctx)
db = db.Model(&PendingTransaction{})
// Perform permanent delete by using Unscoped()
result := db.Where("hash = ?", hash.String()).Unscoped().Delete(&PendingTransaction{})
if result.Error != nil {
return fmt.Errorf("failed to delete transaction, err: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("no transaction found with hash: %s", hash.String())
}
if result.RowsAffected > 0 {
log.Warn("Successfully deleted transaction", "hash", hash.String())
}
return nil
}
// UpdateTransactionStatusByTxHash updates the status of a transaction based on the transaction hash.
func (o *PendingTransaction) UpdateTransactionStatusByTxHash(ctx context.Context, hash common.Hash, status types.TxStatus, dbTX ...*gorm.DB) error {
db := o.db
if len(dbTX) > 0 && dbTX[0] != nil {
db = dbTX[0]
}
db = db.WithContext(ctx)
db = db.Model(&PendingTransaction{})
db = db.Where("hash = ?", hash.String())
if err := db.Update("status", status).Error; err != nil {
return fmt.Errorf("failed to UpdateTransactionStatusByTxHash, txHash: %s, error: %w", hash, err)
}
return nil
}
// UpdateTransactionStatusByTxHashes updates the status of multiple transactions by their hashes in one SQL statement
func (o *PendingTransaction) UpdateTransactionStatusByTxHashes(ctx context.Context, txHashes []string, status types.TxStatus, dbTX ...*gorm.DB) error {
if len(txHashes) == 0 {
return nil
}
db := o.db
if len(dbTX) > 0 && dbTX[0] != nil {
db = dbTX[0]
}
db = db.WithContext(ctx)
db = db.Model(&PendingTransaction{})
db = db.Where("hash IN ?", txHashes)
if err := db.Update("status", status).Error; err != nil {
return fmt.Errorf("failed to update transaction status for hashes %v to status %d: %w", txHashes, status, err)
}
return nil
}
// UpdateOtherTransactionsAsFailedByNonce updates the status of all transactions to TxStatusConfirmedFailed for a specific nonce and sender address, excluding a specified transaction hash.
func (o *PendingTransaction) UpdateOtherTransactionsAsFailedByNonce(ctx context.Context, senderAddress string, nonce uint64, hash common.Hash, dbTX ...*gorm.DB) error {
db := o.db
if len(dbTX) > 0 && dbTX[0] != nil {
db = dbTX[0]
}
db = db.WithContext(ctx)
db = db.Model(&PendingTransaction{})
db = db.Where("sender_address = ?", senderAddress)
db = db.Where("nonce = ?", nonce)
db = db.Where("hash != ?", hash.String())
if err := db.Update("status", types.TxStatusConfirmedFailed).Error; err != nil {
return fmt.Errorf("failed to update other transactions as failed by nonce, senderAddress: %s, nonce: %d, txHash: %s, error: %w", senderAddress, nonce, hash, err)
}
return nil
}
// GetMaxNonceBySenderAddress retrieves the maximum nonce for a specific sender address.
// Returns -1 if no transactions are found for the given address.
func (o *PendingTransaction) GetMaxNonceBySenderAddress(ctx context.Context, senderAddress string) (int64, error) {
var result struct {
Nonce int64 `gorm:"column:nonce"`
}
err := o.db.WithContext(ctx).
Model(&PendingTransaction{}).
Select("nonce").
Where("sender_address = ?", senderAddress).
Order("nonce DESC").
First(&result).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return -1, nil
}
return -1, fmt.Errorf("failed to get max nonce by sender address, address: %s, err: %w", senderAddress, err)
}
return result.Nonce, nil
}