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,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
|
||||
}
|
||||
Reference in New Issue
Block a user