mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
Merge pull request #912 from kornpow/listswaps_paginate
add simple pagination to the ListSwaps command
This commit is contained in:
commit
7a620ea4ae
7 changed files with 1295 additions and 864 deletions
|
|
@ -35,6 +35,15 @@ var listSwapsCommand = cli.Command{
|
|||
labelFlag,
|
||||
channelFlag,
|
||||
lastHopFlag,
|
||||
cli.Uint64Flag{
|
||||
Name: "max_swaps",
|
||||
Usage: "Max number of swaps to return after filtering",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "start_time_ns",
|
||||
Usage: "Unix timestamp in nanoseconds to select swaps initiated " +
|
||||
"after this time",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -99,9 +108,19 @@ func listSwaps(ctx *cli.Context) error {
|
|||
filter.Label = ctx.String(labelFlag.Name)
|
||||
}
|
||||
|
||||
// Parse start timestamp if set.
|
||||
if ctx.IsSet("start_time_ns") {
|
||||
startTimestamp, err := strconv.ParseInt(ctx.String("start_time_ns"), 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing start timestamp: %w", err)
|
||||
}
|
||||
filter.StartTimestampNs = startTimestamp
|
||||
}
|
||||
|
||||
resp, err := client.ListSwaps(
|
||||
context.Background(), &looprpc.ListSwapsRequest{
|
||||
ListSwapFilter: filter,
|
||||
MaxSwaps: ctx.Uint64("max_swaps"),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ package loopd
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -563,8 +565,11 @@ func (s *swapClientServer) ListSwaps(ctx context.Context,
|
|||
req *looprpc.ListSwapsRequest) (*looprpc.ListSwapsResponse, error) {
|
||||
|
||||
var (
|
||||
rpcSwaps = []*looprpc.SwapStatus{}
|
||||
idx = 0
|
||||
rpcSwaps = []*looprpc.SwapStatus{}
|
||||
swapInfos = []*loop.SwapInfo{}
|
||||
maxSwaps = int(req.MaxSwaps)
|
||||
nextStartTime = int64(0)
|
||||
canPage = false
|
||||
)
|
||||
|
||||
s.swapsLock.Lock()
|
||||
|
|
@ -580,14 +585,43 @@ func (s *swapClientServer) ListSwaps(ctx context.Context,
|
|||
continue
|
||||
}
|
||||
|
||||
rpcSwap, err := s.marshallSwap(ctx, &swp)
|
||||
swapInfos = append(swapInfos, &swp)
|
||||
}
|
||||
|
||||
// Sort the swaps by initiation time in ascending order (oldest first).
|
||||
slices.SortFunc(swapInfos, func(a, b *loop.SwapInfo) int {
|
||||
return cmp.Compare(
|
||||
a.InitiationTime.UnixNano(),
|
||||
b.InitiationTime.UnixNano(),
|
||||
)
|
||||
})
|
||||
|
||||
// Apply the maxSwaps limit if specified.
|
||||
if maxSwaps > 0 && len(swapInfos) > maxSwaps {
|
||||
canPage = true
|
||||
swapInfos = swapInfos[:maxSwaps]
|
||||
}
|
||||
|
||||
// Marshal the filtered and limited swaps.
|
||||
for _, swp := range swapInfos {
|
||||
rpcSwap, err := s.marshallSwap(ctx, swp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rpcSwaps = append(rpcSwaps, rpcSwap)
|
||||
idx++
|
||||
}
|
||||
return &looprpc.ListSwapsResponse{Swaps: rpcSwaps}, nil
|
||||
|
||||
// Set the next start time for pagination if needed.
|
||||
if canPage && len(rpcSwaps) > 0 {
|
||||
// Use the initiation time of the last swap plus 1 nanosecond.
|
||||
nextStartTime = rpcSwaps[len(rpcSwaps)-1].InitiationTime + 1
|
||||
}
|
||||
|
||||
response := looprpc.ListSwapsResponse{
|
||||
Swaps: rpcSwaps,
|
||||
NextStartTime: nextStartTime,
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// filterSwap filters the given swap based on the provided filter.
|
||||
|
|
@ -617,6 +651,13 @@ func filterSwap(swapInfo *loop.SwapInfo, filter *looprpc.ListSwapsFilter) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// If timestamp filters are set, only return swaps within the specified time range.
|
||||
if filter.StartTimestampNs > 0 &&
|
||||
swapInfo.InitiationTime.UnixNano() < filter.StartTimestampNs {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// If the swap is of type loop out and the outgoing channel filter is
|
||||
// set, we only return swaps that match the filter.
|
||||
if swapInfo.SwapType == swap.TypeOut && filter.OutgoingChanSet != nil {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
|
|
@ -11,8 +12,11 @@ import (
|
|||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop"
|
||||
"github.com/lightninglabs/loop/labels"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/looprpc"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
mock_lnd "github.com/lightninglabs/loop/test"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -595,3 +599,295 @@ func TestHasBandwidth(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestListSwapsFilterAndPagination tests the filtering and
|
||||
// paging of the ListSwaps command.
|
||||
func TestListSwapsFilterAndPagination(t *testing.T) {
|
||||
unixTime := time.Unix(0, 0)
|
||||
firstSwapStartTime := unixTime.Add(10 * time.Minute)
|
||||
secondSwapStartTime := unixTime.Add(20 * time.Minute)
|
||||
thirdSwapStartTime := unixTime.Add(30 * time.Minute)
|
||||
|
||||
// Create a set of test swaps of various types which contain the minimal
|
||||
// viable amount of info to successfully be run through marshallSwap.
|
||||
swapInOrder0 := loop.SwapInfo{
|
||||
SwapStateData: loopdb.SwapStateData{
|
||||
State: loopdb.StateInitiated,
|
||||
Cost: loopdb.SwapCost{},
|
||||
},
|
||||
SwapContract: loopdb.SwapContract{
|
||||
InitiationTime: firstSwapStartTime,
|
||||
},
|
||||
LastUpdate: time.Now(),
|
||||
SwapHash: lntypes.Hash{1},
|
||||
SwapType: swap.TypeIn,
|
||||
HtlcAddressP2WSH: testnetAddr,
|
||||
HtlcAddressP2TR: testnetAddr,
|
||||
}
|
||||
|
||||
swapOutOrder1 := loop.SwapInfo{
|
||||
SwapStateData: loopdb.SwapStateData{
|
||||
State: loopdb.StateInitiated,
|
||||
Cost: loopdb.SwapCost{},
|
||||
},
|
||||
SwapContract: loopdb.SwapContract{
|
||||
InitiationTime: secondSwapStartTime,
|
||||
},
|
||||
LastUpdate: time.Now(),
|
||||
SwapHash: lntypes.Hash{2},
|
||||
SwapType: swap.TypeOut,
|
||||
HtlcAddressP2WSH: testnetAddr,
|
||||
HtlcAddressP2TR: testnetAddr,
|
||||
}
|
||||
|
||||
swapOutOrder2 := loop.SwapInfo{
|
||||
SwapStateData: loopdb.SwapStateData{
|
||||
State: loopdb.StateInitiated,
|
||||
Cost: loopdb.SwapCost{},
|
||||
},
|
||||
SwapContract: loopdb.SwapContract{
|
||||
InitiationTime: thirdSwapStartTime,
|
||||
},
|
||||
LastUpdate: time.Now(),
|
||||
SwapHash: lntypes.Hash{3},
|
||||
SwapType: swap.TypeOut,
|
||||
HtlcAddressP2WSH: testnetAddr,
|
||||
HtlcAddressP2TR: testnetAddr,
|
||||
}
|
||||
|
||||
mockSwaps := []loop.SwapInfo{swapInOrder0, swapOutOrder1, swapOutOrder2}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
// Define the mock swaps that will be stored in the mock client.
|
||||
mockSwaps []loop.SwapInfo
|
||||
req *looprpc.ListSwapsRequest
|
||||
// These hashes must be in the correct return order as the response.
|
||||
expectedReturnedSwaps []lntypes.Hash
|
||||
expectedNextStartTime int64
|
||||
}{
|
||||
{
|
||||
name: "fetch with defaults",
|
||||
mockSwaps: mockSwaps,
|
||||
req: &looprpc.ListSwapsRequest{},
|
||||
expectedReturnedSwaps: []lntypes.Hash{
|
||||
swapInOrder0.SwapHash,
|
||||
swapOutOrder1.SwapHash,
|
||||
swapOutOrder2.SwapHash,
|
||||
},
|
||||
expectedNextStartTime: 0,
|
||||
},
|
||||
{
|
||||
name: "fetch with swaptype=loopin filter",
|
||||
mockSwaps: mockSwaps,
|
||||
req: &looprpc.ListSwapsRequest{
|
||||
ListSwapFilter: &looprpc.ListSwapsFilter{
|
||||
SwapType: looprpc.ListSwapsFilter_LOOP_IN},
|
||||
},
|
||||
expectedReturnedSwaps: []lntypes.Hash{
|
||||
swapInOrder0.SwapHash,
|
||||
},
|
||||
expectedNextStartTime: 0,
|
||||
},
|
||||
{
|
||||
name: "fetch with swaptype=loopout filter",
|
||||
mockSwaps: mockSwaps,
|
||||
req: &looprpc.ListSwapsRequest{
|
||||
ListSwapFilter: &looprpc.ListSwapsFilter{
|
||||
SwapType: looprpc.ListSwapsFilter_LOOP_OUT,
|
||||
},
|
||||
},
|
||||
expectedReturnedSwaps: []lntypes.Hash{
|
||||
swapOutOrder1.SwapHash,
|
||||
swapOutOrder2.SwapHash,
|
||||
},
|
||||
expectedNextStartTime: 0,
|
||||
},
|
||||
{
|
||||
name: "fetch with limit",
|
||||
mockSwaps: mockSwaps,
|
||||
req: &looprpc.ListSwapsRequest{
|
||||
MaxSwaps: 2,
|
||||
},
|
||||
expectedReturnedSwaps: []lntypes.Hash{
|
||||
swapInOrder0.SwapHash,
|
||||
swapOutOrder1.SwapHash,
|
||||
},
|
||||
expectedNextStartTime: secondSwapStartTime.UnixNano() + 1,
|
||||
},
|
||||
{
|
||||
name: "fetch with limit set to default",
|
||||
mockSwaps: mockSwaps,
|
||||
req: &looprpc.ListSwapsRequest{
|
||||
MaxSwaps: 0,
|
||||
},
|
||||
expectedReturnedSwaps: []lntypes.Hash{
|
||||
swapInOrder0.SwapHash,
|
||||
swapOutOrder1.SwapHash,
|
||||
swapOutOrder2.SwapHash,
|
||||
},
|
||||
expectedNextStartTime: 0,
|
||||
},
|
||||
{
|
||||
name: "fetch with time filter #1",
|
||||
mockSwaps: mockSwaps,
|
||||
req: &looprpc.ListSwapsRequest{
|
||||
ListSwapFilter: &looprpc.ListSwapsFilter{
|
||||
StartTimestampNs: unixTime.Add(25 * time.Minute).UnixNano(),
|
||||
},
|
||||
},
|
||||
expectedReturnedSwaps: []lntypes.Hash{
|
||||
swapOutOrder2.SwapHash,
|
||||
},
|
||||
expectedNextStartTime: 0,
|
||||
},
|
||||
{
|
||||
name: "fetch with time filter #2",
|
||||
mockSwaps: mockSwaps,
|
||||
req: &looprpc.ListSwapsRequest{
|
||||
ListSwapFilter: &looprpc.ListSwapsFilter{
|
||||
StartTimestampNs: unixTime.Add(5 * time.Minute).UnixNano(),
|
||||
},
|
||||
},
|
||||
expectedReturnedSwaps: []lntypes.Hash{
|
||||
swapInOrder0.SwapHash,
|
||||
swapOutOrder1.SwapHash,
|
||||
swapOutOrder2.SwapHash,
|
||||
},
|
||||
expectedNextStartTime: 0,
|
||||
},
|
||||
{
|
||||
name: "fetch with swaptype=loopout filter, time filter, limit set",
|
||||
mockSwaps: mockSwaps,
|
||||
req: &looprpc.ListSwapsRequest{
|
||||
ListSwapFilter: &looprpc.ListSwapsFilter{
|
||||
SwapType: looprpc.ListSwapsFilter_LOOP_OUT,
|
||||
StartTimestampNs: unixTime.Add(15 * time.Minute).UnixNano(),
|
||||
},
|
||||
MaxSwaps: 1,
|
||||
},
|
||||
expectedReturnedSwaps: []lntypes.Hash{
|
||||
swapOutOrder1.SwapHash,
|
||||
},
|
||||
expectedNextStartTime: secondSwapStartTime.UnixNano() + 1,
|
||||
},
|
||||
{
|
||||
name: "fetch with time filter, limit set",
|
||||
mockSwaps: mockSwaps,
|
||||
req: &looprpc.ListSwapsRequest{
|
||||
ListSwapFilter: &looprpc.ListSwapsFilter{
|
||||
StartTimestampNs: unixTime.UnixNano(),
|
||||
},
|
||||
MaxSwaps: 2,
|
||||
},
|
||||
expectedReturnedSwaps: []lntypes.Hash{
|
||||
swapInOrder0.SwapHash,
|
||||
swapOutOrder1.SwapHash,
|
||||
},
|
||||
expectedNextStartTime: secondSwapStartTime.UnixNano() + 1,
|
||||
},
|
||||
{
|
||||
name: "fetch with time filter, limit set 2",
|
||||
mockSwaps: mockSwaps,
|
||||
req: &looprpc.ListSwapsRequest{
|
||||
ListSwapFilter: &looprpc.ListSwapsFilter{
|
||||
StartTimestampNs: unixTime.UnixNano(),
|
||||
},
|
||||
MaxSwaps: 3,
|
||||
},
|
||||
expectedReturnedSwaps: []lntypes.Hash{
|
||||
swapInOrder0.SwapHash,
|
||||
swapOutOrder1.SwapHash,
|
||||
swapOutOrder2.SwapHash,
|
||||
},
|
||||
expectedNextStartTime: 0,
|
||||
},
|
||||
{
|
||||
name: "fetch with time filter edge case 1",
|
||||
mockSwaps: mockSwaps,
|
||||
req: &looprpc.ListSwapsRequest{
|
||||
ListSwapFilter: &looprpc.ListSwapsFilter{
|
||||
StartTimestampNs: secondSwapStartTime.UnixNano(),
|
||||
},
|
||||
},
|
||||
expectedReturnedSwaps: []lntypes.Hash{
|
||||
swapOutOrder1.SwapHash,
|
||||
swapOutOrder2.SwapHash,
|
||||
},
|
||||
expectedNextStartTime: 0,
|
||||
},
|
||||
{
|
||||
name: "fetch with time filter edge case 2",
|
||||
mockSwaps: mockSwaps,
|
||||
req: &looprpc.ListSwapsRequest{
|
||||
ListSwapFilter: &looprpc.ListSwapsFilter{
|
||||
StartTimestampNs: secondSwapStartTime.UnixNano() + 1,
|
||||
},
|
||||
},
|
||||
expectedReturnedSwaps: []lntypes.Hash{
|
||||
swapOutOrder2.SwapHash,
|
||||
},
|
||||
expectedNextStartTime: 0,
|
||||
},
|
||||
{
|
||||
name: "fetch with time filter edge case 3",
|
||||
mockSwaps: mockSwaps,
|
||||
req: &looprpc.ListSwapsRequest{
|
||||
ListSwapFilter: &looprpc.ListSwapsFilter{
|
||||
StartTimestampNs: secondSwapStartTime.UnixNano() - 1,
|
||||
},
|
||||
},
|
||||
expectedReturnedSwaps: []lntypes.Hash{
|
||||
swapOutOrder1.SwapHash,
|
||||
swapOutOrder2.SwapHash,
|
||||
},
|
||||
expectedNextStartTime: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create the swap client server with our mock client.
|
||||
server := &swapClientServer{
|
||||
swaps: make(map[lntypes.Hash]loop.SwapInfo),
|
||||
}
|
||||
|
||||
// Populate the server's swap cache with our mock swaps.
|
||||
for _, swap := range test.mockSwaps {
|
||||
server.swaps[swap.SwapHash] = swap
|
||||
}
|
||||
|
||||
// Call the ListSwaps method.
|
||||
resp, err := server.ListSwaps(context.Background(), test.req)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(
|
||||
t,
|
||||
resp.Swaps,
|
||||
len(test.expectedReturnedSwaps),
|
||||
"incorrect returned count",
|
||||
)
|
||||
|
||||
// Check order of returned swaps is exactly as expected.
|
||||
for idx, aswap := range resp.Swaps {
|
||||
newhash, err := lntypes.MakeHash(aswap.GetIdBytes())
|
||||
require.NoError(t, err)
|
||||
require.Equal(
|
||||
t,
|
||||
test.expectedReturnedSwaps[idx],
|
||||
newhash,
|
||||
"iteration order mismatch",
|
||||
)
|
||||
}
|
||||
|
||||
require.Equal(
|
||||
t,
|
||||
test.expectedNextStartTime,
|
||||
resp.NextStartTime,
|
||||
"incorrect next start time",
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1752
looprpc/client.pb.go
1752
looprpc/client.pb.go
File diff suppressed because it is too large
Load diff
|
|
@ -675,6 +675,9 @@ enum FailureReason {
|
|||
message ListSwapsRequest {
|
||||
// Optional filter to only return swaps that match the filter.
|
||||
ListSwapsFilter list_swap_filter = 1;
|
||||
|
||||
// Set a maximum number of swaps to return in the response.
|
||||
uint64 max_swaps = 2;
|
||||
}
|
||||
|
||||
message ListSwapsFilter {
|
||||
|
|
@ -704,6 +707,9 @@ message ListSwapsFilter {
|
|||
|
||||
// If specified, only returns asset swaps.
|
||||
bool asset_swap_only = 6;
|
||||
|
||||
// If specified, returns swaps initiated after this Unix (ns) timestamp.
|
||||
int64 start_timestamp_ns = 7;
|
||||
}
|
||||
|
||||
message ListSwapsResponse {
|
||||
|
|
@ -711,6 +717,9 @@ message ListSwapsResponse {
|
|||
The list of all currently known swaps and their status.
|
||||
*/
|
||||
repeated SwapStatus swaps = 1;
|
||||
|
||||
// Timestamp to use for paging start_timestamp_ns.
|
||||
int64 next_start_time = 2;
|
||||
}
|
||||
|
||||
message SwapInfoRequest {
|
||||
|
|
|
|||
|
|
@ -625,6 +625,22 @@
|
|||
"in": "query",
|
||||
"required": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "list_swap_filter.start_timestamp_ns",
|
||||
"description": "If specified, returns swaps initiated after this Unix (ns) timestamp.",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"format": "int64"
|
||||
},
|
||||
{
|
||||
"name": "max_swaps",
|
||||
"description": "Set a maximum number of swaps to return in the response.",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"format": "uint64"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
|
|
@ -1403,6 +1419,11 @@
|
|||
"asset_swap_only": {
|
||||
"type": "boolean",
|
||||
"description": "If specified, only returns asset swaps."
|
||||
},
|
||||
"start_timestamp_ns": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "If specified, returns swaps initiated after this Unix (ns) timestamp."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -1415,6 +1436,11 @@
|
|||
"$ref": "#/definitions/looprpcSwapStatus"
|
||||
},
|
||||
"description": "The list of all currently known swaps and their status."
|
||||
},
|
||||
"next_start_time": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "Timestamp of the last swap returned."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ This file tracks release notes for the loop client.
|
|||
## Next release
|
||||
|
||||
#### New Features
|
||||
* [Enhance](https://github.com/lightninglabs/loop/pull/912) the
|
||||
`loop listswaps` command by improving the ability to filter the
|
||||
response. Use `--start_timestamp_ns` to return only swaps after
|
||||
that timestamp. Use `--max_swaps` to limit total swap outputs.
|
||||
Paging is enabled using the `next_start_time` field in the response.
|
||||
|
||||
|
||||
#### Breaking Changes
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue