Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 54 additions & 3 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1059,9 +1059,57 @@ func (r *rpcServer) ListAssets(ctx context.Context,
"and include_leased")
}

constraints := tapfreighter.CommitmentConstraints{
MinAmt: req.MinAmount,
MaxAmt: req.MaxAmount,
CoinSelectType: tapsend.DefaultCoinSelectType,
}

if len(req.GroupKey) > 0 {
groupKey, err := btcec.ParsePubKey(req.GroupKey)
if err != nil {
return nil, fmt.Errorf("error parsing group key: %w",
err)
}

constraints.AssetSpecifier = asset.NewSpecifierFromGroupKey(
*groupKey,
)
}

filters := &tapdb.AssetQueryFilters{
CommitmentConstraints: constraints,
}

if req.ScriptKey != nil {
scriptKey, err := taprpc.UnmarshalScriptKey(req.ScriptKey)
if err != nil {
return nil, fmt.Errorf("unable to decode script key: "+
"%w", err)
}

filters.ScriptKey = scriptKey
}

if req.AnchorOutpoint != nil {
txid, err := chainhash.NewHash(req.AnchorOutpoint.Txid)
if err != nil {
return nil, fmt.Errorf("error parsing outpoint: %w",
err)
}
outPoint := &wire.OutPoint{
Hash: *txid,
Index: req.AnchorOutpoint.OutputIndex,
}

filters.AnchorPoint = outPoint
}

rpcAssets, err := r.fetchRpcAssets(
ctx, req.WithWitness, req.IncludeSpent, req.IncludeLeased,
filters,
)

if err != nil {
return nil, err
}
Expand Down Expand Up @@ -1114,10 +1162,11 @@ func (r *rpcServer) ListAssets(ctx context.Context,
}

func (r *rpcServer) fetchRpcAssets(ctx context.Context, withWitness,
includeSpent, includeLeased bool) ([]*taprpc.Asset, error) {
includeSpent, includeLeased bool,
queryFilters *tapdb.AssetQueryFilters) ([]*taprpc.Asset, error) {

assets, err := r.cfg.AssetStore.FetchAllAssets(
ctx, includeSpent, includeLeased, nil,
ctx, includeSpent, includeLeased, queryFilters,
)
if err != nil {
return nil, fmt.Errorf("unable to read chain assets: %w", err)
Expand Down Expand Up @@ -1243,7 +1292,9 @@ func (r *rpcServer) listBalancesByGroupKey(ctx context.Context,
func (r *rpcServer) ListUtxos(ctx context.Context,
req *taprpc.ListUtxosRequest) (*taprpc.ListUtxosResponse, error) {

rpcAssets, err := r.fetchRpcAssets(ctx, false, false, req.IncludeLeased)
rpcAssets, err := r.fetchRpcAssets(
ctx, false, false, req.IncludeLeased, nil,
)
if err != nil {
return nil, err
}
Expand Down
48 changes: 40 additions & 8 deletions tapdb/assets_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,7 @@ func (a *AssetStore) dbAssetsToChainAssets(dbAssets []ConfirmedAsset,
// constraintsToDbFilter maps application level constraints to the set of
// filters we use in the SQL queries.
func (a *AssetStore) constraintsToDbFilter(
query *AssetQueryFilters) QueryAssetFilters {
query *AssetQueryFilters) (QueryAssetFilters, error) {

assetFilter := QueryAssetFilters{
Now: sql.NullTime{
Expand All @@ -868,17 +868,36 @@ func (a *AssetStore) constraintsToDbFilter(
}
if query != nil {
if query.MinAmt != 0 {
assetFilter.MinAmt = sql.NullInt64{
Int64: int64(query.MinAmt),
Valid: true,
}
assetFilter.MinAmt = sqlInt64(query.MinAmt)
}

if query.MaxAmt != 0 {
assetFilter.MaxAmt = sqlInt64(query.MaxAmt)
}

if query.MinAnchorHeight != 0 {
assetFilter.MinAnchorHeight = sqlInt32(
query.MinAnchorHeight,
)
}

if query.ScriptKey != nil {
key := query.ScriptKey.PubKey
assetFilter.TweakedScriptKey = key.SerializeCompressed()
}

if query.AnchorPoint != nil {
anchorPointBytes, err := encodeOutpoint(
*query.AnchorPoint,
)
if err != nil {
return QueryAssetFilters{}, fmt.Errorf(
"unable to encode outpoint: %w", err)
}

assetFilter.AnchorPoint = anchorPointBytes
}

// Add asset ID bytes and group key bytes to the filter. These
// byte arrays are empty if the asset ID or group key is not
// specified in the query.
Expand All @@ -903,7 +922,7 @@ func (a *AssetStore) constraintsToDbFilter(
}
}

return assetFilter
return assetFilter, nil
}

// specificAssetFilter maps the given asset parameters to the set of filters
Expand Down Expand Up @@ -975,6 +994,13 @@ type AssetQueryFilters struct {
// MinAnchorHeight is the minimum block height the asset's anchor tx
// must have been confirmed at.
MinAnchorHeight int32

// ScriptKey allows filtering by asset script key.
ScriptKey *asset.ScriptKey

// AnchorPoint allows filtering by the outpoint the asset is anchored
// to.
AnchorPoint *wire.OutPoint
}

// QueryBalancesByAsset queries the balances for assets or alternatively
Expand Down Expand Up @@ -1194,7 +1220,10 @@ func (a *AssetStore) FetchAllAssets(ctx context.Context, includeSpent,

// We'll now map the application level filtering to the type of
// filtering our database query understands.
assetFilter := a.constraintsToDbFilter(query)
assetFilter, err := a.constraintsToDbFilter(query)
if err != nil {
return nil, err
}

// By default, the spent boolean is null, which means we'll fetch all
// assets. Only if we should exclude spent assets, we'll set the spent
Expand Down Expand Up @@ -1995,9 +2024,12 @@ func (a *AssetStore) ListEligibleCoins(ctx context.Context,

// First, we'll map the commitment constraints to our database query
// filters.
assetFilter := a.constraintsToDbFilter(&AssetQueryFilters{
assetFilter, err := a.constraintsToDbFilter(&AssetQueryFilters{
CommitmentConstraints: constraints,
})
if err != nil {
return nil, err
}

// We only want to select unspent and non-leased commitments.
assetFilter.Spent = sqlBool(false)
Expand Down
75 changes: 74 additions & 1 deletion tapdb/assets_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,12 @@ func filterMinAmt(amt uint64) filterOpt {
}
}

func filterMaxAmt(amt uint64) filterOpt {
return func(f *AssetQueryFilters) {
f.MaxAmt = amt
}
}

func filterCoinSelectType(typ tapsend.CoinSelectType) filterOpt {
return func(f *AssetQueryFilters) {
f.CoinSelectType = typ
Expand All @@ -742,6 +748,18 @@ func filterAnchorHeight(height int32) filterOpt {
}
}

func filterAnchorPoint(point *wire.OutPoint) filterOpt {
return func(f *AssetQueryFilters) {
f.AnchorPoint = point
}
}

func filterScriptKey(key *asset.ScriptKey) filterOpt {
return func(f *AssetQueryFilters) {
f.ScriptKey = key
}
}

// TestFetchAllAssets tests that the different AssetQueryFilters work as
// expected.
func TestFetchAllAssets(t *testing.T) {
Expand Down Expand Up @@ -780,6 +798,7 @@ func TestFetchAllAssets(t *testing.T) {
assetGen: assetGen.assetGens[2],
anchorPoint: assetGen.anchorPoints[0],
amt: 34,
scriptKey: scriptKeyWithScript,
}, {
assetGen: assetGen.assetGens[3],
anchorPoint: assetGen.anchorPoints[1],
Expand Down Expand Up @@ -898,6 +917,38 @@ func TestFetchAllAssets(t *testing.T) {
includeLeased: true,
includeSpent: true,
numAssets: 8,
}, {
name: "max amount",
filter: makeFilter(
filterMaxAmt(100),
filterCoinSelectType(tapsend.ScriptTreesAllowed),
),
numAssets: 6,
}, {
name: "max amount, include spent",
filter: makeFilter(
filterMaxAmt(100),
filterCoinSelectType(tapsend.ScriptTreesAllowed),
),
includeSpent: true,
numAssets: 7,
}, {
name: "max amount, include leased",
filter: makeFilter(
filterMaxAmt(100),
filterCoinSelectType(tapsend.ScriptTreesAllowed),
),
includeLeased: true,
numAssets: 8,
}, {
name: "max amount, include leased, include spent",
filter: makeFilter(
filterMaxAmt(100),
filterCoinSelectType(tapsend.ScriptTreesAllowed),
),
includeLeased: true,
includeSpent: true,
numAssets: 9,
}, {
name: "default min height, include spent",
filter: makeFilter(
Expand All @@ -914,7 +965,7 @@ func TestFetchAllAssets(t *testing.T) {
),
numAssets: 0,
}, {
name: "default min height, include spent",
name: "specific height, include spent",
filter: makeFilter(
filterAnchorHeight(502),
filterCoinSelectType(tapsend.ScriptTreesAllowed),
Expand All @@ -928,6 +979,21 @@ func TestFetchAllAssets(t *testing.T) {
filterCoinSelectType(tapsend.Bip86Only),
),
numAssets: 0,
}, {
name: "query by script key",
filter: makeFilter(
filterScriptKey(scriptKeyWithScript),
filterCoinSelectType(tapsend.ScriptTreesAllowed),
),
numAssets: 1,
}, {
name: "query by script key, include leased",
filter: makeFilter(
filterScriptKey(scriptKeyWithScript),
filterCoinSelectType(tapsend.ScriptTreesAllowed),
),
includeLeased: true,
numAssets: 2,
}, {
name: "query by group key only",
filter: makeFilter(
Expand All @@ -952,6 +1018,13 @@ func TestFetchAllAssets(t *testing.T) {
)), filterDistinctSpecifier(),
),
numAssets: 2,
}, {
name: "query by anchor point",
filter: makeFilter(
filterAnchorPoint(&assetGen.anchorPoints[0]),
filterCoinSelectType(tapsend.ScriptTreesAllowed),
),
numAssets: 3,
}}

for _, tc := range testCases {
Expand Down
17 changes: 10 additions & 7 deletions tapdb/sqlc/assets.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tapdb/sqlc/queries/assets.sql
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ JOIN chain_txns txns
-- specified.
WHERE (
assets.amount >= COALESCE(sqlc.narg('min_amt'), assets.amount) AND
assets.amount <= COALESCE(sqlc.narg('max_amt'), assets.amount) AND
assets.spent = COALESCE(sqlc.narg('spent'), assets.spent) AND
(key_group_info_view.tweaked_group_key = sqlc.narg('key_group_filter') OR
sqlc.narg('key_group_filter') IS NULL) AND
Expand Down
4 changes: 4 additions & 0 deletions tapfreighter/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ type CommitmentConstraints struct {
// to satisfy the constraints.
MinAmt uint64

// MaxAmt specifies the maximum amount that an asset commitment needs to
// hold to satisfy the constraints.
MaxAmt uint64

// PrevIDs are the set of inputs allowed to be used.
PrevIDs []asset.PrevID

Expand Down
Loading
Loading