-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtransaction.go
More file actions
211 lines (173 loc) · 6.06 KB
/
Copy pathtransaction.go
File metadata and controls
211 lines (173 loc) · 6.06 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
// internal/daemon/controller/transaction.go
package controller
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/altuslabsxyz/devnet-builder/internal/daemon/store"
"github.com/altuslabsxyz/devnet-builder/internal/daemon/types"
"github.com/altuslabsxyz/devnet-builder/pkg/network"
)
// TxController reconciles Transaction resources.
type TxController struct {
store store.Store
runtime TxRuntime
logger *slog.Logger
// Cache for in-flight transactions (unsigned tx bytes)
unsignedTxCache map[string]*network.UnsignedTx
}
// NewTxController creates a new TxController.
func NewTxController(s store.Store, r TxRuntime) *TxController {
return &TxController{
store: s,
runtime: r,
logger: slog.Default(),
unsignedTxCache: make(map[string]*network.UnsignedTx),
}
}
// SetLogger sets the logger.
func (c *TxController) SetLogger(logger *slog.Logger) {
c.logger = logger
}
// Reconcile processes a single transaction by name.
func (c *TxController) Reconcile(ctx context.Context, key string) error {
c.logger.Debug("reconciling transaction", "key", key)
tx, err := c.store.GetTransaction(ctx, key)
if err != nil {
if store.IsNotFound(err) {
c.logger.Debug("transaction not found (deleted?)", "key", key)
return nil
}
return err
}
switch tx.Status.Phase {
case "", types.TxPhasePending:
return c.reconcilePending(ctx, tx)
case types.TxPhaseBuilding:
return c.reconcileBuilding(ctx, tx)
case types.TxPhaseSigning:
return c.reconcileSigning(ctx, tx)
case types.TxPhaseSubmitted:
return c.reconcileSubmitted(ctx, tx)
case types.TxPhaseConfirmed, types.TxPhaseFailed:
return nil // Terminal states
default:
c.logger.Warn("unknown transaction phase", "key", key, "phase", tx.Status.Phase)
return nil
}
}
func (c *TxController) reconcilePending(ctx context.Context, tx *types.Transaction) error {
c.logger.Info("transaction pending, moving to building",
"name", tx.Metadata.Name,
"devnet", tx.Spec.DevnetRef)
tx.Status.Phase = types.TxPhaseBuilding
tx.Status.Message = "Building transaction"
tx.Metadata.UpdatedAt = time.Now()
return c.store.UpdateTransaction(ctx, tx)
}
func (c *TxController) reconcileBuilding(ctx context.Context, tx *types.Transaction) error {
c.logger.Debug("building transaction", "name", tx.Metadata.Name)
builder, err := c.runtime.GetTxBuilder(ctx, tx.Spec.DevnetRef)
if err != nil {
return c.setFailed(ctx, tx, fmt.Sprintf("failed to get TxBuilder: %v", err))
}
// Use configured gas limit, defaulting to 200000 if not specified
gasLimit := tx.Spec.GasLimit
if gasLimit == 0 {
gasLimit = 200000
}
unsignedTx, err := builder.BuildTx(ctx, &network.TxBuildRequest{
TxType: network.TxType(tx.Spec.TxType),
Sender: tx.Spec.Signer,
Payload: tx.Spec.Payload,
GasLimit: gasLimit,
})
if err != nil {
return c.setFailed(ctx, tx, fmt.Sprintf("failed to build tx: %v", err))
}
// Cache unsigned tx for signing phase
c.unsignedTxCache[tx.Metadata.Name] = unsignedTx
tx.Status.Phase = types.TxPhaseSigning
tx.Status.Message = "Signing transaction"
tx.Metadata.UpdatedAt = time.Now()
return c.store.UpdateTransaction(ctx, tx)
}
func (c *TxController) reconcileSigning(ctx context.Context, tx *types.Transaction) error {
c.logger.Debug("signing transaction", "name", tx.Metadata.Name)
// Get TxBuilder for signing
builder, err := c.runtime.GetTxBuilder(ctx, tx.Spec.DevnetRef)
if err != nil {
return c.setFailed(ctx, tx, fmt.Sprintf("failed to get TxBuilder: %v", err))
}
// Get cached unsigned tx
unsignedTx, ok := c.unsignedTxCache[tx.Metadata.Name]
if !ok {
// If not in cache, rebuild it with same gas limit as building phase
gasLimit := tx.Spec.GasLimit
if gasLimit == 0 {
gasLimit = 200000
}
unsignedTx, err = builder.BuildTx(ctx, &network.TxBuildRequest{
TxType: network.TxType(tx.Spec.TxType),
Sender: tx.Spec.Signer,
Payload: tx.Spec.Payload,
GasLimit: gasLimit,
})
if err != nil {
return c.setFailed(ctx, tx, fmt.Sprintf("failed to rebuild tx: %v", err))
}
}
// Get signing key
key, err := c.runtime.GetSigningKey(ctx, tx.Spec.DevnetRef, tx.Spec.Signer)
if err != nil {
return c.setFailed(ctx, tx, fmt.Sprintf("failed to get signing key: %v", err))
}
// Sign
signedTx, err := builder.SignTx(ctx, unsignedTx, key)
if err != nil {
return c.setFailed(ctx, tx, fmt.Sprintf("failed to sign tx: %v", err))
}
// Broadcast
result, err := builder.BroadcastTx(ctx, signedTx)
if err != nil {
return c.setFailed(ctx, tx, fmt.Sprintf("failed to broadcast tx: %v", err))
}
// Clean up cache
delete(c.unsignedTxCache, tx.Metadata.Name)
tx.Status.Phase = types.TxPhaseSubmitted
tx.Status.TxHash = result.TxHash
tx.Status.Message = fmt.Sprintf("Submitted: %s", result.TxHash)
tx.Metadata.UpdatedAt = time.Now()
return c.store.UpdateTransaction(ctx, tx)
}
func (c *TxController) reconcileSubmitted(ctx context.Context, tx *types.Transaction) error {
c.logger.Debug("waiting for confirmation", "name", tx.Metadata.Name, "txHash", tx.Status.TxHash)
receipt, err := c.runtime.WaitForConfirmation(ctx, tx.Spec.DevnetRef, tx.Status.TxHash)
if err != nil {
return c.setFailed(ctx, tx, fmt.Sprintf("confirmation failed: %v", err))
}
if !receipt.Success {
return c.setFailed(ctx, tx, fmt.Sprintf("tx failed: %s", receipt.Log))
}
tx.Status.Phase = types.TxPhaseConfirmed
tx.Status.Height = receipt.Height
tx.Status.GasUsed = receipt.GasUsed
tx.Status.Message = fmt.Sprintf("Confirmed at height %d", receipt.Height)
tx.Metadata.UpdatedAt = time.Now()
c.logger.Info("transaction confirmed",
"name", tx.Metadata.Name,
"txHash", tx.Status.TxHash,
"height", tx.Status.Height)
return c.store.UpdateTransaction(ctx, tx)
}
func (c *TxController) setFailed(ctx context.Context, tx *types.Transaction, errMsg string) error {
c.logger.Error("transaction failed", "name", tx.Metadata.Name, "error", errMsg)
tx.Status.Phase = types.TxPhaseFailed
tx.Status.Error = errMsg
tx.Status.Message = "Transaction failed"
tx.Metadata.UpdatedAt = time.Now()
// Clean up cache
delete(c.unsignedTxCache, tx.Metadata.Name)
return c.store.UpdateTransaction(ctx, tx)
}