Add transaction accelerator (prioritisetransaction UI + API)
Full-stack feature for boosting transactions via bitcoind's prioritisetransaction RPC: Backend: - New accelerator package with Accelerate, Cancel, MaxFeerate, List, and background Cleanup goroutine (removes confirmed/dropped txs) - RPC wrappers: GetMempoolEntry, PrioritiseTransaction, GetRawMempoolVerbose, IsRPCError helpers - SQLite boosted_txs table for persistence across restarts - Revenue impact measured via getblocktemplate before/after comparison - Hard cap at 2000 sat/vB; MaxFeerate uses fees.base (not modified) to ignore our own prior priority adjustments Frontend: - Rocket icon in header with 10s jiggle animation - AcceleratorPage with flame-gradient border, txid input, feerate input, "Prioritize above all" button with spinner, boost list with cancel buttons, and dismissible error/success messages - Hash-based routing (#/accelerator) - SharesBar font size bump
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/kamadopool/kamado-api/internal/bitcoind"
|
||||
"github.com/kamadopool/kamado-api/internal/store"
|
||||
)
|
||||
|
||||
var txidRE = regexp.MustCompile(`^[0-9a-fA-F]{64}$`)
|
||||
|
||||
type accelerateReq struct {
|
||||
Txid string `json:"txid"`
|
||||
FeerateVB float64 `json:"fee_rate_satvb"`
|
||||
}
|
||||
|
||||
type cancelReq struct {
|
||||
Txid string `json:"txid"`
|
||||
}
|
||||
|
||||
func (s *Server) accelerate(w http.ResponseWriter, r *http.Request) {
|
||||
if s.Acc == nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, map[string]any{"error": "accelerator not available (no store)"})
|
||||
return
|
||||
}
|
||||
var req accelerateReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if !txidRE.MatchString(req.Txid) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid txid: must be 64 hex characters"})
|
||||
return
|
||||
}
|
||||
if req.FeerateVB <= 0 {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "fee_rate_satvb must be positive"})
|
||||
return
|
||||
}
|
||||
if req.FeerateVB > 2000 {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "fee_rate_satvb cannot exceed 2000 sat/vB"})
|
||||
return
|
||||
}
|
||||
|
||||
result, err := s.Acc.Accelerate(r.Context(), req.Txid, req.FeerateVB)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if bitcoind.IsRPCError(err, -5) {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
writeJSON(w, status, map[string]any{"error": bitcoind.RPCErrorMessage(err)})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (s *Server) accelerateCancel(w http.ResponseWriter, r *http.Request) {
|
||||
if s.Acc == nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, map[string]any{"error": "accelerator not available"})
|
||||
return
|
||||
}
|
||||
var req cancelReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if !txidRE.MatchString(req.Txid) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid txid"})
|
||||
return
|
||||
}
|
||||
if err := s.Acc.Cancel(r.Context(), req.Txid); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
func (s *Server) accelerateMax(w http.ResponseWriter, r *http.Request) {
|
||||
if s.Acc == nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, map[string]any{"error": "accelerator not available"})
|
||||
return
|
||||
}
|
||||
rate, err := s.Acc.MaxFeerate(r.Context())
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"max_feerate_satvb": rate})
|
||||
}
|
||||
|
||||
func (s *Server) accelerateList(w http.ResponseWriter, r *http.Request) {
|
||||
if s.Acc == nil {
|
||||
writeJSON(w, http.StatusOK, []any{})
|
||||
return
|
||||
}
|
||||
txs, err := s.Acc.List()
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if txs == nil {
|
||||
txs = []store.BoostedTx{}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, txs)
|
||||
}
|
||||
Reference in New Issue
Block a user