Skip to content
Open
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
102 changes: 3 additions & 99 deletions cmd/waved/taprootassets.go
Original file line number Diff line number Diff line change
@@ -1,107 +1,11 @@
package main

import (
"context"
"path/filepath"

tapsdk "github.com/lightninglabs/tap-sdk"
tapgrpc "github.com/lightninglabs/tap-sdk/grpc"
"github.com/lightninglabs/tap-sdk/macaroon"
"github.com/lightninglabs/wavelength/tapassets"
"github.com/lightninglabs/wavelength/waved"
"google.golang.org/grpc"
)

// configureTaprootAssets installs a lazy daemon registrar. The authenticated
// tapd connection is opened only after waved has validated and initialized its
// own runtime, and is closed by the normal daemon shutdown path.
// configureTaprootAssets delegates production runtime registration to waved so
// command and embedded consumers install the same tapd lifecycle.
func configureTaprootAssets(cfg *waved.Config) {
if cfg == nil || cfg.TaprootAssets == nil ||
!cfg.TaprootAssets.Enabled ||
cfg.TaprootAssetOORPreparer != nil {
return
}

cfg.RPCServiceRegistrars = append(
cfg.RPCServiceRegistrars,
func(_ context.Context, _ *grpc.Server,
rpcServer *waved.RPCServer, daemonCfg *waved.Config) (
func(), error) {

assetCfg := daemonCfg.TaprootAssets
clientCfg := &tapgrpc.Config{
Host: assetCfg.Host,
Network: tapsdk.Network(daemonCfg.Network),
RPCTimeout: assetCfg.RPCTimeout,
}
if assetCfg.MacaroonPath != "" {
clientCfg.Macaroon = macaroon.FromPath(
assetCfg.MacaroonPath,
)
}
switch {
case assetCfg.Insecure:
clientCfg.TLS = tapgrpc.TLSInsecure()

case assetCfg.TLSCertPath != "":
clientCfg.TLS = tapgrpc.TLSFromPath(
assetCfg.TLSCertPath,
)
}

client, err := tapgrpc.NewClient(clientCfg)
if err != nil {
return nil, err
}
wallet := tapsdk.NewWallet(
client, tapsdk.Network(daemonCfg.Network),
)
closeWallet := func() {
_ = wallet.Close()
}

journalDir := assetCfg.PreparationDir
if journalDir == "" {
journalDir = filepath.Join(
daemonCfg.NetworkDir(),
"taproot-assets-oor",
)
}
store, err := tapassets.NewFileStore(journalDir)
if err != nil {
closeWallet()

return nil, err
}
reservationStore, err := rpcServer.OORReservationStore()
if err != nil {
closeWallet()

return nil, err
}
preparer, err := tapassets.NewPreparer(
tapassets.PreparerConfig{
Wallet: wallet,
Store: store,
ReservationStore: reservationStore,
},
)
if err != nil {
closeWallet()

return nil, err
}
daemonCfg.TaprootAssetOORPreparer = preparer
if err := rpcServer.ConfigureTaprootAssetOnboarding(
wallet, store,
); err != nil {

closeWallet()

return nil, err
}

return closeWallet, nil
},
)
waved.ConfigureTaprootAssets(cfg)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's remove configureTaprootAssets function altogether and just call waved.ConfigureTaprootAssets(cfg)

}
9 changes: 6 additions & 3 deletions cmd/waved/taprootassets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"github.com/stretchr/testify/require"
)

// TestConfigureTaprootAssetsIsOptIn proves the standalone runtime only installs
// a tapd lifecycle hook after explicit configuration.
func TestConfigureTaprootAssetsIsOptIn(t *testing.T) {
// TestConfigureTaprootAssetsDelegatesToWaved proves the command setup installs
// the exported production registrar and inherits its idempotency.
func TestConfigureTaprootAssetsDelegatesToWaved(t *testing.T) {
t.Parallel()

disabled := waved.DefaultConfig()
Expand All @@ -21,6 +21,9 @@ func TestConfigureTaprootAssetsIsOptIn(t *testing.T) {
enabled.TaprootAssets.Enabled = true
configureTaprootAssets(enabled)
require.Len(t, enabled.RPCServiceRegistrars, 1)

configureTaprootAssets(enabled)
require.Len(t, enabled.RPCServiceRegistrars, 1)
}

// TestTaprootAssetFlagsExposePoCConfiguration keeps the runnable adapter
Expand Down
4 changes: 4 additions & 0 deletions waved/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,10 @@ type Config struct {
// adapter is disabled by default and runs only in the client daemon.
TaprootAssets *TaprootAssetsConfig `mapstructure:"taprootassets"`

// taprootAssetsRuntimeConfigured prevents embedded and command setup
// paths from installing the production lifecycle registrar twice.
taprootAssetsRuntimeConfigured bool

// FailUnrollBroadcastReason is a TEST-ONLY hook. When non-empty, the
// unroll subsystem's tx-confirmation requests are rejected with this
// reason before any broadcast, simulating a proof tx that cannot enter
Expand Down
116 changes: 116 additions & 0 deletions waved/taproot_assets_runtime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package waved

import (
"context"
"path/filepath"

tapsdk "github.com/lightninglabs/tap-sdk"
tapgrpc "github.com/lightninglabs/tap-sdk/grpc"
"github.com/lightninglabs/tap-sdk/macaroon"
"github.com/lightninglabs/wavelength/tapassets"
"google.golang.org/grpc"
)

// ConfigureTaprootAssets registers the production tap-sdk and tapd runtime on
// cfg. The runtime remains opt-in through TaprootAssets.Enabled and is
// installed at most once per Config. A caller that injects its own Taproot
// Asset OOR preparer retains ownership of the integration, so this helper is a
// no-op in that case.
//
// Registration is lazy: no authenticated tapd connection or preparation store
// is opened until waved starts its gRPC services. The daemon shutdown path
// closes the connection returned by the registrar. Embedded consumers should
// call this helper before Main instead of recreating the tapd credential,
// journal, reservation, OOR preparation, and onboarding wiring.
func ConfigureTaprootAssets(cfg *Config) {
if cfg == nil || cfg.TaprootAssets == nil ||
!cfg.TaprootAssets.Enabled ||
cfg.TaprootAssetOORPreparer != nil ||
cfg.taprootAssetsRuntimeConfigured {
return
}

cfg.taprootAssetsRuntimeConfigured = true
cfg.RPCServiceRegistrars = append(
cfg.RPCServiceRegistrars, registerTaprootAssets,
)
}

// registerTaprootAssets constructs the production Taproot Assets services
// after waved has initialized the shared runtime dependencies they require.
func registerTaprootAssets(_ context.Context, _ *grpc.Server,
rpcServer *RPCServer, daemonCfg *Config) (func(), error) {

assetCfg := daemonCfg.TaprootAssets
clientCfg := &tapgrpc.Config{
Host: assetCfg.Host,
Network: tapsdk.Network(daemonCfg.Network),
RPCTimeout: assetCfg.RPCTimeout,
}
if assetCfg.MacaroonPath != "" {
clientCfg.Macaroon = macaroon.FromPath(
assetCfg.MacaroonPath,
)
}
switch {
case assetCfg.Insecure:
clientCfg.TLS = tapgrpc.TLSInsecure()

case assetCfg.TLSCertPath != "":
clientCfg.TLS = tapgrpc.TLSFromPath(
assetCfg.TLSCertPath,
)
}

client, err := tapgrpc.NewClient(clientCfg)
if err != nil {
return nil, err
}
wallet := tapsdk.NewWallet(
client, tapsdk.Network(daemonCfg.Network),
)
closeWallet := func() {
_ = wallet.Close()
}

journalDir := assetCfg.PreparationDir
if journalDir == "" {
journalDir = filepath.Join(
daemonCfg.NetworkDir(),
"taproot-assets-oor",
)
}
store, err := tapassets.NewFileStore(journalDir)
if err != nil {
closeWallet()

return nil, err
}
reservationStore, err := rpcServer.OORReservationStore()
if err != nil {
closeWallet()

return nil, err
}
preparer, err := tapassets.NewPreparer(tapassets.PreparerConfig{
Wallet: wallet,
Store: store,
ReservationStore: reservationStore,
})
if err != nil {
closeWallet()

return nil, err
}
daemonCfg.TaprootAssetOORPreparer = preparer
if err := rpcServer.ConfigureTaprootAssetOnboarding(
wallet, store,
); err != nil {

closeWallet()

return nil, err
}

return closeWallet, nil
}
57 changes: 57 additions & 0 deletions waved/taproot_assets_runtime_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package waved

import (
"testing"

"github.com/stretchr/testify/require"
)

// TestConfigureTaprootAssetsDisabled proves production registration remains
// explicitly opt-in and nil-safe for embedded consumers.
func TestConfigureTaprootAssetsDisabled(t *testing.T) {
t.Parallel()

ConfigureTaprootAssets(nil)

cfg := DefaultConfig()
ConfigureTaprootAssets(cfg)
require.Empty(t, cfg.RPCServiceRegistrars)

cfg.TaprootAssets = nil
ConfigureTaprootAssets(cfg)
require.Empty(t, cfg.RPCServiceRegistrars)
}

// TestConfigureTaprootAssetsRegistersOnce proves repeated setup paths install
// exactly one lazy production registrar.
func TestConfigureTaprootAssetsRegistersOnce(t *testing.T) {
t.Parallel()

cfg := DefaultConfig()
cfg.TaprootAssets.Enabled = true

ConfigureTaprootAssets(cfg)
require.Len(t, cfg.RPCServiceRegistrars, 1)
require.True(t, cfg.taprootAssetsRuntimeConfigured)
require.Nil(t, cfg.TaprootAssetOORPreparer)
require.Nil(t, cfg.TaprootAssetOnboarder)

ConfigureTaprootAssets(cfg)
require.Len(t, cfg.RPCServiceRegistrars, 1)
}

// TestConfigureTaprootAssetsPreservesInjectedPreparer proves an embedding
// application can retain ownership of its explicitly supplied integration.
func TestConfigureTaprootAssetsPreservesInjectedPreparer(t *testing.T) {
t.Parallel()

preparer := &testTaprootAssetOORPreparer{}
cfg := DefaultConfig()
cfg.TaprootAssets.Enabled = true
cfg.TaprootAssetOORPreparer = preparer

ConfigureTaprootAssets(cfg)
require.Empty(t, cfg.RPCServiceRegistrars)
require.False(t, cfg.taprootAssetsRuntimeConfigured)
require.Same(t, preparer, cfg.TaprootAssetOORPreparer)
}