Skip to content

Commit 2253bd3

Browse files
committed
walletrpc: add SubmitPackage RPC
1 parent 326f160 commit 2253bd3

File tree

6 files changed

+901
-326
lines changed

6 files changed

+901
-326
lines changed

lnrpc/walletrpc/walletkit.pb.go

Lines changed: 631 additions & 326 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lnrpc/walletrpc/walletkit.pb.json.go

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lnrpc/walletrpc/walletkit.proto

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,14 @@ service WalletKit {
357357
unlock/release any locked UTXOs in case of an error in this method.
358358
*/
359359
rpc FinalizePsbt (FinalizePsbtRequest) returns (FinalizePsbtResponse);
360+
361+
/*
362+
SubmitPackage attempts to broadcast a transaction package, consisting of
363+
one or more parent transactions and exactly one child transaction.
364+
The package is submitted to the backend node's mempool atomically.
365+
This RPC is primarily used for Child-Pays-For-Parent (CPFP) fee bumping.
366+
*/
367+
rpc SubmitPackage (SubmitPackageRequest) returns (SubmitPackageResponse);
360368
}
361369

362370
message ListUnspentRequest {
@@ -1592,3 +1600,60 @@ message ListLeasesResponse {
15921600
// The list of currently leased utxos.
15931601
repeated UtxoLease locked_utxos = 1;
15941602
}
1603+
1604+
message SubmitPackageRequest {
1605+
// A list of raw, serialized parent transactions. These transactions are
1606+
// typically the ones being fee-bumped.
1607+
repeated bytes parent_txs = 1;
1608+
1609+
// The raw, serialized child transaction. This transaction spends outputs
1610+
// from one or more of the parent_txs and should provide sufficient fees for
1611+
// the entire package.
1612+
bytes child_tx = 2;
1613+
1614+
// An optional fee rate cap for the package, expressed in satoshis per
1615+
// kilo-weight (sat/kw). If non-zero, the backend node may reject the
1616+
// package if its effective fee rate exceeds this value. A value of 0
1617+
// indicates no specific limit should be enforced by the caller.
1618+
uint32 max_fee_rate_sat_per_kw = 3;
1619+
}
1620+
1621+
message SubmitPackageResponse {
1622+
// Overall status message for the package submission (e.g., "success",
1623+
// or a general reason if the entire package is rejected before individual
1624+
// transaction checks).
1625+
string package_msg = 1;
1626+
1627+
// Detailed result for each transaction within the submitted package.
1628+
// The key is the wtxid (witness transaction ID, hex-encoded) of the
1629+
// transaction as submitted in the package.
1630+
map<string, TxResult> tx_results = 2;
1631+
1632+
// A list of hex-encoded transaction IDs of transactions that were evicted
1633+
// from the mempool due to replacement by transactions in this package
1634+
// (as a result of Package RBF).
1635+
repeated string replaced_transactions = 3;
1636+
1637+
// TxResult provides information about a single transaction within the
1638+
// submitted package.
1639+
message TxResult {
1640+
// The transaction hash (txid, hex-encoded) of this transaction.
1641+
string txid = 1;
1642+
1643+
// If the submitted transaction was recognized as a witness-replacement
1644+
// for an existing mempool transaction (i.e., same txid but different
1645+
// witness), this field will contain the wtxid (hex-encoded) of the
1646+
// pre-existing transaction that was kept in the mempool.
1647+
// This field will be empty if this was not a witness-replacement
1648+
// scenario or if the submitted transaction was preferred.
1649+
string other_wtxid = 2;
1650+
1651+
// If the transaction was rejected or failed processing for any reason
1652+
// (either individual checks or package validation rules), this field
1653+
// contains a human-readable error message.
1654+
// This field will be empty if the transaction was successfully
1655+
// processed (i.e., accepted into the mempool or was already present and
1656+
// valid).
1657+
string error_message = 3;
1658+
}
1659+
}

lnrpc/walletrpc/walletkit.swagger.json

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,24 @@
968968
}
969969
}
970970
},
971+
"SubmitPackageResponseTxResult": {
972+
"type": "object",
973+
"properties": {
974+
"txid": {
975+
"type": "string",
976+
"description": "The transaction hash (txid, hex-encoded) of this transaction."
977+
},
978+
"other_wtxid": {
979+
"type": "string",
980+
"description": "If the submitted transaction was recognized as a witness-replacement\nfor an existing mempool transaction (i.e., same txid but different\nwitness), this field will contain the wtxid (hex-encoded) of the\npre-existing transaction that was kept in the mempool.\nThis field will be empty if this was not a witness-replacement\nscenario or if the submitted transaction was preferred."
981+
},
982+
"error_message": {
983+
"type": "string",
984+
"description": "If the transaction was rejected or failed processing for any reason\n(either individual checks or package validation rules), this field\ncontains a human-readable error message.\nThis field will be empty if the transaction was successfully\nprocessed (i.e., accepted into the mempool or was already present and\nvalid)."
985+
}
986+
},
987+
"description": "TxResult provides information about a single transaction within the\nsubmitted package."
988+
},
971989
"lnrpcAddressType": {
972990
"type": "string",
973991
"enum": [
@@ -2180,6 +2198,29 @@
21802198
}
21812199
}
21822200
},
2201+
"walletrpcSubmitPackageResponse": {
2202+
"type": "object",
2203+
"properties": {
2204+
"package_msg": {
2205+
"type": "string",
2206+
"description": "Overall status message for the package submission (e.g., \"success\",\nor a general reason if the entire package is rejected before individual\ntransaction checks)."
2207+
},
2208+
"tx_results": {
2209+
"type": "object",
2210+
"additionalProperties": {
2211+
"$ref": "#/definitions/SubmitPackageResponseTxResult"
2212+
},
2213+
"description": "Detailed result for each transaction within the submitted package.\nThe key is the wtxid (witness transaction ID, hex-encoded) of the\ntransaction as submitted in the package."
2214+
},
2215+
"replaced_transactions": {
2216+
"type": "array",
2217+
"items": {
2218+
"type": "string"
2219+
},
2220+
"description": "A list of hex-encoded transaction IDs of transactions that were evicted\nfrom the mempool due to replacement by transactions in this package\n(as a result of Package RBF)."
2221+
}
2222+
}
2223+
},
21832224
"walletrpcTapLeaf": {
21842225
"type": "object",
21852226
"properties": {

lnrpc/walletrpc/walletkit_grpc.pb.go

Lines changed: 44 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lnrpc/walletrpc/walletkit_server.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ var (
185185
Entity: "onchain",
186186
Action: "write",
187187
}},
188+
"/walletrpc.WalletKit/SubmitPackage": {{
189+
Entity: "onchain",
190+
Action: "write",
191+
}},
188192
}
189193

190194
// DefaultWalletKitMacFilename is the default name of the wallet kit
@@ -3078,3 +3082,94 @@ func (w *WalletKit) ImportTapscript(_ context.Context,
30783082
P2TrAddress: addr.Address().String(),
30793083
}, nil
30803084
}
3085+
3086+
// SubmitPackage attempts to broadcast a transaction package, consisting of one
3087+
// or more parent transactions and exactly one child transaction. The package is
3088+
// submitted to the backend node's mempool atomically. This RPC is primarily
3089+
// used for Child-Pays-For-Parent (CPFP) fee bumping.
3090+
func (w *WalletKit) SubmitPackage(ctx context.Context,
3091+
req *SubmitPackageRequest) (*SubmitPackageResponse, error) {
3092+
3093+
// Validate that there's at least one parent transaction.
3094+
if len(req.ParentTxs) == 0 {
3095+
return nil, fmt.Errorf("at least one parent transaction is " +
3096+
"required")
3097+
}
3098+
3099+
// Validate that there's a child transaction specified.
3100+
if len(req.ChildTx) == 0 {
3101+
return nil, fmt.Errorf("child transaction is required")
3102+
}
3103+
3104+
// Deserialize parent transactions.
3105+
parents := make([]*wire.MsgTx, 0, len(req.ParentTxs))
3106+
for i, parentBytes := range req.ParentTxs {
3107+
if len(parentBytes) == 0 {
3108+
return nil, fmt.Errorf("parent transaction at index "+
3109+
"%d is empty", i)
3110+
}
3111+
3112+
parentTx := wire.NewMsgTx(wire.TxVersion)
3113+
err := parentTx.Deserialize(bytes.NewReader(parentBytes))
3114+
if err != nil {
3115+
return nil, fmt.Errorf("failed to deserialize parent "+
3116+
"transaction at index %d: %w", i, err)
3117+
}
3118+
3119+
parents = append(parents, parentTx)
3120+
}
3121+
3122+
// Deserialize child transaction.
3123+
childTx := wire.NewMsgTx(wire.TxVersion)
3124+
err := childTx.Deserialize(bytes.NewReader(req.ChildTx))
3125+
if err != nil {
3126+
return nil, fmt.Errorf("failed to deserialize child "+
3127+
"transaction: %w", err)
3128+
}
3129+
3130+
// Attempt to submit the package using the underlying wallet.
3131+
submitPackageResult, err := w.cfg.Wallet.SubmitPackage(
3132+
parents, childTx,
3133+
chainfee.SatPerKWeight(req.MaxFeeRateSatPerKw),
3134+
)
3135+
if err != nil {
3136+
return nil, fmt.Errorf("failed to submit package: %w", err)
3137+
}
3138+
3139+
// Translate the wallet's Go result to the protobuf response.
3140+
protoResponse := &SubmitPackageResponse{
3141+
PackageMsg: submitPackageResult.PackageMsg,
3142+
TxResults: make(map[string]*SubmitPackageResponse_TxResult),
3143+
}
3144+
3145+
// Translate TxResults.
3146+
for wtxid, txRes := range submitPackageResult.TxResults {
3147+
protoTxRes := &SubmitPackageResponse_TxResult{
3148+
Txid: txRes.TxID.String(),
3149+
}
3150+
if txRes.OtherWtxid != nil {
3151+
protoTxRes.OtherWtxid = txRes.OtherWtxid.String()
3152+
}
3153+
if txRes.Error != nil {
3154+
protoTxRes.ErrorMessage = *txRes.Error
3155+
}
3156+
3157+
protoResponse.TxResults[wtxid] = protoTxRes
3158+
}
3159+
3160+
// Translate ReplacedTransactions.
3161+
if len(submitPackageResult.ReplacedTransactions) > 0 {
3162+
protoResponse.ReplacedTransactions = make(
3163+
[]string, 0,
3164+
len(submitPackageResult.ReplacedTransactions),
3165+
)
3166+
for _, hash := range submitPackageResult.ReplacedTransactions {
3167+
protoResponse.ReplacedTransactions = append(
3168+
protoResponse.ReplacedTransactions,
3169+
hash.String(),
3170+
)
3171+
}
3172+
}
3173+
3174+
return protoResponse, nil
3175+
}

0 commit comments

Comments
 (0)