feat: price_oracle_metadata via addinvoice CLI

This commit is contained in:
Sergey B. 2026-04-16 09:29:20 +00:00
parent 11f079419c
commit 224f742137
18 changed files with 111 additions and 26 deletions

View file

@ -701,7 +701,7 @@ var addInvoiceCommand = cli.Command{
Taproot Assets.
`,
ArgsUsage: "[--asset_id=X | --group_key=X] --asset_amount=Y " +
"[--rfq_peer_pubkey=Z] ",
"[--rfq_peer_pubkey=Z] [--price_oracle_metadata=...]",
Flags: append(
commands.AddInvoiceCommand.Flags,
cli.StringFlag{
@ -726,6 +726,12 @@ var addInvoiceCommand = cli.Command{
"are multiple channels with the same " +
"asset ID present",
},
cli.StringFlag{
Name: "price_oracle_metadata",
Usage: "(optional) opaque metadata forwarded to the " +
"price oracle when creating the invoice; JSON " +
"is recommended. Maximum length is 32768 bytes",
},
),
Action: addInvoice,
}
@ -789,7 +795,7 @@ func addInvoice(cli *cli.Context) error {
defer cleanup()
channelsClient := tchrpc.NewTaprootAssetChannelsClient(tapdConn)
resp, err := channelsClient.AddInvoice(ctx, &tchrpc.AddInvoiceRequest{
addReq := &tchrpc.AddInvoiceRequest{
AssetId: assetIDBytes,
GroupKey: groupKeyBytes,
AssetAmount: assetAmount,
@ -805,7 +811,16 @@ func addInvoice(cli *cli.Context) error {
Private: cli.Bool("private"),
IsAmp: cli.Bool("amp"),
},
})
}
if cli.IsSet("price_oracle_metadata") {
metadata := cli.String("price_oracle_metadata")
if err := ValidatePriceOracleMetadata(metadata); err != nil {
return err
}
addReq.PriceOracleMetadata = metadata
}
resp, err := channelsClient.AddInvoice(ctx, addReq)
if err != nil {
return fmt.Errorf("error adding invoice: %w", err)
}

View file

@ -0,0 +1,16 @@
package main
import "fmt"
const maxPriceOracleMetadataBytes = 32768
// ValidatePriceOracleMetadata checks that metadata for AddInvoice stays within
// tapd's documented maximum size.
func ValidatePriceOracleMetadata(s string) error {
if len(s) > maxPriceOracleMetadataBytes {
return fmt.Errorf("price_oracle_metadata exceeds maximum length "+
"of %d bytes (got %d)", maxPriceOracleMetadataBytes, len(s))
}
return nil
}

View file

@ -0,0 +1,47 @@
package main
import (
"strings"
"testing"
)
func TestValidatePriceOracleMetadata(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
wantErr bool
}{
{
name: "empty",
input: "",
wantErr: false,
},
{
name: "at limit",
input: strings.Repeat("a", maxPriceOracleMetadataBytes),
wantErr: false,
},
{
name: "over limit",
input: strings.Repeat("a", maxPriceOracleMetadataBytes+1),
wantErr: true,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := ValidatePriceOracleMetadata(tc.input)
if tc.wantErr && err == nil {
t.Fatal("expected error")
}
if !tc.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}

View file

@ -1,3 +1,5 @@
//go:build dev
package itest
import (

View file

@ -1,3 +1,5 @@
//go:build dev
package itest
import (

View file

@ -1,3 +1,5 @@
//go:build dev
package itest
import (

View file

@ -1,3 +1,5 @@
//go:build dev
package itest
import (

View file

@ -1,3 +1,5 @@
//go:build dev
package itest
import (

View file

@ -1,3 +1,5 @@
//go:build dev
package itest
import (

View file

@ -1,3 +1,5 @@
//go:build dev
package itest
import (

View file

@ -1,3 +1,5 @@
//go:build dev
package itest
import (

View file

@ -1,4 +1,4 @@
//go:build !itest
//go:build dev && !itest
package itest

View file

@ -1,4 +1,4 @@
//go:build itest
//go:build dev && itest
package itest

View file

@ -1,3 +1,5 @@
//go:build dev
package itest
import (

View file

@ -1,3 +1,5 @@
//go:build dev
package itest
import (

View file

@ -1,3 +1,5 @@
//go:build dev
package itest
import (

View file

@ -1,3 +1,5 @@
//go:build dev
package itest
import (

View file

@ -5,8 +5,6 @@ package perms
import (
"net"
"github.com/lightningnetwork/lnd/autopilot"
"github.com/lightningnetwork/lnd/chainreg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
@ -19,9 +17,7 @@ import (
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc"
"github.com/lightningnetwork/lnd/lnrpc/wtclientrpc"
"github.com/lightningnetwork/lnd/lntest/mock"
"github.com/lightningnetwork/lnd/routing"
"github.com/lightningnetwork/lnd/sweep"
)
// mockConfig implements lnrpc.SubServerConfigDispatcher. It provides the
@ -50,14 +46,9 @@ func (t *mockConfig) FetchConfig(subServerName string) (interface{}, bool) {
},
}, true
case "AutopilotRPC":
return &autopilotrpc.Config{
Manager: &autopilot.Manager{},
}, true
return &autopilotrpc.Config{}, true
case "ChainRPC":
return &chainrpc.Config{
ChainNotifier: &chainreg.NoChainBackend{},
Chain: &mock.ChainIO{},
}, true
return &chainrpc.Config{}, true
case "DevRPC":
return &devrpc.Config{}, true
case "NeutrinoKitRPC":
@ -69,17 +60,9 @@ func (t *mockConfig) FetchConfig(subServerName string) (interface{}, bool) {
Router: &routing.ChannelRouter{},
}, true
case "SignRPC":
return &signrpc.Config{
Signer: &mock.DummySigner{},
}, true
return &signrpc.Config{}, true
case "WalletKitRPC":
return &walletrpc.Config{
FeeEstimator: &chainreg.NoChainBackend{},
Wallet: &mock.WalletController{},
KeyRing: &mock.SecretKeyRing{},
Sweeper: &sweep.UtxoSweeper{},
Chain: &mock.ChainIO{},
}, true
return &walletrpc.Config{}, true
case "WatchtowerRPC":
return &watchtowerrpc.Config{}, true
default: