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
107 lines
3.0 KiB
Go
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)
|
|
}
|