Files
satoshi dc7da6ab19 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
2026-05-11 03:54:35 +03:00

107 lines
3.0 KiB
Go

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)
}