From dc7da6ab19def8ff55974af06fd8e7f330cc89ff Mon Sep 17 00:00:00 2001 From: satoshi Date: Mon, 11 May 2026 03:54:35 +0300 Subject: [PATCH] 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 --- api/cmd/kamado-api/main.go | 13 +- api/internal/accelerator/accelerator.go | 236 +++++++++ api/internal/bitcoind/rpc.go | 70 +++ api/internal/httpapi/accelerate.go | 106 ++++ api/internal/httpapi/server.go | 10 +- api/internal/store/accelerator.go | 79 +++ api/internal/store/blocks.go | 4 + ui/src/App.svelte | 3 + ui/src/lib/AcceleratorPage.svelte | 650 ++++++++++++++++++++++++ ui/src/lib/Header.svelte | 46 ++ ui/src/lib/SharesBar.svelte | 32 +- ui/src/stores/selection.svelte.ts | 23 +- 12 files changed, 1247 insertions(+), 25 deletions(-) create mode 100644 api/internal/accelerator/accelerator.go create mode 100644 api/internal/httpapi/accelerate.go create mode 100644 api/internal/store/accelerator.go create mode 100644 ui/src/lib/AcceleratorPage.svelte diff --git a/api/cmd/kamado-api/main.go b/api/cmd/kamado-api/main.go index 725d41a..a6cd74d 100644 --- a/api/cmd/kamado-api/main.go +++ b/api/cmd/kamado-api/main.go @@ -18,6 +18,7 @@ import ( "syscall" "time" + "github.com/kamadopool/kamado-api/internal/accelerator" "github.com/kamadopool/kamado-api/internal/bitcoind" "github.com/kamadopool/kamado-api/internal/ckpool" "github.com/kamadopool/kamado-api/internal/config" @@ -65,10 +66,16 @@ func main() { agg.Store = blockStore agg.MempoolBaseURL = cfg.MempoolBaseURL + // Transaction accelerator (prioritisetransaction). + var accSvc *accelerator.Service + if blockStore != nil { + accSvc = accelerator.NewService(rpc, blockStore, log) + } + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() - api := httpapi.New(agg, log) + api := httpapi.New(agg, accSvc, log) // Wire snapshot refreshes into the WebSocket hub so subscribers get // real-time updates without polling. @@ -130,6 +137,10 @@ func main() { go agg.IngestAttemptEvents(ctx, tailer.Attempts) go agg.IngestLatencyEvents(ctx, tailer.Latencies) + if accSvc != nil { + go accSvc.Cleanup(ctx) + } + srv := &http.Server{ Addr: cfg.ListenAddr, Handler: api.Handler(), diff --git a/api/internal/accelerator/accelerator.go b/api/internal/accelerator/accelerator.go new file mode 100644 index 0000000..982aca5 --- /dev/null +++ b/api/internal/accelerator/accelerator.go @@ -0,0 +1,236 @@ +// Package accelerator manages transaction priority boosting via +// bitcoind's prioritisetransaction RPC. +package accelerator + +import ( + "context" + "database/sql" + "errors" + "fmt" + "log/slog" + "math" + "time" + + "github.com/kamadopool/kamado-api/internal/bitcoind" + "github.com/kamadopool/kamado-api/internal/store" +) + +// Service manages the lifecycle of boosted transactions. +type Service struct { + RPC *bitcoind.RPC + Store *store.BlockStore + Log *slog.Logger +} + +func NewService(rpc *bitcoind.RPC, st *store.BlockStore, log *slog.Logger) *Service { + return &Service{RPC: rpc, Store: st, Log: log} +} + +// MaxFeerateVB is the hard cap to prevent accidental extreme boosts. +const MaxFeerateVB = 2000.0 + +// AccelerateResult is returned on a successful boost. +type AccelerateResult struct { + Txid string `json:"txid"` + OriginalFeerate float64 `json:"original_feerate"` + BoostedFeerate float64 `json:"boosted_feerate"` + FeeDelta int64 `json:"fee_delta"` + Vsize int64 `json:"vsize"` + // Actual revenue impact: coinbasevalue_before - coinbasevalue_after. + // Zero means the mempool isn't full (no tx was displaced). + // Negative means getblocktemplate failed (couldn't measure). + FeeLostSats int64 `json:"fee_lost_sats"` + FeeLostError string `json:"fee_lost_error,omitempty"` +} + +// Accelerate boosts a transaction to the target feerate (sat/vB). +func (s *Service) Accelerate(ctx context.Context, txid string, targetFeerateVB float64) (*AccelerateResult, error) { + if targetFeerateVB > MaxFeerateVB { + return nil, fmt.Errorf("target feerate %.1f exceeds maximum %d sat/vB", targetFeerateVB, int(MaxFeerateVB)) + } + + entry, err := s.RPC.GetMempoolEntry(ctx, txid) + if err != nil { + return nil, fmt.Errorf("getmempoolentry: %w", err) + } + + // Use base fee (real, not priority-adjusted) to compute current feerate. + baseFeeSats := entry.Fees.Base * 1e8 + currentFeerate := baseFeeSats / float64(entry.Vsize) + + // Delta is relative to the modified fee (which includes any prior boosts). + modifiedFeeSats := entry.Fees.Modified * 1e8 + targetFeeSats := targetFeerateVB * float64(entry.Vsize) + delta := int64(math.Ceil(targetFeeSats - modifiedFeeSats)) + + if delta <= 0 { + return nil, fmt.Errorf("target feerate %.2f sat/vB is not higher than current effective %.2f sat/vB", + targetFeerateVB, modifiedFeeSats/float64(entry.Vsize)) + } + + // Snapshot coinbasevalue BEFORE the boost to measure actual impact. + var coinbaseBefore int64 + var tplErr string + tplBefore, err := s.RPC.GetBlockTemplate(ctx) + if err != nil { + tplErr = fmt.Sprintf("getblocktemplate (before): %s", bitcoind.RPCErrorMessage(err)) + s.Log.Warn("accelerate: getblocktemplate failed (before)", "err", err) + } else { + coinbaseBefore = tplBefore.CoinbaseValue + } + + if err := s.RPC.PrioritiseTransaction(ctx, txid, delta); err != nil { + return nil, fmt.Errorf("prioritisetransaction: %w", err) + } + + // Measure coinbasevalue AFTER — the difference is the real fee lost. + var feeLost int64 + if coinbaseBefore > 0 { + tplAfter, err := s.RPC.GetBlockTemplate(ctx) + if err != nil { + tplErr = fmt.Sprintf("getblocktemplate (after): %s", bitcoind.RPCErrorMessage(err)) + s.Log.Warn("accelerate: getblocktemplate failed (after)", "err", err) + } else { + feeLost = coinbaseBefore - tplAfter.CoinbaseValue + if feeLost < 0 { + feeLost = 0 // template improved (new tx arrived between calls) + } + s.Log.Info("accelerate: fee impact measured", + "before", coinbaseBefore, "after", tplAfter.CoinbaseValue, "lost", feeLost) + } + } + + rec := store.BoostedTx{ + Txid: txid, + OriginalFeerate: currentFeerate, + BoostedFeerate: targetFeerateVB, + FeeDelta: delta, + Vsize: entry.Vsize, + BoostedAt: time.Now().Unix(), + } + if s.Store != nil { + if err := s.Store.InsertBoostedTx(rec); err != nil { + s.Log.Warn("failed to persist boosted tx", "txid", txid, "err", err) + } + } + + s.Log.Info("tx accelerated", "txid", txid, "delta_sats", delta, + "original_rate", currentFeerate, "target_rate", targetFeerateVB, + "fee_lost_sats", feeLost) + + return &AccelerateResult{ + Txid: txid, + OriginalFeerate: currentFeerate, + BoostedFeerate: targetFeerateVB, + FeeDelta: delta, + Vsize: entry.Vsize, + FeeLostSats: feeLost, + FeeLostError: tplErr, + }, nil +} + +// Cancel reverses a previously applied priority boost. +func (s *Service) Cancel(ctx context.Context, txid string) error { + if s.Store == nil { + return fmt.Errorf("no store available") + } + rec, err := s.Store.GetBoostedTx(txid) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("transaction %s is not in the boosted list", txid) + } + return err + } + + // Reverse the delta. + if err := s.RPC.PrioritiseTransaction(ctx, txid, -rec.FeeDelta); err != nil { + // If tx is no longer in mempool, the reversal is a no-op at the + // bitcoind level but we still remove our record. + if !bitcoind.IsRPCError(err, -5) { + return fmt.Errorf("prioritisetransaction (cancel): %w", err) + } + } + + if err := s.Store.RemoveBoostedTx(txid); err != nil { + return fmt.Errorf("remove record: %w", err) + } + + s.Log.Info("tx acceleration cancelled", "txid", txid, "reversed_delta", rec.FeeDelta) + return nil +} + +// MaxFeerate returns the highest real feerate (ignoring priority +// adjustments) in the mempool, doubled, capped at MaxFeerateVB. +// Uses fees.base so previously-boosted txs don't skew the result. +func (s *Service) MaxFeerate(ctx context.Context) (float64, error) { + pool, err := s.RPC.GetRawMempoolVerbose(ctx) + if err != nil { + return 0, fmt.Errorf("getrawmempool: %w", err) + } + var maxRate float64 + for _, entry := range pool { + if entry.Vsize <= 0 { + continue + } + // Use base fee (actual fee paid), not modified (includes priority boosts). + rate := (entry.Fees.Base * 1e8) / float64(entry.Vsize) + if rate > maxRate { + maxRate = rate + } + } + if maxRate == 0 { + return 100, nil // sensible default if mempool is empty + } + result := math.Ceil(maxRate * 2) + if result > MaxFeerateVB { + result = MaxFeerateVB + } + return result, nil +} + +// List returns all currently tracked boosted transactions. +func (s *Service) List() ([]store.BoostedTx, error) { + if s.Store == nil { + return nil, nil + } + return s.Store.ListBoostedTxs() +} + +// Cleanup periodically checks if boosted txs have left the mempool +// (confirmed or evicted) and removes them from tracking. Run as a +// background goroutine. +func (s *Service) Cleanup(ctx context.Context) { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + s.cleanupOnce(ctx) + } + } +} + +func (s *Service) cleanupOnce(ctx context.Context) { + if s.Store == nil { + return + } + txs, err := s.Store.ListBoostedTxs() + if err != nil { + s.Log.Warn("accelerator cleanup: list failed", "err", err) + return + } + for _, tx := range txs { + _, err := s.RPC.GetMempoolEntry(ctx, tx.Txid) + if err != nil && bitcoind.IsRPCError(err, -5) { + // Tx no longer in mempool — confirmed or dropped. + if err := s.Store.RemoveBoostedTx(tx.Txid); err != nil { + s.Log.Warn("accelerator cleanup: remove failed", "txid", tx.Txid, "err", err) + } else { + s.Log.Info("accelerator: tx left mempool, removed from tracking", "txid", tx.Txid) + } + } + } +} diff --git a/api/internal/bitcoind/rpc.go b/api/internal/bitcoind/rpc.go index 9a5a0c4..84bd9e6 100644 --- a/api/internal/bitcoind/rpc.go +++ b/api/internal/bitcoind/rpc.go @@ -276,3 +276,73 @@ func (c *RPC) GetBlockTemplate(ctx context.Context) (*BlockTemplate, error) { } return &out, nil } + +// ---- mempool / priority methods ---- + +// MempoolEntryFees mirrors the "fees" object inside getmempoolentry. +type MempoolEntryFees struct { + Base float64 `json:"base"` + Modified float64 `json:"modified"` + Ancestor float64 `json:"ancestor"` + Descendant float64 `json:"descendant"` +} + +// MempoolEntry is the result of getmempoolentry . +type MempoolEntry struct { + Vsize int64 `json:"vsize"` + Fees MempoolEntryFees `json:"fees"` +} + +// GetMempoolEntry returns the mempool entry for a transaction. +// Returns rpcError code -5 if the tx is not in the mempool. +func (c *RPC) GetMempoolEntry(ctx context.Context, txid string) (*MempoolEntry, error) { + var out MempoolEntry + if err := c.Call(ctx, "getmempoolentry", []any{txid}, &out); err != nil { + return nil, err + } + return &out, nil +} + +// PrioritiseTransaction adjusts a transaction's apparent fee for block +// template selection. feeDelta is in satoshis (positive = boost, +// negative = de-prioritise). +func (c *RPC) PrioritiseTransaction(ctx context.Context, txid string, feeDelta int64) error { + var ok bool + if err := c.Call(ctx, "prioritisetransaction", []any{txid, 0, feeDelta}, &ok); err != nil { + return err + } + return nil +} + +// RawMempoolEntry is the verbose form of a mempool entry from getrawmempool true. +type RawMempoolEntry struct { + Vsize int64 `json:"vsize"` + Fees MempoolEntryFees `json:"fees"` +} + +// GetRawMempoolVerbose returns all mempool transactions with their details. +func (c *RPC) GetRawMempoolVerbose(ctx context.Context) (map[string]RawMempoolEntry, error) { + var out map[string]RawMempoolEntry + if err := c.Call(ctx, "getrawmempool", []any{true}, &out); err != nil { + return nil, err + } + return out, nil +} + +// IsRPCError checks if err is an rpcError with a specific code. +func IsRPCError(err error, code int) bool { + var rerr *rpcError + if errors.As(err, &rerr) { + return rerr.Code == code + } + return false +} + +// RPCErrorMessage extracts the message from an rpcError, or the error string. +func RPCErrorMessage(err error) string { + var rerr *rpcError + if errors.As(err, &rerr) { + return rerr.Message + } + return err.Error() +} diff --git a/api/internal/httpapi/accelerate.go b/api/internal/httpapi/accelerate.go new file mode 100644 index 0000000..9ec5195 --- /dev/null +++ b/api/internal/httpapi/accelerate.go @@ -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) +} diff --git a/api/internal/httpapi/server.go b/api/internal/httpapi/server.go index fd8d969..5c724a6 100644 --- a/api/internal/httpapi/server.go +++ b/api/internal/httpapi/server.go @@ -10,18 +10,20 @@ import ( "net/http" "strings" + "github.com/kamadopool/kamado-api/internal/accelerator" "github.com/kamadopool/kamado-api/internal/state" "github.com/kamadopool/kamado-api/internal/webui" ) type Server struct { Agg *state.Aggregator + Acc *accelerator.Service Hub *Hub Log *slog.Logger } -func New(agg *state.Aggregator, log *slog.Logger) *Server { - return &Server{Agg: agg, Hub: NewHub(), Log: log} +func New(agg *state.Aggregator, acc *accelerator.Service, log *slog.Logger) *Server { + return &Server{Agg: agg, Acc: acc, Hub: NewHub(), Log: log} } // Handler returns an http.Handler with all kamado routes mounted under @@ -39,6 +41,10 @@ func (s *Server) Handler() http.Handler { mux.HandleFunc("GET /api/ws", s.handleWS) mux.HandleFunc("GET /api/admin/debug-blocks", s.debugBlocks) mux.HandleFunc("POST /api/admin/reset-latency", s.resetLatency) + mux.HandleFunc("POST /api/accelerate", s.accelerate) + mux.HandleFunc("POST /api/accelerate/cancel", s.accelerateCancel) + mux.HandleFunc("POST /api/accelerate/max", s.accelerateMax) + mux.HandleFunc("GET /api/accelerate/list", s.accelerateList) mux.Handle("/", spaHandler(webui.FS())) return mux } diff --git a/api/internal/store/accelerator.go b/api/internal/store/accelerator.go new file mode 100644 index 0000000..82e19bb --- /dev/null +++ b/api/internal/store/accelerator.go @@ -0,0 +1,79 @@ +package store + +// BoostedTx represents a transaction whose priority has been adjusted +// via bitcoind's prioritisetransaction RPC. +type BoostedTx struct { + Txid string `json:"txid"` + OriginalFeerate float64 `json:"original_feerate"` // sat/vB before boost + BoostedFeerate float64 `json:"boosted_feerate"` // sat/vB after boost + FeeDelta int64 `json:"fee_delta"` // sats applied + Vsize int64 `json:"vsize"` + BoostedAt int64 `json:"boosted_at"` // unix timestamp +} + +const acceleratorSchema = ` +CREATE TABLE IF NOT EXISTS boosted_txs ( + txid TEXT PRIMARY KEY, + original_feerate REAL NOT NULL, + boosted_feerate REAL NOT NULL, + fee_delta INTEGER NOT NULL, + vsize INTEGER NOT NULL, + boosted_at INTEGER NOT NULL +); +` + +// migrateAccelerator ensures the boosted_txs table exists. +func (s *BlockStore) migrateAccelerator() error { + _, err := s.db.Exec(acceleratorSchema) + return err +} + +// InsertBoostedTx records a newly boosted transaction. +func (s *BlockStore) InsertBoostedTx(tx BoostedTx) error { + _, err := s.db.Exec( + `INSERT OR REPLACE INTO boosted_txs(txid, original_feerate, boosted_feerate, fee_delta, vsize, boosted_at) + VALUES (?, ?, ?, ?, ?, ?)`, + tx.Txid, tx.OriginalFeerate, tx.BoostedFeerate, tx.FeeDelta, tx.Vsize, tx.BoostedAt, + ) + return err +} + +// RemoveBoostedTx deletes a boosted tx record (confirmed, dropped, or cancelled). +func (s *BlockStore) RemoveBoostedTx(txid string) error { + _, err := s.db.Exec(`DELETE FROM boosted_txs WHERE txid = ?`, txid) + return err +} + +// ListBoostedTxs returns all currently boosted transactions, newest first. +func (s *BlockStore) ListBoostedTxs() ([]BoostedTx, error) { + rows, err := s.db.Query( + `SELECT txid, original_feerate, boosted_feerate, fee_delta, vsize, boosted_at + FROM boosted_txs ORDER BY boosted_at DESC`, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var out []BoostedTx + for rows.Next() { + var tx BoostedTx + if err := rows.Scan(&tx.Txid, &tx.OriginalFeerate, &tx.BoostedFeerate, &tx.FeeDelta, &tx.Vsize, &tx.BoostedAt); err != nil { + return nil, err + } + out = append(out, tx) + } + return out, rows.Err() +} + +// GetBoostedTx returns a single boosted tx by txid, or nil if not found. +func (s *BlockStore) GetBoostedTx(txid string) (*BoostedTx, error) { + var tx BoostedTx + err := s.db.QueryRow( + `SELECT txid, original_feerate, boosted_feerate, fee_delta, vsize, boosted_at + FROM boosted_txs WHERE txid = ?`, txid, + ).Scan(&tx.Txid, &tx.OriginalFeerate, &tx.BoostedFeerate, &tx.FeeDelta, &tx.Vsize, &tx.BoostedAt) + if err != nil { + return nil, err + } + return &tx, nil +} diff --git a/api/internal/store/blocks.go b/api/internal/store/blocks.go index 0e32df9..15908c0 100644 --- a/api/internal/store/blocks.go +++ b/api/internal/store/blocks.go @@ -109,6 +109,10 @@ func Open(path string) (*BlockStore, error) { _ = db.Close() return nil, err } + if err := s.migrateAccelerator(); err != nil { + _ = db.Close() + return nil, fmt.Errorf("store: accelerator schema: %w", err) + } return s, nil } diff --git a/ui/src/App.svelte b/ui/src/App.svelte index 3b4f296..98c1c25 100644 --- a/ui/src/App.svelte +++ b/ui/src/App.svelte @@ -11,6 +11,7 @@ import BestShares from "./lib/BestShares.svelte"; import UserDetailPage from "./lib/UserDetailPage.svelte"; import WorkerDetailPage from "./lib/WorkerDetailPage.svelte"; + import AcceleratorPage from "./lib/AcceleratorPage.svelte"; import SharesBar from "./lib/SharesBar.svelte"; import BlockFoundAnimation from "./lib/BlockFoundAnimation.svelte"; @@ -34,6 +35,8 @@
{snap.error}
{/if} + {:else if selection.page === "accelerator"} + {:else if selection.worker} {:else if selection.user} diff --git a/ui/src/lib/AcceleratorPage.svelte b/ui/src/lib/AcceleratorPage.svelte new file mode 100644 index 0000000..bae79d2 --- /dev/null +++ b/ui/src/lib/AcceleratorPage.svelte @@ -0,0 +1,650 @@ + + + + +
+ + +
+ +
+

+ + + + + + + Transaction Accelerator +

+

+ Boost a transaction's priority in your next block template using + prioritisetransaction. When your pool finds a block, + the boosted transaction will be included regardless of its real feerate. +

+
+

+ How it works: The boosted transaction takes the place of + the lowest-feerate transaction at the bottom of the block template. If the + mempool isn't full (all transactions already fit), there is zero cost. +

+

+ Revenue impact: After boosting, the server compares the block template's + coinbase value before and after. The difference (if any) is the actual fee revenue + lost per block mined. This is shown after each boost. +

+

+ Your pool: {formatHashrate(poolHashrate)} / + {formatHashrate(networkHashrate)} network + ({poolShare > 0 ? `${(poolShare * 100).toFixed(6)}%` : "—"} of blocks). +

+
+
+
+ +
+

Boost a Transaction

+ +
+ + + {#if txid && !validTxid} + Must be exactly 64 hex characters + {/if} +
+ +
+ +
+ + +
+ {#if overCap} + Cannot exceed 2,000 sat/vB + {/if} +
+ + + + {#if error} +
+ {error} + +
+ {/if} + {#if success} +
+ {success} + +
+ {/if} +
+ +
+

Boosted Transactions {#if boostedList.length > 0}{boostedList.length}{/if}

+ {#if boostedList.length === 0} +
No transactions currently boosted. They are automatically removed when confirmed or dropped from the mempool.
+ {:else} +
+ + + + + + + + + + + + + + {#each boostedList as tx (tx.txid)} + + + + + + + + + + {/each} + +
TransactionOriginalBoosted toDeltavsizeWhen
+ {tx.txid.slice(0, 10)}...{tx.txid.slice(-10)} + {tx.original_feerate.toFixed(1)} sat/vB{tx.boosted_feerate.toFixed(1)} sat/vB{tx.fee_delta.toLocaleString()} sats{tx.vsize.toLocaleString()}{fmtAgo(tx.boosted_at)} + +
+
+ {/if} +
+
+ + diff --git a/ui/src/lib/Header.svelte b/ui/src/lib/Header.svelte index b600b71..2ab186f 100644 --- a/ui/src/lib/Header.svelte +++ b/ui/src/lib/Header.svelte @@ -1,5 +1,6 @@