From 290aff6e22c2460a39588cec5abdac72e421bff6 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Fri, 17 Jul 2026 08:32:14 -0500 Subject: [PATCH 1/9] rpc: Add fee estimate to RefreshVTXOs dry-run response In this commit, we add a RefreshFeeEstimate message to RefreshVTXOsResponse so a dry_run refresh can carry an advisory, itemized operator-fee preview for the selected VTXOs. Today the refresh path never surfaces the fee anywhere: the binding amount is set by the server-issued JoinRoundQuote at seal time and auto-accepted against the client's MaxOperatorFeeSat cap, so the user only learns the charge afterwards via the fee history. The per-outpoint rows carry the amount and remaining lifetime the daemon resolved for each VTXO (removing the manual amount / remaining-blocks input the standalone fees estimate command requires) plus the operator's liquidity / on-chain share / margin breakdown. Rows are deliberately NOT waiver-adjusted so their components always sum to their total; the selection-level free_refresh_eligible flag and zeroed total express the free-late-refresh waiver instead, mirroring the operator's all-or-nothing seal-time rule from lumos#675. estimate_error keeps dry_run usable as a validity probe when the operator or chain height is unavailable: the preview still returns and the caller is told the numbers are absent rather than zero. The daemon-side population of the new field lands in the next commit. Part of #986. (cherry picked from commit f5324180c37cbbc85a0affe511bd723bec3988e9) --- waverpc/daemon.pb.go | 1076 ++++++++++++++++++++++++++---------------- waverpc/daemon.proto | 86 ++++ 2 files changed, 755 insertions(+), 407 deletions(-) diff --git a/waverpc/daemon.pb.go b/waverpc/daemon.pb.go index d4c5194ed..5234a6056 100644 --- a/waverpc/daemon.pb.go +++ b/waverpc/daemon.pb.go @@ -4528,7 +4528,12 @@ type RefreshVTXOsResponse struct { // for refresh. QueuedOutpoints []string `protobuf:"bytes,1,rep,name=queued_outpoints,json=queuedOutpoints,proto3" json:"queued_outpoints,omitempty"` // status is the result: "queued" on success, "preview" on dry_run. - Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + // fee_estimate carries the advisory operator-fee preview for the + // selected VTXOs. Only populated on dry_run previews with a + // non-empty selection. The binding fee is set by the server-issued + // JoinRoundQuote at seal time and may differ from this estimate. + FeeEstimate *RefreshFeeEstimate `protobuf:"bytes,3,opt,name=fee_estimate,json=feeEstimate,proto3" json:"fee_estimate,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -4577,6 +4582,239 @@ func (x *RefreshVTXOsResponse) GetStatus() string { return "" } +func (x *RefreshVTXOsResponse) GetFeeEstimate() *RefreshFeeEstimate { + if x != nil { + return x.FeeEstimate + } + return nil +} + +// RefreshFeeEstimate is the advisory operator-fee preview attached to a +// RefreshVTXOs dry_run response. It itemizes what refreshing the +// selected VTXOs is expected to cost at the operator's current rates, +// including whether the operator's free-refresh window applies. All +// values are advisory: the binding fee is set at seal time. +type RefreshFeeEstimate struct { + state protoimpl.MessageState `protogen:"open.v1"` + // estimated_total_fee_sat is the advisory total operator fee for + // refreshing the full selection. Explicit presence: the field is + // only set when the total is meaningful — either the per-outpoint + // quotes all succeeded, or free_refresh_eligible is true (the + // waiver is computed locally, so its zero total stays valid even + // when estimate_error is set). When the estimate degrades the + // field is absent, never zero, so a missing number can not be + // misread as a free refresh. + EstimatedTotalFeeSat *int64 `protobuf:"varint,1,opt,name=estimated_total_fee_sat,json=estimatedTotalFeeSat,proto3,oneof" json:"estimated_total_fee_sat,omitempty"` + // outpoints itemizes the advisory per-outpoint quotes at the + // operator's current rates. Each row is the ordinary paid price and + // is NOT waiver-adjusted: its components always sum to the row's + // total_fee_sat, so under a free-refresh-eligible selection the rows + // show what the waiver saves. + Outpoints []*OutpointFeeEstimate `protobuf:"bytes,2,rep,name=outpoints,proto3" json:"outpoints,omitempty"` + // free_refresh_eligible is true when every selected VTXO is inside + // the operator's advertised free-refresh window, so refreshing + // exactly this selection on its own is expected to seal with all + // fee components waived. The waiver is all-or-nothing per round: + // batching this selection with boarding, leave, or out-of-window + // inputs forfeits it. + FreeRefreshEligible bool `protobuf:"varint,3,opt,name=free_refresh_eligible,json=freeRefreshEligible,proto3" json:"free_refresh_eligible,omitempty"` + // estimate_error is non-empty when the per-outpoint operator quotes + // could not be fetched (operator unreachable, chain height + // unavailable). The preview is still returned so dry_run keeps + // working as a validity probe; when set, treat the per-outpoint fee + // components and (unless free_refresh_eligible) the total as absent + // rather than zero. + EstimateError string `protobuf:"bytes,4,opt,name=estimate_error,json=estimateError,proto3" json:"estimate_error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RefreshFeeEstimate) Reset() { + *x = RefreshFeeEstimate{} + mi := &file_daemon_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RefreshFeeEstimate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RefreshFeeEstimate) ProtoMessage() {} + +func (x *RefreshFeeEstimate) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[52] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RefreshFeeEstimate.ProtoReflect.Descriptor instead. +func (*RefreshFeeEstimate) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{52} +} + +func (x *RefreshFeeEstimate) GetEstimatedTotalFeeSat() int64 { + if x != nil && x.EstimatedTotalFeeSat != nil { + return *x.EstimatedTotalFeeSat + } + return 0 +} + +func (x *RefreshFeeEstimate) GetOutpoints() []*OutpointFeeEstimate { + if x != nil { + return x.Outpoints + } + return nil +} + +func (x *RefreshFeeEstimate) GetFreeRefreshEligible() bool { + if x != nil { + return x.FreeRefreshEligible + } + return false +} + +func (x *RefreshFeeEstimate) GetEstimateError() string { + if x != nil { + return x.EstimateError + } + return "" +} + +// OutpointFeeEstimate is one selected VTXO's advisory refresh-fee +// quote, resolved from the daemon's own view of the VTXO (amount and +// remaining lifetime) so callers do not have to supply either. +type OutpointFeeEstimate struct { + state protoimpl.MessageState `protogen:"open.v1"` + // outpoint identifies the VTXO in "txid:index" format. + Outpoint string `protobuf:"bytes,1,opt,name=outpoint,proto3" json:"outpoint,omitempty"` + // amount_sat is the VTXO amount the quote is for. + AmountSat int64 `protobuf:"varint,2,opt,name=amount_sat,json=amountSat,proto3" json:"amount_sat,omitempty"` + // remaining_blocks is the remaining VTXO lifetime used for the + // quote: batch expiry minus the current chain height. An expired or + // same-block-expiring VTXO is clamped to 1 so the quote never falls + // back to the operator's full-lifetime default for a zero value. + RemainingBlocks uint32 `protobuf:"varint,3,opt,name=remaining_blocks,json=remainingBlocks,proto3" json:"remaining_blocks,omitempty"` + // liquidity_fee_sat is the time-value-of-money component. + LiquidityFeeSat int64 `protobuf:"varint,4,opt,name=liquidity_fee_sat,json=liquidityFeeSat,proto3" json:"liquidity_fee_sat,omitempty"` + // onchain_share_sat is the per-participant on-chain cost share. + OnchainShareSat int64 `protobuf:"varint,5,opt,name=onchain_share_sat,json=onchainShareSat,proto3" json:"onchain_share_sat,omitempty"` + // margin_sat is the fixed operator margin. + MarginSat int64 `protobuf:"varint,6,opt,name=margin_sat,json=marginSat,proto3" json:"margin_sat,omitempty"` + // total_fee_sat is the sum of the components: the ordinary paid + // price for refreshing this VTXO, before any free-refresh waiver. + TotalFeeSat int64 `protobuf:"varint,7,opt,name=total_fee_sat,json=totalFeeSat,proto3" json:"total_fee_sat,omitempty"` + // in_free_refresh_window is true when this VTXO's remaining + // lifetime is inside the operator's advertised free-refresh window. + InFreeRefreshWindow bool `protobuf:"varint,8,opt,name=in_free_refresh_window,json=inFreeRefreshWindow,proto3" json:"in_free_refresh_window,omitempty"` + // below_dust_warning mirrors the operator's warning that this + // VTXO's amount is below the minimum economically viable VTXO at + // current rates: refreshing it costs fees on a coin that already + // costs more to exit than it holds. + BelowDustWarning bool `protobuf:"varint,9,opt,name=below_dust_warning,json=belowDustWarning,proto3" json:"below_dust_warning,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OutpointFeeEstimate) Reset() { + *x = OutpointFeeEstimate{} + mi := &file_daemon_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OutpointFeeEstimate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OutpointFeeEstimate) ProtoMessage() {} + +func (x *OutpointFeeEstimate) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[53] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OutpointFeeEstimate.ProtoReflect.Descriptor instead. +func (*OutpointFeeEstimate) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{53} +} + +func (x *OutpointFeeEstimate) GetOutpoint() string { + if x != nil { + return x.Outpoint + } + return "" +} + +func (x *OutpointFeeEstimate) GetAmountSat() int64 { + if x != nil { + return x.AmountSat + } + return 0 +} + +func (x *OutpointFeeEstimate) GetRemainingBlocks() uint32 { + if x != nil { + return x.RemainingBlocks + } + return 0 +} + +func (x *OutpointFeeEstimate) GetLiquidityFeeSat() int64 { + if x != nil { + return x.LiquidityFeeSat + } + return 0 +} + +func (x *OutpointFeeEstimate) GetOnchainShareSat() int64 { + if x != nil { + return x.OnchainShareSat + } + return 0 +} + +func (x *OutpointFeeEstimate) GetMarginSat() int64 { + if x != nil { + return x.MarginSat + } + return 0 +} + +func (x *OutpointFeeEstimate) GetTotalFeeSat() int64 { + if x != nil { + return x.TotalFeeSat + } + return 0 +} + +func (x *OutpointFeeEstimate) GetInFreeRefreshWindow() bool { + if x != nil { + return x.InFreeRefreshWindow + } + return false +} + +func (x *OutpointFeeEstimate) GetBelowDustWarning() bool { + if x != nil { + return x.BelowDustWarning + } + return false +} + // CustomRefreshVTXOInput specifies one custom-policy VTXO input to refresh in // a round. The input can be outside the wallet-managed live VTXO set. type CustomRefreshVTXOInput struct { @@ -4611,7 +4849,7 @@ type CustomRefreshVTXOInput struct { func (x *CustomRefreshVTXOInput) Reset() { *x = CustomRefreshVTXOInput{} - mi := &file_daemon_proto_msgTypes[52] + mi := &file_daemon_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4623,7 +4861,7 @@ func (x *CustomRefreshVTXOInput) String() string { func (*CustomRefreshVTXOInput) ProtoMessage() {} func (x *CustomRefreshVTXOInput) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[52] + mi := &file_daemon_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4636,7 +4874,7 @@ func (x *CustomRefreshVTXOInput) ProtoReflect() protoreflect.Message { // Deprecated: Use CustomRefreshVTXOInput.ProtoReflect.Descriptor instead. func (*CustomRefreshVTXOInput) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{52} + return file_daemon_proto_rawDescGZIP(), []int{54} } func (x *CustomRefreshVTXOInput) GetOutpoint() string { @@ -4710,7 +4948,7 @@ type CustomRefreshVTXOOutput struct { func (x *CustomRefreshVTXOOutput) Reset() { *x = CustomRefreshVTXOOutput{} - mi := &file_daemon_proto_msgTypes[53] + mi := &file_daemon_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4722,7 +4960,7 @@ func (x *CustomRefreshVTXOOutput) String() string { func (*CustomRefreshVTXOOutput) ProtoMessage() {} func (x *CustomRefreshVTXOOutput) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[53] + mi := &file_daemon_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4735,7 +4973,7 @@ func (x *CustomRefreshVTXOOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use CustomRefreshVTXOOutput.ProtoReflect.Descriptor instead. func (*CustomRefreshVTXOOutput) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{53} + return file_daemon_proto_rawDescGZIP(), []int{55} } func (x *CustomRefreshVTXOOutput) GetAmountSat() int64 { @@ -4782,7 +5020,7 @@ type RefreshCustomVTXOsRequest struct { func (x *RefreshCustomVTXOsRequest) Reset() { *x = RefreshCustomVTXOsRequest{} - mi := &file_daemon_proto_msgTypes[54] + mi := &file_daemon_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4794,7 +5032,7 @@ func (x *RefreshCustomVTXOsRequest) String() string { func (*RefreshCustomVTXOsRequest) ProtoMessage() {} func (x *RefreshCustomVTXOsRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[54] + mi := &file_daemon_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4807,7 +5045,7 @@ func (x *RefreshCustomVTXOsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RefreshCustomVTXOsRequest.ProtoReflect.Descriptor instead. func (*RefreshCustomVTXOsRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{54} + return file_daemon_proto_rawDescGZIP(), []int{56} } func (x *RefreshCustomVTXOsRequest) GetInputs() []*CustomRefreshVTXOInput { @@ -4843,7 +5081,7 @@ type RefreshCustomVTXOsResponse struct { func (x *RefreshCustomVTXOsResponse) Reset() { *x = RefreshCustomVTXOsResponse{} - mi := &file_daemon_proto_msgTypes[55] + mi := &file_daemon_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4855,7 +5093,7 @@ func (x *RefreshCustomVTXOsResponse) String() string { func (*RefreshCustomVTXOsResponse) ProtoMessage() {} func (x *RefreshCustomVTXOsResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[55] + mi := &file_daemon_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4868,7 +5106,7 @@ func (x *RefreshCustomVTXOsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RefreshCustomVTXOsResponse.ProtoReflect.Descriptor instead. func (*RefreshCustomVTXOsResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{55} + return file_daemon_proto_rawDescGZIP(), []int{57} } func (x *RefreshCustomVTXOsResponse) GetQueuedOutpoints() []string { @@ -4937,7 +5175,7 @@ type PendingForfeitParticipantSignatureRequest struct { func (x *PendingForfeitParticipantSignatureRequest) Reset() { *x = PendingForfeitParticipantSignatureRequest{} - mi := &file_daemon_proto_msgTypes[56] + mi := &file_daemon_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4949,7 +5187,7 @@ func (x *PendingForfeitParticipantSignatureRequest) String() string { func (*PendingForfeitParticipantSignatureRequest) ProtoMessage() {} func (x *PendingForfeitParticipantSignatureRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[56] + mi := &file_daemon_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4962,7 +5200,7 @@ func (x *PendingForfeitParticipantSignatureRequest) ProtoReflect() protoreflect. // Deprecated: Use PendingForfeitParticipantSignatureRequest.ProtoReflect.Descriptor instead. func (*PendingForfeitParticipantSignatureRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{56} + return file_daemon_proto_rawDescGZIP(), []int{58} } func (x *PendingForfeitParticipantSignatureRequest) GetRequestId() []byte { @@ -5076,7 +5314,7 @@ type ListPendingForfeitParticipantSignatureRequestsRequest struct { func (x *ListPendingForfeitParticipantSignatureRequestsRequest) Reset() { *x = ListPendingForfeitParticipantSignatureRequestsRequest{} - mi := &file_daemon_proto_msgTypes[57] + mi := &file_daemon_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5088,7 +5326,7 @@ func (x *ListPendingForfeitParticipantSignatureRequestsRequest) String() string func (*ListPendingForfeitParticipantSignatureRequestsRequest) ProtoMessage() {} func (x *ListPendingForfeitParticipantSignatureRequestsRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[57] + mi := &file_daemon_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5101,7 +5339,7 @@ func (x *ListPendingForfeitParticipantSignatureRequestsRequest) ProtoReflect() p // Deprecated: Use ListPendingForfeitParticipantSignatureRequestsRequest.ProtoReflect.Descriptor instead. func (*ListPendingForfeitParticipantSignatureRequestsRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{57} + return file_daemon_proto_rawDescGZIP(), []int{59} } func (x *ListPendingForfeitParticipantSignatureRequestsRequest) GetAfterSequence() uint64 { @@ -5128,7 +5366,7 @@ type ListPendingForfeitParticipantSignatureRequestsResponse struct { func (x *ListPendingForfeitParticipantSignatureRequestsResponse) Reset() { *x = ListPendingForfeitParticipantSignatureRequestsResponse{} - mi := &file_daemon_proto_msgTypes[58] + mi := &file_daemon_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5140,7 +5378,7 @@ func (x *ListPendingForfeitParticipantSignatureRequestsResponse) String() string func (*ListPendingForfeitParticipantSignatureRequestsResponse) ProtoMessage() {} func (x *ListPendingForfeitParticipantSignatureRequestsResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[58] + mi := &file_daemon_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5153,7 +5391,7 @@ func (x *ListPendingForfeitParticipantSignatureRequestsResponse) ProtoReflect() // Deprecated: Use ListPendingForfeitParticipantSignatureRequestsResponse.ProtoReflect.Descriptor instead. func (*ListPendingForfeitParticipantSignatureRequestsResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{58} + return file_daemon_proto_rawDescGZIP(), []int{60} } func (x *ListPendingForfeitParticipantSignatureRequestsResponse) GetRequests() []*PendingForfeitParticipantSignatureRequest { @@ -5185,7 +5423,7 @@ type ForfeitParticipantSignature struct { func (x *ForfeitParticipantSignature) Reset() { *x = ForfeitParticipantSignature{} - mi := &file_daemon_proto_msgTypes[59] + mi := &file_daemon_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5197,7 +5435,7 @@ func (x *ForfeitParticipantSignature) String() string { func (*ForfeitParticipantSignature) ProtoMessage() {} func (x *ForfeitParticipantSignature) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[59] + mi := &file_daemon_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5210,7 +5448,7 @@ func (x *ForfeitParticipantSignature) ProtoReflect() protoreflect.Message { // Deprecated: Use ForfeitParticipantSignature.ProtoReflect.Descriptor instead. func (*ForfeitParticipantSignature) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{59} + return file_daemon_proto_rawDescGZIP(), []int{61} } func (x *ForfeitParticipantSignature) GetPubkey() []byte { @@ -5244,7 +5482,7 @@ type SubmitForfeitParticipantSignaturesRequest struct { func (x *SubmitForfeitParticipantSignaturesRequest) Reset() { *x = SubmitForfeitParticipantSignaturesRequest{} - mi := &file_daemon_proto_msgTypes[60] + mi := &file_daemon_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5256,7 +5494,7 @@ func (x *SubmitForfeitParticipantSignaturesRequest) String() string { func (*SubmitForfeitParticipantSignaturesRequest) ProtoMessage() {} func (x *SubmitForfeitParticipantSignaturesRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[60] + mi := &file_daemon_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5269,7 +5507,7 @@ func (x *SubmitForfeitParticipantSignaturesRequest) ProtoReflect() protoreflect. // Deprecated: Use SubmitForfeitParticipantSignaturesRequest.ProtoReflect.Descriptor instead. func (*SubmitForfeitParticipantSignaturesRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{60} + return file_daemon_proto_rawDescGZIP(), []int{62} } func (x *SubmitForfeitParticipantSignaturesRequest) GetRequestId() []byte { @@ -5294,7 +5532,7 @@ type SubmitForfeitParticipantSignaturesResponse struct { func (x *SubmitForfeitParticipantSignaturesResponse) Reset() { *x = SubmitForfeitParticipantSignaturesResponse{} - mi := &file_daemon_proto_msgTypes[61] + mi := &file_daemon_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5306,7 +5544,7 @@ func (x *SubmitForfeitParticipantSignaturesResponse) String() string { func (*SubmitForfeitParticipantSignaturesResponse) ProtoMessage() {} func (x *SubmitForfeitParticipantSignaturesResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[61] + mi := &file_daemon_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5319,7 +5557,7 @@ func (x *SubmitForfeitParticipantSignaturesResponse) ProtoReflect() protoreflect // Deprecated: Use SubmitForfeitParticipantSignaturesResponse.ProtoReflect.Descriptor instead. func (*SubmitForfeitParticipantSignaturesResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{61} + return file_daemon_proto_rawDescGZIP(), []int{63} } // LeaveDestination describes where a single leave output should land. @@ -5339,7 +5577,7 @@ type LeaveDestination struct { func (x *LeaveDestination) Reset() { *x = LeaveDestination{} - mi := &file_daemon_proto_msgTypes[62] + mi := &file_daemon_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5351,7 +5589,7 @@ func (x *LeaveDestination) String() string { func (*LeaveDestination) ProtoMessage() {} func (x *LeaveDestination) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[62] + mi := &file_daemon_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5364,7 +5602,7 @@ func (x *LeaveDestination) ProtoReflect() protoreflect.Message { // Deprecated: Use LeaveDestination.ProtoReflect.Descriptor instead. func (*LeaveDestination) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{62} + return file_daemon_proto_rawDescGZIP(), []int{64} } func (x *LeaveDestination) GetTarget() isLeaveDestination_Target { @@ -5445,7 +5683,7 @@ type LeaveVTXOsRequest struct { func (x *LeaveVTXOsRequest) Reset() { *x = LeaveVTXOsRequest{} - mi := &file_daemon_proto_msgTypes[63] + mi := &file_daemon_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5457,7 +5695,7 @@ func (x *LeaveVTXOsRequest) String() string { func (*LeaveVTXOsRequest) ProtoMessage() {} func (x *LeaveVTXOsRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[63] + mi := &file_daemon_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5470,7 +5708,7 @@ func (x *LeaveVTXOsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LeaveVTXOsRequest.ProtoReflect.Descriptor instead. func (*LeaveVTXOsRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{63} + return file_daemon_proto_rawDescGZIP(), []int{65} } func (x *LeaveVTXOsRequest) GetSelection() isLeaveVTXOsRequest_Selection { @@ -5553,7 +5791,7 @@ type LeaveVTXOsResponse struct { func (x *LeaveVTXOsResponse) Reset() { *x = LeaveVTXOsResponse{} - mi := &file_daemon_proto_msgTypes[64] + mi := &file_daemon_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5565,7 +5803,7 @@ func (x *LeaveVTXOsResponse) String() string { func (*LeaveVTXOsResponse) ProtoMessage() {} func (x *LeaveVTXOsResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[64] + mi := &file_daemon_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5578,7 +5816,7 @@ func (x *LeaveVTXOsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LeaveVTXOsResponse.ProtoReflect.Descriptor instead. func (*LeaveVTXOsResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{64} + return file_daemon_proto_rawDescGZIP(), []int{66} } func (x *LeaveVTXOsResponse) GetQueuedOutpoints() []string { @@ -5616,7 +5854,7 @@ type SendOnChainRequest struct { func (x *SendOnChainRequest) Reset() { *x = SendOnChainRequest{} - mi := &file_daemon_proto_msgTypes[65] + mi := &file_daemon_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5628,7 +5866,7 @@ func (x *SendOnChainRequest) String() string { func (*SendOnChainRequest) ProtoMessage() {} func (x *SendOnChainRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[65] + mi := &file_daemon_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5641,7 +5879,7 @@ func (x *SendOnChainRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SendOnChainRequest.ProtoReflect.Descriptor instead. func (*SendOnChainRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{65} + return file_daemon_proto_rawDescGZIP(), []int{67} } func (x *SendOnChainRequest) GetDestination() *LeaveDestination { @@ -5749,7 +5987,7 @@ type SendOnChainResponse struct { func (x *SendOnChainResponse) Reset() { *x = SendOnChainResponse{} - mi := &file_daemon_proto_msgTypes[66] + mi := &file_daemon_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5761,7 +5999,7 @@ func (x *SendOnChainResponse) String() string { func (*SendOnChainResponse) ProtoMessage() {} func (x *SendOnChainResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[66] + mi := &file_daemon_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5774,7 +6012,7 @@ func (x *SendOnChainResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SendOnChainResponse.ProtoReflect.Descriptor instead. func (*SendOnChainResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{66} + return file_daemon_proto_rawDescGZIP(), []int{68} } func (x *SendOnChainResponse) GetActualAmountSat() int64 { @@ -5840,7 +6078,7 @@ type BoardRequest struct { func (x *BoardRequest) Reset() { *x = BoardRequest{} - mi := &file_daemon_proto_msgTypes[67] + mi := &file_daemon_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5852,7 +6090,7 @@ func (x *BoardRequest) String() string { func (*BoardRequest) ProtoMessage() {} func (x *BoardRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[67] + mi := &file_daemon_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5865,7 +6103,7 @@ func (x *BoardRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use BoardRequest.ProtoReflect.Descriptor instead. func (*BoardRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{67} + return file_daemon_proto_rawDescGZIP(), []int{69} } func (x *BoardRequest) GetTargetVtxoCount() uint32 { @@ -5896,7 +6134,7 @@ type BoardResponse struct { func (x *BoardResponse) Reset() { *x = BoardResponse{} - mi := &file_daemon_proto_msgTypes[68] + mi := &file_daemon_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5908,7 +6146,7 @@ func (x *BoardResponse) String() string { func (*BoardResponse) ProtoMessage() {} func (x *BoardResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[68] + mi := &file_daemon_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5921,7 +6159,7 @@ func (x *BoardResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use BoardResponse.ProtoReflect.Descriptor instead. func (*BoardResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{68} + return file_daemon_proto_rawDescGZIP(), []int{70} } func (x *BoardResponse) GetStatus() string { @@ -5946,7 +6184,7 @@ type JoinNextRoundRequest struct { func (x *JoinNextRoundRequest) Reset() { *x = JoinNextRoundRequest{} - mi := &file_daemon_proto_msgTypes[69] + mi := &file_daemon_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5958,7 +6196,7 @@ func (x *JoinNextRoundRequest) String() string { func (*JoinNextRoundRequest) ProtoMessage() {} func (x *JoinNextRoundRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[69] + mi := &file_daemon_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5971,7 +6209,7 @@ func (x *JoinNextRoundRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use JoinNextRoundRequest.ProtoReflect.Descriptor instead. func (*JoinNextRoundRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{69} + return file_daemon_proto_rawDescGZIP(), []int{71} } type JoinNextRoundResponse struct { @@ -5986,7 +6224,7 @@ type JoinNextRoundResponse struct { func (x *JoinNextRoundResponse) Reset() { *x = JoinNextRoundResponse{} - mi := &file_daemon_proto_msgTypes[70] + mi := &file_daemon_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5998,7 +6236,7 @@ func (x *JoinNextRoundResponse) String() string { func (*JoinNextRoundResponse) ProtoMessage() {} func (x *JoinNextRoundResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[70] + mi := &file_daemon_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6011,7 +6249,7 @@ func (x *JoinNextRoundResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use JoinNextRoundResponse.ProtoReflect.Descriptor instead. func (*JoinNextRoundResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{70} + return file_daemon_proto_rawDescGZIP(), []int{72} } func (x *JoinNextRoundResponse) GetStatus() string { @@ -6047,7 +6285,7 @@ type SweepBoardingUTXOsRequest struct { func (x *SweepBoardingUTXOsRequest) Reset() { *x = SweepBoardingUTXOsRequest{} - mi := &file_daemon_proto_msgTypes[71] + mi := &file_daemon_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6059,7 +6297,7 @@ func (x *SweepBoardingUTXOsRequest) String() string { func (*SweepBoardingUTXOsRequest) ProtoMessage() {} func (x *SweepBoardingUTXOsRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[71] + mi := &file_daemon_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6072,7 +6310,7 @@ func (x *SweepBoardingUTXOsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SweepBoardingUTXOsRequest.ProtoReflect.Descriptor instead. func (*SweepBoardingUTXOsRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{71} + return file_daemon_proto_rawDescGZIP(), []int{73} } func (x *SweepBoardingUTXOsRequest) GetOutpoints() []string { @@ -6125,7 +6363,7 @@ type BoardingSweepOutput struct { func (x *BoardingSweepOutput) Reset() { *x = BoardingSweepOutput{} - mi := &file_daemon_proto_msgTypes[72] + mi := &file_daemon_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6137,7 +6375,7 @@ func (x *BoardingSweepOutput) String() string { func (*BoardingSweepOutput) ProtoMessage() {} func (x *BoardingSweepOutput) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[72] + mi := &file_daemon_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6150,7 +6388,7 @@ func (x *BoardingSweepOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use BoardingSweepOutput.ProtoReflect.Descriptor instead. func (*BoardingSweepOutput) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{72} + return file_daemon_proto_rawDescGZIP(), []int{74} } func (x *BoardingSweepOutput) GetOutpoint() string { @@ -6216,7 +6454,7 @@ type SweepBoardingUTXOsResponse struct { func (x *SweepBoardingUTXOsResponse) Reset() { *x = SweepBoardingUTXOsResponse{} - mi := &file_daemon_proto_msgTypes[73] + mi := &file_daemon_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6228,7 +6466,7 @@ func (x *SweepBoardingUTXOsResponse) String() string { func (*SweepBoardingUTXOsResponse) ProtoMessage() {} func (x *SweepBoardingUTXOsResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[73] + mi := &file_daemon_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6241,7 +6479,7 @@ func (x *SweepBoardingUTXOsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SweepBoardingUTXOsResponse.ProtoReflect.Descriptor instead. func (*SweepBoardingUTXOsResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{73} + return file_daemon_proto_rawDescGZIP(), []int{75} } func (x *SweepBoardingUTXOsResponse) GetStatus() string { @@ -6345,7 +6583,7 @@ type ListBoardingSweepsRequest struct { func (x *ListBoardingSweepsRequest) Reset() { *x = ListBoardingSweepsRequest{} - mi := &file_daemon_proto_msgTypes[74] + mi := &file_daemon_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6357,7 +6595,7 @@ func (x *ListBoardingSweepsRequest) String() string { func (*ListBoardingSweepsRequest) ProtoMessage() {} func (x *ListBoardingSweepsRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[74] + mi := &file_daemon_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6370,7 +6608,7 @@ func (x *ListBoardingSweepsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListBoardingSweepsRequest.ProtoReflect.Descriptor instead. func (*ListBoardingSweepsRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{74} + return file_daemon_proto_rawDescGZIP(), []int{76} } func (x *ListBoardingSweepsRequest) GetStatus() string { @@ -6413,7 +6651,7 @@ type BoardingSweepInput struct { func (x *BoardingSweepInput) Reset() { *x = BoardingSweepInput{} - mi := &file_daemon_proto_msgTypes[75] + mi := &file_daemon_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6425,7 +6663,7 @@ func (x *BoardingSweepInput) String() string { func (*BoardingSweepInput) ProtoMessage() {} func (x *BoardingSweepInput) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[75] + mi := &file_daemon_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6438,7 +6676,7 @@ func (x *BoardingSweepInput) ProtoReflect() protoreflect.Message { // Deprecated: Use BoardingSweepInput.ProtoReflect.Descriptor instead. func (*BoardingSweepInput) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{75} + return file_daemon_proto_rawDescGZIP(), []int{77} } func (x *BoardingSweepInput) GetOutpoint() string { @@ -6508,7 +6746,7 @@ type BoardingSweep struct { func (x *BoardingSweep) Reset() { *x = BoardingSweep{} - mi := &file_daemon_proto_msgTypes[76] + mi := &file_daemon_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6520,7 +6758,7 @@ func (x *BoardingSweep) String() string { func (*BoardingSweep) ProtoMessage() {} func (x *BoardingSweep) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[76] + mi := &file_daemon_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6533,7 +6771,7 @@ func (x *BoardingSweep) ProtoReflect() protoreflect.Message { // Deprecated: Use BoardingSweep.ProtoReflect.Descriptor instead. func (*BoardingSweep) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{76} + return file_daemon_proto_rawDescGZIP(), []int{78} } func (x *BoardingSweep) GetTxid() string { @@ -6625,7 +6863,7 @@ type ListBoardingSweepsResponse struct { func (x *ListBoardingSweepsResponse) Reset() { *x = ListBoardingSweepsResponse{} - mi := &file_daemon_proto_msgTypes[77] + mi := &file_daemon_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6637,7 +6875,7 @@ func (x *ListBoardingSweepsResponse) String() string { func (*ListBoardingSweepsResponse) ProtoMessage() {} func (x *ListBoardingSweepsResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[77] + mi := &file_daemon_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6650,7 +6888,7 @@ func (x *ListBoardingSweepsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListBoardingSweepsResponse.ProtoReflect.Descriptor instead. func (*ListBoardingSweepsResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{77} + return file_daemon_proto_rawDescGZIP(), []int{79} } func (x *ListBoardingSweepsResponse) GetSweeps() []*BoardingSweep { @@ -6680,7 +6918,7 @@ type RoundVTXOInfo struct { func (x *RoundVTXOInfo) Reset() { *x = RoundVTXOInfo{} - mi := &file_daemon_proto_msgTypes[78] + mi := &file_daemon_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6692,7 +6930,7 @@ func (x *RoundVTXOInfo) String() string { func (*RoundVTXOInfo) ProtoMessage() {} func (x *RoundVTXOInfo) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[78] + mi := &file_daemon_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6705,7 +6943,7 @@ func (x *RoundVTXOInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use RoundVTXOInfo.ProtoReflect.Descriptor instead. func (*RoundVTXOInfo) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{78} + return file_daemon_proto_rawDescGZIP(), []int{80} } func (x *RoundVTXOInfo) GetOutpoint() string { @@ -6773,7 +7011,7 @@ type RoundInfo struct { func (x *RoundInfo) Reset() { *x = RoundInfo{} - mi := &file_daemon_proto_msgTypes[79] + mi := &file_daemon_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6785,7 +7023,7 @@ func (x *RoundInfo) String() string { func (*RoundInfo) ProtoMessage() {} func (x *RoundInfo) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[79] + mi := &file_daemon_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6798,7 +7036,7 @@ func (x *RoundInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use RoundInfo.ProtoReflect.Descriptor instead. func (*RoundInfo) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{79} + return file_daemon_proto_rawDescGZIP(), []int{81} } func (x *RoundInfo) GetRoundId() string { @@ -6910,7 +7148,7 @@ type ListRoundsRequest struct { func (x *ListRoundsRequest) Reset() { *x = ListRoundsRequest{} - mi := &file_daemon_proto_msgTypes[80] + mi := &file_daemon_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6922,7 +7160,7 @@ func (x *ListRoundsRequest) String() string { func (*ListRoundsRequest) ProtoMessage() {} func (x *ListRoundsRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[80] + mi := &file_daemon_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6935,7 +7173,7 @@ func (x *ListRoundsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoundsRequest.ProtoReflect.Descriptor instead. func (*ListRoundsRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{80} + return file_daemon_proto_rawDescGZIP(), []int{82} } func (x *ListRoundsRequest) GetPageSize() int32 { @@ -6990,7 +7228,7 @@ type GetRoundRequest struct { func (x *GetRoundRequest) Reset() { *x = GetRoundRequest{} - mi := &file_daemon_proto_msgTypes[81] + mi := &file_daemon_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7002,7 +7240,7 @@ func (x *GetRoundRequest) String() string { func (*GetRoundRequest) ProtoMessage() {} func (x *GetRoundRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[81] + mi := &file_daemon_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7015,7 +7253,7 @@ func (x *GetRoundRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetRoundRequest.ProtoReflect.Descriptor instead. func (*GetRoundRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{81} + return file_daemon_proto_rawDescGZIP(), []int{83} } func (x *GetRoundRequest) GetRoundId() string { @@ -7035,7 +7273,7 @@ type GetRoundResponse struct { func (x *GetRoundResponse) Reset() { *x = GetRoundResponse{} - mi := &file_daemon_proto_msgTypes[82] + mi := &file_daemon_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7047,7 +7285,7 @@ func (x *GetRoundResponse) String() string { func (*GetRoundResponse) ProtoMessage() {} func (x *GetRoundResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[82] + mi := &file_daemon_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7060,7 +7298,7 @@ func (x *GetRoundResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetRoundResponse.ProtoReflect.Descriptor instead. func (*GetRoundResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{82} + return file_daemon_proto_rawDescGZIP(), []int{84} } func (x *GetRoundResponse) GetRound() *RoundInfo { @@ -7083,7 +7321,7 @@ type ListRoundsResponse struct { func (x *ListRoundsResponse) Reset() { *x = ListRoundsResponse{} - mi := &file_daemon_proto_msgTypes[83] + mi := &file_daemon_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7095,7 +7333,7 @@ func (x *ListRoundsResponse) String() string { func (*ListRoundsResponse) ProtoMessage() {} func (x *ListRoundsResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[83] + mi := &file_daemon_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7108,7 +7346,7 @@ func (x *ListRoundsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoundsResponse.ProtoReflect.Descriptor instead. func (*ListRoundsResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{83} + return file_daemon_proto_rawDescGZIP(), []int{85} } func (x *ListRoundsResponse) GetRounds() []*RoundInfo { @@ -7133,7 +7371,7 @@ type WatchRoundsRequest struct { func (x *WatchRoundsRequest) Reset() { *x = WatchRoundsRequest{} - mi := &file_daemon_proto_msgTypes[84] + mi := &file_daemon_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7145,7 +7383,7 @@ func (x *WatchRoundsRequest) String() string { func (*WatchRoundsRequest) ProtoMessage() {} func (x *WatchRoundsRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[84] + mi := &file_daemon_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7158,7 +7396,7 @@ func (x *WatchRoundsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchRoundsRequest.ProtoReflect.Descriptor instead. func (*WatchRoundsRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{84} + return file_daemon_proto_rawDescGZIP(), []int{86} } type WatchRoundsResponse struct { @@ -7172,7 +7410,7 @@ type WatchRoundsResponse struct { func (x *WatchRoundsResponse) Reset() { *x = WatchRoundsResponse{} - mi := &file_daemon_proto_msgTypes[85] + mi := &file_daemon_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7184,7 +7422,7 @@ func (x *WatchRoundsResponse) String() string { func (*WatchRoundsResponse) ProtoMessage() {} func (x *WatchRoundsResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[85] + mi := &file_daemon_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7197,7 +7435,7 @@ func (x *WatchRoundsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchRoundsResponse.ProtoReflect.Descriptor instead. func (*WatchRoundsResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{85} + return file_daemon_proto_rawDescGZIP(), []int{87} } func (x *WatchRoundsResponse) GetRound() *RoundInfo { @@ -7235,7 +7473,7 @@ type OORSessionInfo struct { func (x *OORSessionInfo) Reset() { *x = OORSessionInfo{} - mi := &file_daemon_proto_msgTypes[86] + mi := &file_daemon_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7247,7 +7485,7 @@ func (x *OORSessionInfo) String() string { func (*OORSessionInfo) ProtoMessage() {} func (x *OORSessionInfo) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[86] + mi := &file_daemon_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7260,7 +7498,7 @@ func (x *OORSessionInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use OORSessionInfo.ProtoReflect.Descriptor instead. func (*OORSessionInfo) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{86} + return file_daemon_proto_rawDescGZIP(), []int{88} } func (x *OORSessionInfo) GetSessionId() string { @@ -7344,7 +7582,7 @@ type ListOORSessionsRequest struct { func (x *ListOORSessionsRequest) Reset() { *x = ListOORSessionsRequest{} - mi := &file_daemon_proto_msgTypes[87] + mi := &file_daemon_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7356,7 +7594,7 @@ func (x *ListOORSessionsRequest) String() string { func (*ListOORSessionsRequest) ProtoMessage() {} func (x *ListOORSessionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[87] + mi := &file_daemon_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7369,7 +7607,7 @@ func (x *ListOORSessionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListOORSessionsRequest.ProtoReflect.Descriptor instead. func (*ListOORSessionsRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{87} + return file_daemon_proto_rawDescGZIP(), []int{89} } func (x *ListOORSessionsRequest) GetPageSize() int32 { @@ -7413,7 +7651,7 @@ type ListOORSessionsResponse struct { func (x *ListOORSessionsResponse) Reset() { *x = ListOORSessionsResponse{} - mi := &file_daemon_proto_msgTypes[88] + mi := &file_daemon_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7425,7 +7663,7 @@ func (x *ListOORSessionsResponse) String() string { func (*ListOORSessionsResponse) ProtoMessage() {} func (x *ListOORSessionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[88] + mi := &file_daemon_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7438,7 +7676,7 @@ func (x *ListOORSessionsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListOORSessionsResponse.ProtoReflect.Descriptor instead. func (*ListOORSessionsResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{88} + return file_daemon_proto_rawDescGZIP(), []int{90} } func (x *ListOORSessionsResponse) GetSessions() []*OORSessionInfo { @@ -7465,7 +7703,7 @@ type GetOORSessionRequest struct { func (x *GetOORSessionRequest) Reset() { *x = GetOORSessionRequest{} - mi := &file_daemon_proto_msgTypes[89] + mi := &file_daemon_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7477,7 +7715,7 @@ func (x *GetOORSessionRequest) String() string { func (*GetOORSessionRequest) ProtoMessage() {} func (x *GetOORSessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[89] + mi := &file_daemon_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7490,7 +7728,7 @@ func (x *GetOORSessionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetOORSessionRequest.ProtoReflect.Descriptor instead. func (*GetOORSessionRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{89} + return file_daemon_proto_rawDescGZIP(), []int{91} } func (x *GetOORSessionRequest) GetSessionId() string { @@ -7510,7 +7748,7 @@ type GetOORSessionResponse struct { func (x *GetOORSessionResponse) Reset() { *x = GetOORSessionResponse{} - mi := &file_daemon_proto_msgTypes[90] + mi := &file_daemon_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7522,7 +7760,7 @@ func (x *GetOORSessionResponse) String() string { func (*GetOORSessionResponse) ProtoMessage() {} func (x *GetOORSessionResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[90] + mi := &file_daemon_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7535,7 +7773,7 @@ func (x *GetOORSessionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetOORSessionResponse.ProtoReflect.Descriptor instead. func (*GetOORSessionResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{90} + return file_daemon_proto_rawDescGZIP(), []int{92} } func (x *GetOORSessionResponse) GetSession() *OORSessionInfo { @@ -7563,7 +7801,7 @@ type EstimateFeeRequest struct { func (x *EstimateFeeRequest) Reset() { *x = EstimateFeeRequest{} - mi := &file_daemon_proto_msgTypes[91] + mi := &file_daemon_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7575,7 +7813,7 @@ func (x *EstimateFeeRequest) String() string { func (*EstimateFeeRequest) ProtoMessage() {} func (x *EstimateFeeRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[91] + mi := &file_daemon_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7588,7 +7826,7 @@ func (x *EstimateFeeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EstimateFeeRequest.ProtoReflect.Descriptor instead. func (*EstimateFeeRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{91} + return file_daemon_proto_rawDescGZIP(), []int{93} } func (x *EstimateFeeRequest) GetAmountSat() int64 { @@ -7642,7 +7880,7 @@ type EstimateFeeResponse struct { func (x *EstimateFeeResponse) Reset() { *x = EstimateFeeResponse{} - mi := &file_daemon_proto_msgTypes[92] + mi := &file_daemon_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7654,7 +7892,7 @@ func (x *EstimateFeeResponse) String() string { func (*EstimateFeeResponse) ProtoMessage() {} func (x *EstimateFeeResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[92] + mi := &file_daemon_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7667,7 +7905,7 @@ func (x *EstimateFeeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EstimateFeeResponse.ProtoReflect.Descriptor instead. func (*EstimateFeeResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{92} + return file_daemon_proto_rawDescGZIP(), []int{94} } func (x *EstimateFeeResponse) GetLiquidityFeeSat() int64 { @@ -7737,7 +7975,7 @@ type GetFeeHistoryRequest struct { func (x *GetFeeHistoryRequest) Reset() { *x = GetFeeHistoryRequest{} - mi := &file_daemon_proto_msgTypes[93] + mi := &file_daemon_proto_msgTypes[95] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7749,7 +7987,7 @@ func (x *GetFeeHistoryRequest) String() string { func (*GetFeeHistoryRequest) ProtoMessage() {} func (x *GetFeeHistoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[93] + mi := &file_daemon_proto_msgTypes[95] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7762,7 +8000,7 @@ func (x *GetFeeHistoryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFeeHistoryRequest.ProtoReflect.Descriptor instead. func (*GetFeeHistoryRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{93} + return file_daemon_proto_rawDescGZIP(), []int{95} } func (x *GetFeeHistoryRequest) GetLimit() uint32 { @@ -7837,7 +8075,7 @@ type FeeHistoryEntry struct { func (x *FeeHistoryEntry) Reset() { *x = FeeHistoryEntry{} - mi := &file_daemon_proto_msgTypes[94] + mi := &file_daemon_proto_msgTypes[96] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7849,7 +8087,7 @@ func (x *FeeHistoryEntry) String() string { func (*FeeHistoryEntry) ProtoMessage() {} func (x *FeeHistoryEntry) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[94] + mi := &file_daemon_proto_msgTypes[96] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7862,7 +8100,7 @@ func (x *FeeHistoryEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use FeeHistoryEntry.ProtoReflect.Descriptor instead. func (*FeeHistoryEntry) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{94} + return file_daemon_proto_rawDescGZIP(), []int{96} } func (x *FeeHistoryEntry) GetEntryId() int64 { @@ -7942,7 +8180,7 @@ type GetFeeHistoryResponse struct { func (x *GetFeeHistoryResponse) Reset() { *x = GetFeeHistoryResponse{} - mi := &file_daemon_proto_msgTypes[95] + mi := &file_daemon_proto_msgTypes[97] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7954,7 +8192,7 @@ func (x *GetFeeHistoryResponse) String() string { func (*GetFeeHistoryResponse) ProtoMessage() {} func (x *GetFeeHistoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[95] + mi := &file_daemon_proto_msgTypes[97] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7967,7 +8205,7 @@ func (x *GetFeeHistoryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFeeHistoryResponse.ProtoReflect.Descriptor instead. func (*GetFeeHistoryResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{95} + return file_daemon_proto_rawDescGZIP(), []int{97} } func (x *GetFeeHistoryResponse) GetEntries() []*FeeHistoryEntry { @@ -8008,7 +8246,7 @@ type ListTransactionsRequest struct { func (x *ListTransactionsRequest) Reset() { *x = ListTransactionsRequest{} - mi := &file_daemon_proto_msgTypes[96] + mi := &file_daemon_proto_msgTypes[98] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8020,7 +8258,7 @@ func (x *ListTransactionsRequest) String() string { func (*ListTransactionsRequest) ProtoMessage() {} func (x *ListTransactionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[96] + mi := &file_daemon_proto_msgTypes[98] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8033,7 +8271,7 @@ func (x *ListTransactionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListTransactionsRequest.ProtoReflect.Descriptor instead. func (*ListTransactionsRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{96} + return file_daemon_proto_rawDescGZIP(), []int{98} } func (x *ListTransactionsRequest) GetFromUnixS() int64 { @@ -8124,7 +8362,7 @@ type TransactionHistoryEntry struct { func (x *TransactionHistoryEntry) Reset() { *x = TransactionHistoryEntry{} - mi := &file_daemon_proto_msgTypes[97] + mi := &file_daemon_proto_msgTypes[99] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8136,7 +8374,7 @@ func (x *TransactionHistoryEntry) String() string { func (*TransactionHistoryEntry) ProtoMessage() {} func (x *TransactionHistoryEntry) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[97] + mi := &file_daemon_proto_msgTypes[99] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8149,7 +8387,7 @@ func (x *TransactionHistoryEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use TransactionHistoryEntry.ProtoReflect.Descriptor instead. func (*TransactionHistoryEntry) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{97} + return file_daemon_proto_rawDescGZIP(), []int{99} } func (x *TransactionHistoryEntry) GetSource() string { @@ -8286,7 +8524,7 @@ type ListTransactionsResponse struct { func (x *ListTransactionsResponse) Reset() { *x = ListTransactionsResponse{} - mi := &file_daemon_proto_msgTypes[98] + mi := &file_daemon_proto_msgTypes[100] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8298,7 +8536,7 @@ func (x *ListTransactionsResponse) String() string { func (*ListTransactionsResponse) ProtoMessage() {} func (x *ListTransactionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[98] + mi := &file_daemon_proto_msgTypes[100] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8311,7 +8549,7 @@ func (x *ListTransactionsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListTransactionsResponse.ProtoReflect.Descriptor instead. func (*ListTransactionsResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{98} + return file_daemon_proto_rawDescGZIP(), []int{100} } func (x *ListTransactionsResponse) GetTransactions() []*TransactionHistoryEntry { @@ -8346,7 +8584,7 @@ type UnrollRequest struct { func (x *UnrollRequest) Reset() { *x = UnrollRequest{} - mi := &file_daemon_proto_msgTypes[99] + mi := &file_daemon_proto_msgTypes[101] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8358,7 +8596,7 @@ func (x *UnrollRequest) String() string { func (*UnrollRequest) ProtoMessage() {} func (x *UnrollRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[99] + mi := &file_daemon_proto_msgTypes[101] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8371,7 +8609,7 @@ func (x *UnrollRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UnrollRequest.ProtoReflect.Descriptor instead. func (*UnrollRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{99} + return file_daemon_proto_rawDescGZIP(), []int{101} } func (x *UnrollRequest) GetOutpoint() string { @@ -8394,7 +8632,7 @@ type UnrollResponse struct { func (x *UnrollResponse) Reset() { *x = UnrollResponse{} - mi := &file_daemon_proto_msgTypes[100] + mi := &file_daemon_proto_msgTypes[102] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8406,7 +8644,7 @@ func (x *UnrollResponse) String() string { func (*UnrollResponse) ProtoMessage() {} func (x *UnrollResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[100] + mi := &file_daemon_proto_msgTypes[102] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8419,7 +8657,7 @@ func (x *UnrollResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UnrollResponse.ProtoReflect.Descriptor instead. func (*UnrollResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{100} + return file_daemon_proto_rawDescGZIP(), []int{102} } func (x *UnrollResponse) GetCreated() bool { @@ -8451,7 +8689,7 @@ type GetUnrollStatusRequest struct { func (x *GetUnrollStatusRequest) Reset() { *x = GetUnrollStatusRequest{} - mi := &file_daemon_proto_msgTypes[101] + mi := &file_daemon_proto_msgTypes[103] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8463,7 +8701,7 @@ func (x *GetUnrollStatusRequest) String() string { func (*GetUnrollStatusRequest) ProtoMessage() {} func (x *GetUnrollStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[101] + mi := &file_daemon_proto_msgTypes[103] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8476,7 +8714,7 @@ func (x *GetUnrollStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetUnrollStatusRequest.ProtoReflect.Descriptor instead. func (*GetUnrollStatusRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{101} + return file_daemon_proto_rawDescGZIP(), []int{103} } func (x *GetUnrollStatusRequest) GetOutpoint() string { @@ -8528,7 +8766,7 @@ type UnrollProgress struct { func (x *UnrollProgress) Reset() { *x = UnrollProgress{} - mi := &file_daemon_proto_msgTypes[102] + mi := &file_daemon_proto_msgTypes[104] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8540,7 +8778,7 @@ func (x *UnrollProgress) String() string { func (*UnrollProgress) ProtoMessage() {} func (x *UnrollProgress) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[102] + mi := &file_daemon_proto_msgTypes[104] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8553,7 +8791,7 @@ func (x *UnrollProgress) ProtoReflect() protoreflect.Message { // Deprecated: Use UnrollProgress.ProtoReflect.Descriptor instead. func (*UnrollProgress) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{102} + return file_daemon_proto_rawDescGZIP(), []int{104} } func (x *UnrollProgress) GetConfirmedTxs() uint32 { @@ -8638,7 +8876,7 @@ type UnrollCSV struct { func (x *UnrollCSV) Reset() { *x = UnrollCSV{} - mi := &file_daemon_proto_msgTypes[103] + mi := &file_daemon_proto_msgTypes[105] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8650,7 +8888,7 @@ func (x *UnrollCSV) String() string { func (*UnrollCSV) ProtoMessage() {} func (x *UnrollCSV) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[103] + mi := &file_daemon_proto_msgTypes[105] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8663,7 +8901,7 @@ func (x *UnrollCSV) ProtoReflect() protoreflect.Message { // Deprecated: Use UnrollCSV.ProtoReflect.Descriptor instead. func (*UnrollCSV) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{103} + return file_daemon_proto_rawDescGZIP(), []int{105} } func (x *UnrollCSV) GetTargetConfirmHeight() int32 { @@ -8728,7 +8966,7 @@ type UnrollFees struct { func (x *UnrollFees) Reset() { *x = UnrollFees{} - mi := &file_daemon_proto_msgTypes[104] + mi := &file_daemon_proto_msgTypes[106] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8740,7 +8978,7 @@ func (x *UnrollFees) String() string { func (*UnrollFees) ProtoMessage() {} func (x *UnrollFees) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[104] + mi := &file_daemon_proto_msgTypes[106] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8753,7 +8991,7 @@ func (x *UnrollFees) ProtoReflect() protoreflect.Message { // Deprecated: Use UnrollFees.ProtoReflect.Descriptor instead. func (*UnrollFees) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{104} + return file_daemon_proto_rawDescGZIP(), []int{106} } func (x *UnrollFees) GetCpfpFeeSat() int64 { @@ -8849,7 +9087,7 @@ type GetUnrollStatusResponse struct { func (x *GetUnrollStatusResponse) Reset() { *x = GetUnrollStatusResponse{} - mi := &file_daemon_proto_msgTypes[105] + mi := &file_daemon_proto_msgTypes[107] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8861,7 +9099,7 @@ func (x *GetUnrollStatusResponse) String() string { func (*GetUnrollStatusResponse) ProtoMessage() {} func (x *GetUnrollStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[105] + mi := &file_daemon_proto_msgTypes[107] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8874,7 +9112,7 @@ func (x *GetUnrollStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetUnrollStatusResponse.ProtoReflect.Descriptor instead. func (*GetUnrollStatusResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{105} + return file_daemon_proto_rawDescGZIP(), []int{107} } func (x *GetUnrollStatusResponse) GetFound() bool { @@ -8998,7 +9236,7 @@ type ArmVHTLCRecoveryRequest struct { func (x *ArmVHTLCRecoveryRequest) Reset() { *x = ArmVHTLCRecoveryRequest{} - mi := &file_daemon_proto_msgTypes[106] + mi := &file_daemon_proto_msgTypes[108] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9010,7 +9248,7 @@ func (x *ArmVHTLCRecoveryRequest) String() string { func (*ArmVHTLCRecoveryRequest) ProtoMessage() {} func (x *ArmVHTLCRecoveryRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[106] + mi := &file_daemon_proto_msgTypes[108] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9023,7 +9261,7 @@ func (x *ArmVHTLCRecoveryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ArmVHTLCRecoveryRequest.ProtoReflect.Descriptor instead. func (*ArmVHTLCRecoveryRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{106} + return file_daemon_proto_rawDescGZIP(), []int{108} } func (x *ArmVHTLCRecoveryRequest) GetRequestId() string { @@ -9166,7 +9404,7 @@ type ArmVHTLCRecoveryResponse struct { func (x *ArmVHTLCRecoveryResponse) Reset() { *x = ArmVHTLCRecoveryResponse{} - mi := &file_daemon_proto_msgTypes[107] + mi := &file_daemon_proto_msgTypes[109] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9178,7 +9416,7 @@ func (x *ArmVHTLCRecoveryResponse) String() string { func (*ArmVHTLCRecoveryResponse) ProtoMessage() {} func (x *ArmVHTLCRecoveryResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[107] + mi := &file_daemon_proto_msgTypes[109] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9191,7 +9429,7 @@ func (x *ArmVHTLCRecoveryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ArmVHTLCRecoveryResponse.ProtoReflect.Descriptor instead. func (*ArmVHTLCRecoveryResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{107} + return file_daemon_proto_rawDescGZIP(), []int{109} } func (x *ArmVHTLCRecoveryResponse) GetRecoveryId() string { @@ -9231,7 +9469,7 @@ type EscalateVHTLCRecoveryRequest struct { func (x *EscalateVHTLCRecoveryRequest) Reset() { *x = EscalateVHTLCRecoveryRequest{} - mi := &file_daemon_proto_msgTypes[108] + mi := &file_daemon_proto_msgTypes[110] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9243,7 +9481,7 @@ func (x *EscalateVHTLCRecoveryRequest) String() string { func (*EscalateVHTLCRecoveryRequest) ProtoMessage() {} func (x *EscalateVHTLCRecoveryRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[108] + mi := &file_daemon_proto_msgTypes[110] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9256,7 +9494,7 @@ func (x *EscalateVHTLCRecoveryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EscalateVHTLCRecoveryRequest.ProtoReflect.Descriptor instead. func (*EscalateVHTLCRecoveryRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{108} + return file_daemon_proto_rawDescGZIP(), []int{110} } func (x *EscalateVHTLCRecoveryRequest) GetRecoveryId() string { @@ -9290,7 +9528,7 @@ type EscalateVHTLCRecoveryResponse struct { func (x *EscalateVHTLCRecoveryResponse) Reset() { *x = EscalateVHTLCRecoveryResponse{} - mi := &file_daemon_proto_msgTypes[109] + mi := &file_daemon_proto_msgTypes[111] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9302,7 +9540,7 @@ func (x *EscalateVHTLCRecoveryResponse) String() string { func (*EscalateVHTLCRecoveryResponse) ProtoMessage() {} func (x *EscalateVHTLCRecoveryResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[109] + mi := &file_daemon_proto_msgTypes[111] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9315,7 +9553,7 @@ func (x *EscalateVHTLCRecoveryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EscalateVHTLCRecoveryResponse.ProtoReflect.Descriptor instead. func (*EscalateVHTLCRecoveryResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{109} + return file_daemon_proto_rawDescGZIP(), []int{111} } func (x *EscalateVHTLCRecoveryResponse) GetStatus() *VHTLCRecoveryStatus { @@ -9339,7 +9577,7 @@ type CancelVHTLCRecoveryRequest struct { func (x *CancelVHTLCRecoveryRequest) Reset() { *x = CancelVHTLCRecoveryRequest{} - mi := &file_daemon_proto_msgTypes[110] + mi := &file_daemon_proto_msgTypes[112] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9351,7 +9589,7 @@ func (x *CancelVHTLCRecoveryRequest) String() string { func (*CancelVHTLCRecoveryRequest) ProtoMessage() {} func (x *CancelVHTLCRecoveryRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[110] + mi := &file_daemon_proto_msgTypes[112] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9364,7 +9602,7 @@ func (x *CancelVHTLCRecoveryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CancelVHTLCRecoveryRequest.ProtoReflect.Descriptor instead. func (*CancelVHTLCRecoveryRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{110} + return file_daemon_proto_rawDescGZIP(), []int{112} } func (x *CancelVHTLCRecoveryRequest) GetRecoveryId() string { @@ -9398,7 +9636,7 @@ type CancelVHTLCRecoveryResponse struct { func (x *CancelVHTLCRecoveryResponse) Reset() { *x = CancelVHTLCRecoveryResponse{} - mi := &file_daemon_proto_msgTypes[111] + mi := &file_daemon_proto_msgTypes[113] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9410,7 +9648,7 @@ func (x *CancelVHTLCRecoveryResponse) String() string { func (*CancelVHTLCRecoveryResponse) ProtoMessage() {} func (x *CancelVHTLCRecoveryResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[111] + mi := &file_daemon_proto_msgTypes[113] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9423,7 +9661,7 @@ func (x *CancelVHTLCRecoveryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CancelVHTLCRecoveryResponse.ProtoReflect.Descriptor instead. func (*CancelVHTLCRecoveryResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{111} + return file_daemon_proto_rawDescGZIP(), []int{113} } func (x *CancelVHTLCRecoveryResponse) GetStatus() *VHTLCRecoveryStatus { @@ -9443,7 +9681,7 @@ type GetVHTLCRecoveryStatusRequest struct { func (x *GetVHTLCRecoveryStatusRequest) Reset() { *x = GetVHTLCRecoveryStatusRequest{} - mi := &file_daemon_proto_msgTypes[112] + mi := &file_daemon_proto_msgTypes[114] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9455,7 +9693,7 @@ func (x *GetVHTLCRecoveryStatusRequest) String() string { func (*GetVHTLCRecoveryStatusRequest) ProtoMessage() {} func (x *GetVHTLCRecoveryStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[112] + mi := &file_daemon_proto_msgTypes[114] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9468,7 +9706,7 @@ func (x *GetVHTLCRecoveryStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetVHTLCRecoveryStatusRequest.ProtoReflect.Descriptor instead. func (*GetVHTLCRecoveryStatusRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{112} + return file_daemon_proto_rawDescGZIP(), []int{114} } func (x *GetVHTLCRecoveryStatusRequest) GetRecoveryId() string { @@ -9490,7 +9728,7 @@ type GetVHTLCRecoveryStatusResponse struct { func (x *GetVHTLCRecoveryStatusResponse) Reset() { *x = GetVHTLCRecoveryStatusResponse{} - mi := &file_daemon_proto_msgTypes[113] + mi := &file_daemon_proto_msgTypes[115] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9502,7 +9740,7 @@ func (x *GetVHTLCRecoveryStatusResponse) String() string { func (*GetVHTLCRecoveryStatusResponse) ProtoMessage() {} func (x *GetVHTLCRecoveryStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[113] + mi := &file_daemon_proto_msgTypes[115] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9515,7 +9753,7 @@ func (x *GetVHTLCRecoveryStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetVHTLCRecoveryStatusResponse.ProtoReflect.Descriptor instead. func (*GetVHTLCRecoveryStatusResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{113} + return file_daemon_proto_rawDescGZIP(), []int{115} } func (x *GetVHTLCRecoveryStatusResponse) GetFound() bool { @@ -9542,7 +9780,7 @@ type ListVHTLCRecoveriesRequest struct { func (x *ListVHTLCRecoveriesRequest) Reset() { *x = ListVHTLCRecoveriesRequest{} - mi := &file_daemon_proto_msgTypes[114] + mi := &file_daemon_proto_msgTypes[116] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9554,7 +9792,7 @@ func (x *ListVHTLCRecoveriesRequest) String() string { func (*ListVHTLCRecoveriesRequest) ProtoMessage() {} func (x *ListVHTLCRecoveriesRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[114] + mi := &file_daemon_proto_msgTypes[116] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9567,7 +9805,7 @@ func (x *ListVHTLCRecoveriesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListVHTLCRecoveriesRequest.ProtoReflect.Descriptor instead. func (*ListVHTLCRecoveriesRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{114} + return file_daemon_proto_rawDescGZIP(), []int{116} } func (x *ListVHTLCRecoveriesRequest) GetIncludeTerminal() bool { @@ -9587,7 +9825,7 @@ type ListVHTLCRecoveriesResponse struct { func (x *ListVHTLCRecoveriesResponse) Reset() { *x = ListVHTLCRecoveriesResponse{} - mi := &file_daemon_proto_msgTypes[115] + mi := &file_daemon_proto_msgTypes[117] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9599,7 +9837,7 @@ func (x *ListVHTLCRecoveriesResponse) String() string { func (*ListVHTLCRecoveriesResponse) ProtoMessage() {} func (x *ListVHTLCRecoveriesResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[115] + mi := &file_daemon_proto_msgTypes[117] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9612,7 +9850,7 @@ func (x *ListVHTLCRecoveriesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListVHTLCRecoveriesResponse.ProtoReflect.Descriptor instead. func (*ListVHTLCRecoveriesResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{115} + return file_daemon_proto_rawDescGZIP(), []int{117} } func (x *ListVHTLCRecoveriesResponse) GetStatuses() []*VHTLCRecoveryStatus { @@ -9687,7 +9925,7 @@ type VHTLCRecoveryStatus struct { func (x *VHTLCRecoveryStatus) Reset() { *x = VHTLCRecoveryStatus{} - mi := &file_daemon_proto_msgTypes[116] + mi := &file_daemon_proto_msgTypes[118] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9699,7 +9937,7 @@ func (x *VHTLCRecoveryStatus) String() string { func (*VHTLCRecoveryStatus) ProtoMessage() {} func (x *VHTLCRecoveryStatus) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[116] + mi := &file_daemon_proto_msgTypes[118] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9712,7 +9950,7 @@ func (x *VHTLCRecoveryStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use VHTLCRecoveryStatus.ProtoReflect.Descriptor instead. func (*VHTLCRecoveryStatus) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{116} + return file_daemon_proto_rawDescGZIP(), []int{118} } func (x *VHTLCRecoveryStatus) GetRecoveryId() string { @@ -10150,10 +10388,29 @@ const file_daemon_proto_rawDesc = "" + "\toutpoints\x18\x01 \x01(\v2\x1a.waverpc.OutpointSelectionH\x00R\toutpoints\x12\x12\n" + "\x03all\x18\x02 \x01(\bH\x00R\x03all\x12\x17\n" + "\adry_run\x18\x03 \x01(\bR\x06dryRunB\v\n" + - "\tselection\"Y\n" + + "\tselection\"\x99\x01\n" + "\x14RefreshVTXOsResponse\x12)\n" + "\x10queued_outpoints\x18\x01 \x03(\tR\x0fqueuedOutpoints\x12\x16\n" + - "\x06status\x18\x02 \x01(\tR\x06status\"\xd0\x02\n" + + "\x06status\x18\x02 \x01(\tR\x06status\x12>\n" + + "\ffee_estimate\x18\x03 \x01(\v2\x1b.waverpc.RefreshFeeEstimateR\vfeeEstimate\"\x83\x02\n" + + "\x12RefreshFeeEstimate\x12:\n" + + "\x17estimated_total_fee_sat\x18\x01 \x01(\x03H\x00R\x14estimatedTotalFeeSat\x88\x01\x01\x12:\n" + + "\toutpoints\x18\x02 \x03(\v2\x1c.waverpc.OutpointFeeEstimateR\toutpoints\x122\n" + + "\x15free_refresh_eligible\x18\x03 \x01(\bR\x13freeRefreshEligible\x12%\n" + + "\x0eestimate_error\x18\x04 \x01(\tR\restimateErrorB\x1a\n" + + "\x18_estimated_total_fee_sat\"\xf9\x02\n" + + "\x13OutpointFeeEstimate\x12\x1a\n" + + "\boutpoint\x18\x01 \x01(\tR\boutpoint\x12\x1d\n" + + "\n" + + "amount_sat\x18\x02 \x01(\x03R\tamountSat\x12)\n" + + "\x10remaining_blocks\x18\x03 \x01(\rR\x0fremainingBlocks\x12*\n" + + "\x11liquidity_fee_sat\x18\x04 \x01(\x03R\x0fliquidityFeeSat\x12*\n" + + "\x11onchain_share_sat\x18\x05 \x01(\x03R\x0fonchainShareSat\x12\x1d\n" + + "\n" + + "margin_sat\x18\x06 \x01(\x03R\tmarginSat\x12\"\n" + + "\rtotal_fee_sat\x18\a \x01(\x03R\vtotalFeeSat\x123\n" + + "\x16in_free_refresh_window\x18\b \x01(\bR\x13inFreeRefreshWindow\x12,\n" + + "\x12below_dust_warning\x18\t \x01(\bR\x10belowDustWarning\"\xd0\x02\n" + "\x16CustomRefreshVTXOInput\x12\x1a\n" + "\boutpoint\x18\x01 \x01(\tR\boutpoint\x12\x1d\n" + "\n" + @@ -10720,7 +10977,7 @@ func file_daemon_proto_rawDescGZIP() []byte { } var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 11) -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 118) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 120) var file_daemon_proto_goTypes = []any{ (WalletState)(0), // 0: waverpc.WalletState (VTXOStatus)(0), // 1: waverpc.VTXOStatus @@ -10785,72 +11042,74 @@ var file_daemon_proto_goTypes = []any{ (*OutpointSelection)(nil), // 60: waverpc.OutpointSelection (*RefreshVTXOsRequest)(nil), // 61: waverpc.RefreshVTXOsRequest (*RefreshVTXOsResponse)(nil), // 62: waverpc.RefreshVTXOsResponse - (*CustomRefreshVTXOInput)(nil), // 63: waverpc.CustomRefreshVTXOInput - (*CustomRefreshVTXOOutput)(nil), // 64: waverpc.CustomRefreshVTXOOutput - (*RefreshCustomVTXOsRequest)(nil), // 65: waverpc.RefreshCustomVTXOsRequest - (*RefreshCustomVTXOsResponse)(nil), // 66: waverpc.RefreshCustomVTXOsResponse - (*PendingForfeitParticipantSignatureRequest)(nil), // 67: waverpc.PendingForfeitParticipantSignatureRequest - (*ListPendingForfeitParticipantSignatureRequestsRequest)(nil), // 68: waverpc.ListPendingForfeitParticipantSignatureRequestsRequest - (*ListPendingForfeitParticipantSignatureRequestsResponse)(nil), // 69: waverpc.ListPendingForfeitParticipantSignatureRequestsResponse - (*ForfeitParticipantSignature)(nil), // 70: waverpc.ForfeitParticipantSignature - (*SubmitForfeitParticipantSignaturesRequest)(nil), // 71: waverpc.SubmitForfeitParticipantSignaturesRequest - (*SubmitForfeitParticipantSignaturesResponse)(nil), // 72: waverpc.SubmitForfeitParticipantSignaturesResponse - (*LeaveDestination)(nil), // 73: waverpc.LeaveDestination - (*LeaveVTXOsRequest)(nil), // 74: waverpc.LeaveVTXOsRequest - (*LeaveVTXOsResponse)(nil), // 75: waverpc.LeaveVTXOsResponse - (*SendOnChainRequest)(nil), // 76: waverpc.SendOnChainRequest - (*SendOnChainResponse)(nil), // 77: waverpc.SendOnChainResponse - (*BoardRequest)(nil), // 78: waverpc.BoardRequest - (*BoardResponse)(nil), // 79: waverpc.BoardResponse - (*JoinNextRoundRequest)(nil), // 80: waverpc.JoinNextRoundRequest - (*JoinNextRoundResponse)(nil), // 81: waverpc.JoinNextRoundResponse - (*SweepBoardingUTXOsRequest)(nil), // 82: waverpc.SweepBoardingUTXOsRequest - (*BoardingSweepOutput)(nil), // 83: waverpc.BoardingSweepOutput - (*SweepBoardingUTXOsResponse)(nil), // 84: waverpc.SweepBoardingUTXOsResponse - (*ListBoardingSweepsRequest)(nil), // 85: waverpc.ListBoardingSweepsRequest - (*BoardingSweepInput)(nil), // 86: waverpc.BoardingSweepInput - (*BoardingSweep)(nil), // 87: waverpc.BoardingSweep - (*ListBoardingSweepsResponse)(nil), // 88: waverpc.ListBoardingSweepsResponse - (*RoundVTXOInfo)(nil), // 89: waverpc.RoundVTXOInfo - (*RoundInfo)(nil), // 90: waverpc.RoundInfo - (*ListRoundsRequest)(nil), // 91: waverpc.ListRoundsRequest - (*GetRoundRequest)(nil), // 92: waverpc.GetRoundRequest - (*GetRoundResponse)(nil), // 93: waverpc.GetRoundResponse - (*ListRoundsResponse)(nil), // 94: waverpc.ListRoundsResponse - (*WatchRoundsRequest)(nil), // 95: waverpc.WatchRoundsRequest - (*WatchRoundsResponse)(nil), // 96: waverpc.WatchRoundsResponse - (*OORSessionInfo)(nil), // 97: waverpc.OORSessionInfo - (*ListOORSessionsRequest)(nil), // 98: waverpc.ListOORSessionsRequest - (*ListOORSessionsResponse)(nil), // 99: waverpc.ListOORSessionsResponse - (*GetOORSessionRequest)(nil), // 100: waverpc.GetOORSessionRequest - (*GetOORSessionResponse)(nil), // 101: waverpc.GetOORSessionResponse - (*EstimateFeeRequest)(nil), // 102: waverpc.EstimateFeeRequest - (*EstimateFeeResponse)(nil), // 103: waverpc.EstimateFeeResponse - (*GetFeeHistoryRequest)(nil), // 104: waverpc.GetFeeHistoryRequest - (*FeeHistoryEntry)(nil), // 105: waverpc.FeeHistoryEntry - (*GetFeeHistoryResponse)(nil), // 106: waverpc.GetFeeHistoryResponse - (*ListTransactionsRequest)(nil), // 107: waverpc.ListTransactionsRequest - (*TransactionHistoryEntry)(nil), // 108: waverpc.TransactionHistoryEntry - (*ListTransactionsResponse)(nil), // 109: waverpc.ListTransactionsResponse - (*UnrollRequest)(nil), // 110: waverpc.UnrollRequest - (*UnrollResponse)(nil), // 111: waverpc.UnrollResponse - (*GetUnrollStatusRequest)(nil), // 112: waverpc.GetUnrollStatusRequest - (*UnrollProgress)(nil), // 113: waverpc.UnrollProgress - (*UnrollCSV)(nil), // 114: waverpc.UnrollCSV - (*UnrollFees)(nil), // 115: waverpc.UnrollFees - (*GetUnrollStatusResponse)(nil), // 116: waverpc.GetUnrollStatusResponse - (*ArmVHTLCRecoveryRequest)(nil), // 117: waverpc.ArmVHTLCRecoveryRequest - (*ArmVHTLCRecoveryResponse)(nil), // 118: waverpc.ArmVHTLCRecoveryResponse - (*EscalateVHTLCRecoveryRequest)(nil), // 119: waverpc.EscalateVHTLCRecoveryRequest - (*EscalateVHTLCRecoveryResponse)(nil), // 120: waverpc.EscalateVHTLCRecoveryResponse - (*CancelVHTLCRecoveryRequest)(nil), // 121: waverpc.CancelVHTLCRecoveryRequest - (*CancelVHTLCRecoveryResponse)(nil), // 122: waverpc.CancelVHTLCRecoveryResponse - (*GetVHTLCRecoveryStatusRequest)(nil), // 123: waverpc.GetVHTLCRecoveryStatusRequest - (*GetVHTLCRecoveryStatusResponse)(nil), // 124: waverpc.GetVHTLCRecoveryStatusResponse - (*ListVHTLCRecoveriesRequest)(nil), // 125: waverpc.ListVHTLCRecoveriesRequest - (*ListVHTLCRecoveriesResponse)(nil), // 126: waverpc.ListVHTLCRecoveriesResponse - (*VHTLCRecoveryStatus)(nil), // 127: waverpc.VHTLCRecoveryStatus - nil, // 128: waverpc.LeaveVTXOsRequest.DestinationsEntry + (*RefreshFeeEstimate)(nil), // 63: waverpc.RefreshFeeEstimate + (*OutpointFeeEstimate)(nil), // 64: waverpc.OutpointFeeEstimate + (*CustomRefreshVTXOInput)(nil), // 65: waverpc.CustomRefreshVTXOInput + (*CustomRefreshVTXOOutput)(nil), // 66: waverpc.CustomRefreshVTXOOutput + (*RefreshCustomVTXOsRequest)(nil), // 67: waverpc.RefreshCustomVTXOsRequest + (*RefreshCustomVTXOsResponse)(nil), // 68: waverpc.RefreshCustomVTXOsResponse + (*PendingForfeitParticipantSignatureRequest)(nil), // 69: waverpc.PendingForfeitParticipantSignatureRequest + (*ListPendingForfeitParticipantSignatureRequestsRequest)(nil), // 70: waverpc.ListPendingForfeitParticipantSignatureRequestsRequest + (*ListPendingForfeitParticipantSignatureRequestsResponse)(nil), // 71: waverpc.ListPendingForfeitParticipantSignatureRequestsResponse + (*ForfeitParticipantSignature)(nil), // 72: waverpc.ForfeitParticipantSignature + (*SubmitForfeitParticipantSignaturesRequest)(nil), // 73: waverpc.SubmitForfeitParticipantSignaturesRequest + (*SubmitForfeitParticipantSignaturesResponse)(nil), // 74: waverpc.SubmitForfeitParticipantSignaturesResponse + (*LeaveDestination)(nil), // 75: waverpc.LeaveDestination + (*LeaveVTXOsRequest)(nil), // 76: waverpc.LeaveVTXOsRequest + (*LeaveVTXOsResponse)(nil), // 77: waverpc.LeaveVTXOsResponse + (*SendOnChainRequest)(nil), // 78: waverpc.SendOnChainRequest + (*SendOnChainResponse)(nil), // 79: waverpc.SendOnChainResponse + (*BoardRequest)(nil), // 80: waverpc.BoardRequest + (*BoardResponse)(nil), // 81: waverpc.BoardResponse + (*JoinNextRoundRequest)(nil), // 82: waverpc.JoinNextRoundRequest + (*JoinNextRoundResponse)(nil), // 83: waverpc.JoinNextRoundResponse + (*SweepBoardingUTXOsRequest)(nil), // 84: waverpc.SweepBoardingUTXOsRequest + (*BoardingSweepOutput)(nil), // 85: waverpc.BoardingSweepOutput + (*SweepBoardingUTXOsResponse)(nil), // 86: waverpc.SweepBoardingUTXOsResponse + (*ListBoardingSweepsRequest)(nil), // 87: waverpc.ListBoardingSweepsRequest + (*BoardingSweepInput)(nil), // 88: waverpc.BoardingSweepInput + (*BoardingSweep)(nil), // 89: waverpc.BoardingSweep + (*ListBoardingSweepsResponse)(nil), // 90: waverpc.ListBoardingSweepsResponse + (*RoundVTXOInfo)(nil), // 91: waverpc.RoundVTXOInfo + (*RoundInfo)(nil), // 92: waverpc.RoundInfo + (*ListRoundsRequest)(nil), // 93: waverpc.ListRoundsRequest + (*GetRoundRequest)(nil), // 94: waverpc.GetRoundRequest + (*GetRoundResponse)(nil), // 95: waverpc.GetRoundResponse + (*ListRoundsResponse)(nil), // 96: waverpc.ListRoundsResponse + (*WatchRoundsRequest)(nil), // 97: waverpc.WatchRoundsRequest + (*WatchRoundsResponse)(nil), // 98: waverpc.WatchRoundsResponse + (*OORSessionInfo)(nil), // 99: waverpc.OORSessionInfo + (*ListOORSessionsRequest)(nil), // 100: waverpc.ListOORSessionsRequest + (*ListOORSessionsResponse)(nil), // 101: waverpc.ListOORSessionsResponse + (*GetOORSessionRequest)(nil), // 102: waverpc.GetOORSessionRequest + (*GetOORSessionResponse)(nil), // 103: waverpc.GetOORSessionResponse + (*EstimateFeeRequest)(nil), // 104: waverpc.EstimateFeeRequest + (*EstimateFeeResponse)(nil), // 105: waverpc.EstimateFeeResponse + (*GetFeeHistoryRequest)(nil), // 106: waverpc.GetFeeHistoryRequest + (*FeeHistoryEntry)(nil), // 107: waverpc.FeeHistoryEntry + (*GetFeeHistoryResponse)(nil), // 108: waverpc.GetFeeHistoryResponse + (*ListTransactionsRequest)(nil), // 109: waverpc.ListTransactionsRequest + (*TransactionHistoryEntry)(nil), // 110: waverpc.TransactionHistoryEntry + (*ListTransactionsResponse)(nil), // 111: waverpc.ListTransactionsResponse + (*UnrollRequest)(nil), // 112: waverpc.UnrollRequest + (*UnrollResponse)(nil), // 113: waverpc.UnrollResponse + (*GetUnrollStatusRequest)(nil), // 114: waverpc.GetUnrollStatusRequest + (*UnrollProgress)(nil), // 115: waverpc.UnrollProgress + (*UnrollCSV)(nil), // 116: waverpc.UnrollCSV + (*UnrollFees)(nil), // 117: waverpc.UnrollFees + (*GetUnrollStatusResponse)(nil), // 118: waverpc.GetUnrollStatusResponse + (*ArmVHTLCRecoveryRequest)(nil), // 119: waverpc.ArmVHTLCRecoveryRequest + (*ArmVHTLCRecoveryResponse)(nil), // 120: waverpc.ArmVHTLCRecoveryResponse + (*EscalateVHTLCRecoveryRequest)(nil), // 121: waverpc.EscalateVHTLCRecoveryRequest + (*EscalateVHTLCRecoveryResponse)(nil), // 122: waverpc.EscalateVHTLCRecoveryResponse + (*CancelVHTLCRecoveryRequest)(nil), // 123: waverpc.CancelVHTLCRecoveryRequest + (*CancelVHTLCRecoveryResponse)(nil), // 124: waverpc.CancelVHTLCRecoveryResponse + (*GetVHTLCRecoveryStatusRequest)(nil), // 125: waverpc.GetVHTLCRecoveryStatusRequest + (*GetVHTLCRecoveryStatusResponse)(nil), // 126: waverpc.GetVHTLCRecoveryStatusResponse + (*ListVHTLCRecoveriesRequest)(nil), // 127: waverpc.ListVHTLCRecoveriesRequest + (*ListVHTLCRecoveriesResponse)(nil), // 128: waverpc.ListVHTLCRecoveriesResponse + (*VHTLCRecoveryStatus)(nil), // 129: waverpc.VHTLCRecoveryStatus + nil, // 130: waverpc.LeaveVTXOsRequest.DestinationsEntry } var file_daemon_proto_depIdxs = []int32{ 0, // 0: waverpc.GetInfoResponse.wallet_state:type_name -> waverpc.WalletState @@ -10877,144 +11136,146 @@ var file_daemon_proto_depIdxs = []int32{ 50, // 21: waverpc.SignOORCustomInputResponse.signature:type_name -> waverpc.TaprootScriptSignature 3, // 22: waverpc.ForfeitSigningContext.signing_route:type_name -> waverpc.ForfeitSigningRoute 60, // 23: waverpc.RefreshVTXOsRequest.outpoints:type_name -> waverpc.OutpointSelection - 59, // 24: waverpc.CustomRefreshVTXOInput.forfeit_signing_context:type_name -> waverpc.ForfeitSigningContext - 63, // 25: waverpc.RefreshCustomVTXOsRequest.inputs:type_name -> waverpc.CustomRefreshVTXOInput - 64, // 26: waverpc.RefreshCustomVTXOsRequest.outputs:type_name -> waverpc.CustomRefreshVTXOOutput - 3, // 27: waverpc.PendingForfeitParticipantSignatureRequest.signing_route:type_name -> waverpc.ForfeitSigningRoute - 67, // 28: waverpc.ListPendingForfeitParticipantSignatureRequestsResponse.requests:type_name -> waverpc.PendingForfeitParticipantSignatureRequest - 70, // 29: waverpc.SubmitForfeitParticipantSignaturesRequest.signatures:type_name -> waverpc.ForfeitParticipantSignature - 60, // 30: waverpc.LeaveVTXOsRequest.outpoints:type_name -> waverpc.OutpointSelection - 73, // 31: waverpc.LeaveVTXOsRequest.default_destination:type_name -> waverpc.LeaveDestination - 128, // 32: waverpc.LeaveVTXOsRequest.destinations:type_name -> waverpc.LeaveVTXOsRequest.DestinationsEntry - 73, // 33: waverpc.SendOnChainRequest.destination:type_name -> waverpc.LeaveDestination - 83, // 34: waverpc.SweepBoardingUTXOsResponse.sweepable_outputs:type_name -> waverpc.BoardingSweepOutput - 86, // 35: waverpc.BoardingSweep.inputs:type_name -> waverpc.BoardingSweepInput - 87, // 36: waverpc.ListBoardingSweepsResponse.sweeps:type_name -> waverpc.BoardingSweep - 4, // 37: waverpc.RoundInfo.state:type_name -> waverpc.RoundState - 89, // 38: waverpc.RoundInfo.vtxos:type_name -> waverpc.RoundVTXOInfo - 4, // 39: waverpc.ListRoundsRequest.state_filter:type_name -> waverpc.RoundState - 90, // 40: waverpc.GetRoundResponse.round:type_name -> waverpc.RoundInfo - 90, // 41: waverpc.ListRoundsResponse.rounds:type_name -> waverpc.RoundInfo - 90, // 42: waverpc.WatchRoundsResponse.round:type_name -> waverpc.RoundInfo - 5, // 43: waverpc.OORSessionInfo.direction:type_name -> waverpc.OORSessionDirection - 6, // 44: waverpc.OORSessionInfo.status:type_name -> waverpc.OORSessionStatus - 5, // 45: waverpc.ListOORSessionsRequest.direction_filter:type_name -> waverpc.OORSessionDirection - 6, // 46: waverpc.ListOORSessionsRequest.status_filter:type_name -> waverpc.OORSessionStatus - 97, // 47: waverpc.ListOORSessionsResponse.sessions:type_name -> waverpc.OORSessionInfo - 97, // 48: waverpc.GetOORSessionResponse.session:type_name -> waverpc.OORSessionInfo - 105, // 49: waverpc.GetFeeHistoryResponse.entries:type_name -> waverpc.FeeHistoryEntry - 108, // 50: waverpc.ListTransactionsResponse.transactions:type_name -> waverpc.TransactionHistoryEntry - 7, // 51: waverpc.GetUnrollStatusResponse.status:type_name -> waverpc.UnrollJobStatus - 113, // 52: waverpc.GetUnrollStatusResponse.progress:type_name -> waverpc.UnrollProgress - 114, // 53: waverpc.GetUnrollStatusResponse.csv:type_name -> waverpc.UnrollCSV - 115, // 54: waverpc.GetUnrollStatusResponse.fees:type_name -> waverpc.UnrollFees - 8, // 55: waverpc.ArmVHTLCRecoveryRequest.direction:type_name -> waverpc.VHTLCRecoveryDirection - 9, // 56: waverpc.ArmVHTLCRecoveryRequest.action:type_name -> waverpc.VHTLCRecoveryAction - 127, // 57: waverpc.ArmVHTLCRecoveryResponse.status:type_name -> waverpc.VHTLCRecoveryStatus - 127, // 58: waverpc.EscalateVHTLCRecoveryResponse.status:type_name -> waverpc.VHTLCRecoveryStatus - 127, // 59: waverpc.CancelVHTLCRecoveryResponse.status:type_name -> waverpc.VHTLCRecoveryStatus - 127, // 60: waverpc.GetVHTLCRecoveryStatusResponse.status:type_name -> waverpc.VHTLCRecoveryStatus - 127, // 61: waverpc.ListVHTLCRecoveriesResponse.statuses:type_name -> waverpc.VHTLCRecoveryStatus - 8, // 62: waverpc.VHTLCRecoveryStatus.direction:type_name -> waverpc.VHTLCRecoveryDirection - 9, // 63: waverpc.VHTLCRecoveryStatus.action:type_name -> waverpc.VHTLCRecoveryAction - 10, // 64: waverpc.VHTLCRecoveryStatus.state:type_name -> waverpc.VHTLCRecoveryState - 7, // 65: waverpc.VHTLCRecoveryStatus.unroll_status:type_name -> waverpc.UnrollJobStatus - 73, // 66: waverpc.LeaveVTXOsRequest.DestinationsEntry.value:type_name -> waverpc.LeaveDestination - 11, // 67: waverpc.DaemonService.GetInfo:input_type -> waverpc.GetInfoRequest - 14, // 68: waverpc.DaemonService.GenSeed:input_type -> waverpc.GenSeedRequest - 16, // 69: waverpc.DaemonService.InitWallet:input_type -> waverpc.InitWalletRequest - 18, // 70: waverpc.DaemonService.UnlockWallet:input_type -> waverpc.UnlockWalletRequest - 20, // 71: waverpc.DaemonService.GetBalance:input_type -> waverpc.GetBalanceRequest - 25, // 72: waverpc.DaemonService.ListVTXOs:input_type -> waverpc.ListVTXOsRequest - 27, // 73: waverpc.DaemonService.NewAddress:input_type -> waverpc.NewAddressRequest - 29, // 74: waverpc.DaemonService.NewReceiveScript:input_type -> waverpc.NewReceiveScriptRequest - 31, // 75: waverpc.DaemonService.ReceiveAuthKey:input_type -> waverpc.ReceiveAuthKeyRequest - 33, // 76: waverpc.DaemonService.SignReceiveAuthMessage:input_type -> waverpc.SignReceiveAuthMessageRequest - 35, // 77: waverpc.DaemonService.SignReceiveAuthMessageCompact:input_type -> waverpc.SignReceiveAuthMessageCompactRequest - 37, // 78: waverpc.DaemonService.ReceiveAuthECDH:input_type -> waverpc.ReceiveAuthECDHRequest - 39, // 79: waverpc.DaemonService.GetIndexedVTXOByPkScript:input_type -> waverpc.GetIndexedVTXOByPkScriptRequest - 41, // 80: waverpc.DaemonService.GetVTXOExpiryInfo:input_type -> waverpc.GetVTXOExpiryInfoRequest - 43, // 81: waverpc.DaemonService.GetIndexedOORSessionByTxid:input_type -> waverpc.GetIndexedOORSessionByTxidRequest - 46, // 82: waverpc.DaemonService.SendVTXO:input_type -> waverpc.SendVTXORequest - 48, // 83: waverpc.DaemonService.SendOOR:input_type -> waverpc.SendOORRequest - 52, // 84: waverpc.DaemonService.PrepareOOR:input_type -> waverpc.PrepareOORRequest - 55, // 85: waverpc.DaemonService.SignOORCustomInput:input_type -> waverpc.SignOORCustomInputRequest - 57, // 86: waverpc.DaemonService.SignVTXOForfeit:input_type -> waverpc.SignVTXOForfeitRequest - 61, // 87: waverpc.DaemonService.RefreshVTXOs:input_type -> waverpc.RefreshVTXOsRequest - 65, // 88: waverpc.DaemonService.RefreshCustomVTXOs:input_type -> waverpc.RefreshCustomVTXOsRequest - 68, // 89: waverpc.DaemonService.ListPendingForfeitParticipantSignatureRequests:input_type -> waverpc.ListPendingForfeitParticipantSignatureRequestsRequest - 71, // 90: waverpc.DaemonService.SubmitForfeitParticipantSignatures:input_type -> waverpc.SubmitForfeitParticipantSignaturesRequest - 74, // 91: waverpc.DaemonService.LeaveVTXOs:input_type -> waverpc.LeaveVTXOsRequest - 76, // 92: waverpc.DaemonService.SendOnChain:input_type -> waverpc.SendOnChainRequest - 78, // 93: waverpc.DaemonService.Board:input_type -> waverpc.BoardRequest - 80, // 94: waverpc.DaemonService.JoinNextRound:input_type -> waverpc.JoinNextRoundRequest - 82, // 95: waverpc.DaemonService.SweepBoardingUTXOs:input_type -> waverpc.SweepBoardingUTXOsRequest - 85, // 96: waverpc.DaemonService.ListBoardingSweeps:input_type -> waverpc.ListBoardingSweepsRequest - 91, // 97: waverpc.DaemonService.ListRounds:input_type -> waverpc.ListRoundsRequest - 92, // 98: waverpc.DaemonService.GetRound:input_type -> waverpc.GetRoundRequest - 95, // 99: waverpc.DaemonService.WatchRounds:input_type -> waverpc.WatchRoundsRequest - 98, // 100: waverpc.DaemonService.ListOORSessions:input_type -> waverpc.ListOORSessionsRequest - 100, // 101: waverpc.DaemonService.GetOORSession:input_type -> waverpc.GetOORSessionRequest - 102, // 102: waverpc.DaemonService.EstimateFee:input_type -> waverpc.EstimateFeeRequest - 104, // 103: waverpc.DaemonService.GetFeeHistory:input_type -> waverpc.GetFeeHistoryRequest - 107, // 104: waverpc.DaemonService.ListTransactions:input_type -> waverpc.ListTransactionsRequest - 110, // 105: waverpc.DaemonService.Unroll:input_type -> waverpc.UnrollRequest - 112, // 106: waverpc.DaemonService.GetUnrollStatus:input_type -> waverpc.GetUnrollStatusRequest - 117, // 107: waverpc.DaemonService.ArmVHTLCRecovery:input_type -> waverpc.ArmVHTLCRecoveryRequest - 119, // 108: waverpc.DaemonService.EscalateVHTLCRecovery:input_type -> waverpc.EscalateVHTLCRecoveryRequest - 121, // 109: waverpc.DaemonService.CancelVHTLCRecovery:input_type -> waverpc.CancelVHTLCRecoveryRequest - 123, // 110: waverpc.DaemonService.GetVHTLCRecoveryStatus:input_type -> waverpc.GetVHTLCRecoveryStatusRequest - 125, // 111: waverpc.DaemonService.ListVHTLCRecoveries:input_type -> waverpc.ListVHTLCRecoveriesRequest - 12, // 112: waverpc.DaemonService.GetInfo:output_type -> waverpc.GetInfoResponse - 15, // 113: waverpc.DaemonService.GenSeed:output_type -> waverpc.GenSeedResponse - 17, // 114: waverpc.DaemonService.InitWallet:output_type -> waverpc.InitWalletResponse - 19, // 115: waverpc.DaemonService.UnlockWallet:output_type -> waverpc.UnlockWalletResponse - 21, // 116: waverpc.DaemonService.GetBalance:output_type -> waverpc.GetBalanceResponse - 26, // 117: waverpc.DaemonService.ListVTXOs:output_type -> waverpc.ListVTXOsResponse - 28, // 118: waverpc.DaemonService.NewAddress:output_type -> waverpc.NewAddressResponse - 30, // 119: waverpc.DaemonService.NewReceiveScript:output_type -> waverpc.NewReceiveScriptResponse - 32, // 120: waverpc.DaemonService.ReceiveAuthKey:output_type -> waverpc.ReceiveAuthKeyResponse - 34, // 121: waverpc.DaemonService.SignReceiveAuthMessage:output_type -> waverpc.SignReceiveAuthMessageResponse - 36, // 122: waverpc.DaemonService.SignReceiveAuthMessageCompact:output_type -> waverpc.SignReceiveAuthMessageCompactResponse - 38, // 123: waverpc.DaemonService.ReceiveAuthECDH:output_type -> waverpc.ReceiveAuthECDHResponse - 40, // 124: waverpc.DaemonService.GetIndexedVTXOByPkScript:output_type -> waverpc.GetIndexedVTXOByPkScriptResponse - 42, // 125: waverpc.DaemonService.GetVTXOExpiryInfo:output_type -> waverpc.GetVTXOExpiryInfoResponse - 44, // 126: waverpc.DaemonService.GetIndexedOORSessionByTxid:output_type -> waverpc.GetIndexedOORSessionByTxidResponse - 47, // 127: waverpc.DaemonService.SendVTXO:output_type -> waverpc.SendVTXOResponse - 51, // 128: waverpc.DaemonService.SendOOR:output_type -> waverpc.SendOORResponse - 54, // 129: waverpc.DaemonService.PrepareOOR:output_type -> waverpc.PrepareOORResponse - 56, // 130: waverpc.DaemonService.SignOORCustomInput:output_type -> waverpc.SignOORCustomInputResponse - 58, // 131: waverpc.DaemonService.SignVTXOForfeit:output_type -> waverpc.SignVTXOForfeitResponse - 62, // 132: waverpc.DaemonService.RefreshVTXOs:output_type -> waverpc.RefreshVTXOsResponse - 66, // 133: waverpc.DaemonService.RefreshCustomVTXOs:output_type -> waverpc.RefreshCustomVTXOsResponse - 69, // 134: waverpc.DaemonService.ListPendingForfeitParticipantSignatureRequests:output_type -> waverpc.ListPendingForfeitParticipantSignatureRequestsResponse - 72, // 135: waverpc.DaemonService.SubmitForfeitParticipantSignatures:output_type -> waverpc.SubmitForfeitParticipantSignaturesResponse - 75, // 136: waverpc.DaemonService.LeaveVTXOs:output_type -> waverpc.LeaveVTXOsResponse - 77, // 137: waverpc.DaemonService.SendOnChain:output_type -> waverpc.SendOnChainResponse - 79, // 138: waverpc.DaemonService.Board:output_type -> waverpc.BoardResponse - 81, // 139: waverpc.DaemonService.JoinNextRound:output_type -> waverpc.JoinNextRoundResponse - 84, // 140: waverpc.DaemonService.SweepBoardingUTXOs:output_type -> waverpc.SweepBoardingUTXOsResponse - 88, // 141: waverpc.DaemonService.ListBoardingSweeps:output_type -> waverpc.ListBoardingSweepsResponse - 94, // 142: waverpc.DaemonService.ListRounds:output_type -> waverpc.ListRoundsResponse - 93, // 143: waverpc.DaemonService.GetRound:output_type -> waverpc.GetRoundResponse - 96, // 144: waverpc.DaemonService.WatchRounds:output_type -> waverpc.WatchRoundsResponse - 99, // 145: waverpc.DaemonService.ListOORSessions:output_type -> waverpc.ListOORSessionsResponse - 101, // 146: waverpc.DaemonService.GetOORSession:output_type -> waverpc.GetOORSessionResponse - 103, // 147: waverpc.DaemonService.EstimateFee:output_type -> waverpc.EstimateFeeResponse - 106, // 148: waverpc.DaemonService.GetFeeHistory:output_type -> waverpc.GetFeeHistoryResponse - 109, // 149: waverpc.DaemonService.ListTransactions:output_type -> waverpc.ListTransactionsResponse - 111, // 150: waverpc.DaemonService.Unroll:output_type -> waverpc.UnrollResponse - 116, // 151: waverpc.DaemonService.GetUnrollStatus:output_type -> waverpc.GetUnrollStatusResponse - 118, // 152: waverpc.DaemonService.ArmVHTLCRecovery:output_type -> waverpc.ArmVHTLCRecoveryResponse - 120, // 153: waverpc.DaemonService.EscalateVHTLCRecovery:output_type -> waverpc.EscalateVHTLCRecoveryResponse - 122, // 154: waverpc.DaemonService.CancelVHTLCRecovery:output_type -> waverpc.CancelVHTLCRecoveryResponse - 124, // 155: waverpc.DaemonService.GetVHTLCRecoveryStatus:output_type -> waverpc.GetVHTLCRecoveryStatusResponse - 126, // 156: waverpc.DaemonService.ListVHTLCRecoveries:output_type -> waverpc.ListVHTLCRecoveriesResponse - 112, // [112:157] is the sub-list for method output_type - 67, // [67:112] is the sub-list for method input_type - 67, // [67:67] is the sub-list for extension type_name - 67, // [67:67] is the sub-list for extension extendee - 0, // [0:67] is the sub-list for field type_name + 63, // 24: waverpc.RefreshVTXOsResponse.fee_estimate:type_name -> waverpc.RefreshFeeEstimate + 64, // 25: waverpc.RefreshFeeEstimate.outpoints:type_name -> waverpc.OutpointFeeEstimate + 59, // 26: waverpc.CustomRefreshVTXOInput.forfeit_signing_context:type_name -> waverpc.ForfeitSigningContext + 65, // 27: waverpc.RefreshCustomVTXOsRequest.inputs:type_name -> waverpc.CustomRefreshVTXOInput + 66, // 28: waverpc.RefreshCustomVTXOsRequest.outputs:type_name -> waverpc.CustomRefreshVTXOOutput + 3, // 29: waverpc.PendingForfeitParticipantSignatureRequest.signing_route:type_name -> waverpc.ForfeitSigningRoute + 69, // 30: waverpc.ListPendingForfeitParticipantSignatureRequestsResponse.requests:type_name -> waverpc.PendingForfeitParticipantSignatureRequest + 72, // 31: waverpc.SubmitForfeitParticipantSignaturesRequest.signatures:type_name -> waverpc.ForfeitParticipantSignature + 60, // 32: waverpc.LeaveVTXOsRequest.outpoints:type_name -> waverpc.OutpointSelection + 75, // 33: waverpc.LeaveVTXOsRequest.default_destination:type_name -> waverpc.LeaveDestination + 130, // 34: waverpc.LeaveVTXOsRequest.destinations:type_name -> waverpc.LeaveVTXOsRequest.DestinationsEntry + 75, // 35: waverpc.SendOnChainRequest.destination:type_name -> waverpc.LeaveDestination + 85, // 36: waverpc.SweepBoardingUTXOsResponse.sweepable_outputs:type_name -> waverpc.BoardingSweepOutput + 88, // 37: waverpc.BoardingSweep.inputs:type_name -> waverpc.BoardingSweepInput + 89, // 38: waverpc.ListBoardingSweepsResponse.sweeps:type_name -> waverpc.BoardingSweep + 4, // 39: waverpc.RoundInfo.state:type_name -> waverpc.RoundState + 91, // 40: waverpc.RoundInfo.vtxos:type_name -> waverpc.RoundVTXOInfo + 4, // 41: waverpc.ListRoundsRequest.state_filter:type_name -> waverpc.RoundState + 92, // 42: waverpc.GetRoundResponse.round:type_name -> waverpc.RoundInfo + 92, // 43: waverpc.ListRoundsResponse.rounds:type_name -> waverpc.RoundInfo + 92, // 44: waverpc.WatchRoundsResponse.round:type_name -> waverpc.RoundInfo + 5, // 45: waverpc.OORSessionInfo.direction:type_name -> waverpc.OORSessionDirection + 6, // 46: waverpc.OORSessionInfo.status:type_name -> waverpc.OORSessionStatus + 5, // 47: waverpc.ListOORSessionsRequest.direction_filter:type_name -> waverpc.OORSessionDirection + 6, // 48: waverpc.ListOORSessionsRequest.status_filter:type_name -> waverpc.OORSessionStatus + 99, // 49: waverpc.ListOORSessionsResponse.sessions:type_name -> waverpc.OORSessionInfo + 99, // 50: waverpc.GetOORSessionResponse.session:type_name -> waverpc.OORSessionInfo + 107, // 51: waverpc.GetFeeHistoryResponse.entries:type_name -> waverpc.FeeHistoryEntry + 110, // 52: waverpc.ListTransactionsResponse.transactions:type_name -> waverpc.TransactionHistoryEntry + 7, // 53: waverpc.GetUnrollStatusResponse.status:type_name -> waverpc.UnrollJobStatus + 115, // 54: waverpc.GetUnrollStatusResponse.progress:type_name -> waverpc.UnrollProgress + 116, // 55: waverpc.GetUnrollStatusResponse.csv:type_name -> waverpc.UnrollCSV + 117, // 56: waverpc.GetUnrollStatusResponse.fees:type_name -> waverpc.UnrollFees + 8, // 57: waverpc.ArmVHTLCRecoveryRequest.direction:type_name -> waverpc.VHTLCRecoveryDirection + 9, // 58: waverpc.ArmVHTLCRecoveryRequest.action:type_name -> waverpc.VHTLCRecoveryAction + 129, // 59: waverpc.ArmVHTLCRecoveryResponse.status:type_name -> waverpc.VHTLCRecoveryStatus + 129, // 60: waverpc.EscalateVHTLCRecoveryResponse.status:type_name -> waverpc.VHTLCRecoveryStatus + 129, // 61: waverpc.CancelVHTLCRecoveryResponse.status:type_name -> waverpc.VHTLCRecoveryStatus + 129, // 62: waverpc.GetVHTLCRecoveryStatusResponse.status:type_name -> waverpc.VHTLCRecoveryStatus + 129, // 63: waverpc.ListVHTLCRecoveriesResponse.statuses:type_name -> waverpc.VHTLCRecoveryStatus + 8, // 64: waverpc.VHTLCRecoveryStatus.direction:type_name -> waverpc.VHTLCRecoveryDirection + 9, // 65: waverpc.VHTLCRecoveryStatus.action:type_name -> waverpc.VHTLCRecoveryAction + 10, // 66: waverpc.VHTLCRecoveryStatus.state:type_name -> waverpc.VHTLCRecoveryState + 7, // 67: waverpc.VHTLCRecoveryStatus.unroll_status:type_name -> waverpc.UnrollJobStatus + 75, // 68: waverpc.LeaveVTXOsRequest.DestinationsEntry.value:type_name -> waverpc.LeaveDestination + 11, // 69: waverpc.DaemonService.GetInfo:input_type -> waverpc.GetInfoRequest + 14, // 70: waverpc.DaemonService.GenSeed:input_type -> waverpc.GenSeedRequest + 16, // 71: waverpc.DaemonService.InitWallet:input_type -> waverpc.InitWalletRequest + 18, // 72: waverpc.DaemonService.UnlockWallet:input_type -> waverpc.UnlockWalletRequest + 20, // 73: waverpc.DaemonService.GetBalance:input_type -> waverpc.GetBalanceRequest + 25, // 74: waverpc.DaemonService.ListVTXOs:input_type -> waverpc.ListVTXOsRequest + 27, // 75: waverpc.DaemonService.NewAddress:input_type -> waverpc.NewAddressRequest + 29, // 76: waverpc.DaemonService.NewReceiveScript:input_type -> waverpc.NewReceiveScriptRequest + 31, // 77: waverpc.DaemonService.ReceiveAuthKey:input_type -> waverpc.ReceiveAuthKeyRequest + 33, // 78: waverpc.DaemonService.SignReceiveAuthMessage:input_type -> waverpc.SignReceiveAuthMessageRequest + 35, // 79: waverpc.DaemonService.SignReceiveAuthMessageCompact:input_type -> waverpc.SignReceiveAuthMessageCompactRequest + 37, // 80: waverpc.DaemonService.ReceiveAuthECDH:input_type -> waverpc.ReceiveAuthECDHRequest + 39, // 81: waverpc.DaemonService.GetIndexedVTXOByPkScript:input_type -> waverpc.GetIndexedVTXOByPkScriptRequest + 41, // 82: waverpc.DaemonService.GetVTXOExpiryInfo:input_type -> waverpc.GetVTXOExpiryInfoRequest + 43, // 83: waverpc.DaemonService.GetIndexedOORSessionByTxid:input_type -> waverpc.GetIndexedOORSessionByTxidRequest + 46, // 84: waverpc.DaemonService.SendVTXO:input_type -> waverpc.SendVTXORequest + 48, // 85: waverpc.DaemonService.SendOOR:input_type -> waverpc.SendOORRequest + 52, // 86: waverpc.DaemonService.PrepareOOR:input_type -> waverpc.PrepareOORRequest + 55, // 87: waverpc.DaemonService.SignOORCustomInput:input_type -> waverpc.SignOORCustomInputRequest + 57, // 88: waverpc.DaemonService.SignVTXOForfeit:input_type -> waverpc.SignVTXOForfeitRequest + 61, // 89: waverpc.DaemonService.RefreshVTXOs:input_type -> waverpc.RefreshVTXOsRequest + 67, // 90: waverpc.DaemonService.RefreshCustomVTXOs:input_type -> waverpc.RefreshCustomVTXOsRequest + 70, // 91: waverpc.DaemonService.ListPendingForfeitParticipantSignatureRequests:input_type -> waverpc.ListPendingForfeitParticipantSignatureRequestsRequest + 73, // 92: waverpc.DaemonService.SubmitForfeitParticipantSignatures:input_type -> waverpc.SubmitForfeitParticipantSignaturesRequest + 76, // 93: waverpc.DaemonService.LeaveVTXOs:input_type -> waverpc.LeaveVTXOsRequest + 78, // 94: waverpc.DaemonService.SendOnChain:input_type -> waverpc.SendOnChainRequest + 80, // 95: waverpc.DaemonService.Board:input_type -> waverpc.BoardRequest + 82, // 96: waverpc.DaemonService.JoinNextRound:input_type -> waverpc.JoinNextRoundRequest + 84, // 97: waverpc.DaemonService.SweepBoardingUTXOs:input_type -> waverpc.SweepBoardingUTXOsRequest + 87, // 98: waverpc.DaemonService.ListBoardingSweeps:input_type -> waverpc.ListBoardingSweepsRequest + 93, // 99: waverpc.DaemonService.ListRounds:input_type -> waverpc.ListRoundsRequest + 94, // 100: waverpc.DaemonService.GetRound:input_type -> waverpc.GetRoundRequest + 97, // 101: waverpc.DaemonService.WatchRounds:input_type -> waverpc.WatchRoundsRequest + 100, // 102: waverpc.DaemonService.ListOORSessions:input_type -> waverpc.ListOORSessionsRequest + 102, // 103: waverpc.DaemonService.GetOORSession:input_type -> waverpc.GetOORSessionRequest + 104, // 104: waverpc.DaemonService.EstimateFee:input_type -> waverpc.EstimateFeeRequest + 106, // 105: waverpc.DaemonService.GetFeeHistory:input_type -> waverpc.GetFeeHistoryRequest + 109, // 106: waverpc.DaemonService.ListTransactions:input_type -> waverpc.ListTransactionsRequest + 112, // 107: waverpc.DaemonService.Unroll:input_type -> waverpc.UnrollRequest + 114, // 108: waverpc.DaemonService.GetUnrollStatus:input_type -> waverpc.GetUnrollStatusRequest + 119, // 109: waverpc.DaemonService.ArmVHTLCRecovery:input_type -> waverpc.ArmVHTLCRecoveryRequest + 121, // 110: waverpc.DaemonService.EscalateVHTLCRecovery:input_type -> waverpc.EscalateVHTLCRecoveryRequest + 123, // 111: waverpc.DaemonService.CancelVHTLCRecovery:input_type -> waverpc.CancelVHTLCRecoveryRequest + 125, // 112: waverpc.DaemonService.GetVHTLCRecoveryStatus:input_type -> waverpc.GetVHTLCRecoveryStatusRequest + 127, // 113: waverpc.DaemonService.ListVHTLCRecoveries:input_type -> waverpc.ListVHTLCRecoveriesRequest + 12, // 114: waverpc.DaemonService.GetInfo:output_type -> waverpc.GetInfoResponse + 15, // 115: waverpc.DaemonService.GenSeed:output_type -> waverpc.GenSeedResponse + 17, // 116: waverpc.DaemonService.InitWallet:output_type -> waverpc.InitWalletResponse + 19, // 117: waverpc.DaemonService.UnlockWallet:output_type -> waverpc.UnlockWalletResponse + 21, // 118: waverpc.DaemonService.GetBalance:output_type -> waverpc.GetBalanceResponse + 26, // 119: waverpc.DaemonService.ListVTXOs:output_type -> waverpc.ListVTXOsResponse + 28, // 120: waverpc.DaemonService.NewAddress:output_type -> waverpc.NewAddressResponse + 30, // 121: waverpc.DaemonService.NewReceiveScript:output_type -> waverpc.NewReceiveScriptResponse + 32, // 122: waverpc.DaemonService.ReceiveAuthKey:output_type -> waverpc.ReceiveAuthKeyResponse + 34, // 123: waverpc.DaemonService.SignReceiveAuthMessage:output_type -> waverpc.SignReceiveAuthMessageResponse + 36, // 124: waverpc.DaemonService.SignReceiveAuthMessageCompact:output_type -> waverpc.SignReceiveAuthMessageCompactResponse + 38, // 125: waverpc.DaemonService.ReceiveAuthECDH:output_type -> waverpc.ReceiveAuthECDHResponse + 40, // 126: waverpc.DaemonService.GetIndexedVTXOByPkScript:output_type -> waverpc.GetIndexedVTXOByPkScriptResponse + 42, // 127: waverpc.DaemonService.GetVTXOExpiryInfo:output_type -> waverpc.GetVTXOExpiryInfoResponse + 44, // 128: waverpc.DaemonService.GetIndexedOORSessionByTxid:output_type -> waverpc.GetIndexedOORSessionByTxidResponse + 47, // 129: waverpc.DaemonService.SendVTXO:output_type -> waverpc.SendVTXOResponse + 51, // 130: waverpc.DaemonService.SendOOR:output_type -> waverpc.SendOORResponse + 54, // 131: waverpc.DaemonService.PrepareOOR:output_type -> waverpc.PrepareOORResponse + 56, // 132: waverpc.DaemonService.SignOORCustomInput:output_type -> waverpc.SignOORCustomInputResponse + 58, // 133: waverpc.DaemonService.SignVTXOForfeit:output_type -> waverpc.SignVTXOForfeitResponse + 62, // 134: waverpc.DaemonService.RefreshVTXOs:output_type -> waverpc.RefreshVTXOsResponse + 68, // 135: waverpc.DaemonService.RefreshCustomVTXOs:output_type -> waverpc.RefreshCustomVTXOsResponse + 71, // 136: waverpc.DaemonService.ListPendingForfeitParticipantSignatureRequests:output_type -> waverpc.ListPendingForfeitParticipantSignatureRequestsResponse + 74, // 137: waverpc.DaemonService.SubmitForfeitParticipantSignatures:output_type -> waverpc.SubmitForfeitParticipantSignaturesResponse + 77, // 138: waverpc.DaemonService.LeaveVTXOs:output_type -> waverpc.LeaveVTXOsResponse + 79, // 139: waverpc.DaemonService.SendOnChain:output_type -> waverpc.SendOnChainResponse + 81, // 140: waverpc.DaemonService.Board:output_type -> waverpc.BoardResponse + 83, // 141: waverpc.DaemonService.JoinNextRound:output_type -> waverpc.JoinNextRoundResponse + 86, // 142: waverpc.DaemonService.SweepBoardingUTXOs:output_type -> waverpc.SweepBoardingUTXOsResponse + 90, // 143: waverpc.DaemonService.ListBoardingSweeps:output_type -> waverpc.ListBoardingSweepsResponse + 96, // 144: waverpc.DaemonService.ListRounds:output_type -> waverpc.ListRoundsResponse + 95, // 145: waverpc.DaemonService.GetRound:output_type -> waverpc.GetRoundResponse + 98, // 146: waverpc.DaemonService.WatchRounds:output_type -> waverpc.WatchRoundsResponse + 101, // 147: waverpc.DaemonService.ListOORSessions:output_type -> waverpc.ListOORSessionsResponse + 103, // 148: waverpc.DaemonService.GetOORSession:output_type -> waverpc.GetOORSessionResponse + 105, // 149: waverpc.DaemonService.EstimateFee:output_type -> waverpc.EstimateFeeResponse + 108, // 150: waverpc.DaemonService.GetFeeHistory:output_type -> waverpc.GetFeeHistoryResponse + 111, // 151: waverpc.DaemonService.ListTransactions:output_type -> waverpc.ListTransactionsResponse + 113, // 152: waverpc.DaemonService.Unroll:output_type -> waverpc.UnrollResponse + 118, // 153: waverpc.DaemonService.GetUnrollStatus:output_type -> waverpc.GetUnrollStatusResponse + 120, // 154: waverpc.DaemonService.ArmVHTLCRecovery:output_type -> waverpc.ArmVHTLCRecoveryResponse + 122, // 155: waverpc.DaemonService.EscalateVHTLCRecovery:output_type -> waverpc.EscalateVHTLCRecoveryResponse + 124, // 156: waverpc.DaemonService.CancelVHTLCRecovery:output_type -> waverpc.CancelVHTLCRecoveryResponse + 126, // 157: waverpc.DaemonService.GetVHTLCRecoveryStatus:output_type -> waverpc.GetVHTLCRecoveryStatusResponse + 128, // 158: waverpc.DaemonService.ListVHTLCRecoveries:output_type -> waverpc.ListVHTLCRecoveriesResponse + 114, // [114:159] is the sub-list for method output_type + 69, // [69:114] is the sub-list for method input_type + 69, // [69:69] is the sub-list for extension type_name + 69, // [69:69] is the sub-list for extension extendee + 0, // [0:69] is the sub-list for field type_name } func init() { file_daemon_proto_init() } @@ -11035,15 +11296,16 @@ func file_daemon_proto_init() { (*RefreshVTXOsRequest_Outpoints)(nil), (*RefreshVTXOsRequest_All)(nil), } - file_daemon_proto_msgTypes[62].OneofWrappers = []any{ + file_daemon_proto_msgTypes[52].OneofWrappers = []any{} + file_daemon_proto_msgTypes[64].OneofWrappers = []any{ (*LeaveDestination_Address)(nil), (*LeaveDestination_PkScript)(nil), } - file_daemon_proto_msgTypes[63].OneofWrappers = []any{ + file_daemon_proto_msgTypes[65].OneofWrappers = []any{ (*LeaveVTXOsRequest_Outpoints)(nil), (*LeaveVTXOsRequest_All)(nil), } - file_daemon_proto_msgTypes[65].OneofWrappers = []any{ + file_daemon_proto_msgTypes[67].OneofWrappers = []any{ (*SendOnChainRequest_AmountSat)(nil), (*SendOnChainRequest_SweepAll)(nil), } @@ -11053,7 +11315,7 @@ func file_daemon_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_proto_rawDesc), len(file_daemon_proto_rawDesc)), NumEnums: 11, - NumMessages: 118, + NumMessages: 120, NumExtensions: 0, NumServices: 1, }, diff --git a/waverpc/daemon.proto b/waverpc/daemon.proto index 4ccc2c00b..237c611b0 100644 --- a/waverpc/daemon.proto +++ b/waverpc/daemon.proto @@ -1244,6 +1244,92 @@ message RefreshVTXOsResponse { // status is the result: "queued" on success, "preview" on dry_run. string status = 2; + + // fee_estimate carries the advisory operator-fee preview for the + // selected VTXOs. Only populated on dry_run previews with a + // non-empty selection. The binding fee is set by the server-issued + // JoinRoundQuote at seal time and may differ from this estimate. + RefreshFeeEstimate fee_estimate = 3; +} + +// RefreshFeeEstimate is the advisory operator-fee preview attached to a +// RefreshVTXOs dry_run response. It itemizes what refreshing the +// selected VTXOs is expected to cost at the operator's current rates, +// including whether the operator's free-refresh window applies. All +// values are advisory: the binding fee is set at seal time. +message RefreshFeeEstimate { + // estimated_total_fee_sat is the advisory total operator fee for + // refreshing the full selection. Explicit presence: the field is + // only set when the total is meaningful — either the per-outpoint + // quotes all succeeded, or free_refresh_eligible is true (the + // waiver is computed locally, so its zero total stays valid even + // when estimate_error is set). When the estimate degrades the + // field is absent, never zero, so a missing number can not be + // misread as a free refresh. + optional int64 estimated_total_fee_sat = 1; + + // outpoints itemizes the advisory per-outpoint quotes at the + // operator's current rates. Each row is the ordinary paid price and + // is NOT waiver-adjusted: its components always sum to the row's + // total_fee_sat, so under a free-refresh-eligible selection the rows + // show what the waiver saves. + repeated OutpointFeeEstimate outpoints = 2; + + // free_refresh_eligible is true when every selected VTXO is inside + // the operator's advertised free-refresh window, so refreshing + // exactly this selection on its own is expected to seal with all + // fee components waived. The waiver is all-or-nothing per round: + // batching this selection with boarding, leave, or out-of-window + // inputs forfeits it. + bool free_refresh_eligible = 3; + + // estimate_error is non-empty when the per-outpoint operator quotes + // could not be fetched (operator unreachable, chain height + // unavailable). The preview is still returned so dry_run keeps + // working as a validity probe; when set, treat the per-outpoint fee + // components and (unless free_refresh_eligible) the total as absent + // rather than zero. + string estimate_error = 4; +} + +// OutpointFeeEstimate is one selected VTXO's advisory refresh-fee +// quote, resolved from the daemon's own view of the VTXO (amount and +// remaining lifetime) so callers do not have to supply either. +message OutpointFeeEstimate { + // outpoint identifies the VTXO in "txid:index" format. + string outpoint = 1; + + // amount_sat is the VTXO amount the quote is for. + int64 amount_sat = 2; + + // remaining_blocks is the remaining VTXO lifetime used for the + // quote: batch expiry minus the current chain height. An expired or + // same-block-expiring VTXO is clamped to 1 so the quote never falls + // back to the operator's full-lifetime default for a zero value. + uint32 remaining_blocks = 3; + + // liquidity_fee_sat is the time-value-of-money component. + int64 liquidity_fee_sat = 4; + + // onchain_share_sat is the per-participant on-chain cost share. + int64 onchain_share_sat = 5; + + // margin_sat is the fixed operator margin. + int64 margin_sat = 6; + + // total_fee_sat is the sum of the components: the ordinary paid + // price for refreshing this VTXO, before any free-refresh waiver. + int64 total_fee_sat = 7; + + // in_free_refresh_window is true when this VTXO's remaining + // lifetime is inside the operator's advertised free-refresh window. + bool in_free_refresh_window = 8; + + // below_dust_warning mirrors the operator's warning that this + // VTXO's amount is below the minimum economically viable VTXO at + // current rates: refreshing it costs fees on a coin that already + // costs more to exit than it holds. + bool below_dust_warning = 9; } // CustomRefreshVTXOInput specifies one custom-policy VTXO input to refresh in From 0b6a51bb71eec545ddefedac9e79d93358599f02 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Fri, 17 Jul 2026 08:47:10 -0500 Subject: [PATCH 2/9] waved: Estimate refresh fees on dry run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In this commit, we populate the new RefreshVTXOsResponse fee estimate on the dry-run path. The daemon resolves every selected VTXO to its full descriptor, computes each remaining lifetime from the chain tip, and fetches the operator's itemized quote through the existing EstimateFee proxy, deduped on (amount, remaining blocks). The remaining-blocks figure is clamped to 1 for expiring VTXOs because the operator treats zero as "price the full sweep-delay lifetime". The explicit-outpoint path now looks targets up in the VTXO store instead of only parsing strings: an unknown or non-live outpoint surfaces as InvalidArgument instead of echoing back as a plausible preview, mirroring the LiveState filter the --all path already applies — the real refresh can never execute either, and dry_run is the validity probe the CLI consent prompt trusts. Explicit selections are also deduped (order-preserving) so a repeated outpoint can neither double-count in the estimate nor register a doomed duplicate forfeit pair with the wallet. The free-late-refresh waiver (lumos#675) is applied locally: the operator's EstimateFee prices every refresh as paid, so quoting a free late refresh through it alone would over-quote. The daemon already caches the advertised window in its operator terms and knows a refresh selection is the pure one-for-one renewal shape the waiver requires, so a selection fully inside the window previews with an explicit zero total while the rows keep the ordinary paid quote to show what the waiver saves. The estimate is strictly best-effort and degrades all-or-nothing: every quote is fetched and vetted (no negative or beyond-money- supply values, no overflowing selection total) before any component is written to a row, an unreachable operator or chain backend sets estimate_error while the preview and the locally computed waiver verdict survive, and the total uses explicit proto presence — it is only set when meaningful, so a degraded estimate can never be misread as a free refresh. The dry-run branch also moves ahead of the wallet-ready gate, matching the LeaveVTXOs ordering rule that pure-argument validation and previews must not depend on wallet state (the H-5 fix's comment already claimed this parity). A side effect is that an empty selection=all dry run now honestly reports status "preview" instead of "queued". The real refresh path still gates on wallet readiness before queuing. Part of #986. (cherry picked from commit b5eefe7447cedb90cf45db02949113cf2583a24e) --- waved/congestion_roundtrip_test.go | 27 + waved/rpc_fees.go | 27 +- waved/rpc_refresh_estimate.go | 369 +++++++++ waved/rpc_refresh_estimate_test.go | 1134 ++++++++++++++++++++++++++++ waved/rpc_server.go | 110 +-- 5 files changed, 1617 insertions(+), 50 deletions(-) create mode 100644 waved/rpc_refresh_estimate.go create mode 100644 waved/rpc_refresh_estimate_test.go diff --git a/waved/congestion_roundtrip_test.go b/waved/congestion_roundtrip_test.go index 09753c24c..82e16a363 100644 --- a/waved/congestion_roundtrip_test.go +++ b/waved/congestion_roundtrip_test.go @@ -32,6 +32,22 @@ type fakeArkService struct { // fails the fee quote. estimateFeeErr error + // responseFn, when non-nil, computes the EstimateFee reply from + // the request instead of returning the canned response. Used by + // tests that quote several distinct (amount, remaining-blocks) + // pairs in one RPC and need per-pair replies. + responseFn func(*arkrpc.EstimateFeeRequest) *arkrpc.EstimateFeeResponse + + // errFn, when non-nil, lets a test fail EstimateFee selectively + // per request (e.g. only the second distinct quote of a + // selection). A nil return falls through to responseFn / the + // canned response. + errFn func(*arkrpc.EstimateFeeRequest) error + + // estimateFeeCalls counts EstimateFee invocations so tests can + // assert quote deduplication. + estimateFeeCalls int + // getInfoResponse is the canned GetInfo reply. Optional; // tests that don't exercise the fetchOperatorTerms path can // leave it nil, in which case GetInfo falls through to the @@ -54,11 +70,22 @@ func (f *fakeArkService) EstimateFee(_ context.Context, req *arkrpc.EstimateFeeRequest) (*arkrpc.EstimateFeeResponse, error) { f.lastRequest = req + f.estimateFeeCalls++ if f.estimateFeeErr != nil { return nil, f.estimateFeeErr } + if f.errFn != nil { + if err := f.errFn(req); err != nil { + return nil, err + } + } + + if f.responseFn != nil { + return f.responseFn(req), nil + } + return f.response, nil } diff --git a/waved/rpc_fees.go b/waved/rpc_fees.go index 229810aca..a35865293 100644 --- a/waved/rpc_fees.go +++ b/waved/rpc_fees.go @@ -49,9 +49,28 @@ const ( func (s *Server) quoteOperatorFee(ctx context.Context, amountSat int64, isBoarding bool, remainingBlocks uint32) (btcutil.Amount, error) { + resp, err := s.quoteOperatorFeeBreakdown( + ctx, amountSat, isBoarding, remainingBlocks, + ) + if err != nil { + return 0, err + } + + return btcutil.Amount(resp.TotalFeeSat), nil +} + +// quoteOperatorFeeBreakdown fetches the operator's full itemized +// EstimateFee response for one (amount, boarding, remaining-blocks) +// tuple. quoteOperatorFee wraps it for callers that only need the +// total; the refresh dry-run estimate consumes the component +// breakdown directly. +func (s *Server) quoteOperatorFeeBreakdown(ctx context.Context, amountSat int64, + isBoarding bool, remainingBlocks uint32) (*arkrpc.EstimateFeeResponse, + error) { + client := s.operatorArkClient() if client == nil { - return 0, status.Errorf(codes.Unavailable, "operator "+ + return nil, status.Errorf(codes.Unavailable, "operator "+ "connection not initialized") } @@ -63,7 +82,7 @@ func (s *Server) quoteOperatorFee(ctx context.Context, amountSat int64, }, ) if err != nil { - return 0, err + return nil, err } // Defensive nil-guard: well-behaved gRPC servers always return @@ -73,11 +92,11 @@ func (s *Server) quoteOperatorFee(ctx context.Context, amountSat int64, // clean codes.Internal so the caller can fall back instead of // crashing the daemon. if resp == nil { - return 0, status.Errorf(codes.Internal, "operator returned "+ + return nil, status.Errorf(codes.Internal, "operator returned "+ "empty fee response") } - return btcutil.Amount(resp.TotalFeeSat), nil + return resp, nil } // autoRefreshFeeQuoter builds the vtxo.RefreshFeeQuoter the VTXO diff --git a/waved/rpc_refresh_estimate.go b/waved/rpc_refresh_estimate.go new file mode 100644 index 000000000..a295a55a3 --- /dev/null +++ b/waved/rpc_refresh_estimate.go @@ -0,0 +1,369 @@ +package waved + +import ( + "context" + "errors" + "fmt" + + "github.com/btcsuite/btcd/btcutil/v2" + "github.com/btcsuite/btcd/wire/v2" + "github.com/lightninglabs/wavelength/arkrpc" + "github.com/lightninglabs/wavelength/vtxo" + "github.com/lightninglabs/wavelength/waverpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// refreshQuoteKey dedupes operator EstimateFee calls across the selected +// VTXOs. The operator quote is a pure function of (amount, remaining +// blocks) — VTXOs from the same round share an expiry, so a wallet-wide +// dry run typically collapses to a handful of distinct quotes instead of +// one round-trip per VTXO. +type refreshQuoteKey struct { + amountSat int64 + remainingBlocks uint32 +} + +// refreshDryRunPreview services the dry_run branch of RefreshVTXOs: it +// resolves the selected VTXOs against the SQL store, echoes the preview +// outpoint list, and attaches the advisory fee estimate. It runs before +// the wallet-ready gate (mirroring the LeaveVTXOs dry-run ordering) so +// callers can preview a refresh — including its cost — without an +// unlocked wallet; everything it needs is the SQL VTXO view, the chain +// tip, and the operator connection. +func (r *RPCServer) refreshDryRunPreview(ctx context.Context, + explicit []wire.OutPoint, all bool) (*waverpc.RefreshVTXOsResponse, + error) { + + // A store-less daemon (partially wired setups, tests) cannot + // resolve amounts or lifetimes: keep the pre-existing preview + // echo for explicit outpoints and degrade the estimate instead + // of dropping the echo along with it. The selection=all echo is + // empty here by construction, matching the LeaveVTXOs nil-store + // tolerance. + if r.server.vtxoStore == nil { + outpointStrs := make([]string, 0, len(explicit)) + for _, op := range explicit { + outpointStrs = append( + outpointStrs, + fmt.Sprintf("%s:%d", op.Hash, op.Index), + ) + } + + resp := &waverpc.RefreshVTXOsResponse{ + QueuedOutpoints: outpointStrs, + Status: "preview", + } + if len(explicit) > 0 { + resp.FeeEstimate = &waverpc.RefreshFeeEstimate{ + EstimateError: "VTXO store unavailable; " + + "fee estimate not computed", + } + } + + return resp, nil + } + + descs, err := r.resolveRefreshPreviewTargets(ctx, explicit, all) + if err != nil { + return nil, err + } + + outpointStrs := make([]string, 0, len(descs)) + for _, desc := range descs { + outpointStrs = append( + outpointStrs, fmt.Sprintf("%s:%d", desc.Outpoint.Hash, + desc.Outpoint.Index), + ) + } + + resp := &waverpc.RefreshVTXOsResponse{ + QueuedOutpoints: outpointStrs, + Status: "preview", + } + + // An empty selection (refresh --all with no live VTXOs) has + // nothing to price: leave fee_estimate unset rather than + // attaching a vacuous zero-fee estimate a caller could misread + // as "free". + if len(descs) > 0 { + resp.FeeEstimate = r.estimateRefreshFees(ctx, descs) + } + + return resp, nil +} + +// resolveRefreshPreviewTargets maps the request selection onto full VTXO +// descriptors so the preview can price each target without the caller +// supplying amounts or lifetimes. Explicit outpoints are validated +// against the store: an unknown or non-live outpoint is a caller +// mistake and surfaces as InvalidArgument, because the real refresh can +// never execute it — a preview that echoed and priced it anyway would +// defeat dry_run's role as the validity probe behind the CLI's consent +// prompt. The caller guarantees a non-nil vtxoStore. +func (r *RPCServer) resolveRefreshPreviewTargets(ctx context.Context, + explicit []wire.OutPoint, all bool) ([]*vtxo.Descriptor, error) { + + if all { + liveVTXOs, err := r.server.vtxoStore.ListLiveVTXOs(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "list live "+ + "VTXOs: %v", err) + } + + // Keep only VTXOs actually in LiveState, matching the + // real refresh path's filter: anything already on its + // way through a round must not be double-counted in the + // preview either. + descs := make([]*vtxo.Descriptor, 0, len(liveVTXOs)) + for _, desc := range liveVTXOs { + if desc.Status != vtxo.VTXOStatusLive { + continue + } + + descs = append(descs, desc) + } + + return descs, nil + } + + descs := make([]*vtxo.Descriptor, 0, len(explicit)) + for _, op := range explicit { + desc, err := r.server.vtxoStore.GetVTXO(ctx, op) + if err != nil { + if errors.Is(err, vtxo.ErrVTXONotFound) { + return nil, status.Errorf(codes.InvalidArgument, + "unknown VTXO outpoint %s:%d", op.Hash, + op.Index) + } + + return nil, status.Errorf(codes.Internal, "get VTXO "+ + "%s:%d: %v", op.Hash, op.Index, err) + } + + // GetVTXO resolves rows in any lifecycle state; the --all + // branch filters to LiveState, and the explicit branch + // must be as strict — a spent or in-flight VTXO would + // preview as refreshable (and be priced) while the real + // dispatch is guaranteed to fail on it. + if desc.Status != vtxo.VTXOStatusLive { + return nil, status.Errorf(codes.InvalidArgument, + "VTXO %s:%d is not refreshable (status %v)", + op.Hash, op.Index, desc.Status) + } + + descs = append(descs, desc) + } + + return descs, nil +} + +// degradeRefreshEstimate marks the estimate as degraded with the given +// caller-safe message. The locally computed waiver verdict survives +// degradation: a waiver-eligible selection keeps its explicit zero +// total, while any other degraded estimate leaves the total absent so a +// missing number can never be misread as a free refresh. +func degradeRefreshEstimate(est *waverpc.RefreshFeeEstimate, + msg string) *waverpc.RefreshFeeEstimate { + + est.EstimateError = msg + if est.FreeRefreshEligible { + est.EstimatedTotalFeeSat = proto.Int64(0) + } + + return est +} + +// refreshQuoteSane rejects operator quotes carrying negative or +// beyond-money-supply values. Such a quote is operator nonsense; it is +// contained here so a broken (or hostile) operator can neither render +// negative components nor poison the selection total. +func refreshQuoteSane(quote *arkrpc.EstimateFeeResponse) bool { + return quote.LiquidityFeeSat >= 0 && + quote.OnchainShareSat >= 0 && + quote.MarginSat >= 0 && + quote.TotalFeeSat >= 0 && + quote.TotalFeeSat <= int64(btcutil.MaxSatoshi) +} + +// estimateRefreshFees builds the advisory RefreshFeeEstimate for the +// resolved targets. The estimate is strictly best-effort: any failure to +// reach the chain backend or the operator sets estimate_error and +// returns whatever facts are known locally (amounts, remaining +// lifetimes, free-window membership) instead of failing the preview — +// dry_run must keep working as a validity probe in degraded mode. +// +// The free-refresh waiver (lumos#675) is applied here rather than by the +// operator: the operator's EstimateFee prices every refresh as paid, so +// quoting a free late refresh through it alone would over-quote. The +// daemon knows the advertised window (cached operator terms), each +// VTXO's remaining lifetime, and that a RefreshVTXOs selection is +// exactly the pure one-for-one renewal shape the waiver requires, so +// eligibility is computable locally. Per-outpoint rows keep the +// ordinary paid quote (components sum to the row total) and only the +// selection-level total reflects the waiver, mirroring the operator's +// all-or-nothing seal-time rule. +func (r *RPCServer) estimateRefreshFees(ctx context.Context, + descs []*vtxo.Descriptor) *waverpc.RefreshFeeEstimate { + + est := &waverpc.RefreshFeeEstimate{} + + // The advertised free-refresh window rides on the cached + // operator terms; a cold cache reads as "no window", which only + // ever over-quotes (waiver missed, never invented). + var window uint32 + if terms := r.server.loadOperatorTerms(); terms != nil { + window = terms.FreeRefreshWindowBlocks + } + + height, err := r.currentBlockHeight(ctx) + if err != nil { + r.server.log.WarnS(ctx, "Refresh fee estimate: chain "+ + "height unavailable", err) + + // Without the chain tip neither remaining lifetimes nor + // window membership can be computed; return the + // amount-only rows so the caller still sees what was + // selected. + for _, desc := range descs { + est.Outpoints = append( + est.Outpoints, &waverpc.OutpointFeeEstimate{ + Outpoint: fmt.Sprintf( + "%s:%d", desc.Outpoint.Hash, + desc.Outpoint.Index, + ), + AmountSat: int64(desc.Amount), + }, + ) + } + + return degradeRefreshEstimate( + est, + "chain height unavailable; fee estimate not computed", + ) + } + + // First pass: per-outpoint facts the daemon knows locally. The + // quote's remaining-blocks figure is clamped to 1 because the + // operator treats zero as "use the full sweep-delay lifetime", + // which would massively over-quote an expiring VTXO; window + // membership uses the unclamped value (an expired VTXO is not + // waiver-eligible). + allInWindow := true + for _, desc := range descs { + remaining := vtxo.BlocksUntilExpiry(desc, height) + clamped := remaining + if clamped < 1 { + clamped = 1 + } + + inWindow := window > 0 && remaining > 0 && + uint32(remaining) <= window + if !inWindow { + allInWindow = false + } + + est.Outpoints = append( + est.Outpoints, &waverpc.OutpointFeeEstimate{ + Outpoint: fmt.Sprintf( + "%s:%d", desc.Outpoint.Hash, + desc.Outpoint.Index, + ), + AmountSat: int64(desc.Amount), + RemainingBlocks: uint32(clamped), + InFreeRefreshWindow: inWindow, + }, + ) + } + est.FreeRefreshEligible = allInWindow + + // Second pass: fetch and validate the operator quotes, deduped + // on (amount, remaining blocks), and pre-compute the selection + // total. Every quote is vetted before any component is written + // to a row, so a mid-selection failure degrades all-or-nothing — + // rows either all carry components or none do, which is exactly + // what estimate_error promises the caller. + quotes := make(map[refreshQuoteKey]*arkrpc.EstimateFeeResponse) + + var total int64 + for _, row := range est.Outpoints { + key := refreshQuoteKey{ + amountSat: row.AmountSat, + remainingBlocks: row.RemainingBlocks, + } + + quote, ok := quotes[key] + if !ok { + var err error + quote, err = r.server.quoteOperatorFeeBreakdown( + ctx, key.amountSat, false, /* isBoarding */ + key.remainingBlocks, + ) + if err != nil { + r.server.log.WarnS(ctx, "Refresh fee "+ + "estimate: operator quote "+ + "unavailable", err) + + // Match the EstimateFee proxy's + // sanitization stance: log the upstream + // detail, never echo operator-internal + // error text to the caller. + return degradeRefreshEstimate( + est, + "operator fee estimate unavailable", + ) + } + + if !refreshQuoteSane(quote) { + r.server.log.WarnS(ctx, "Refresh fee "+ + "estimate: operator returned "+ + "nonsense fee", + fmt.Errorf("total_fee_sat=%d for %s", + quote.TotalFeeSat, row.Outpoint)) + + return degradeRefreshEstimate( + est, "operator fee estimate invalid", + ) + } + + quotes[key] = quote + } + + // Each vetted quote is bounded by MaxSatoshi, but a large + // selection could still wrap the sum; a total beyond the + // money supply is as nonsensical as a negative quote. + total += quote.TotalFeeSat + if total > int64(btcutil.MaxSatoshi) { + r.server.log.WarnS(ctx, "Refresh fee estimate: "+ + "selection total overflows", + fmt.Errorf("total=%d", total)) + + return degradeRefreshEstimate( + est, "operator fee estimate invalid", + ) + } + } + + // Third pass: apply the fully vetted quotes to the rows. + for _, row := range est.Outpoints { + quote := quotes[refreshQuoteKey{ + amountSat: row.AmountSat, + remainingBlocks: row.RemainingBlocks, + }] + + row.LiquidityFeeSat = quote.LiquidityFeeSat + row.OnchainShareSat = quote.OnchainShareSat + row.MarginSat = quote.MarginSat + row.TotalFeeSat = quote.TotalFeeSat + row.BelowDustWarning = quote.BelowDustWarning + } + + if est.FreeRefreshEligible { + est.EstimatedTotalFeeSat = proto.Int64(0) + } else { + est.EstimatedTotalFeeSat = proto.Int64(total) + } + + return est +} diff --git a/waved/rpc_refresh_estimate_test.go b/waved/rpc_refresh_estimate_test.go new file mode 100644 index 000000000..c8fe0c51c --- /dev/null +++ b/waved/rpc_refresh_estimate_test.go @@ -0,0 +1,1134 @@ +package waved + +import ( + "context" + "database/sql" + "fmt" + "sync" + "testing" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil/v2" + "github.com/btcsuite/btcd/chainhash/v2" + "github.com/btcsuite/btcd/wire/v2" + "github.com/btcsuite/btclog/v2" + "github.com/lightninglabs/wavelength/arkrpc" + "github.com/lightninglabs/wavelength/baselib/actor" + "github.com/lightninglabs/wavelength/chainsource" + "github.com/lightninglabs/wavelength/db" + "github.com/lightninglabs/wavelength/lib/arkscript" + "github.com/lightninglabs/wavelength/lib/types" + "github.com/lightninglabs/wavelength/vtxo" + "github.com/lightninglabs/wavelength/wallet" + "github.com/lightninglabs/wavelength/waverpc" + "github.com/lightningnetwork/lnd/clock" + fn "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/keychain" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// scaledEstimateFn prices a quote as a pure function of the request so +// tests covering several distinct (amount, remaining-blocks) pairs can +// assert each row was matched to its own quote: liquidity scales with +// amount, the other components are fixed. +func scaledEstimateFn( + req *arkrpc.EstimateFeeRequest) *arkrpc.EstimateFeeResponse { + + liquidity := req.AmountSat / 1_000 + + return &arkrpc.EstimateFeeResponse{ + LiquidityFeeSat: liquidity, + OnchainShareSat: 25, + MarginSat: 100, + TotalFeeSat: liquidity + 125, + } +} + +// newRefreshEstimateServer wires the minimal daemon surface the refresh +// dry-run preview needs — SQL VTXO store, chain tip, operator +// connection — and deliberately nothing wallet-related: the preview +// must work without a ready wallet. +func newRefreshEstimateServer(t *testing.T, svc *fakeArkService, + height int32) (*RPCServer, *db.VTXOPersistenceStore) { + + t.Helper() + + sqlDB := db.NewTestDB(t) + roundDB := db.NewTransactionExecutor( + sqlDB.BaseDB, + func(tx *sql.Tx) db.RoundStore { + return sqlDB.WithTx(tx) + }, + btclog.Disabled, + ) + vtxoStore := db.NewVTXOPersistenceStore( + roundDB, clock.NewDefaultClock(), + ) + + s := &Server{ + serverConn: newBufconnClient(t, svc), + log: btclog.Disabled, + chainBackend: &heightOnlyChainBackend{ + height: height, + }, + vtxoStore: vtxoStore, + } + + return &RPCServer{server: s}, vtxoStore +} + +// newRefreshEstimateVTXO builds a persistable live VTXO descriptor with +// the amount and batch expiry the estimate under test needs. +func newRefreshEstimateVTXO(t *testing.T, hashByte byte, amount btcutil.Amount, + batchExpiry int32) *vtxo.Descriptor { + + t.Helper() + + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + operatorKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + const exitDelay uint32 = 10 + + policyTemplate, err := arkscript.EncodeStandardVTXOTemplate( + clientKey.PubKey(), operatorKey.PubKey(), exitDelay, + ) + require.NoError(t, err) + + template, err := arkscript.DecodePolicyTemplate(policyTemplate) + require.NoError(t, err) + + pkScript, err := template.PkScript() + require.NoError(t, err) + + tapScript, err := arkscript.VTXOTapScript( + clientKey.PubKey(), operatorKey.PubKey(), exitDelay, + ) + require.NoError(t, err) + + var outpointHash chainhash.Hash + outpointHash[0] = hashByte + + var commitmentTxID chainhash.Hash + commitmentTxID[0] = hashByte + commitmentTxID[1] = 0xc0 + + return &vtxo.Descriptor{ + Outpoint: wire.OutPoint{ + Hash: outpointHash, + Index: uint32(hashByte), + }, + Amount: amount, + PolicyTemplate: policyTemplate, + PkScript: pkScript, + ClientKey: keychain.KeyDescriptor{ + PubKey: clientKey.PubKey(), + KeyLocator: keychain.KeyLocator{ + Index: uint32(hashByte), + }, + }, + OperatorKey: operatorKey.PubKey(), + TapScript: tapScript, + RoundID: fmt.Sprintf("refresh-est-round-%x", hashByte), + CommitmentTxID: commitmentTxID, + BatchExpiry: batchExpiry, + RelativeExpiry: exitDelay, + CreatedHeight: 1, + Status: vtxo.VTXOStatusLive, + } +} + +// outpointStr renders a descriptor's outpoint in the "txid:index" wire +// format the RPC uses. +func outpointStr(desc *vtxo.Descriptor) string { + return fmt.Sprintf("%s:%d", desc.Outpoint.Hash, desc.Outpoint.Index) +} + +// TestRefreshDryRunEstimateAllPath covers the selection=all preview: the +// estimate must price every live VTXO from the daemon's own store view +// (no caller-supplied amounts or lifetimes) — and must do so without any +// wallet wiring, since the dry-run path short-circuits before the +// wallet-ready gate. +func TestRefreshDryRunEstimateAllPath(t *testing.T) { + t.Parallel() + + const height = int32(900) + + svc := &fakeArkService{responseFn: scaledEstimateFn} + r, vtxoStore := newRefreshEstimateServer(t, svc, height) + + descA := newRefreshEstimateVTXO(t, 0x01, 100_000, 1_000) + descB := newRefreshEstimateVTXO(t, 0x02, 250_000, 1_400) + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), descA)) + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), descB)) + + resp, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_All{ + All: true, + }, + DryRun: true, + }, + ) + require.NoError(t, err) + require.Equal(t, "preview", resp.Status) + require.ElementsMatch( + t, []string{outpointStr(descA), outpointStr(descB)}, + resp.QueuedOutpoints, + ) + + est := resp.FeeEstimate + require.NotNil(t, est) + require.Empty(t, est.EstimateError) + require.False( + t, est.FreeRefreshEligible, + "no operator terms cached, so no free window applies", + ) + require.Len(t, est.Outpoints, 2) + + rows := make(map[string]*waverpc.OutpointFeeEstimate) + for _, row := range est.Outpoints { + rows[row.Outpoint] = row + } + + rowA := rows[outpointStr(descA)] + require.NotNil(t, rowA) + require.Equal(t, int64(100_000), rowA.AmountSat) + require.Equal( + t, uint32(100), rowA.RemainingBlocks, + "remaining = batch expiry 1000 - height 900", + ) + require.Equal(t, int64(100), rowA.LiquidityFeeSat) + require.Equal(t, int64(25), rowA.OnchainShareSat) + require.Equal(t, int64(100), rowA.MarginSat) + require.Equal(t, int64(225), rowA.TotalFeeSat) + require.False(t, rowA.InFreeRefreshWindow) + + rowB := rows[outpointStr(descB)] + require.NotNil(t, rowB) + require.Equal(t, int64(250_000), rowB.AmountSat) + require.Equal(t, uint32(500), rowB.RemainingBlocks) + require.Equal(t, int64(375), rowB.TotalFeeSat) + + require.Equal( + t, int64(600), est.GetEstimatedTotalFeeSat(), + "total is the sum of the per-outpoint quotes", + ) + + // Distinct (amount, remaining) pairs require distinct quotes. + require.Equal(t, 2, svc.estimateFeeCalls) +} + +// TestRefreshDryRunEstimateExplicitOutpointLookup covers the explicit +// selection path: the daemon must resolve amount and remaining lifetime +// from its store — the manual amount / remaining-blocks inputs of the +// standalone fees estimate command must not be needed — and must ask +// the operator for a refresh (not boarding) quote. +func TestRefreshDryRunEstimateExplicitOutpointLookup(t *testing.T) { + t.Parallel() + + const height = int32(700) + + svc := &fakeArkService{responseFn: scaledEstimateFn} + r, vtxoStore := newRefreshEstimateServer(t, svc, height) + + desc := newRefreshEstimateVTXO(t, 0x03, 42_000, 1_000) + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), desc)) + + resp, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_Outpoints{ + Outpoints: &waverpc.OutpointSelection{ + Outpoints: []string{ + outpointStr(desc), + }, + }, + }, + DryRun: true, + }, + ) + require.NoError(t, err) + require.Equal(t, "preview", resp.Status) + + est := resp.FeeEstimate + require.NotNil(t, est) + require.Len(t, est.Outpoints, 1) + require.Equal(t, int64(42_000), est.Outpoints[0].AmountSat) + require.Equal(t, uint32(300), est.Outpoints[0].RemainingBlocks) + + require.NotNil(t, svc.lastRequest) + require.False( + t, svc.lastRequest.IsBoarding, + "refresh quotes must not be priced as boarding", + ) + require.Equal(t, int64(42_000), svc.lastRequest.AmountSat) + require.Equal(t, uint32(300), svc.lastRequest.RemainingBlocks) +} + +// TestRefreshDryRunEstimateUnknownOutpoint verifies an explicit outpoint +// the store does not know is a caller mistake (InvalidArgument naming +// the outpoint), not a silent echo: the previous behavior echoed any +// well-formed outpoint, which let a typo'd preview read as valid. +func TestRefreshDryRunEstimateUnknownOutpoint(t *testing.T) { + t.Parallel() + + svc := &fakeArkService{responseFn: scaledEstimateFn} + r, _ := newRefreshEstimateServer(t, svc, 700) + + missing := newRefreshEstimateVTXO(t, 0x04, 10_000, 1_000) + + _, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_Outpoints{ + Outpoints: &waverpc.OutpointSelection{ + Outpoints: []string{ + outpointStr(missing), + }, + }, + }, + DryRun: true, + }, + ) + require.Error(t, err) + require.Equal(t, codes.InvalidArgument, status.Code(err)) + require.Contains(t, err.Error(), outpointStr(missing)) +} + +// TestRefreshDryRunEstimateDedupesQuotes verifies VTXOs sharing an +// (amount, remaining-blocks) pair share one operator round-trip: a +// wallet-wide dry run over a round's fan-out must not turn into one +// RPC per VTXO. +func TestRefreshDryRunEstimateDedupesQuotes(t *testing.T) { + t.Parallel() + + const height = int32(500) + + svc := &fakeArkService{responseFn: scaledEstimateFn} + r, vtxoStore := newRefreshEstimateServer(t, svc, height) + + // Two identical (amount, expiry) pairs plus one distinct. + descA := newRefreshEstimateVTXO(t, 0x05, 50_000, 1_000) + descB := newRefreshEstimateVTXO(t, 0x06, 50_000, 1_000) + descC := newRefreshEstimateVTXO(t, 0x07, 80_000, 1_000) + for _, desc := range []*vtxo.Descriptor{descA, descB, descC} { + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), desc)) + } + + resp, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_All{ + All: true, + }, + DryRun: true, + }, + ) + require.NoError(t, err) + + est := resp.FeeEstimate + require.NotNil(t, est) + require.Len(t, est.Outpoints, 3) + require.Equal( + t, 2, svc.estimateFeeCalls, + "two distinct (amount, remaining) pairs, two quotes", + ) + + // The deduped pair still carries per-row components. + rows := make(map[string]*waverpc.OutpointFeeEstimate) + for _, row := range est.Outpoints { + rows[row.Outpoint] = row + } + require.Equal( + t, rows[outpointStr(descA)].TotalFeeSat, + rows[outpointStr(descB)].TotalFeeSat, + ) + require.Equal(t, int64(175), rows[outpointStr(descA)].TotalFeeSat) + require.Equal(t, int64(205), rows[outpointStr(descC)].TotalFeeSat) +} + +// TestRefreshDryRunEstimateFreeWindow covers the waiver-eligible +// selection: every VTXO inside the operator's advertised free-refresh +// window zeroes the selection total while the per-outpoint rows keep +// the ordinary paid quote, so the caller sees what the waiver saves. +func TestRefreshDryRunEstimateFreeWindow(t *testing.T) { + t.Parallel() + + const height = int32(950) + + svc := &fakeArkService{responseFn: scaledEstimateFn} + r, vtxoStore := newRefreshEstimateServer(t, svc, height) + r.server.storeOperatorTerms(&types.OperatorTerms{ + FreeRefreshWindowBlocks: 100, + }) + + // Remaining lifetimes 50 and 90: both inside the 100-block + // window. + descA := newRefreshEstimateVTXO(t, 0x08, 100_000, 1_000) + descB := newRefreshEstimateVTXO(t, 0x09, 200_000, 1_040) + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), descA)) + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), descB)) + + resp, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_All{ + All: true, + }, + DryRun: true, + }, + ) + require.NoError(t, err) + + est := resp.FeeEstimate + require.NotNil(t, est) + require.True(t, est.FreeRefreshEligible) + require.NotNil( + t, est.EstimatedTotalFeeSat, + "the waiver's zero total is explicit, not absent", + ) + require.Zero( + t, est.GetEstimatedTotalFeeSat(), + "waiver-eligible selection previews as free", + ) + + for _, row := range est.Outpoints { + require.True(t, row.InFreeRefreshWindow, row.Outpoint) + require.Positive( + t, row.TotalFeeSat, "rows keep the ordinary paid "+ + "quote so the caller sees what the waiver "+ + "saves", + ) + } +} + +// TestRefreshDryRunEstimateWindowBrokenByOneOutpoint verifies the +// waiver's all-or-nothing rule: one out-of-window VTXO in the selection +// prices the whole refresh as paid, mirroring the operator's seal-time +// isFreeRefresh predicate. +func TestRefreshDryRunEstimateWindowBrokenByOneOutpoint(t *testing.T) { + t.Parallel() + + const height = int32(950) + + svc := &fakeArkService{responseFn: scaledEstimateFn} + r, vtxoStore := newRefreshEstimateServer(t, svc, height) + r.server.storeOperatorTerms(&types.OperatorTerms{ + FreeRefreshWindowBlocks: 100, + }) + + // Remaining lifetimes 50 (inside) and 250 (outside). + inside := newRefreshEstimateVTXO(t, 0x0a, 100_000, 1_000) + outside := newRefreshEstimateVTXO(t, 0x0b, 200_000, 1_200) + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), inside)) + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), outside)) + + resp, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_All{ + All: true, + }, + DryRun: true, + }, + ) + require.NoError(t, err) + + est := resp.FeeEstimate + require.NotNil(t, est) + require.False(t, est.FreeRefreshEligible) + require.Positive(t, est.GetEstimatedTotalFeeSat()) + + rows := make(map[string]*waverpc.OutpointFeeEstimate) + for _, row := range est.Outpoints { + rows[row.Outpoint] = row + } + require.True(t, rows[outpointStr(inside)].InFreeRefreshWindow) + require.False(t, rows[outpointStr(outside)].InFreeRefreshWindow) +} + +// TestRefreshDryRunEstimateOperatorDown verifies the degraded mode the +// issue requires: with the operator unreachable the preview still +// returns (dry_run stays usable as a validity probe), the locally +// known facts stay populated, and estimate_error tells the caller the +// fee numbers are absent rather than zero. +func TestRefreshDryRunEstimateOperatorDown(t *testing.T) { + t.Parallel() + + const height = int32(900) + + svc := &fakeArkService{ + estimateFeeErr: status.Error( + codes.Unavailable, "operator down", + ), + } + r, vtxoStore := newRefreshEstimateServer(t, svc, height) + + desc := newRefreshEstimateVTXO(t, 0x0c, 100_000, 1_000) + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), desc)) + + resp, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_All{ + All: true, + }, + DryRun: true, + }, + ) + require.NoError(t, err, "estimate failure must not fail dry_run") + require.Equal(t, "preview", resp.Status) + + est := resp.FeeEstimate + require.NotNil(t, est) + require.NotEmpty(t, est.EstimateError) + require.NotContains( + t, est.EstimateError, "operator down", + "upstream error text must not leak to the caller", + ) + require.Nil( + t, est.EstimatedTotalFeeSat, "a degraded paid estimate "+ + "leaves the total absent so it can not be misread "+ + "as free", + ) + require.Len(t, est.Outpoints, 1) + + // Locally known facts survive the degradation. + require.Equal(t, int64(100_000), est.Outpoints[0].AmountSat) + require.Equal(t, uint32(100), est.Outpoints[0].RemainingBlocks) + require.Zero(t, est.Outpoints[0].TotalFeeSat) +} + +// TestRefreshDryRunEstimateOperatorDownFreeWindow verifies the +// locally computed waiver survives operator unavailability: a +// waiver-eligible selection still previews as free (with +// estimate_error set for the missing per-outpoint quotes). +func TestRefreshDryRunEstimateOperatorDownFreeWindow(t *testing.T) { + t.Parallel() + + const height = int32(950) + + svc := &fakeArkService{ + estimateFeeErr: status.Error( + codes.Unavailable, "operator down", + ), + } + r, vtxoStore := newRefreshEstimateServer(t, svc, height) + r.server.storeOperatorTerms(&types.OperatorTerms{ + FreeRefreshWindowBlocks: 100, + }) + + desc := newRefreshEstimateVTXO(t, 0x0d, 100_000, 1_000) + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), desc)) + + resp, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_All{ + All: true, + }, + DryRun: true, + }, + ) + require.NoError(t, err) + + est := resp.FeeEstimate + require.NotNil(t, est) + require.True(t, est.FreeRefreshEligible) + require.NotNil(t, est.EstimatedTotalFeeSat) + require.Zero(t, est.GetEstimatedTotalFeeSat()) + require.NotEmpty(t, est.EstimateError) +} + +// TestRefreshDryRunEstimateExpiredClampsRemaining verifies an already +// expired VTXO quotes at a clamped remaining lifetime of 1 instead of +// 0: the operator treats zero as "use the full sweep-delay lifetime", +// which would massively over-quote. An expired VTXO is also never +// waiver-eligible. +func TestRefreshDryRunEstimateExpiredClampsRemaining(t *testing.T) { + t.Parallel() + + const height = int32(2_000) + + svc := &fakeArkService{responseFn: scaledEstimateFn} + r, vtxoStore := newRefreshEstimateServer(t, svc, height) + r.server.storeOperatorTerms(&types.OperatorTerms{ + FreeRefreshWindowBlocks: 100, + }) + + // Batch expiry far behind the tip. + desc := newRefreshEstimateVTXO(t, 0x0e, 100_000, 1_000) + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), desc)) + + resp, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_All{ + All: true, + }, + DryRun: true, + }, + ) + require.NoError(t, err) + + est := resp.FeeEstimate + require.NotNil(t, est) + require.Len(t, est.Outpoints, 1) + require.Equal(t, uint32(1), est.Outpoints[0].RemainingBlocks) + require.False(t, est.Outpoints[0].InFreeRefreshWindow) + require.False(t, est.FreeRefreshEligible) + + require.NotNil(t, svc.lastRequest) + require.Equal(t, uint32(1), svc.lastRequest.RemainingBlocks) +} + +// TestRefreshDryRunEstimateRejectsNonsenseQuote verifies a negative +// operator quote is treated like an unreachable operator instead of +// being summed into a misleading total. +func TestRefreshDryRunEstimateRejectsNonsenseQuote(t *testing.T) { + t.Parallel() + + svc := &fakeArkService{ + response: &arkrpc.EstimateFeeResponse{ + TotalFeeSat: -1, + }, + } + r, vtxoStore := newRefreshEstimateServer(t, svc, 900) + + desc := newRefreshEstimateVTXO(t, 0x0f, 100_000, 1_000) + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), desc)) + + resp, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_All{ + All: true, + }, + DryRun: true, + }, + ) + require.NoError(t, err) + + est := resp.FeeEstimate + require.NotNil(t, est) + require.NotEmpty(t, est.EstimateError) + require.Nil(t, est.EstimatedTotalFeeSat) + require.Len(t, est.Outpoints, 1) + require.Zero(t, est.Outpoints[0].TotalFeeSat) +} + +// TestRefreshDryRunEmptyAllReturnsPreview pins the reordered empty +// case: selection=all over an empty store previews as "preview" (it +// used to report "queued" because the empty-target early return ran +// before the dry-run branch) and attaches no vacuous fee estimate a +// caller could misread as "free". +func TestRefreshDryRunEmptyAllReturnsPreview(t *testing.T) { + t.Parallel() + + svc := &fakeArkService{responseFn: scaledEstimateFn} + r, _ := newRefreshEstimateServer(t, svc, 900) + + resp, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_All{ + All: true, + }, + DryRun: true, + }, + ) + require.NoError(t, err) + require.Equal(t, "preview", resp.Status) + require.Empty(t, resp.QueuedOutpoints) + require.Nil(t, resp.FeeEstimate) + require.Zero(t, svc.estimateFeeCalls) +} + +// TestRefreshVTXOsSelectionRequiredBeforeWalletGate verifies +// pure-argument validation outranks the wallet-ready gate, matching +// the LeaveVTXOs ordering rule: a request-shape bug must surface as +// InvalidArgument regardless of wallet state. +func TestRefreshVTXOsSelectionRequiredBeforeWalletGate(t *testing.T) { + t.Parallel() + + r := newTestRPCServer() + + _, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{}, + ) + require.Error(t, err) + require.Equal(t, codes.InvalidArgument, status.Code(err)) +} + +// TestRefreshVTXOsRealPathStillRequiresWallet pins that only the +// dry-run preview moved ahead of the wallet-ready gate: a real refresh +// on an unready wallet still fails before any queuing. +func TestRefreshVTXOsRealPathStillRequiresWallet(t *testing.T) { + t.Parallel() + + r := newTestRPCServer() + + _, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_Outpoints{ + Outpoints: &waverpc.OutpointSelection{ + Outpoints: []string{ + "deadbeefdeadbeefdeadbeef" + + "deadbeefdeadbeefdead" + + "beefdeadbeef00000000" + + ":0", + }, + }, + }, + }, + ) + require.Error(t, err) + require.NotEqual( + t, codes.InvalidArgument, status.Code(err), + "a well-formed real refresh must fail on wallet readiness, "+ + "not argument validation", + ) +} + +// errChainBackend is a chain backend whose BestBlock always fails, for +// exercising the chain-height-unavailable degrade path. +type errChainBackend struct { + chainsource.ChainBackend +} + +func (b *errChainBackend) BestBlock(context.Context) (int32, chainhash.Hash, + error) { + + return 0, chainhash.Hash{}, fmt.Errorf("backend still syncing") +} + +// TestRefreshDryRunEstimateChainHeightUnavailable verifies the first +// degrade mode: a failing chain backend yields amount-only rows, an +// estimate error, no operator calls, an absent total — and a preview +// that still succeeds, keeping dry_run usable as a validity probe while +// the backend syncs. +func TestRefreshDryRunEstimateChainHeightUnavailable(t *testing.T) { + t.Parallel() + + svc := &fakeArkService{responseFn: scaledEstimateFn} + r, vtxoStore := newRefreshEstimateServer(t, svc, 0) + r.server.chainBackend = &errChainBackend{} + + desc := newRefreshEstimateVTXO(t, 0x10, 100_000, 1_000) + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), desc)) + + resp, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_All{ + All: true, + }, + DryRun: true, + }, + ) + require.NoError(t, err, "chain failure must not fail dry_run") + require.Equal(t, "preview", resp.Status) + + est := resp.FeeEstimate + require.NotNil(t, est) + require.Contains(t, est.EstimateError, "chain height") + require.Nil(t, est.EstimatedTotalFeeSat) + require.False(t, est.FreeRefreshEligible) + require.Len(t, est.Outpoints, 1) + require.Equal(t, int64(100_000), est.Outpoints[0].AmountSat) + require.Zero(t, est.Outpoints[0].RemainingBlocks) + require.Zero( + t, svc.estimateFeeCalls, + "no operator quote without a chain tip", + ) +} + +// TestRefreshDryRunEstimatePartialQuoteFailure verifies the +// all-or-nothing degrade when the operator fails partway through a +// multi-quote selection: no row may keep components from the quotes +// that succeeded before the failure, because estimate_error tells the +// caller the components are absent. +func TestRefreshDryRunEstimatePartialQuoteFailure(t *testing.T) { + t.Parallel() + + const height = int32(500) + + svc := &fakeArkService{ + responseFn: scaledEstimateFn, + errFn: func(req *arkrpc.EstimateFeeRequest) error { + if req.AmountSat == 80_000 { + return status.Error( + codes.Unavailable, "quote two down", + ) + } + + return nil + }, + } + r, vtxoStore := newRefreshEstimateServer(t, svc, height) + + descA := newRefreshEstimateVTXO(t, 0x11, 50_000, 1_000) + descB := newRefreshEstimateVTXO(t, 0x12, 80_000, 1_000) + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), descA)) + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), descB)) + + resp, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_All{ + All: true, + }, + DryRun: true, + }, + ) + require.NoError(t, err) + + est := resp.FeeEstimate + require.NotNil(t, est) + require.NotEmpty(t, est.EstimateError) + require.Nil(t, est.EstimatedTotalFeeSat) + + // ListLiveVTXOs enumeration order is storage-dependent, so the + // failing quote may be attempted first (one call) or second + // (two calls); either way the failure must abort further + // quoting. + require.GreaterOrEqual(t, svc.estimateFeeCalls, 1) + require.LessOrEqual(t, svc.estimateFeeCalls, 2) + + require.Len(t, est.Outpoints, 2) + for _, row := range est.Outpoints { + require.Zero(t, row.LiquidityFeeSat, row.Outpoint) + require.Zero(t, row.OnchainShareSat, row.Outpoint) + require.Zero(t, row.MarginSat, row.Outpoint) + require.Zero(t, row.TotalFeeSat, row.Outpoint) + } +} + +// TestRefreshDryRunEstimateRejectsNegativeComponent verifies the quote +// sanity check covers the component fields, not just the total: a +// negative component would break the documented invariant that a row's +// components sum to its total. +func TestRefreshDryRunEstimateRejectsNegativeComponent(t *testing.T) { + t.Parallel() + + svc := &fakeArkService{ + response: &arkrpc.EstimateFeeResponse{ + LiquidityFeeSat: -50, + MarginSat: 100, + TotalFeeSat: 50, + }, + } + r, vtxoStore := newRefreshEstimateServer(t, svc, 900) + + desc := newRefreshEstimateVTXO(t, 0x13, 100_000, 1_000) + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), desc)) + + resp, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_All{ + All: true, + }, + DryRun: true, + }, + ) + require.NoError(t, err) + + est := resp.FeeEstimate + require.NotNil(t, est) + require.Contains(t, est.EstimateError, "invalid") + require.Nil(t, est.EstimatedTotalFeeSat) + require.Len(t, est.Outpoints, 1) + require.Zero(t, est.Outpoints[0].LiquidityFeeSat) + require.Zero(t, est.Outpoints[0].TotalFeeSat) +} + +// TestRefreshDryRunEstimateWindowBoundary pins the inclusive window +// comparison against the operator's IsFreeRefreshWindow semantics: a +// VTXO at exactly the advertised window is waiver-eligible, one block +// beyond is not. +func TestRefreshDryRunEstimateWindowBoundary(t *testing.T) { + t.Parallel() + + const height = int32(900) + + cases := []struct { + name string + remaining int32 + eligible bool + }{ + { + name: "exactly at window", + remaining: 100, + eligible: true, + }, + { + name: "one beyond window", + remaining: 101, + eligible: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + svc := &fakeArkService{ + responseFn: scaledEstimateFn, + } + r, vtxoStore := newRefreshEstimateServer( + t, svc, height, + ) + r.server.storeOperatorTerms(&types.OperatorTerms{ + FreeRefreshWindowBlocks: 100, + }) + + desc := newRefreshEstimateVTXO( + t, 0x14, 100_000, height+tc.remaining, + ) + require.NoError( + t, + vtxoStore.SaveVTXO( + t.Context(), desc, + ), + ) + + resp, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc. + RefreshVTXOsRequest_All{ + All: true, + }, + DryRun: true, + }, + ) + require.NoError(t, err) + + est := resp.FeeEstimate + require.NotNil(t, est) + require.Len(t, est.Outpoints, 1) + require.Equal( + t, tc.eligible, + est.Outpoints[0].InFreeRefreshWindow, + ) + require.Equal( + t, tc.eligible, est.FreeRefreshEligible, + ) + }) + } +} + +// TestRefreshDryRunEstimateDuplicateOutpointCollapsed verifies a +// repeated explicit outpoint is collapsed before resolution: one row, +// one echo, one quote — never a doubled total for a single VTXO. +func TestRefreshDryRunEstimateDuplicateOutpointCollapsed(t *testing.T) { + t.Parallel() + + const height = int32(700) + + svc := &fakeArkService{responseFn: scaledEstimateFn} + r, vtxoStore := newRefreshEstimateServer(t, svc, height) + + desc := newRefreshEstimateVTXO(t, 0x15, 50_000, 1_000) + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), desc)) + + resp, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_Outpoints{ + Outpoints: &waverpc.OutpointSelection{ + Outpoints: []string{ + outpointStr(desc), + outpointStr(desc), + }, + }, + }, + DryRun: true, + }, + ) + require.NoError(t, err) + require.Equal(t, []string{outpointStr(desc)}, resp.QueuedOutpoints) + + est := resp.FeeEstimate + require.NotNil(t, est) + require.Len(t, est.Outpoints, 1) + require.Equal( + t, int64(175), est.GetEstimatedTotalFeeSat(), + "one VTXO, one quote, never a doubled total", + ) + require.Equal(t, 1, svc.estimateFeeCalls) +} + +// TestRefreshDryRunEstimateRejectsNonLiveOutpoint verifies an explicit +// outpoint in a non-live state is a caller mistake, mirroring the +// --all branch's LiveState filter: the real refresh can never execute +// it, so a preview that echoed and priced it would overpromise +// validity right where the CLI consent prompt trusts it. +func TestRefreshDryRunEstimateRejectsNonLiveOutpoint(t *testing.T) { + t.Parallel() + + svc := &fakeArkService{responseFn: scaledEstimateFn} + r, vtxoStore := newRefreshEstimateServer(t, svc, 700) + + desc := newRefreshEstimateVTXO(t, 0x16, 50_000, 1_000) + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), desc)) + + // SaveVTXO persists rows as live; move the fixture into its + // non-live state explicitly. + require.NoError( + t, + vtxoStore.UpdateVTXOStatus( + t.Context(), desc.Outpoint, vtxo.VTXOStatusSpent, + ), + ) + + _, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_Outpoints{ + Outpoints: &waverpc.OutpointSelection{ + Outpoints: []string{ + outpointStr(desc), + }, + }, + }, + DryRun: true, + }, + ) + require.Error(t, err) + require.Equal(t, codes.InvalidArgument, status.Code(err)) + require.Contains(t, err.Error(), "not refreshable") + require.Zero(t, svc.estimateFeeCalls) +} + +// TestRefreshDryRunNilStoreEchoesExplicit pins the store-less degrade: +// the preview echo for explicit outpoints survives (as before this +// feature) and the estimate degrades explicitly instead of silently +// vanishing. +func TestRefreshDryRunNilStoreEchoesExplicit(t *testing.T) { + t.Parallel() + + r := newTestRPCServer() + + op := "1111111111111111111111111111111111111111111111111111111111" + + "111111:0" + resp, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_Outpoints{ + Outpoints: &waverpc.OutpointSelection{ + Outpoints: []string{op}, + }, + }, + DryRun: true, + }, + ) + require.NoError(t, err) + require.Equal(t, "preview", resp.Status) + require.Len(t, resp.QueuedOutpoints, 1) + + est := resp.FeeEstimate + require.NotNil(t, est) + require.Contains(t, est.EstimateError, "store unavailable") + require.Nil(t, est.EstimatedTotalFeeSat) +} + +// refreshTestWallet is a minimal wallet actor recording the refresh +// requests it receives, for asserting what the real (non-dry-run) +// path dispatches. +type refreshTestWallet struct { + mu sync.Mutex + reqs []*wallet.RefreshVTXOsRequest +} + +func (w *refreshTestWallet) Receive(_ context.Context, + msg wallet.WalletMsg) fn.Result[wallet.WalletResp] { + + w.mu.Lock() + defer w.mu.Unlock() + + switch msg := msg.(type) { + case *wallet.RefreshVTXOsRequest: + reqCopy := *msg + w.reqs = append(w.reqs, &reqCopy) + + return fn.Ok[wallet.WalletResp]( + &wallet.RefreshVTXOsResponse{ + RefreshingCount: len(msg.TargetOutpoints), + }, + ) + + default: + return fn.Err[wallet.WalletResp]( + fmt.Errorf("unexpected wallet msg %T", msg), + ) + } +} + +// TestRefreshVTXOsAllQueuesLiveTargets pins the real (non-dry-run) +// --all path after the handler restructure: live VTXOs are expanded +// into explicit wallet targets, non-live rows are filtered, and the +// response echoes what the wallet actually queued. +func TestRefreshVTXOsAllQueuesLiveTargets(t *testing.T) { + t.Parallel() + + svc := &fakeArkService{responseFn: scaledEstimateFn} + r, vtxoStore := newRefreshEstimateServer(t, svc, 900) + + live1 := newRefreshEstimateVTXO(t, 0x17, 40_000, 1_000) + live2 := newRefreshEstimateVTXO(t, 0x18, 60_000, 1_000) + pending := newRefreshEstimateVTXO(t, 0x19, 80_000, 1_000) + for _, desc := range []*vtxo.Descriptor{live1, live2, pending} { + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), desc)) + } + + // SaveVTXO persists rows as live; move the in-flight fixture + // into PendingForfeit explicitly so the filter has something + // to exclude. + require.NoError( + t, + vtxoStore.UpdateVTXOStatus( + t.Context(), pending.Outpoint, + vtxo.VTXOStatusPendingForfeit, + ), + ) + + system := actor.NewActorSystem() + t.Cleanup(func() { + shutdownCtx, cancel := context.WithTimeout( + context.Background(), 5*time.Second, + ) + defer cancel() + + require.NoError(t, system.Shutdown(shutdownCtx)) + }) + + testWallet := &refreshTestWallet{} + walletKey := actor.NewServiceKey[ + wallet.WalletMsg, wallet.WalletResp, + ]( + "refresh-all-test-wallet", + ) + walletRef := walletKey.Spawn( + system, "refresh-all-test-wallet", testWallet, + ) + + walletReady := make(chan struct{}) + close(walletReady) + r.server.walletReady = walletReady + r.server.walletRef = fn.Some(walletRef) + + resp, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_All{ + All: true, + }, + }, + ) + require.NoError(t, err) + require.Equal(t, "queued", resp.Status) + require.ElementsMatch( + t, []string{outpointStr(live1), outpointStr(live2)}, + resp.QueuedOutpoints, + ) + require.Nil(t, resp.FeeEstimate, + "the real path attaches no estimate") + + require.Len(t, testWallet.reqs, 1) + require.ElementsMatch( + t, []wire.OutPoint{live1.Outpoint, live2.Outpoint}, + testWallet.reqs[0].TargetOutpoints, + "only live VTXOs reach the wallet actor", + ) + require.True(t, testWallet.reqs[0].ForceRefresh) +} diff --git a/waved/rpc_server.go b/waved/rpc_server.go index b070b8a60..e15dbd472 100644 --- a/waved/rpc_server.go +++ b/waved/rpc_server.go @@ -1533,6 +1533,68 @@ func (r *RPCServer) RefreshVTXOs(ctx context.Context, req *waverpc.RefreshVTXOsRequest) (*waverpc.RefreshVTXOsResponse, error) { + // Pure-argument validation (selection shape / outpoint syntax) + // runs before the wallet-ready gate so a malformed request + // surfaces InvalidArgument regardless of wallet state, matching + // the LeaveVTXOs ordering rule: client bugs should always look + // like client bugs. + var ( + explicit []wire.OutPoint + refreshAll bool + ) + switch sel := req.Selection.(type) { + case *waverpc.RefreshVTXOsRequest_All: + if !sel.All { + return nil, status.Errorf(codes.InvalidArgument, + "refresh selection 'all' must be true when set") + } + refreshAll = true + + case *waverpc.RefreshVTXOsRequest_Outpoints: + if sel.Outpoints == nil || + len(sel.Outpoints.Outpoints) == 0 { + return nil, status.Errorf(codes.InvalidArgument, + "outpoints list is empty") + } + + // Dedupe while preserving order so both the preview and + // the real path see one canonical selection: a repeated + // outpoint would double-count in the fee estimate and + // register a doomed duplicate forfeit pair in the wallet. + seen := make( + map[wire.OutPoint]struct{}, + len(sel.Outpoints.Outpoints), + ) + for _, opStr := range sel.Outpoints.Outpoints { + op, err := parseOutpointString(opStr) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, + "invalid outpoint %q: %v", opStr, err) + } + + if _, dup := seen[op]; dup { + continue + } + seen[op] = struct{}{} + + explicit = append(explicit, op) + } + + default: + return nil, status.Errorf(codes.InvalidArgument, "selection "+ + "is required (outpoints or all)") + } + + // The dry-run preview short-circuits before the wallet-ready + // gate (parity with the LeaveVTXOs dry-run ordering): resolving + // the selection and pricing it only needs the SQL VTXO view, the + // chain tip, and the operator connection, so callers can preview + // a refresh — including its advisory fee — without an unlocked + // wallet. + if req.DryRun { + return r.refreshDryRunPreview(ctx, explicit, refreshAll) + } + if err := r.requireWalletReady(); err != nil { return nil, err } @@ -1554,15 +1616,9 @@ func (r *RPCServer) RefreshVTXOs(ctx context.Context, // synthesised `queued_outpoints` from a separate ListLiveVTXOs // call below, which made the caller believe the refresh // succeeded. - var targets []wire.OutPoint - - switch sel := req.Selection.(type) { - case *waverpc.RefreshVTXOsRequest_All: - if !sel.All { - return nil, status.Errorf(codes.InvalidArgument, - "refresh selection 'all' must be true when set") - } + targets := explicit + if refreshAll { if r.server.vtxoStore == nil { return nil, status.Errorf(codes.Internal, "VTXO "+ "store not available for refresh --all") @@ -1589,27 +1645,6 @@ func (r *RPCServer) RefreshVTXOs(ctx context.Context, } targets = append(targets, v.Outpoint) } - - case *waverpc.RefreshVTXOsRequest_Outpoints: - if sel.Outpoints == nil || - len(sel.Outpoints.Outpoints) == 0 { - return nil, status.Errorf(codes.InvalidArgument, - "outpoints list is empty") - } - - for _, opStr := range sel.Outpoints.Outpoints { - op, err := parseOutpointString(opStr) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, - "invalid outpoint %q: %v", opStr, err) - } - - targets = append(targets, op) - } - - default: - return nil, status.Errorf(codes.InvalidArgument, "selection "+ - "is required (outpoints or all)") } // `--all` against a wallet with no live VTXOs is not an error — @@ -1622,23 +1657,6 @@ func (r *RPCServer) RefreshVTXOs(ctx context.Context, }, nil } - // For dry_run, validate inputs and return a preview without - // actually queuing anything. - if req.DryRun { - outpointStrs := make([]string, 0, len(targets)) - for _, op := range targets { - outpointStrs = append( - outpointStrs, - fmt.Sprintf("%s:%d", op.Hash, op.Index), - ) - } - - return &waverpc.RefreshVTXOsResponse{ - QueuedOutpoints: outpointStrs, - Status: "preview", - }, nil - } - // Send the refresh request to the wallet actor and await its // response. refreshReq := &wallet.RefreshVTXOsRequest{ From c8a70bb3168e5f0bf5f10c0416c76297f76a47e4 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Fri, 17 Jul 2026 08:50:47 -0500 Subject: [PATCH 3/9] wavecli: Surface refresh fee estimate on dry run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In this commit, we render the dry-run fee estimate on the refresh command. The itemized numbers stay in the JSON body on stdout; stderr gains a short human-readable summary so an operator reading the terminal sees the headline cost — or the free-refresh-window verdict, or the degraded-mode warning — without stdout consumers having to strip prose. Every wording branch repeats that the value is advisory and the binding fee is set at seal time, and the degraded branch explicitly says a fee still applies so a missing estimate is never read as a free refresh. VTXOs below the operator's minimum viable amount add a count-level warning, mirroring the fees estimate command's below-dust handling. A non-empty dry-run preview that carries no estimate at all can only come from a daemon predating the feature, so the CLI warns about that skew explicitly instead of silently dropping the preview the flag help promises. The fees estimate help now points refresh users at `ark vtxos refresh --dry_run`, which resolves each selected VTXO's amount and remaining lifetime automatically instead of requiring both by hand. Part of #986. (cherry picked from commit 1a25ab3d0ba0a0da95d24c9ebbf5791cf7637e6a) --- cmd/wavecli/waveclicommands/cmd_fees.go | 8 +- cmd/wavecli/waveclicommands/cmd_vtxos.go | 104 ++++++++++++++++- .../waveclicommands/cmd_vtxos_refresh_test.go | 109 ++++++++++++++++++ 3 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 cmd/wavecli/waveclicommands/cmd_vtxos_refresh_test.go diff --git a/cmd/wavecli/waveclicommands/cmd_fees.go b/cmd/wavecli/waveclicommands/cmd_fees.go index d533688e7..6df026f07 100644 --- a/cmd/wavecli/waveclicommands/cmd_fees.go +++ b/cmd/wavecli/waveclicommands/cmd_fees.go @@ -39,7 +39,13 @@ func newFeesEstimateCmd() *cobra.Command { "seal time and may differ from this estimate. " + "Shows liquidity fee, on-chain share, margin, " + "total fee, effective rate, and minimum " + - "viable VTXO.", + "viable VTXO.\n\n" + + "To preview the fee for refreshing specific " + + "VTXOs without looking up their amounts and " + + "remaining lifetimes by hand, use `ark vtxos " + + "refresh --dry_run` instead: it resolves each " + + "selected VTXO and returns a per-outpoint " + + "estimate.", RunE: feesEstimate, } diff --git a/cmd/wavecli/waveclicommands/cmd_vtxos.go b/cmd/wavecli/waveclicommands/cmd_vtxos.go index e2cb840be..7d01a5907 100644 --- a/cmd/wavecli/waveclicommands/cmd_vtxos.go +++ b/cmd/wavecli/waveclicommands/cmd_vtxos.go @@ -161,7 +161,8 @@ func newVTXOsRefreshCmd() *cobra.Command { "refresh all live VTXOs") cmd.Flags().Bool("dry_run", false, - "validate without queuing") + "validate without queuing and preview the estimated "+ + "operator fee") cmd.Flags().Bool("no_join", false, "skip the implicit `ark rounds join` follow-up so this "+ @@ -221,11 +222,112 @@ func vtxosRefresh(cmd *cobra.Command, _ []string) error { return err } + // The itemized estimate is in the JSON body on stdout; the + // summary goes to stderr so a human reading the terminal sees + // the headline number without stdout consumers having to strip + // prose. + for _, line := range summarizeRefreshFeeEstimate( + resp.GetFeeEstimate(), + ) { + fmt.Fprintln(cmd.ErrOrStderr(), line) + } + + // A non-empty dry-run preview without an estimate can only come + // from a daemon that predates the fee preview: the daemon omits + // the estimate solely for empty selections. Warn rather than + // stay silent — the flag help promises a fee preview, and + // silence here would read as "free". + if req.DryRun && len(resp.GetQueuedOutpoints()) > 0 && + resp.GetFeeEstimate() == nil { + + fmt.Fprintln( + cmd.ErrOrStderr(), + "warning: the daemon returned no fee estimate "+ + "(older daemon?); a refresh is still "+ + "charged the binding seal-time fee", + ) + } + noJoin, _ := cmd.Flags().GetBool("no_join") return maybeJoinNextRound(cmd, client, req.DryRun, noJoin) } +// summarizeRefreshFeeEstimate renders the dry-run fee estimate as +// human-readable stderr lines. Split from the IO wrapper so the +// wording rules are covered by a focused unit test. A nil estimate +// (real refresh, empty selection) renders nothing. +// +// Every branch repeats that the number is advisory: the binding fee is +// set by the seal-time quote, and telling the user otherwise here +// would overpromise exactly the guarantee the refresh flow cannot +// make. +func summarizeRefreshFeeEstimate(est *waverpc.RefreshFeeEstimate) []string { + if est == nil { + return nil + } + + var lines []string + + count := len(est.Outpoints) + + switch { + case est.EstimateError != "": + lines = append( + lines, fmt.Sprintf("warning: refresh fee estimate "+ + "unavailable (%s); a refresh is still "+ + "charged the binding seal-time fee", + est.EstimateError), + ) + + if est.FreeRefreshEligible { + lines = append( + lines, fmt.Sprintf("expected free: all %d "+ + "selected VTXO(s) are inside the "+ + "operator's free-refresh window "+ + "(advisory)", count), + ) + } + + case est.FreeRefreshEligible: + lines = append( + lines, fmt.Sprintf("estimated refresh fee: 0 sat — "+ + "all %d selected VTXO(s) are inside the "+ + "operator's free-refresh window (advisory; "+ + "the binding fee is set at seal time)", count), + ) + + default: + lines = append( + lines, + fmt.Sprintf( + "estimated refresh fee: %d sat across %d "+ + "VTXO(s) (advisory; the binding fee "+ + "is set at seal time)", + est.GetEstimatedTotalFeeSat(), count, + ), + ) + } + + var belowDust int + for _, row := range est.Outpoints { + if row.BelowDustWarning { + belowDust++ + } + } + if belowDust > 0 { + lines = append( + lines, fmt.Sprintf("warning: %d selected VTXO(s) are "+ + "below the operator's minimum viable amount; "+ + "refreshing them pays fees on coins that "+ + "already cost more to exit than they hold", + belowDust), + ) + } + + return lines +} + // newVTXOsLeaveCmd creates the vtxos leave subcommand. func newVTXOsLeaveCmd() *cobra.Command { cmd := &cobra.Command{ diff --git a/cmd/wavecli/waveclicommands/cmd_vtxos_refresh_test.go b/cmd/wavecli/waveclicommands/cmd_vtxos_refresh_test.go new file mode 100644 index 000000000..eeabfd55e --- /dev/null +++ b/cmd/wavecli/waveclicommands/cmd_vtxos_refresh_test.go @@ -0,0 +1,109 @@ +package waveclicommands + +import ( + "testing" + + "github.com/lightninglabs/wavelength/waverpc" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +// TestSummarizeRefreshFeeEstimateNil verifies a response without an +// estimate (real refresh, empty selection) renders no stderr lines. +func TestSummarizeRefreshFeeEstimateNil(t *testing.T) { + t.Parallel() + + require.Empty(t, summarizeRefreshFeeEstimate(nil)) +} + +// TestSummarizeRefreshFeeEstimatePaid verifies the ordinary paid +// preview renders the headline total, the selection size, and the +// advisory caveat. +func TestSummarizeRefreshFeeEstimatePaid(t *testing.T) { + t.Parallel() + + lines := summarizeRefreshFeeEstimate(&waverpc.RefreshFeeEstimate{ + EstimatedTotalFeeSat: proto.Int64(1_234), + Outpoints: []*waverpc.OutpointFeeEstimate{ + {Outpoint: "aa:0"}, {Outpoint: "bb:1"}, + }, + }) + require.Len(t, lines, 1) + require.Contains(t, lines[0], "1234 sat") + require.Contains(t, lines[0], "2 VTXO(s)") + require.Contains(t, lines[0], "advisory") + require.Contains(t, lines[0], "seal time") +} + +// TestSummarizeRefreshFeeEstimateFree verifies a waiver-eligible +// selection leads with the zero fee and names the free-refresh window +// as the reason. +func TestSummarizeRefreshFeeEstimateFree(t *testing.T) { + t.Parallel() + + lines := summarizeRefreshFeeEstimate(&waverpc.RefreshFeeEstimate{ + FreeRefreshEligible: true, + Outpoints: []*waverpc.OutpointFeeEstimate{ + {Outpoint: "aa:0", TotalFeeSat: 500}, + }, + }) + require.Len(t, lines, 1) + require.Contains(t, lines[0], "0 sat") + require.Contains(t, lines[0], "free-refresh window") + require.Contains(t, lines[0], "advisory") +} + +// TestSummarizeRefreshFeeEstimateError verifies a degraded estimate +// warns that the numbers are absent — and still says a fee applies — +// rather than printing a zero a user could misread as free. +func TestSummarizeRefreshFeeEstimateError(t *testing.T) { + t.Parallel() + + lines := summarizeRefreshFeeEstimate(&waverpc.RefreshFeeEstimate{ + EstimateError: "operator fee estimate unavailable", + Outpoints: []*waverpc.OutpointFeeEstimate{ + {Outpoint: "aa:0"}, + }, + }) + require.Len(t, lines, 1) + require.Contains(t, lines[0], "warning") + require.Contains(t, lines[0], "operator fee estimate unavailable") + require.Contains(t, lines[0], "still charged") + require.NotContains(t, lines[0], "0 sat") +} + +// TestSummarizeRefreshFeeEstimateErrorFreeWindow verifies the locally +// computed waiver still surfaces when the operator quote failed: the +// user learns the refresh is expected free even in degraded mode. +func TestSummarizeRefreshFeeEstimateErrorFreeWindow(t *testing.T) { + t.Parallel() + + lines := summarizeRefreshFeeEstimate(&waverpc.RefreshFeeEstimate{ + EstimateError: "operator fee estimate unavailable", + FreeRefreshEligible: true, + Outpoints: []*waverpc.OutpointFeeEstimate{ + {Outpoint: "aa:0"}, + }, + }) + require.Len(t, lines, 2) + require.Contains(t, lines[0], "warning") + require.Contains(t, lines[1], "expected free") +} + +// TestSummarizeRefreshFeeEstimateBelowDust verifies uneconomic VTXOs in +// the selection add a count-level warning on top of the fee headline. +func TestSummarizeRefreshFeeEstimateBelowDust(t *testing.T) { + t.Parallel() + + lines := summarizeRefreshFeeEstimate(&waverpc.RefreshFeeEstimate{ + EstimatedTotalFeeSat: proto.Int64(400), + Outpoints: []*waverpc.OutpointFeeEstimate{ + {Outpoint: "aa:0", BelowDustWarning: true}, + {Outpoint: "bb:1"}, + {Outpoint: "cc:2", BelowDustWarning: true}, + }, + }) + require.Len(t, lines, 2) + require.Contains(t, lines[1], "2 selected VTXO(s)") + require.Contains(t, lines[1], "minimum viable") +} From f0fdbee0c7818894f7d5ac77ba562093016b6cb7 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Fri, 17 Jul 2026 09:00:14 -0500 Subject: [PATCH 4/9] wavecli: Gate refresh behind fee confirmation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In this commit, we stop dispatching a real refresh without consent. Before this change the number a dry run can now surface was not actionable: by the time a user read anything, the refresh was queued, auto-joined, and priced by the auto-accepted seal-time quote, with no cancel surface anywhere in between. The refresh command now mirrors the leave --all consent pattern: --yes skips the gate for scripted use, non-interactive stdin refuses to prompt and directs the caller to --yes or --dry_run (so agents are never blocked, per the issue's acceptance criteria), and only an interactive TTY prompts. The interactive prompt shows the advisory estimate first — fetched through the same RPC in dry-run form — so the operator consents to a number rather than a mystery fee, and the prompt goes to stderr: stdout stays reserved for the JSON body, so a piped invocation can never swallow the question and read as a hung command. A preview rejected as InvalidArgument aborts outright (the real dispatch would reject the same request shape); any other preview failure degrades to prompting with an explicit "still charged the seal-time fee" warning, so a broken estimate path never makes refreshes unconfirmable and a missing estimate is never read as free. An empty selection (refresh --all with no live VTXOs) skips the prompt entirely — there is no fee to consent to, and warning about a charge for a no-op would be false. The schema registry entry gains the yes parameter and now names the fee in its description, so schema consumers see the same contract as the flag surface. getDaemonClient becomes a package-level indirection (mirroring stdinIsTTY) so wiring tests can drive the full command path against an in-process bufconn daemon and pin that the gate runs before any dispatch: a refused invocation must reach the daemon zero times. Part of #986. (cherry picked from commit 4f4cf8c32ec82213f087aeb98caae97448ff49e2) --- cmd/wavecli/waveclicommands/client.go | 11 +- cmd/wavecli/waveclicommands/cmd_vtxos.go | 150 +++++ .../waveclicommands/cmd_vtxos_refresh_test.go | 539 ++++++++++++++++++ .../waveclicommands/schema_registry.go | 12 +- 4 files changed, 709 insertions(+), 3 deletions(-) diff --git a/cmd/wavecli/waveclicommands/client.go b/cmd/wavecli/waveclicommands/client.go index 6eb7d28f9..e60f7ea12 100644 --- a/cmd/wavecli/waveclicommands/client.go +++ b/cmd/wavecli/waveclicommands/client.go @@ -97,8 +97,15 @@ func getDaemonConn(cmd *cobra.Command) (*grpc.ClientConn, error) { // getDaemonClient establishes a gRPC connection to the daemon and returns a // DaemonServiceClient. The caller is responsible for closing the returned -// connection. -func getDaemonClient(cmd *cobra.Command) (waverpc.DaemonServiceClient, +// connection. Package-level indirection over defaultGetDaemonClient so +// command-wiring tests can substitute an in-process bufconn daemon +// (mirroring the stdinIsTTY indirection); production code never +// overrides it. +var getDaemonClient = defaultGetDaemonClient + +// defaultGetDaemonClient dials the daemon from the command's connection +// flags. +func defaultGetDaemonClient(cmd *cobra.Command) (waverpc.DaemonServiceClient, *grpc.ClientConn, error) { conn, err := getDaemonConn(cmd) diff --git a/cmd/wavecli/waveclicommands/cmd_vtxos.go b/cmd/wavecli/waveclicommands/cmd_vtxos.go index 7d01a5907..e7f0def65 100644 --- a/cmd/wavecli/waveclicommands/cmd_vtxos.go +++ b/cmd/wavecli/waveclicommands/cmd_vtxos.go @@ -9,6 +9,8 @@ import ( "github.com/lightninglabs/wavelength/waverpc" "github.com/spf13/cobra" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" ) @@ -140,6 +142,13 @@ func newVTXOsRefreshCmd() *cobra.Command { Long: "Queues one or more VTXOs for refresh and then " + "commits the queued intents to the next round, " + "extending the VTXOs' expiry.\n\n" + + "A refresh is charged an operator fee, set by the " + + "server-issued quote at seal time. Pass " + + "`--dry_run` to preview an itemized advisory " + + "estimate without queuing anything. An " + + "interactive refresh shows the estimate and asks " + + "for confirmation; pass `--yes` to skip the " + + "prompt (required on non-interactive stdin).\n\n" + "Under the hood the refresh intent first " + "accumulates in the round actor's " + "PendingRoundAssembly state. By default this " + @@ -164,6 +173,9 @@ func newVTXOsRefreshCmd() *cobra.Command { "validate without queuing and preview the estimated "+ "operator fee") + cmd.Flags().Bool("yes", false, + "skip the interactive fee confirmation") + cmd.Flags().Bool("no_join", false, "skip the implicit `ark rounds join` follow-up so this "+ "refresh can batch with other queued intents") @@ -211,6 +223,16 @@ func vtxosRefresh(cmd *cobra.Command, _ []string) error { return err } + if err := confirmRefreshIfNeeded( + cmd, req, func(preview *waverpc.RefreshVTXOsRequest) ( + *waverpc.RefreshVTXOsResponse, error) { + + return client.RefreshVTXOs(cmd.Context(), preview) + }, + ); err != nil { + return err + } + resp, err := client.RefreshVTXOs( cmd.Context(), req, ) @@ -328,6 +350,134 @@ func summarizeRefreshFeeEstimate(est *waverpc.RefreshFeeEstimate) []string { return lines } +// confirmRefreshIfNeeded gates dispatch of a real refresh on explicit +// consent: every refresh is charged an operator fee at seal time, and +// before this gate the CLI queued the refresh without ever mentioning +// it (#986). The check runs after parseRequest returns so it covers +// both the flag-driven path and the --json path, mirroring +// confirmLeaveAllIfNeeded. Skip when --yes is set (explicit scripted +// consent) or when req.DryRun is set (previewing, not spending). +// +// When stdin is not a TTY (agents, CI, pipelines), the function +// refuses to prompt — the caller must pass --yes or --dry_run — so a +// non-interactive invocation is never blocked on a y/N it cannot +// answer. Only on a TTY does it fetch the advisory estimate (via the +// same RPC in dry-run form, through fetchPreview) and prompt, so the +// operator consents to a number rather than a mystery fee. A preview +// that fails with InvalidArgument aborts — the real refresh would +// reject the same request shape — while any other preview failure +// degrades to prompting with an "estimate unavailable" warning: a +// broken estimate path must not make refreshes unconfirmable. +func confirmRefreshIfNeeded(cmd *cobra.Command, + req *waverpc.RefreshVTXOsRequest, + fetchPreview func(*waverpc.RefreshVTXOsRequest) ( + *waverpc.RefreshVTXOsResponse, error)) error { + + if req.DryRun { + return nil + } + + confirmYes, _ := cmd.Flags().GetBool("yes") + if confirmYes { + return nil + } + + if !stdinIsTTY(cmd) { + return PrintError( + "INVALID_ARGS", "refresh is charged an operator "+ + "fee at seal time and requires --yes "+ + "(explicit consent) or --dry_run (fee "+ + "preview) on non-interactive stdin; "+ + "refusing to prompt because an agent "+ + "cannot respond to y/N", + ) + } + + cloned := proto.Clone(req) + preview, ok := cloned.(*waverpc.RefreshVTXOsRequest) + if !ok { + + // Unreachable: proto.Clone preserves the concrete type. + return fmt.Errorf("unexpected preview request type %T", cloned) + } + preview.DryRun = true + + var lines []string + resp, err := fetchPreview(preview) + switch { + case status.Code(err) == codes.InvalidArgument: + // The dry-run validates the same request shape the real + // dispatch would; a caller mistake (unknown or malformed + // outpoint) should abort here, not after a confirmed + // queue. + return fmt.Errorf("refresh preview rejected: %w", err) + + case err != nil: + lines = []string{ + "warning: refresh fee estimate unavailable " + + "(preview failed); a refresh is still " + + "charged the binding seal-time fee", + } + + default: + // An empty preview means the selection resolves to + // nothing (refresh --all with no live VTXOs): the real + // dispatch queues nothing and charges nothing, so there + // is no fee to consent to and warning about a charge + // would be false. + if resp.GetStatus() == "preview" && + len(resp.GetQueuedOutpoints()) == 0 { + + fmt.Fprintln( + cmd.ErrOrStderr(), + "no live VTXOs to refresh; nothing will be "+ + "queued", + ) + + return nil + } + + lines = summarizeRefreshFeeEstimate(resp.GetFeeEstimate()) + if len(lines) == 0 { + lines = []string{ + "warning: the daemon returned no fee " + + "estimate; a refresh is still " + + "charged the binding seal-time fee", + } + } + } + + return promptRefreshConfirmation(cmd, lines) +} + +// promptRefreshConfirmation shows the fee-estimate lines and asks the +// operator to confirm the refresh on stdin. Any answer other than "y" +// or "yes" (case-insensitive) aborts the command. Only called when +// stdin is known to be a TTY (see confirmRefreshIfNeeded). All prompt +// output goes to stderr: stdout is reserved for the JSON body, and a +// prompt swallowed by a shell pipe would read as a hung command. +func promptRefreshConfirmation(cmd *cobra.Command, lines []string) error { + out := cmd.ErrOrStderr() + + for _, line := range lines { + fmt.Fprintln(out, line) + } + fmt.Fprint(out, "Proceed with refresh? [y/N]: ") + + reader := bufio.NewReader(cmd.InOrStdin()) + answer, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("read confirmation: %w", err) + } + + answer = strings.TrimSpace(strings.ToLower(answer)) + if answer != "y" && answer != "yes" { + return fmt.Errorf("aborted by user") + } + + return nil +} + // newVTXOsLeaveCmd creates the vtxos leave subcommand. func newVTXOsLeaveCmd() *cobra.Command { cmd := &cobra.Command{ diff --git a/cmd/wavecli/waveclicommands/cmd_vtxos_refresh_test.go b/cmd/wavecli/waveclicommands/cmd_vtxos_refresh_test.go index eeabfd55e..f85b65ab0 100644 --- a/cmd/wavecli/waveclicommands/cmd_vtxos_refresh_test.go +++ b/cmd/wavecli/waveclicommands/cmd_vtxos_refresh_test.go @@ -1,13 +1,270 @@ package waveclicommands import ( + "bytes" + "context" + "net" + "strings" + "sync" "testing" "github.com/lightninglabs/wavelength/waverpc" + "github.com/spf13/cobra" "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + "google.golang.org/grpc/test/bufconn" "google.golang.org/protobuf/proto" ) +// newRefreshTestCmd returns a vtxos refresh cobra command with the +// supplied stdin plumbed in so the prompt path is testable, plus the +// combined output buffer for wording assertions. +func newRefreshTestCmd(t *testing.T, + stdin string) (*cobra.Command, *bytes.Buffer) { + + t.Helper() + + cmd := newVTXOsRefreshCmd() + cmd.SetContext(t.Context()) + cmd.SetIn(strings.NewReader(stdin)) + + out := &bytes.Buffer{} + cmd.SetOut(out) + cmd.SetErr(out) + + return cmd, out +} + +// explicitRefreshReq returns a well-formed single-outpoint refresh +// request for gate tests. +func explicitRefreshReq() *waverpc.RefreshVTXOsRequest { + return &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_Outpoints{ + Outpoints: &waverpc.OutpointSelection{ + Outpoints: []string{ + "aa:0", + }, + }, + }, + } +} + +// staticPreview returns a fetchPreview stub that yields a fixed +// response and records whether it was called. +func staticPreview(called *bool, resp *waverpc.RefreshVTXOsResponse, + err error, +) func(*waverpc.RefreshVTXOsRequest) (*waverpc.RefreshVTXOsResponse, error) { + + return func(req *waverpc.RefreshVTXOsRequest) ( + *waverpc.RefreshVTXOsResponse, error) { + + *called = true + + return resp, err + } +} + +// TestConfirmRefreshDryRunBypasses verifies a dry-run request is not +// gated: the user is previewing, not spending. +func TestConfirmRefreshDryRunBypasses(t *testing.T) { + t.Parallel() + + cmd, _ := newRefreshTestCmd(t, "" /* no stdin */) + + req := explicitRefreshReq() + req.DryRun = true + + var previewed bool + require.NoError( + t, + confirmRefreshIfNeeded( + cmd, req, staticPreview(&previewed, nil, nil), + ), + ) + require.False(t, previewed) +} + +// TestConfirmRefreshYesFlagBypasses verifies --yes short-circuits both +// the prompt and the extra preview round-trip for scripted use. +func TestConfirmRefreshYesFlagBypasses(t *testing.T) { + t.Parallel() + + cmd, _ := newRefreshTestCmd(t, "" /* no stdin */) + require.NoError(t, cmd.Flags().Set("yes", "true")) + + var previewed bool + require.NoError( + t, + confirmRefreshIfNeeded( + cmd, explicitRefreshReq(), + staticPreview(&previewed, nil, nil), + ), + ) + require.False(t, previewed, + "--yes must not spend a preview round-trip") +} + +// TestConfirmRefreshNonTTYRefusesPrompt is the agent-safety guard the +// issue's acceptance criteria require: a non-interactive invocation +// without --yes or --dry_run fails fast with an INVALID_ARGS envelope +// instead of blocking on a prompt an agent cannot answer. +func TestConfirmRefreshNonTTYRefusesPrompt(t *testing.T) { + // NOT t.Parallel() — overrides the package-level stdinIsTTY + // indirection. + + prev := stdinIsTTY + stdinIsTTY = func(*cobra.Command) bool { return false } + defer func() { + stdinIsTTY = prev + }() + + cmd := newVTXOsRefreshCmd() + cmd.SetContext(t.Context()) + + var previewed bool + err := confirmRefreshIfNeeded( + cmd, explicitRefreshReq(), staticPreview(&previewed, nil, nil), + ) + require.Error(t, err) + require.Contains(t, err.Error(), "--yes") + require.Contains(t, err.Error(), "--dry_run") + require.True( + t, ErrorWasPrinted(err), + "expected a printedError so main.go exits with the "+ + "INVALID_ARGS code", + ) + require.False(t, previewed) +} + +// TestConfirmRefreshPromptShowsEstimateAndAcceptsYes verifies the +// interactive path consents to a number: the preview estimate is +// printed before the prompt and "y" proceeds. +func TestConfirmRefreshPromptShowsEstimateAndAcceptsYes(t *testing.T) { + t.Parallel() + + cmd, out := newRefreshTestCmd(t, "y\n") + + var previewed bool + preview := staticPreview( + &previewed, &waverpc.RefreshVTXOsResponse{ + QueuedOutpoints: []string{"aa:0"}, + Status: "preview", + FeeEstimate: &waverpc.RefreshFeeEstimate{ + EstimatedTotalFeeSat: proto.Int64(777), + Outpoints: []*waverpc.OutpointFeeEstimate{ + {Outpoint: "aa:0"}, + }, + }, + }, nil, + ) + + require.NoError( + t, + confirmRefreshIfNeeded( + cmd, explicitRefreshReq(), preview, + ), + ) + require.True(t, previewed) + require.Contains(t, out.String(), "777 sat") + require.Contains(t, out.String(), "Proceed with refresh? [y/N]") +} + +// TestConfirmRefreshPromptRejectsNo verifies "n" aborts before any +// dispatch, default-N posture included. +func TestConfirmRefreshPromptRejectsNo(t *testing.T) { + t.Parallel() + + for _, answer := range []string{"n\n", "\n"} { + cmd, _ := newRefreshTestCmd(t, answer) + + var previewed bool + err := confirmRefreshIfNeeded( + cmd, explicitRefreshReq(), + staticPreview(&previewed, nil, nil), + ) + require.Error(t, err) + require.Contains(t, err.Error(), "aborted by user") + } +} + +// TestConfirmRefreshPreviewInvalidArgumentAborts verifies a preview +// rejected as InvalidArgument (unknown or malformed outpoint) aborts +// the flow: the real dispatch would reject the same request shape, so +// prompting the user to confirm it would be noise. +func TestConfirmRefreshPreviewInvalidArgumentAborts(t *testing.T) { + t.Parallel() + + cmd, _ := newRefreshTestCmd(t, "y\n") + + var previewed bool + err := confirmRefreshIfNeeded( + cmd, explicitRefreshReq(), + staticPreview( + &previewed, nil, status.Error( + codes.InvalidArgument, "unknown VTXO outpoint", + ), + ), + ) + require.Error(t, err) + require.Contains(t, err.Error(), "refresh preview rejected") +} + +// TestConfirmRefreshPreviewFailureStillPrompts verifies a broken +// estimate path (operator down, old daemon) degrades to prompting +// with a warning instead of making refreshes unconfirmable — and the +// warning says a fee still applies. +func TestConfirmRefreshPreviewFailureStillPrompts(t *testing.T) { + t.Parallel() + + cmd, out := newRefreshTestCmd(t, "y\n") + + var previewed bool + require.NoError( + t, + confirmRefreshIfNeeded( + cmd, explicitRefreshReq(), + staticPreview( + &previewed, nil, status.Error( + codes.Unavailable, "operator down", + ), + ), + ), + ) + require.Contains(t, out.String(), "estimate unavailable") + require.Contains(t, out.String(), "still charged") + require.Contains(t, out.String(), "Proceed with refresh? [y/N]") +} + +// TestConfirmRefreshPreviewWithoutEstimateStillPrompts verifies a +// daemon that returns a preview without a fee estimate (an older +// daemon behind a newer CLI) still yields a warning plus the prompt, +// never a silent zero. +func TestConfirmRefreshPreviewWithoutEstimateStillPrompts(t *testing.T) { + t.Parallel() + + cmd, out := newRefreshTestCmd(t, "y\n") + + var previewed bool + require.NoError( + t, + confirmRefreshIfNeeded( + cmd, explicitRefreshReq(), + staticPreview( + &previewed, &waverpc.RefreshVTXOsResponse{ + QueuedOutpoints: []string{"aa:0"}, + Status: "preview", + }, + nil, + ), + ), + ) + require.Contains(t, out.String(), "no fee estimate") + require.Contains(t, out.String(), "still charged") +} + // TestSummarizeRefreshFeeEstimateNil verifies a response without an // estimate (real refresh, empty selection) renders no stderr lines. func TestSummarizeRefreshFeeEstimateNil(t *testing.T) { @@ -107,3 +364,285 @@ func TestSummarizeRefreshFeeEstimateBelowDust(t *testing.T) { require.Contains(t, lines[1], "2 selected VTXO(s)") require.Contains(t, lines[1], "minimum viable") } + +// fakeRefreshDaemon is an in-process DaemonService recording refresh +// and join traffic, for wiring tests that drive the full vtxosRefresh +// command path. +type fakeRefreshDaemon struct { + waverpc.UnimplementedDaemonServiceServer + + mu sync.Mutex + refreshReqs []*waverpc.RefreshVTXOsRequest + joinCalls int + + previewResp *waverpc.RefreshVTXOsResponse + queuedResp *waverpc.RefreshVTXOsResponse +} + +func (f *fakeRefreshDaemon) RefreshVTXOs(_ context.Context, + req *waverpc.RefreshVTXOsRequest) (*waverpc.RefreshVTXOsResponse, + error) { + + f.mu.Lock() + defer f.mu.Unlock() + + reqCopy, ok := proto.Clone(req).(*waverpc.RefreshVTXOsRequest) + if !ok { + return nil, status.Error(codes.Internal, "clone failed") + } + f.refreshReqs = append(f.refreshReqs, reqCopy) + + if req.DryRun { + if f.previewResp != nil { + return f.previewResp, nil + } + + return &waverpc.RefreshVTXOsResponse{ + QueuedOutpoints: []string{ + "aa:0", + }, + Status: "preview", + }, nil + } + + if f.queuedResp != nil { + return f.queuedResp, nil + } + + return &waverpc.RefreshVTXOsResponse{ + QueuedOutpoints: []string{ + "aa:0", + }, + Status: "queued", + }, nil +} + +func (f *fakeRefreshDaemon) JoinNextRound(_ context.Context, + _ *waverpc.JoinNextRoundRequest) (*waverpc.JoinNextRoundResponse, + error) { + + f.mu.Lock() + defer f.mu.Unlock() + + f.joinCalls++ + + return &waverpc.JoinNextRoundResponse{}, nil +} + +// realRefreshCalls returns how many non-dry-run refresh dispatches the +// fake daemon received. +func (f *fakeRefreshDaemon) realRefreshCalls() int { + f.mu.Lock() + defer f.mu.Unlock() + + n := 0 + for _, req := range f.refreshReqs { + if !req.DryRun { + n++ + } + } + + return n +} + +// withFakeRefreshDaemon serves the fake over bufconn and points the +// getDaemonClient indirection at it for the duration of the test. +// Tests using it must NOT be parallel: it swaps package-level state. +func withFakeRefreshDaemon(t *testing.T, fake *fakeRefreshDaemon) { + t.Helper() + + const bufSize = 1024 * 1024 + lis := bufconn.Listen(bufSize) + + srv := grpc.NewServer() + waverpc.RegisterDaemonServiceServer(srv, fake) + + go func() { + _ = srv.Serve(lis) + }() + t.Cleanup(srv.Stop) + + dialer := func(context.Context, string) (net.Conn, error) { + return lis.Dial() + } + + prev := getDaemonClient + getDaemonClient = func(*cobra.Command) (waverpc.DaemonServiceClient, + *grpc.ClientConn, error) { + + conn, err := grpc.NewClient( + "passthrough:///bufconn", + grpc.WithContextDialer(dialer), + grpc.WithTransportCredentials( + insecure.NewCredentials(), + ), + ) + if err != nil { + return nil, nil, err + } + + return waverpc.NewDaemonServiceClient(conn), conn, nil + } + t.Cleanup(func() { + getDaemonClient = prev + }) +} + +// TestVTXOsRefreshWiringNonTTYNeverDispatches drives the full command +// path: a non-interactive real refresh without --yes must refuse +// before ANY RPC reaches the daemon — the gate is wired ahead of +// dispatch, not beside it. +func TestVTXOsRefreshWiringNonTTYNeverDispatches(t *testing.T) { + // NOT t.Parallel() — overrides getDaemonClient and stdinIsTTY. + + fake := &fakeRefreshDaemon{} + withFakeRefreshDaemon(t, fake) + + prev := stdinIsTTY + stdinIsTTY = func(*cobra.Command) bool { return false } + defer func() { + stdinIsTTY = prev + }() + + cmd := newVTXOsRefreshCmd() + cmd.SetContext(t.Context()) + require.NoError(t, cmd.Flags().Set("outpoint", "aa:0")) + + err := vtxosRefresh(cmd, nil) + require.Error(t, err) + require.True(t, ErrorWasPrinted(err)) + require.Empty( + t, fake.refreshReqs, "the refusal must fire before any RPC", + ) + require.Zero(t, fake.joinCalls) +} + +// TestVTXOsRefreshWiringYesDispatchesOnce verifies --yes skips the +// gate and performs exactly one real dispatch plus the auto-join — +// no preview round-trip is spent on scripted consent. +func TestVTXOsRefreshWiringYesDispatchesOnce(t *testing.T) { + // NOT t.Parallel() — overrides getDaemonClient. + + fake := &fakeRefreshDaemon{} + withFakeRefreshDaemon(t, fake) + + cmd, _ := newRefreshTestCmd(t, "" /* no stdin */) + require.NoError(t, cmd.Flags().Set("outpoint", "aa:0")) + require.NoError(t, cmd.Flags().Set("yes", "true")) + + require.NoError(t, vtxosRefresh(cmd, nil)) + require.Len(t, fake.refreshReqs, 1) + require.False(t, fake.refreshReqs[0].DryRun) + require.Equal(t, 1, fake.joinCalls) +} + +// TestVTXOsRefreshWiringDeclineNeverDispatches verifies an +// interactive "n" leaves the daemon untouched beyond the preview: +// one dry-run fetch, zero real dispatches, zero joins. +func TestVTXOsRefreshWiringDeclineNeverDispatches(t *testing.T) { + // NOT t.Parallel() — overrides getDaemonClient. + + fake := &fakeRefreshDaemon{} + withFakeRefreshDaemon(t, fake) + + cmd, _ := newRefreshTestCmd(t, "n\n") + require.NoError(t, cmd.Flags().Set("outpoint", "aa:0")) + + err := vtxosRefresh(cmd, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "aborted by user") + require.Len(t, fake.refreshReqs, 1, + "exactly the preview fetch") + require.True(t, fake.refreshReqs[0].DryRun) + require.Zero(t, fake.realRefreshCalls()) + require.Zero(t, fake.joinCalls) +} + +// TestVTXOsRefreshWiringDryRunPrintsSummary verifies the --dry_run +// flag path end to end: one dry-run RPC, no join, no prompt, and the +// stderr summary carries the estimate headline. +func TestVTXOsRefreshWiringDryRunPrintsSummary(t *testing.T) { + // NOT t.Parallel() — overrides getDaemonClient. + + fake := &fakeRefreshDaemon{ + previewResp: &waverpc.RefreshVTXOsResponse{ + QueuedOutpoints: []string{ + "aa:0", + }, + Status: "preview", + FeeEstimate: &waverpc.RefreshFeeEstimate{ + EstimatedTotalFeeSat: proto.Int64(432), + Outpoints: []*waverpc.OutpointFeeEstimate{ + { + Outpoint: "aa:0", + }, + }, + }, + }, + } + withFakeRefreshDaemon(t, fake) + + cmd, out := newRefreshTestCmd(t, "" /* no stdin */) + require.NoError(t, cmd.Flags().Set("outpoint", "aa:0")) + require.NoError(t, cmd.Flags().Set("dry_run", "true")) + + require.NoError(t, vtxosRefresh(cmd, nil)) + require.Len(t, fake.refreshReqs, 1) + require.True(t, fake.refreshReqs[0].DryRun) + require.Zero(t, fake.joinCalls, "dry run must not auto-join") + require.Contains(t, out.String(), "432 sat") + require.NotContains(t, out.String(), "Proceed with refresh?") +} + +// TestVTXOsRefreshWiringOldDaemonWarns verifies the version-skew +// warning: a dry-run preview with outpoints but no fee estimate can +// only come from a pre-feature daemon, and the CLI must say so +// instead of silently dropping the promised preview. +func TestVTXOsRefreshWiringOldDaemonWarns(t *testing.T) { + // NOT t.Parallel() — overrides getDaemonClient. + + fake := &fakeRefreshDaemon{ + previewResp: &waverpc.RefreshVTXOsResponse{ + QueuedOutpoints: []string{ + "aa:0", + }, + Status: "preview", + }, + } + withFakeRefreshDaemon(t, fake) + + cmd, out := newRefreshTestCmd(t, "" /* no stdin */) + require.NoError(t, cmd.Flags().Set("outpoint", "aa:0")) + require.NoError(t, cmd.Flags().Set("dry_run", "true")) + + require.NoError(t, vtxosRefresh(cmd, nil)) + require.Contains(t, out.String(), "no fee estimate") + require.Contains(t, out.String(), "still charged") +} + +// TestVTXOsRefreshWiringEmptyAllSkipsPrompt verifies a TTY refresh +// --all over an empty wallet does not warn about a charge for a +// selection that queues nothing: the gate skips the prompt and says +// so. +func TestVTXOsRefreshWiringEmptyAllSkipsPrompt(t *testing.T) { + // NOT t.Parallel() — overrides getDaemonClient. + + fake := &fakeRefreshDaemon{ + previewResp: &waverpc.RefreshVTXOsResponse{ + Status: "preview", + }, + } + withFakeRefreshDaemon(t, fake) + + // No stdin: an unexpected prompt would fail loudly on EOF. + cmd, out := newRefreshTestCmd(t, "") + require.NoError(t, cmd.Flags().Set("all", "true")) + + require.NoError(t, vtxosRefresh(cmd, nil)) + require.Contains(t, out.String(), "no live VTXOs to refresh") + require.NotContains(t, out.String(), "Proceed with refresh?") + require.Equal( + t, 1, fake.realRefreshCalls(), + "the harmless empty dispatch still proceeds", + ) +} diff --git a/cmd/wavecli/waveclicommands/schema_registry.go b/cmd/wavecli/waveclicommands/schema_registry.go index d8deb0439..3fded35de 100644 --- a/cmd/wavecli/waveclicommands/schema_registry.go +++ b/cmd/wavecli/waveclicommands/schema_registry.go @@ -638,7 +638,11 @@ func arkVTXOMethodRegistry() []schemaMethod { { Method: "ark.vtxos.refresh", Description: "Queue VTXOs for refresh and join the " + - "next round (auto-joins unless --no_join)", + "next round (auto-joins unless --no_join). " + + "Charged an operator fee at seal time; " + + "--dry_run previews an itemized estimate, " + + "and a real refresh requires --yes on " + + "non-interactive stdin", Params: []schemaParam{ { Name: "outpoint", @@ -651,6 +655,12 @@ func arkVTXOMethodRegistry() []schemaMethod { Type: "bool", Description: "refresh all live VTXOs", }, + { + Name: "yes", + Type: "bool", + Description: "skip the interactive " + + "fee confirmation", + }, { Name: "no_join", Type: "bool", From 5bc5295b0f1bbf27f214a1086ebd4f6703e134f1 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Fri, 17 Jul 2026 10:53:27 -0500 Subject: [PATCH 5/9] wavecli: Prompt leave confirmation on stderr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In this commit, we move the leave --all confirmation prompt from stdout to stderr, matching the refresh confirmation gate and the package's stream-split invariant: stdout is reserved for the JSON body, diagnostics go to stderr. Before this change, piping the command (e.g. into jq) on an interactive terminal swallowed the prompt into the pipe — the command read as hung, and a user who typed y blind fed prose into the JSON consumer. Part of #986. (cherry picked from commit 35ab4172eaf505e2d3c171fd3a3114b674f1ceae) --- cmd/wavecli/waveclicommands/cmd_vtxos.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/wavecli/waveclicommands/cmd_vtxos.go b/cmd/wavecli/waveclicommands/cmd_vtxos.go index e7f0def65..b52af0af0 100644 --- a/cmd/wavecli/waveclicommands/cmd_vtxos.go +++ b/cmd/wavecli/waveclicommands/cmd_vtxos.go @@ -861,9 +861,12 @@ func defaultStdinIsTTY(cmd *cobra.Command) bool { // promptLeaveAllConfirmation asks the operator to confirm a // --all invocation on stdin. Any answer other than "y" or "yes" // (case-insensitive) aborts the command. Only called when stdin is -// known to be a TTY (see confirmLeaveAllIfNeeded). +// known to be a TTY (see confirmLeaveAllIfNeeded). The prompt goes to +// stderr, matching promptRefreshConfirmation: stdout is reserved for +// the JSON body, and a prompt swallowed by a shell pipe would read as +// a hung command. func promptLeaveAllConfirmation(cmd *cobra.Command) error { - out := cmd.OutOrStdout() + out := cmd.ErrOrStderr() fmt.Fprintln( out, "About to queue ALL live VTXOs for cooperative leave. "+ From 5c7964882d6b5050e7662d1bac4537bf68af2774 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Fri, 17 Jul 2026 10:57:24 -0500 Subject: [PATCH 6/9] wavecli: Gate MCP refresh on explicit consent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In this commit, we extend the refresh fee-consent contract to the MCP surface. The MCP tool calls the daemon directly rather than executing the cobra command, so the CLI gate never ran there: an agent could queue a fee-incurring refresh with no warning, while the schema registry — documented as the shared source of truth for CLI commands and MCP tools — promised that a real refresh requires yes. The tool description also never mentioned the fee. The ark.vtxos.refresh tool now takes a yes acknowledgement: a real (non-dry-run) call without it returns an immediate, actionable error naming both the dry_run:true preview and the yes:true acknowledgement path — nothing can block on MCP, matching the CLI's non-interactive refusal — and the description now names the seal-time fee and the preview-first flow. The dry-run preview carries the same itemized fee estimate as every other surface. Part of #986. (cherry picked from commit c503b62886c3c8c0c96120ca9175c485fe060710) --- cmd/wavecli/waveclicommands/cmd_mcp.go | 42 ++++++++++++++++--- .../waveclicommands/mcp_wallet_test.go | 19 +++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/cmd/wavecli/waveclicommands/cmd_mcp.go b/cmd/wavecli/waveclicommands/cmd_mcp.go index 0a2f93cb1..6a77e3c06 100644 --- a/cmd/wavecli/waveclicommands/cmd_mcp.go +++ b/cmd/wavecli/waveclicommands/cmd_mcp.go @@ -91,6 +91,23 @@ func buildMCPServer(client waverpc.DaemonServiceClient, return server } +// checkMCPRefreshConsent enforces the refresh fee-consent contract on +// the MCP surface: a real (non-dry-run) refresh requires the explicit +// yes acknowledgement, because every refresh is charged an operator +// fee at seal time and MCP has no interactive prompt to fall back to. +// The returned error is immediate and actionable — an agent is never +// blocked, matching the CLI's non-interactive refusal. +func checkMCPRefreshConsent(dryRun, yes bool) error { + if dryRun || yes { + return nil + } + + return fmt.Errorf("a refresh is charged an operator fee at seal " + + "time; call with dry_run:true first to preview the itemized " + + "advisory estimate, then pass yes:true to acknowledge the " + + "fee and queue the refresh") +} + // mcpResult builds a CallToolResult from a proto message response. func mcpResult(msg proto.Message) (*mcp.CallToolResult, error) { opts := protojson.MarshalOptions{ @@ -256,16 +273,31 @@ func registerMCPTools(s *mcp.Server, client waverpc.DaemonServiceClient) { // vtxos_refresh — refresh VTXOs. type vtxosRefreshArgs struct { - Outpoints []string `json:"outpoints,omitempty" jsonschema:"VTXO outpoint(s) to refresh (txid:index)"` //nolint:ll - All bool `json:"all,omitempty" jsonschema:"refresh all live VTXOs"` //nolint:ll - DryRun bool `json:"dry_run,omitempty" jsonschema:"validate without queuing"` //nolint:ll + Outpoints []string `json:"outpoints,omitempty" jsonschema:"VTXO outpoint(s) to refresh (txid:index)"` //nolint:ll + All bool `json:"all,omitempty" jsonschema:"refresh all live VTXOs"` //nolint:ll + DryRun bool `json:"dry_run,omitempty" jsonschema:"validate without queuing and preview the estimated operator fee"` //nolint:ll + Yes bool `json:"yes,omitempty" jsonschema:"acknowledge the operator fee and queue the refresh (required unless dry_run)"` //nolint:ll } mcp.AddTool(s, &mcp.Tool{ - Name: "ark.vtxos.refresh", - Description: "Queue VTXOs for refresh in next round", + Name: "ark.vtxos.refresh", + Description: "Queue VTXOs for refresh in next round. A " + + "refresh is charged an operator fee at seal time: " + + "call with dry_run:true first for an itemized " + + "advisory estimate, then pass yes:true to " + + "acknowledge the fee and queue", }, func(ctx context.Context, req *mcp.CallToolRequest, args vtxosRefreshArgs) (*mcp.CallToolResult, any, error) { + // Same consent contract as the CLI gate: a real refresh + // needs explicit acknowledgement. The error returns + // immediately (nothing can block on MCP) and tells the + // agent exactly how to proceed. + if err := checkMCPRefreshConsent( + args.DryRun, args.Yes, + ); err != nil { + return nil, nil, err + } + rpcReq := &waverpc.RefreshVTXOsRequest{ DryRun: args.DryRun, } diff --git a/cmd/wavecli/waveclicommands/mcp_wallet_test.go b/cmd/wavecli/waveclicommands/mcp_wallet_test.go index 6e1ae944d..b40891033 100644 --- a/cmd/wavecli/waveclicommands/mcp_wallet_test.go +++ b/cmd/wavecli/waveclicommands/mcp_wallet_test.go @@ -136,3 +136,22 @@ func TestBuildWalletActivityRequestHappyPath(t *testing.T) { require.Equal(t, uint32(50), req.GetLimit()) require.Equal(t, "cursor-token", req.GetCursor()) } + +// TestCheckMCPRefreshConsent pins the MCP refresh fee-consent +// contract: a dry-run preview and an acknowledged refresh pass, a +// bare real refresh is refused with an actionable error that names +// both the preview and the acknowledgement path — matching the +// consent the schema registry promises for this method. +func TestCheckMCPRefreshConsent(t *testing.T) { + t.Parallel() + + require.NoError(t, checkMCPRefreshConsent(true, false)) + require.NoError(t, checkMCPRefreshConsent(false, true)) + require.NoError(t, checkMCPRefreshConsent(true, true)) + + err := checkMCPRefreshConsent(false, false) + require.Error(t, err) + require.Contains(t, err.Error(), "dry_run:true") + require.Contains(t, err.Error(), "yes:true") + require.Contains(t, err.Error(), "operator fee") +} From c8ddc19d251066d7ecc14dfb8c6f4d3de1545ffe Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Fri, 17 Jul 2026 09:07:35 -0500 Subject: [PATCH 7/9] docs: Document refresh fee preview and consent gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In this commit, we document the refresh-fee visibility surface. The CLI guide's refresh section now names the operator fee and the seal-time quote that binds it, describes the dry-run estimate (per-outpoint amounts and lifetimes resolved by the daemon, the free-refresh-window verdict, the degraded estimate_error mode), and covers the interactive confirmation plus the non-interactive --yes requirement, with examples updated to match. The waveclicommands agent docs gain the refresh consent-gate invariant — CLI prompt, non-interactive refusal, the MCP tool's yes argument, and the absent-not-zero wire semantics of a degraded total — alongside the existing leave --all and recovery escalate prompt-refusal postures. The waved agent docs gain the dry-run estimation invariant (pre-wallet-gate ordering, store-resolved live-only outpoints, quote dedupe and validation, locally computed waiver, degrade-only failure mode). The repo's own agent-facing refresh examples in the waved skill and CONTEXT.md pick up --dry_run/--yes so an agent following them is not stopped cold by the new non-interactive refusal. Both members of each touched CLAUDE/AGENTS pair receive the same edit; the waved pair's pre-existing divergence on unrelated OOR text is left to the doc-gardening workflow. Part of #986. (cherry picked from commit 021094c6f0261ceddf340af56b0cc5457ea09556) --- .claude/skills/waved.md | 5 ++++- CONTEXT.md | 2 +- cmd/wavecli/waveclicommands/AGENTS.md | 10 +++++++++ cmd/wavecli/waveclicommands/CLAUDE.md | 10 +++++++++ docs/daemon_cli_guide.md | 32 +++++++++++++++++++++++---- waved/AGENTS.md | 11 +++++++++ waved/CLAUDE.md | 11 +++++++++ 7 files changed, 75 insertions(+), 6 deletions(-) diff --git a/.claude/skills/waved.md b/.claude/skills/waved.md index 776cc8f50..431b3d33f 100644 --- a/.claude/skills/waved.md +++ b/.claude/skills/waved.md @@ -128,7 +128,10 @@ surfaces the raw waverpc methods underlying them. # Raw VTXO inventory + lifecycle wavecli ark vtxos list --no-tls wavecli ark vtxos list --status live --min_amount 10000 --no-tls -wavecli ark vtxos refresh --all --no-tls +# A real refresh is fee-gated: preview with --dry_run, consent with +# --yes (required on non-interactive stdin). +wavecli ark vtxos refresh --all --dry_run --no-tls +wavecli ark vtxos refresh --all --yes --no-tls # Raw transaction history (the wallet-shaped feed is `activity`) wavecli ark listtransactions --no-tls diff --git a/CONTEXT.md b/CONTEXT.md index 6778e04f9..6e3280482 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -103,7 +103,7 @@ wavecli ark vtxos list --ndjson | jq '.amount_sat' # Refresh — see BUGS_FOUND.md bug-1/bug-2; the refresh path is # currently not landing on the operator. wavecli ark vtxos refresh --outpoint txid:0 --dry_run -wavecli ark vtxos refresh --outpoint txid:0 +wavecli ark vtxos refresh --outpoint txid:0 --yes ``` ### Send operations diff --git a/cmd/wavecli/waveclicommands/AGENTS.md b/cmd/wavecli/waveclicommands/AGENTS.md index de16e0581..2ee80fe6e 100644 --- a/cmd/wavecli/waveclicommands/AGENTS.md +++ b/cmd/wavecli/waveclicommands/AGENTS.md @@ -152,6 +152,16 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/cmd/wave - `recovery escalate` refuses to run on non-interactive stdin unless `--yes` is passed — it never blocks on a y/N prompt an agent can't answer. +- `ark vtxos refresh` is gated on fee consent: a real refresh fetches + the dry-run estimate and prompts with it on a TTY, and refuses on + non-interactive stdin without `--yes` (same posture as `leave --all` + and `recovery escalate`). The MCP tool enforces the same contract + through its `yes` argument — no prompt exists there, so a bare real + refresh returns an immediate actionable error. `--dry_run` previews + the itemized advisory estimate and never prompts. A failed estimate + degrades to a "still charged the seal-time fee" warning and its + total is absent on the wire (explicit proto presence) — it never + blocks the flow and is never rendered as a zero fee. ## Deep Docs diff --git a/cmd/wavecli/waveclicommands/CLAUDE.md b/cmd/wavecli/waveclicommands/CLAUDE.md index de16e0581..2ee80fe6e 100644 --- a/cmd/wavecli/waveclicommands/CLAUDE.md +++ b/cmd/wavecli/waveclicommands/CLAUDE.md @@ -152,6 +152,16 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/cmd/wave - `recovery escalate` refuses to run on non-interactive stdin unless `--yes` is passed — it never blocks on a y/N prompt an agent can't answer. +- `ark vtxos refresh` is gated on fee consent: a real refresh fetches + the dry-run estimate and prompts with it on a TTY, and refuses on + non-interactive stdin without `--yes` (same posture as `leave --all` + and `recovery escalate`). The MCP tool enforces the same contract + through its `yes` argument — no prompt exists there, so a bare real + refresh returns an immediate actionable error. `--dry_run` previews + the itemized advisory estimate and never prompts. A failed estimate + degrades to a "still charged the seal-time fee" warning and its + total is absent on the wire (explicit proto presence) — it never + blocks the flow and is never rendered as a zero fee. ## Deep Docs diff --git a/docs/daemon_cli_guide.md b/docs/daemon_cli_guide.md index 514794505..2c22ac5f6 100644 --- a/docs/daemon_cli_guide.md +++ b/docs/daemon_cli_guide.md @@ -399,19 +399,43 @@ on later terms refreshes). When the cached boundary fires, it fetches a fresh An operator fee-schedule hot reload is not pushed proactively to already-connected wallets. +A refresh is charged an operator fee (on-chain share + liquidity + +margin), set by the server-issued quote at seal time and auto-accepted +up to the daemon's `maxoperatorfeesat` cap. `--dry_run` previews an +itemized advisory estimate for the selected VTXOs — per outpoint: +amount, remaining lifetime, liquidity / on-chain / margin components — +resolved entirely from the daemon's own view, with no manual amount or +remaining-blocks input. Selections fully inside the operator's +advertised free-refresh window preview with a zero total +(`free_refresh_eligible`), while the per-outpoint rows keep the +ordinary paid quote to show what the waiver saves. The estimate is +best-effort: if the operator or chain tip is unreachable the preview +still returns with `estimate_error` set. The binding fee remains the +seal-time quote and may differ from any estimate. + +An interactive real refresh shows the estimate and asks for +confirmation. On non-interactive stdin (agents, pipelines) the command +refuses to prompt: pass `--yes` (explicit consent) or `--dry_run` +(preview). This mirrors the `leave --all` consent gate. + | Flag | Type | Description | |------|------|-------------| | `--outpoint` | string[] | VTXO outpoint(s) to refresh (txid:index) | | `--all` | bool | Refresh all live VTXOs | -| `--dry_run` | bool | Validate without queuing | +| `--dry_run` | bool | Validate without queuing and preview the estimated fee | +| `--yes` | bool | Skip the interactive fee confirmation | | `--no_join` | bool | Skip the implicit `ark rounds join` follow-up | ```bash -# Explicit outpoints (auto-joins the next round) -wavecli ark vtxos refresh --outpoint +# Preview the itemized fee estimate without queuing anything +wavecli ark vtxos refresh --outpoint --dry_run + +# Explicit outpoints (auto-joins the next round; prompts with the +# estimate on a TTY, requires --yes when stdin is not interactive) +wavecli ark vtxos refresh --outpoint --yes # Batch with other intents — explicitly join later -wavecli ark vtxos refresh --outpoint --no_join +wavecli ark vtxos refresh --outpoint --yes --no_join wavecli ark vtxos leave --outpoint --no_join \ --address bcrt1p... wavecli ark rounds join diff --git a/waved/AGENTS.md b/waved/AGENTS.md index d377b0534..2aa26771e 100644 --- a/waved/AGENTS.md +++ b/waved/AGENTS.md @@ -97,6 +97,17 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/waved.= 1, and the + free-refresh waiver is computed locally from the cached operator + terms (the operator's EstimateFee does not apply it). Estimate + failures set `estimate_error` and never fail the preview. The real + refresh path still gates on wallet readiness. - `SendVTXO` enforces `maxRecipients = 256`, rejects per-recipient amounts outside `(0, MaxSatoshi]`, and uses overflow-safe summation; the wallet actor repeats these checks as defense-in-depth. diff --git a/waved/CLAUDE.md b/waved/CLAUDE.md index 2c6266894..391a38bce 100644 --- a/waved/CLAUDE.md +++ b/waved/CLAUDE.md @@ -97,6 +97,17 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/waved.= 1, and the + free-refresh waiver is computed locally from the cached operator + terms (the operator's EstimateFee does not apply it). Estimate + failures set `estimate_error` and never fail the preview. The real + refresh path still gates on wallet readiness. - `SendVTXO` enforces `maxRecipients = 256`, rejects per-recipient amounts outside `(0, MaxSatoshi]`, and uses overflow-safe summation; the wallet actor repeats these checks as defense-in-depth. From 2038abd0818f82c9595efc45c6f6d1d7c948a167 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Mon, 20 Jul 2026 13:18:54 -0500 Subject: [PATCH 8/9] waved: Require operator quote components to sum to the total In this commit, we extend the refresh quote sanity check to enforce the reconciliation the proto documents: OutpointFeeEstimate promises its components always sum to the row's total_fee_sat, and the operator's fee schedule defines the total as exactly liquidity + on-chain share + margin, but refreshQuoteSane only vetted each field for negative or beyond-money-supply values. A broken (or hostile) operator could return an itemization that contradicts its own total and the daemon would echo the inconsistency into the advisory rows. Each component is now individually bounded by MaxSatoshi before the wrap-free sum is compared against total_fee_sat, so a mismatched quote degrades the estimate all-or-nothing exactly like a negative one. Raised in the automated review of #987. Part of #986. (cherry picked from commit bb8257763436735cc7278da752e9e34b5284a904) --- waved/rpc_refresh_estimate.go | 36 +++++++++++++++++++++------- waved/rpc_refresh_estimate_test.go | 38 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/waved/rpc_refresh_estimate.go b/waved/rpc_refresh_estimate.go index a295a55a3..8879685d9 100644 --- a/waved/rpc_refresh_estimate.go +++ b/waved/rpc_refresh_estimate.go @@ -175,16 +175,34 @@ func degradeRefreshEstimate(est *waverpc.RefreshFeeEstimate, return est } -// refreshQuoteSane rejects operator quotes carrying negative or -// beyond-money-supply values. Such a quote is operator nonsense; it is -// contained here so a broken (or hostile) operator can neither render -// negative components nor poison the selection total. +// refreshQuoteSane rejects operator quotes carrying negative, +// beyond-money-supply, or internally inconsistent values. Such a quote +// is operator nonsense; it is contained here so a broken (or hostile) +// operator can neither render negative components, poison the +// selection total, nor advertise an itemization that doesn't reconcile +// with the total the seal charges. func refreshQuoteSane(quote *arkrpc.EstimateFeeResponse) bool { - return quote.LiquidityFeeSat >= 0 && - quote.OnchainShareSat >= 0 && - quote.MarginSat >= 0 && - quote.TotalFeeSat >= 0 && - quote.TotalFeeSat <= int64(btcutil.MaxSatoshi) + // Bound every component individually before summing: three + // values each capped at MaxSatoshi cannot overflow int64, so + // the reconciliation below is wrap-free. + maxSat := int64(btcutil.MaxSatoshi) + components := []int64{ + quote.LiquidityFeeSat, + quote.OnchainShareSat, + quote.MarginSat, + } + var sum int64 + for _, c := range components { + if c < 0 || c > maxSat { + return false + } + sum += c + } + + // The operator's fee schedule defines total_fee_sat as exactly + // liquidity + on-chain share + margin; a quote whose parts + // don't sum to its total is as nonsensical as a negative one. + return quote.TotalFeeSat == sum && quote.TotalFeeSat <= maxSat } // estimateRefreshFees builds the advisory RefreshFeeEstimate for the diff --git a/waved/rpc_refresh_estimate_test.go b/waved/rpc_refresh_estimate_test.go index c8fe0c51c..cc753dace 100644 --- a/waved/rpc_refresh_estimate_test.go +++ b/waved/rpc_refresh_estimate_test.go @@ -836,6 +836,44 @@ func TestRefreshDryRunEstimateRejectsNegativeComponent(t *testing.T) { require.Zero(t, est.Outpoints[0].TotalFeeSat) } +// TestRefreshDryRunEstimateRejectsMismatchedComponents verifies the +// quote sanity check enforces the reconciliation the proto documents: +// components that don't sum to total_fee_sat degrade the estimate +// instead of echoing an itemization that contradicts its own total. +func TestRefreshDryRunEstimateRejectsMismatchedComponents(t *testing.T) { + t.Parallel() + + svc := &fakeArkService{ + response: &arkrpc.EstimateFeeResponse{ + LiquidityFeeSat: 50, + OnchainShareSat: 25, + MarginSat: 100, + TotalFeeSat: 999, + }, + } + r, vtxoStore := newRefreshEstimateServer(t, svc, 900) + + desc := newRefreshEstimateVTXO(t, 0x14, 100_000, 1_000) + require.NoError(t, vtxoStore.SaveVTXO(t.Context(), desc)) + + resp, err := r.RefreshVTXOs( + t.Context(), &waverpc.RefreshVTXOsRequest{ + Selection: &waverpc.RefreshVTXOsRequest_All{ + All: true, + }, + DryRun: true, + }, + ) + require.NoError(t, err) + + est := resp.FeeEstimate + require.NotNil(t, est) + require.Contains(t, est.EstimateError, "invalid") + require.Nil(t, est.EstimatedTotalFeeSat) + require.Len(t, est.Outpoints, 1) + require.Zero(t, est.Outpoints[0].TotalFeeSat) +} + // TestRefreshDryRunEstimateWindowBoundary pins the inclusive window // comparison against the operator's IsFreeRefreshWindow semantics: a // VTXO at exactly the advertised window is waiver-eligible, one block From 0e9b4278eada2a394484cc6708819d597f7ccdc3 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Mon, 20 Jul 2026 13:19:12 -0500 Subject: [PATCH 9/9] wavecli: Reject combining --outpoint with --all on refresh In this commit, we extract the refresh selection parsing into a shared buildRefreshVTXOsRequest builder, mirroring the buildLeaveVTXOsRequest pattern, and add the mutual-exclusion guard the leave surface already has: passing both --outpoint and --all previously let --all win silently, refreshing (and charging for) every live VTXO while the named outpoints were dropped. The MCP tool now goes through the same builder, closing the same silent-drop gap on that surface and turning an empty selection into an immediate client-side error instead of a daemon round trip. Raised in the automated review of #987. Part of #986. (cherry picked from commit 5551d2f632846671d458c4e2751c37076d7c7fd5) --- cmd/wavecli/waveclicommands/cmd_mcp.go | 20 ++---- cmd/wavecli/waveclicommands/cmd_vtxos.go | 62 ++++++++++++++----- .../waveclicommands/cmd_vtxos_refresh_test.go | 54 ++++++++++++++++ 3 files changed, 105 insertions(+), 31 deletions(-) diff --git a/cmd/wavecli/waveclicommands/cmd_mcp.go b/cmd/wavecli/waveclicommands/cmd_mcp.go index 6a77e3c06..c5aeb02ef 100644 --- a/cmd/wavecli/waveclicommands/cmd_mcp.go +++ b/cmd/wavecli/waveclicommands/cmd_mcp.go @@ -298,21 +298,13 @@ func registerMCPTools(s *mcp.Server, client waverpc.DaemonServiceClient) { return nil, nil, err } - rpcReq := &waverpc.RefreshVTXOsRequest{ - DryRun: args.DryRun, - } - if args.All { - rpcReq.Selection = &waverpc.RefreshVTXOsRequest_All{ - All: true, - } - } else if len(args.Outpoints) > 0 { - sel := &waverpc.RefreshVTXOsRequest_Outpoints{ - Outpoints: &waverpc.OutpointSelection{ - Outpoints: args.Outpoints, - }, - } - rpcReq.Selection = sel + rpcReq, err := buildRefreshVTXOsRequest( + args.Outpoints, args.All, args.DryRun, + ) + if err != nil { + return nil, nil, err } + resp, err := client.RefreshVTXOs(ctx, rpcReq) if err != nil { return nil, nil, err diff --git a/cmd/wavecli/waveclicommands/cmd_vtxos.go b/cmd/wavecli/waveclicommands/cmd_vtxos.go index b52af0af0..f31f4f493 100644 --- a/cmd/wavecli/waveclicommands/cmd_vtxos.go +++ b/cmd/wavecli/waveclicommands/cmd_vtxos.go @@ -183,7 +183,42 @@ func newVTXOsRefreshCmd() *cobra.Command { return cmd } -// vtxosRefresh executes the RefreshVTXOs RPC. +// buildRefreshVTXOsRequest translates the flag surface into a +// RefreshVTXOsRequest. Extracted so the MCP tool and the CLI share +// the same selection guards — divergence here would let one surface +// silently refresh a different VTXO set than the other. +func buildRefreshVTXOsRequest(outpoints []string, all, + dryRun bool) (*waverpc.RefreshVTXOsRequest, error) { + + if !all && len(outpoints) == 0 { + return nil, fmt.Errorf("either --outpoint or --all is required") + } + + if all && len(outpoints) > 0 { + return nil, fmt.Errorf("--outpoint and --all are mutually " + + "exclusive") + } + + req := &waverpc.RefreshVTXOsRequest{DryRun: dryRun} + + if all { + req.Selection = &waverpc.RefreshVTXOsRequest_All{ + All: true, + } + } else { + req.Selection = &waverpc.RefreshVTXOsRequest_Outpoints{ + Outpoints: &waverpc.OutpointSelection{ + Outpoints: outpoints, + }, + } + } + + return req, nil +} + +// vtxosRefresh executes the RefreshVTXOs RPC. Flag handling lives in +// buildRefreshVTXOsRequest so the same builder can be reused by the +// MCP tool and covered with a focused unit test. func vtxosRefresh(cmd *cobra.Command, _ []string) error { client, conn, err := getDaemonClient(cmd) if err != nil { @@ -199,24 +234,17 @@ func vtxosRefresh(cmd *cobra.Command, _ []string) error { all, _ := cmd.Flags().GetBool("all") dryRun, _ := cmd.Flags().GetBool("dry_run") - if !all && len(outpoints) == 0 { - return fmt.Errorf("either --outpoint or --all is " + - "required") + built, err := buildRefreshVTXOsRequest( + outpoints, all, dryRun, + ) + if err != nil { + return err } - if all { - req.Selection = &waverpc.RefreshVTXOsRequest_All{ - All: true, - } - } else { - sel := &waverpc.RefreshVTXOsRequest_Outpoints{ - Outpoints: &waverpc.OutpointSelection{ - Outpoints: outpoints, - }, - } - req.Selection = sel - } - req.DryRun = dryRun + // Proto pointer-receiver shims: copy built fields onto + // the outer req that parseRequest owns. + req.Selection = built.Selection + req.DryRun = built.DryRun return nil }); err != nil { diff --git a/cmd/wavecli/waveclicommands/cmd_vtxos_refresh_test.go b/cmd/wavecli/waveclicommands/cmd_vtxos_refresh_test.go index f85b65ab0..ed9aaacb2 100644 --- a/cmd/wavecli/waveclicommands/cmd_vtxos_refresh_test.go +++ b/cmd/wavecli/waveclicommands/cmd_vtxos_refresh_test.go @@ -265,6 +265,60 @@ func TestConfirmRefreshPreviewWithoutEstimateStillPrompts(t *testing.T) { require.Contains(t, out.String(), "still charged") } +// TestBuildRefreshVTXOsRequestRejectsOutpointAndAll verifies that a +// caller can't combine --outpoint and --all: before the shared +// builder, --all silently won and the outpoints were dropped. +func TestBuildRefreshVTXOsRequestRejectsOutpointAndAll(t *testing.T) { + t.Parallel() + + _, err := buildRefreshVTXOsRequest( + []string{ + "aa:0", + }, + true, false, + ) + require.Error(t, err) + require.Contains(t, err.Error(), "mutually exclusive") +} + +// TestBuildRefreshVTXOsRequestRejectsNoSelection verifies that a +// caller with neither --outpoint nor --all gets a clean CLI-side +// error rather than hitting the daemon with an empty request. +func TestBuildRefreshVTXOsRequestRejectsNoSelection(t *testing.T) { + t.Parallel() + + _, err := buildRefreshVTXOsRequest(nil, false, false) + require.Error(t, err) + require.Contains(t, err.Error(), "either --outpoint or --all") +} + +// TestBuildRefreshVTXOsRequestBuildsSelections locks in the two +// valid selection shapes and the dry_run passthrough. +func TestBuildRefreshVTXOsRequestBuildsSelections(t *testing.T) { + t.Parallel() + + req, err := buildRefreshVTXOsRequest(nil, true, true) + require.NoError(t, err) + require.True(t, req.GetAll()) + require.True(t, req.DryRun) + + req, err = buildRefreshVTXOsRequest( + []string{ + "aa:0", "bb:1", + }, + false, false, + ) + require.NoError(t, err) + require.Equal( + t, []string{ + "aa:0", + "bb:1", + }, + req.GetOutpoints().GetOutpoints(), + ) + require.False(t, req.DryRun) +} + // TestSummarizeRefreshFeeEstimateNil verifies a response without an // estimate (real refresh, empty selection) renders no stderr lines. func TestSummarizeRefreshFeeEstimateNil(t *testing.T) {