diff --git a/cmd/waved/taprootassets.go b/cmd/waved/taprootassets.go index 7e73b4eec..32a08b4e4 100644 --- a/cmd/waved/taprootassets.go +++ b/cmd/waved/taprootassets.go @@ -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) } diff --git a/cmd/waved/taprootassets_test.go b/cmd/waved/taprootassets_test.go index 725ba05e9..79415a6bf 100644 --- a/cmd/waved/taprootassets_test.go +++ b/cmd/waved/taprootassets_test.go @@ -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() @@ -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 diff --git a/waved/config.go b/waved/config.go index ace031d5f..117cd9b7d 100644 --- a/waved/config.go +++ b/waved/config.go @@ -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 diff --git a/waved/taproot_assets_runtime.go b/waved/taproot_assets_runtime.go new file mode 100644 index 000000000..188b445eb --- /dev/null +++ b/waved/taproot_assets_runtime.go @@ -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 +} diff --git a/waved/taproot_assets_runtime_test.go b/waved/taproot_assets_runtime_test.go new file mode 100644 index 000000000..c73354887 --- /dev/null +++ b/waved/taproot_assets_runtime_test.go @@ -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) +}