mirror of
https://github.com/btcsuite/btcd.git
synced 2026-08-13 12:32:51 +02:00
243 lines
8.6 KiB
Go
243 lines
8.6 KiB
Go
package btcjson
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
|
||
"github.com/btcsuite/btcd/chainhash/v2"
|
||
)
|
||
|
||
// JsonSubmitPackageCmd models the request payload for Bitcoin Core’s
|
||
// experimental `submitpackage` RPC (v26+). It submits a related group of
|
||
// transactions (a package) to the node's mempool for validation and acceptance.
|
||
//
|
||
// Package Requirements:
|
||
// - Topology: Must be a "child-with-unconfirmed-parents" package: exactly one
|
||
// child transaction (last in the list) and all of its unconfirmed parent
|
||
// transactions. Parents cannot depend on each other within the package.
|
||
// - Order: Transactions MUST be topologically sorted (parents before child).
|
||
// - Content: No duplicate transactions. No conflicting transactions (spending
|
||
// the same input) within the package.
|
||
// - Limits: Subject to node limits on package size (e.g., max 25 txs) and
|
||
// total weight (e.g., max 404000 weight units).
|
||
//
|
||
// Validation & Acceptance:
|
||
// - Individual First: Each transaction is first validated individually
|
||
// against mempool policy (including minimum relay fee `minrelaytxfee`).
|
||
// - Package Logic: Transactions failing individual checks (often due to low
|
||
// fee rate) are then evaluated using package logic.
|
||
// - Package Feerate: The total fee of non-mempool transactions divided by
|
||
// their total virtual size. This can overcome the dynamic mempool minimum
|
||
// fee rate (acting like CPFP), but cannot overcome the static
|
||
// `minrelaytxfee`. Any transaction below `minrelaytxfee` will cause
|
||
// rejection.
|
||
// - Deduplication: Transactions already in the mempool (by txid) are ignored
|
||
// during submission, preventing rejection and double-counting fees. See
|
||
// the `other-wtxid` field in the result if a different witness version
|
||
// exists.
|
||
// - Package RBF: Limited Replace-By-Fee logic applies. See the
|
||
// `replaced-transactions` field in the result.
|
||
//
|
||
// This RPC is experimental. Refer to Bitcoin Core's `doc/policy/packages.md`
|
||
// for details. Successful submission does not guarantee network propagation.
|
||
//
|
||
// Reference: https://bitcoincore.org/en/doc/29.0.0/rpc/rawtransactions/submitpackage/
|
||
type JsonSubmitPackageCmd struct {
|
||
// RawTxs holds the hex-encoded raw transactions forming the package.
|
||
// MUST be topologically sorted (parents first, child last) and
|
||
// represent a valid "child-with-unconfirmed-parents" structure.
|
||
RawTxs []string `jsonrpc:"package"`
|
||
|
||
// MaxFeeRate (Optional, BTC/kvB): If set, rejects package transactions
|
||
// exceeding this fee rate. Rates > 1 BTC/kvB are always rejected. If
|
||
// nil, Core's RPC default (e.g., 0.10 BTC/kvB) applies. Set to 0 for no
|
||
// limit (up to 1 BTC/kvB).
|
||
MaxFeeRate *float64 `jsonrpc:"maxfeerate,omitempty"`
|
||
|
||
// MaxBurnAmount (Optional, BTC): If set, rejects packages where the
|
||
// total value of provably unspendable outputs (e.g., OP_RETURN) exceeds
|
||
// this amount. If nil, Core's RPC default (0.00 BTC) applies.
|
||
MaxBurnAmount *float64 `jsonrpc:"maxburnamount,omitempty"`
|
||
}
|
||
|
||
// JsonSubmitPackageFees models the "fees" sub-object in a `submitpackage`
|
||
// response. Values are in BTC. May be omitted if fee info is not applicable.
|
||
type JsonSubmitPackageFees struct {
|
||
// Base is the absolute fee of this specific transaction (in BTC).
|
||
Base float64 `json:"base"`
|
||
|
||
// EffectiveFeeRate (Optional, BTC/kvB): The transaction's effective
|
||
// feerate, potentially considering package context or
|
||
// `prioritisetransaction`.
|
||
EffectiveFeeRate *float64 `json:"effective-feerate,omitempty"`
|
||
|
||
// EffectiveIncludes (Optional): wtxids contributing to
|
||
// `effective-feerate`.
|
||
EffectiveIncludes []string `json:"effective-includes,omitempty"`
|
||
}
|
||
|
||
// JsonSubmitPackageTxResult represents the processing result for a single
|
||
// transaction within the package, keyed by its wtxid in the response map.
|
||
type JsonSubmitPackageTxResult struct {
|
||
// TxID is the transaction hash (txid) in hex.
|
||
TxID string `json:"txid"`
|
||
|
||
// OtherWtxid (Optional): Set if a conflicting tx with the same txid but
|
||
// different witness was already in the mempool (submitted tx was
|
||
// ignored). Relates to deduplication.
|
||
OtherWtxid *string `json:"other-wtxid,omitempty"`
|
||
|
||
// VSize is the virtual size in vbytes. Note: Optional in RPC; defaults
|
||
// to 0 if missing.
|
||
VSize int64 `json:"vsize"`
|
||
|
||
// Fees contains fee information. Note: Optional in RPC; defaults to
|
||
// empty struct if missing.
|
||
Fees JsonSubmitPackageFees `json:"fees"`
|
||
|
||
// Error (Optional): String describing rejection reason, if any. Can
|
||
// result from individual checks (e.g., below `minrelaytxfee`) or
|
||
// package validation failures.
|
||
Error *string `json:"error,omitempty"`
|
||
}
|
||
|
||
// JsonSubmitPackageResult mirrors the JSON object returned by `submitpackage`.
|
||
type JsonSubmitPackageResult struct {
|
||
// PackageMsg is a summary message ("success" or other status).
|
||
PackageMsg string `json:"package_msg"`
|
||
|
||
// TxResults maps each submitted transaction's wtxid to its result.
|
||
TxResults map[string]JsonSubmitPackageTxResult `json:"tx-results"`
|
||
|
||
// ReplacedTransactions (Optional): txids of transactions evicted via
|
||
// Package RBF.
|
||
ReplacedTransactions []string `json:"replaced-transactions,omitempty"`
|
||
}
|
||
|
||
// NewJsonSubmitPackageCmd constructs a JsonSubmitPackageCmd.
|
||
//
|
||
// Parameters:
|
||
// - rawTxs: Slice of hex-encoded txs (topologically sorted
|
||
// child-with-parents).
|
||
// - maxFeeRateBtcKvB: Optional max fee rate (BTC/kvB). Nil uses RPC default.
|
||
// - maxBurnAmountBtc: Optional max burn amount (BTC). Nil uses RPC default.
|
||
func NewJsonSubmitPackageCmd(rawTxs []string,
|
||
maxFeeRateBtcKvB, maxBurnAmountBtc *float64) *JsonSubmitPackageCmd {
|
||
|
||
return &JsonSubmitPackageCmd{
|
||
RawTxs: rawTxs,
|
||
MaxFeeRate: maxFeeRateBtcKvB,
|
||
MaxBurnAmount: maxBurnAmountBtc,
|
||
}
|
||
}
|
||
|
||
// SubmitPackageResult mirrors JsonSubmitPackageResult with higher-level types.
|
||
type SubmitPackageResult struct {
|
||
// PackageMsg is a summary message ("success" or other status).
|
||
PackageMsg string
|
||
|
||
// TxResults maps each submitted transaction's wtxid to its result.
|
||
TxResults map[string]SubmitPackageTxResult
|
||
|
||
// ReplacedTransactions (Optional): txids of transactions evicted via
|
||
// Package RBF.
|
||
ReplacedTransactions []chainhash.Hash
|
||
}
|
||
|
||
// SubmitPackageTxResult mirrors (a subset of) JsonSubmitPackageTxResult with
|
||
// higher-level types.
|
||
type SubmitPackageTxResult struct {
|
||
// TxID is the transaction hash (txid) in hex.
|
||
TxID chainhash.Hash
|
||
|
||
// OtherWtxid (Optional): Set if a conflicting tx with the same txid but
|
||
// different witness was already in the mempool (submitted tx was
|
||
// ignored). Relates to deduplication.
|
||
OtherWtxid *chainhash.Hash
|
||
|
||
// Error (Optional): String describing rejection reason, if any. Can
|
||
// result from individual checks (e.g., below `minrelaytxfee`) or
|
||
// package validation failures.
|
||
Error *string
|
||
}
|
||
|
||
// UnmarshalJSON unmarshals the JsonSubmitPackageResult from the JSON response
|
||
// to the higher-level SubmitPackageResult type. If the function succeeds, the
|
||
// receiver is overwritten with the unmarshalled result.
|
||
func (s *SubmitPackageResult) UnmarshalJSON(data []byte) error {
|
||
var src JsonSubmitPackageResult
|
||
if err := json.Unmarshal(data, &src); err != nil {
|
||
return err
|
||
}
|
||
|
||
dst := SubmitPackageResult{
|
||
PackageMsg: src.PackageMsg,
|
||
TxResults: make(map[string]SubmitPackageTxResult),
|
||
}
|
||
|
||
// Translate TxResults.
|
||
if len(src.TxResults) > 0 {
|
||
|
||
for wtxid, srcTxRes := range src.TxResults {
|
||
var dstTxRes SubmitPackageTxResult
|
||
|
||
// Translate TxID.
|
||
txID, err := chainhash.NewHashFromStr(srcTxRes.TxID)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to parse txid '%s' "+
|
||
"for wtxid '%s': %w", srcTxRes.TxID,
|
||
wtxid, err)
|
||
}
|
||
|
||
dstTxRes.TxID = *txID
|
||
|
||
// Translate Error (direct copy).
|
||
dstTxRes.Error = srcTxRes.Error
|
||
|
||
// Translate OtherWtxid.
|
||
if srcTxRes.OtherWtxid != nil &&
|
||
*srcTxRes.OtherWtxid != "" {
|
||
|
||
otherWtxidHash, err := chainhash.NewHashFromStr(
|
||
*srcTxRes.OtherWtxid,
|
||
)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to parse "+
|
||
"other_wtxid '%s' for wtxid "+
|
||
"'%s': %w",
|
||
*srcTxRes.OtherWtxid, wtxid,
|
||
err)
|
||
}
|
||
|
||
dstTxRes.OtherWtxid = otherWtxidHash
|
||
}
|
||
|
||
dst.TxResults[wtxid] = dstTxRes
|
||
}
|
||
}
|
||
|
||
// Translate ReplacedTransactions.
|
||
if len(src.ReplacedTransactions) > 0 {
|
||
dst.ReplacedTransactions = make(
|
||
[]chainhash.Hash, 0, len(src.ReplacedTransactions),
|
||
)
|
||
|
||
for _, txidStr := range src.ReplacedTransactions {
|
||
hash, err := chainhash.NewHashFromStr(txidStr)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to parse "+
|
||
"replaced_transaction txid '%s': %w",
|
||
txidStr, err)
|
||
}
|
||
|
||
dst.ReplacedTransactions = append(
|
||
dst.ReplacedTransactions, *hash,
|
||
)
|
||
}
|
||
}
|
||
|
||
// Overwrite the receiver with the translated result.
|
||
*s = dst
|
||
|
||
return nil
|
||
}
|