diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index 8a4a41cfc..bed6efa80 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -1966,6 +1966,10 @@ func (s *swapClientServer) ListStaticAddressSwaps(ctx context.Context, var clientSwaps []*looprpc.StaticAddressLoopInSwap for _, swp := range swaps { + if swp == nil { + continue + } + chainParams, err := s.network.ChainParams() if err != nil { return nil, fmt.Errorf("error getting chain params") @@ -2005,6 +2009,9 @@ func (s *swapClientServer) ListStaticAddressSwaps(ctx context.Context, if swp.SelectedAmount > 0 { swapAmount = swp.SelectedAmount } + costServer := staticAddressLoopInSwapServerCost(swp) + initiationTime := staticAddressLoopInTimestamp(swp.InitiationTime) + lastUpdateTime := staticAddressLoopInTimestamp(swp.LastUpdateTime) swap := &looprpc.StaticAddressLoopInSwap{ SwapHash: swp.SwapHash[:], DepositOutpoints: swp.DepositOutpoints, @@ -2012,6 +2019,9 @@ func (s *swapClientServer) ListStaticAddressSwaps(ctx context.Context, SwapAmountSatoshis: int64(swapAmount), PaymentRequestAmountSatoshis: payReqAmount, Deposits: protoDeposits, + InitiationTime: initiationTime, + LastUpdateTime: lastUpdateTime, + CostServer: costServer, } clientSwaps = append(clientSwaps, swap) @@ -2022,6 +2032,31 @@ func (s *swapClientServer) ListStaticAddressSwaps(ctx context.Context, }, nil } +func staticAddressLoopInTimestamp(t time.Time) int64 { + if t.IsZero() { + return 0 + } + + return t.UnixNano() +} + +// staticAddressLoopInSwapServerCost returns the paid server cost using the +// legacy ListSwaps cost semantics. Static loop-ins currently only persist the +// accepted quote fee, and that fee is paid once the swap invoice settles. +// Timeout-path miner fees are not persisted, so cost_onchain and cost_offchain +// remain zero instead of returning an estimate as an actual cost. +func staticAddressLoopInSwapServerCost(swp *loopin.StaticAddressLoopIn) int64 { + switch swp.GetState() { + case loopin.PaymentReceived, loopin.Succeeded, + loopin.SucceededTransitioningFailed: + + return int64(swp.QuotedSwapFee) + + default: + return 0 + } +} + // GetStaticAddressSummary returns a summary of static address-related // information. Amongst deposits and withdrawals and their total values, it also // includes a list of detailed deposit information filtered by their state. diff --git a/loopd/swapclient_server_test.go b/loopd/swapclient_server_test.go index fff26c64e..5084ccc87 100644 --- a/loopd/swapclient_server_test.go +++ b/loopd/swapclient_server_test.go @@ -20,6 +20,7 @@ import ( "github.com/lightninglabs/loop/looprpc" "github.com/lightninglabs/loop/staticaddr/address" "github.com/lightninglabs/loop/staticaddr/deposit" + "github.com/lightninglabs/loop/staticaddr/loopin" "github.com/lightninglabs/loop/staticaddr/script" "github.com/lightninglabs/loop/swap" mock_lnd "github.com/lightninglabs/loop/test" @@ -306,6 +307,72 @@ func TestSetLiquidityParamsRejectsStaticAutoloopWithoutExperimental( require.ErrorContains(t, err, "--experimental") } +// TestStaticAddressLoopInTimestamp verifies that zero timestamps are omitted +// from static loop-in responses instead of passing a zero time to UnixNano. +func TestStaticAddressLoopInTimestamp(t *testing.T) { + require.Zero(t, staticAddressLoopInTimestamp(time.Time{})) + + timestamp := time.Unix(1_234, 567).UTC() + require.Equal( + t, timestamp.UnixNano(), + staticAddressLoopInTimestamp(timestamp), + ) +} + +// TestStaticAddressLoopInSwapServerCost verifies that static loop-in server +// costs are only reported once the invoice payment was received. Timeout path +// costs are not persisted today, so they are intentionally not estimated here. +func TestStaticAddressLoopInSwapServerCost(t *testing.T) { + const quoteFee = btcutil.Amount(1_234) + + tests := []struct { + name string + state fsm.StateType + wantServer int64 + }{ + { + name: "pending before payment", + state: loopin.SignHtlcTx, + }, + { + name: "payment received", + state: loopin.PaymentReceived, + wantServer: int64(quoteFee), + }, + { + name: "succeeded", + state: loopin.Succeeded, + wantServer: int64(quoteFee), + }, + { + name: "succeeded transition failed", + state: loopin.SucceededTransitioningFailed, + wantServer: int64(quoteFee), + }, + { + name: "timeout swept", + state: loopin.HtlcTimeoutSwept, + }, + { + name: "failed", + state: loopin.Failed, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + swap := &loopin.StaticAddressLoopIn{ + QuotedSwapFee: quoteFee, + } + swap.SetState(test.state) + + costServer := staticAddressLoopInSwapServerCost(swap) + + require.Equal(t, test.wantServer, costServer) + }) + } +} + // TestRPCAutoloopReasonStaticLoopInNoCandidate verifies that the new planner // reason is exposed over rpc. func TestRPCAutoloopReasonStaticLoopInNoCandidate(t *testing.T) { diff --git a/looprpc/client.pb.go b/looprpc/client.pb.go index d0cf44f17..3305bfcde 100644 --- a/looprpc/client.pb.go +++ b/looprpc/client.pb.go @@ -5874,7 +5874,17 @@ type StaticAddressLoopInSwap struct { // fees. PaymentRequestAmountSatoshis int64 `protobuf:"varint,5,opt,name=payment_request_amount_satoshis,json=paymentRequestAmountSatoshis,proto3" json:"payment_request_amount_satoshis,omitempty"` // The deposits that were used for this swap. - Deposits []*Deposit `protobuf:"bytes,6,rep,name=deposits,proto3" json:"deposits,omitempty"` + Deposits []*Deposit `protobuf:"bytes,6,rep,name=deposits,proto3" json:"deposits,omitempty"` + // Initiation time of the swap. + InitiationTime int64 `protobuf:"varint,7,opt,name=initiation_time,json=initiationTime,proto3" json:"initiation_time,omitempty"` + // Last update time of the swap. + LastUpdateTime int64 `protobuf:"varint,8,opt,name=last_update_time,json=lastUpdateTime,proto3" json:"last_update_time,omitempty"` + // Swap server cost. + CostServer int64 `protobuf:"varint,9,opt,name=cost_server,json=costServer,proto3" json:"cost_server,omitempty"` + // On-chain transaction cost. + CostOnchain int64 `protobuf:"varint,10,opt,name=cost_onchain,json=costOnchain,proto3" json:"cost_onchain,omitempty"` + // Off-chain routing fees. + CostOffchain int64 `protobuf:"varint,11,opt,name=cost_offchain,json=costOffchain,proto3" json:"cost_offchain,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -5951,6 +5961,41 @@ func (x *StaticAddressLoopInSwap) GetDeposits() []*Deposit { return nil } +func (x *StaticAddressLoopInSwap) GetInitiationTime() int64 { + if x != nil { + return x.InitiationTime + } + return 0 +} + +func (x *StaticAddressLoopInSwap) GetLastUpdateTime() int64 { + if x != nil { + return x.LastUpdateTime + } + return 0 +} + +func (x *StaticAddressLoopInSwap) GetCostServer() int64 { + if x != nil { + return x.CostServer + } + return 0 +} + +func (x *StaticAddressLoopInSwap) GetCostOnchain() int64 { + if x != nil { + return x.CostOnchain + } + return 0 +} + +func (x *StaticAddressLoopInSwap) GetCostOffchain() int64 { + if x != nil { + return x.CostOffchain + } + return 0 +} + type StaticAddressLoopInRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The outpoints of the deposits to loop-in. @@ -6972,14 +7017,21 @@ const file_client_proto_rawDesc = "" + "\x1dtotal_deposit_amount_satoshis\x18\x03 \x01(\x03R\x1atotalDepositAmountSatoshis\x12:\n" + "\x19withdrawn_amount_satoshis\x18\x04 \x01(\x03R\x17withdrawnAmountSatoshis\x124\n" + "\x16change_amount_satoshis\x18\x05 \x01(\x03R\x14changeAmountSatoshis\x12/\n" + - "\x13confirmation_height\x18\x06 \x01(\rR\x12confirmationHeight\"\xc7\x02\n" + + "\x13confirmation_height\x18\x06 \x01(\rR\x12confirmationHeight\"\x83\x04\n" + "\x17StaticAddressLoopInSwap\x12\x1b\n" + "\tswap_hash\x18\x01 \x01(\fR\bswapHash\x12+\n" + "\x11deposit_outpoints\x18\x02 \x03(\tR\x10depositOutpoints\x12;\n" + "\x05state\x18\x03 \x01(\x0e2%.looprpc.StaticAddressLoopInSwapStateR\x05state\x120\n" + "\x14swap_amount_satoshis\x18\x04 \x01(\x03R\x12swapAmountSatoshis\x12E\n" + "\x1fpayment_request_amount_satoshis\x18\x05 \x01(\x03R\x1cpaymentRequestAmountSatoshis\x12,\n" + - "\bdeposits\x18\x06 \x03(\v2\x10.looprpc.DepositR\bdeposits\"\xef\x02\n" + + "\bdeposits\x18\x06 \x03(\v2\x10.looprpc.DepositR\bdeposits\x12'\n" + + "\x0finitiation_time\x18\a \x01(\x03R\x0einitiationTime\x12(\n" + + "\x10last_update_time\x18\b \x01(\x03R\x0elastUpdateTime\x12\x1f\n" + + "\vcost_server\x18\t \x01(\x03R\n" + + "costServer\x12!\n" + + "\fcost_onchain\x18\n" + + " \x01(\x03R\vcostOnchain\x12#\n" + + "\rcost_offchain\x18\v \x01(\x03R\fcostOffchain\"\xef\x02\n" + "\x1aStaticAddressLoopInRequest\x12\x1c\n" + "\toutpoints\x18\x01 \x03(\tR\toutpoints\x121\n" + "\x15max_swap_fee_satoshis\x18\x02 \x01(\x03R\x12maxSwapFeeSatoshis\x12\x19\n" + diff --git a/looprpc/client.proto b/looprpc/client.proto index 52b9fcbb0..3ae690dc3 100644 --- a/looprpc/client.proto +++ b/looprpc/client.proto @@ -2143,6 +2143,31 @@ message StaticAddressLoopInSwap { The deposits that were used for this swap. */ repeated Deposit deposits = 6; + + /* + Initiation time of the swap. + */ + int64 initiation_time = 7; + + /* + Last update time of the swap. + */ + int64 last_update_time = 8; + + /* + Swap server cost. + */ + int64 cost_server = 9; + + /* + On-chain transaction cost. + */ + int64 cost_onchain = 10; + + /* + Off-chain routing fees. + */ + int64 cost_offchain = 11; } enum StaticAddressLoopInSwapState { diff --git a/looprpc/client.swagger.json b/looprpc/client.swagger.json index e336cb7dc..0cdbc1ef0 100644 --- a/looprpc/client.swagger.json +++ b/looprpc/client.swagger.json @@ -2807,6 +2807,31 @@ "$ref": "#/definitions/looprpcDeposit" }, "description": "The deposits that were used for this swap." + }, + "initiation_time": { + "type": "string", + "format": "int64", + "description": "Initiation time of the swap." + }, + "last_update_time": { + "type": "string", + "format": "int64", + "description": "Last update time of the swap." + }, + "cost_server": { + "type": "string", + "format": "int64", + "description": "Swap server cost." + }, + "cost_onchain": { + "type": "string", + "format": "int64", + "description": "On-chain transaction cost." + }, + "cost_offchain": { + "type": "string", + "format": "int64", + "description": "Off-chain routing fees." } } },