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
12 changes: 1 addition & 11 deletions cmd/devnet-builder/commands/manage/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,18 +343,8 @@ For more information, see: https://github.com/altuslabsxyz/devnet-builder/blob/m
return fmt.Errorf("unknown blockchain network: %s (available: %v)", deployBlockchainNetwork, available)
}

// Get network module for DI container
networkModule, err := network.Get(deployBlockchainNetwork)
if err != nil {
return fmt.Errorf("failed to get network module: %w", err)
}

// Check if devnet already exists
svc, err := application.GetServiceWithConfig(application.ServiceConfig{
HomeDir: homeDir,
NetworkModule: networkModule,
DockerMode: deployMode == string(types.ExecutionModeDocker),
})
svc, err := application.GetService(homeDir)
if err != nil {
return fmt.Errorf("failed to initialize service: %w", err)
}
Expand Down
5 changes: 2 additions & 3 deletions internal/application/devnet/docker_deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/altuslabsxyz/devnet-builder/internal/application/dto"
"github.com/altuslabsxyz/devnet-builder/internal/application/ports"
domainports "github.com/altuslabsxyz/devnet-builder/internal/domain/ports"
"github.com/altuslabsxyz/devnet-builder/internal/infrastructure/node"
"github.com/altuslabsxyz/devnet-builder/types"
)

Expand Down Expand Up @@ -281,9 +280,9 @@ func (uc *DockerDestroyUseCase) findDevnetContainers(ctx context.Context, networ
}

// GetDefaultNodePorts returns default ports for a node at given index in Docker mode
func GetDefaultNodePorts(basePort, nodeIndex int) *node.NodePorts {
func GetDefaultNodePorts(basePort, nodeIndex int) *ports.PortConfig {
offset := nodeIndex * 100
return &node.NodePorts{
return &ports.PortConfig{
RPC: basePort + offset,
P2P: basePort + offset + 1,
GRPC: basePort + offset + 2,
Expand Down
90 changes: 73 additions & 17 deletions internal/application/devnet/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import (
"github.com/altuslabsxyz/devnet-builder/internal/application/dto"
"github.com/altuslabsxyz/devnet-builder/internal/application/ports"
domainExport "github.com/altuslabsxyz/devnet-builder/internal/domain/export"
infraExport "github.com/altuslabsxyz/devnet-builder/internal/infrastructure/export"
infraprocess "github.com/altuslabsxyz/devnet-builder/internal/infrastructure/process"
"github.com/altuslabsxyz/devnet-builder/types"
)

Expand All @@ -23,10 +21,9 @@ type ExportUseCase struct {
nodeRepo ports.NodeRepository
exportRepo ports.ExportRepository
nodeLifecycle ports.NodeLifecycleManager // Injected for stop-export-start workflow
hashCalc *infraExport.HashCalculator
heightResolver *infraExport.HeightResolver
exportExec *infraExport.ExportExecutor
processExec ports.ProcessExecutor
hashCalc ports.ExportHashCalculator
heightResolver ports.ExportHeightResolver
exportExec ports.ExportExecutor
logger ports.Logger
}

Expand All @@ -41,17 +38,49 @@ func NewExportUseCase(
nodeLifecycle ports.NodeLifecycleManager,
logger ports.Logger,
) *ExportUseCase {
processExec := infraprocess.NewLocalExecutor()
return NewExportUseCaseWithDeps(
ctx,
devnetRepo,
nodeRepo,
exportRepo,
nodeLifecycle,
logger,
&missingExportHashCalculator{},
&missingExportHeightResolver{},
&missingExportExecutor{},
)
}

// NewExportUseCaseWithDeps creates an ExportUseCase with explicitly injected dependencies.
func NewExportUseCaseWithDeps(
ctx context.Context,
devnetRepo ports.DevnetRepository,
nodeRepo ports.NodeRepository,
exportRepo ports.ExportRepository,
nodeLifecycle ports.NodeLifecycleManager,
logger ports.Logger,
hashCalc ports.ExportHashCalculator,
heightResolver ports.ExportHeightResolver,
exportExec ports.ExportExecutor,
) *ExportUseCase {
if hashCalc == nil {
hashCalc = &missingExportHashCalculator{}
}
if heightResolver == nil {
heightResolver = &missingExportHeightResolver{}
}
if exportExec == nil {
exportExec = &missingExportExecutor{}
}

return &ExportUseCase{
devnetRepo: devnetRepo,
nodeRepo: nodeRepo,
exportRepo: exportRepo,
nodeLifecycle: nodeLifecycle,
hashCalc: infraExport.NewHashCalculator(),
heightResolver: infraExport.NewHeightResolver(),
exportExec: infraExport.NewExportExecutor(),
processExec: processExec,
hashCalc: hashCalc,
heightResolver: heightResolver,
exportExec: exportExec,
logger: logger,
}
}
Expand Down Expand Up @@ -319,29 +348,34 @@ func (uc *ExportUseCase) Inspect(ctx context.Context, exportPath string) (*dto.E
return nil, fmt.Errorf("failed to validate export: %w", err)
}

result, ok := resultInterface.(*infraExport.ValidationResult)
result, ok := resultInterface.(ports.ExportValidationResult)
if !ok {
return nil, fmt.Errorf("invalid validation result type")
}

exportEntity, ok := result.ExportEntity().(*domainExport.Export)
if !ok {
return nil, fmt.Errorf("invalid export entity type")
}

// Calculate directory size
size, _ := calculateDirectorySize(exportPath)

// Calculate genesis file checksum if the file exists
var genesisChecksum string
if result.Export.GenesisFilePath != "" {
checksum, err := uc.hashCalc.CalculateHash(result.Export.GenesisFilePath)
if exportEntity.GenesisFilePath != "" {
checksum, err := uc.hashCalc.CalculateHash(exportEntity.GenesisFilePath)
if err == nil {
genesisChecksum = checksum
}
// If file doesn't exist or can't be read, leave checksum empty
}

output := &dto.ExportInspectOutput{
Metadata: result.Export.Metadata,
Metadata: exportEntity.Metadata,
GenesisChecksum: genesisChecksum,
IsComplete: result.IsComplete,
MissingFiles: result.MissingFiles,
IsComplete: result.IsExportComplete(),
MissingFiles: result.ExportMissingFiles(),
SizeBytes: size,
}

Expand All @@ -364,3 +398,25 @@ func calculateDirectorySize(path string) (int64, error) {

return size, err
}

type missingExportHashCalculator struct{}

func (m *missingExportHashCalculator) CalculateHash(_ string) (string, error) {
return "", fmt.Errorf("export hash calculator is not configured")
}

type missingExportHeightResolver struct{}

func (m *missingExportHeightResolver) GetCurrentHeight(_ context.Context, _ string) (int64, error) {
return 0, fmt.Errorf("export height resolver is not configured")
}

type missingExportExecutor struct{}

func (m *missingExportExecutor) GetBinaryVersion(_ context.Context, _ string) (string, error) {
return "", fmt.Errorf("export executor is not configured")
}

func (m *missingExportExecutor) ExportAtHeight(_ context.Context, _, _ string, _ int64, _ string) (string, error) {
return "", fmt.Errorf("export executor is not configured")
}
39 changes: 27 additions & 12 deletions internal/application/devnet/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/altuslabsxyz/devnet-builder/internal/application/dto"
"github.com/altuslabsxyz/devnet-builder/internal/application/ports"
domainExport "github.com/altuslabsxyz/devnet-builder/internal/domain/export"
infraExport "github.com/altuslabsxyz/devnet-builder/internal/infrastructure/export"
"github.com/altuslabsxyz/devnet-builder/types"
)

Expand Down Expand Up @@ -114,9 +113,25 @@ func (m *mockExportRepository) Validate(ctx context.Context, exportPath string)
if m.validateFunc != nil {
return m.validateFunc(ctx, exportPath)
}
return &infraExport.ValidationResult{
IsComplete: true,
}, nil
return &mockExportValidationResult{isComplete: true}, nil
}

type mockExportValidationResult struct {
export *domainExport.Export
isComplete bool
missingFile []string
}

func (m *mockExportValidationResult) ExportEntity() interface{} {
return m.export
}

func (m *mockExportValidationResult) IsExportComplete() bool {
return m.isComplete
}

func (m *mockExportValidationResult) ExportMissingFiles() []string {
return m.missingFile
}

type mockLogger struct{}
Expand Down Expand Up @@ -366,10 +381,10 @@ func TestExportUseCase_Inspect_Success(t *testing.T) {

exportRepo := &mockExportRepository{
validateFunc: func(ctx context.Context, exportPath string) (interface{}, error) {
return &infraExport.ValidationResult{
Export: export,
IsComplete: true,
MissingFiles: []string{},
return &mockExportValidationResult{
export: export,
isComplete: true,
missingFile: []string{},
}, nil
},
}
Expand Down Expand Up @@ -437,10 +452,10 @@ func TestExportUseCase_Inspect_IncompleteExport(t *testing.T) {

exportRepo := &mockExportRepository{
validateFunc: func(ctx context.Context, exportPath string) (interface{}, error) {
return &infraExport.ValidationResult{
Export: export,
IsComplete: false,
MissingFiles: []string{"genesis.json"},
return &mockExportValidationResult{
export: export,
isComplete: false,
missingFile: []string{"genesis.json"},
}, domainExport.ErrExportIncomplete
},
}
Expand Down
71 changes: 48 additions & 23 deletions internal/application/devnet/provision.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import (
"time"

"github.com/cosmos/cosmos-sdk/types/bech32"
"github.com/pelletier/go-toml/v2"

"github.com/altuslabsxyz/devnet-builder/internal/application/dto"
"github.com/altuslabsxyz/devnet-builder/internal/application/ports"
"github.com/altuslabsxyz/devnet-builder/internal/infrastructure/stateexport"
"github.com/altuslabsxyz/devnet-builder/internal/infrastructure/tomlutil"
"github.com/altuslabsxyz/devnet-builder/internal/paths"
"github.com/altuslabsxyz/devnet-builder/types"
)
Expand Down Expand Up @@ -689,7 +688,7 @@ func (uc *ProvisionUseCase) mergeConfig(filePath string, override []byte) error
return fmt.Errorf("failed to read config: %w", err)
}

merged, err := tomlutil.MergeTOML(base, override)
merged, err := mergeTOML(base, override)
if err != nil {
return err
}
Expand All @@ -701,6 +700,52 @@ func (uc *ProvisionUseCase) mergeConfig(filePath string, override []byte) error
return nil
}

func mergeTOML(base, override []byte) ([]byte, error) {
if len(override) == 0 {
return base, nil
}
if len(base) == 0 {
return override, nil
}

var baseMap map[string]any
if err := toml.Unmarshal(base, &baseMap); err != nil {
return nil, fmt.Errorf("failed to parse base TOML: %w", err)
}

var overrideMap map[string]any
if err := toml.Unmarshal(override, &overrideMap); err != nil {
return nil, fmt.Errorf("failed to parse override TOML: %w", err)
}

deepMergeTOML(baseMap, overrideMap)

merged, err := toml.Marshal(baseMap)
if err != nil {
return nil, fmt.Errorf("failed to marshal merged TOML: %w", err)
}

return merged, nil
}

func deepMergeTOML(base, override map[string]any) {
for key, overrideVal := range override {
baseVal, exists := base[key]
if !exists {
base[key] = overrideVal
continue
}

baseMap, baseIsMap := baseVal.(map[string]any)
overrideMap, overrideIsMap := overrideVal.(map[string]any)
if baseIsMap && overrideIsMap {
deepMergeTOML(baseMap, overrideMap)
continue
}
base[key] = overrideVal
}
}

// buildPersistentPeers builds the persistent peers string from node metadata.
// Format: node_id@127.0.0.1:p2p_port,node_id@127.0.0.1:p2p_port,...
func (uc *ProvisionUseCase) buildPersistentPeers(nodes []*ports.NodeMetadata) string {
Expand Down Expand Up @@ -758,26 +803,6 @@ func (uc *ProvisionUseCase) exportGenesisFromSnapshot(ctx context.Context, input
uc.logger.Success("Using cached snapshot")
}

// Step 1.5: Check genesis cache BEFORE extraction
// If snapshot was cached and genesis cache exists, use it directly
if fromCache && cacheKey != "" && !input.NoCache {
cache, err := stateexport.GetValidGenesisCache(input.HomeDir, cacheKey)
if err == nil && cache != nil {
// Verify the cached genesis is from the same snapshot
if cache.SnapshotURL == snapshotURL {
// Read cached genesis
genesis, err := os.ReadFile(cache.FilePath)
if err == nil {
uc.logger.Info("Using cached genesis export (expires in %s)", cache.TimeUntilExpiry().Round(time.Minute))
return genesis, nil
}
uc.logger.Debug("Failed to read cached genesis: %v", err)
} else {
uc.logger.Debug("Cached genesis is from different snapshot, will re-export")
}
}
}

// Create temp directory for extraction and export
// The snapshot file itself stays in the cache directory
exportDir := filepath.Join(input.HomeDir, "tmp", "state-export")
Expand Down
19 changes: 19 additions & 0 deletions internal/application/ports/export_dependencies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package ports

import "context"

// ExportHashCalculator calculates hashes for exported artifacts.
type ExportHashCalculator interface {
CalculateHash(binaryPath string) (string, error)
}

// ExportHeightResolver resolves current block height from an RPC endpoint.
type ExportHeightResolver interface {
GetCurrentHeight(ctx context.Context, rpcURL string) (int64, error)
}

// ExportExecutor executes binary export commands.
type ExportExecutor interface {
GetBinaryVersion(ctx context.Context, binaryPath string) (string, error)
ExportAtHeight(ctx context.Context, binaryPath, homeDir string, height int64, outputPath string) (string, error)
}
Loading
Loading