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
14 changes: 2 additions & 12 deletions internal/application/devnet/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,16 +270,11 @@ func (uc *ExportUseCase) Execute(ctx context.Context, input dto.ExportInput) (*d
// List returns all exports for a devnet.
func (uc *ExportUseCase) List(ctx context.Context, homeDir string) (*dto.ExportListOutput, error) {
// Load all exports
exportsInterface, err := uc.exportRepo.ListForDevnet(ctx, homeDir)
exports, err := uc.exportRepo.ListForDevnet(ctx, homeDir)
if err != nil {
return nil, fmt.Errorf("failed to list exports: %w", err)
}

exports, ok := exportsInterface.([]*domainExport.Export)
if !ok {
return nil, fmt.Errorf("invalid export list type")
}

// Build summaries
summaries := make([]*dto.ExportSummary, 0, len(exports))
var totalSize int64
Expand Down Expand Up @@ -314,16 +309,11 @@ func (uc *ExportUseCase) List(ctx context.Context, homeDir string) (*dto.ExportL
// Inspect returns detailed information about a specific export.
func (uc *ExportUseCase) Inspect(ctx context.Context, exportPath string) (*dto.ExportInspectOutput, error) {
// Validate export
resultInterface, err := uc.exportRepo.Validate(ctx, exportPath)
result, err := uc.exportRepo.Validate(ctx, exportPath)
if err != nil && err != domainExport.ErrExportIncomplete {
return nil, fmt.Errorf("failed to validate export: %w", err)
}

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

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

Expand Down
65 changes: 15 additions & 50 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 @@ -83,23 +82,23 @@ func (m *mockNodeRepository) Delete(ctx context.Context, homeDir string, index i
}

type mockExportRepository struct {
saveFunc func(ctx context.Context, export interface{}) error
listForDevnetFunc func(ctx context.Context, homeDir string) (interface{}, error)
validateFunc func(ctx context.Context, exportPath string) (interface{}, error)
saveFunc func(ctx context.Context, export *domainExport.Export) error
listForDevnetFunc func(ctx context.Context, homeDir string) ([]*domainExport.Export, error)
validateFunc func(ctx context.Context, exportPath string) (*ports.ExportValidationResult, error)
}

func (m *mockExportRepository) Save(ctx context.Context, export interface{}) error {
func (m *mockExportRepository) Save(ctx context.Context, export *domainExport.Export) error {
if m.saveFunc != nil {
return m.saveFunc(ctx, export)
}
return nil
}

func (m *mockExportRepository) Load(ctx context.Context, exportPath string) (interface{}, error) {
func (m *mockExportRepository) Load(ctx context.Context, exportPath string) (*domainExport.Export, error) {
return nil, nil
}

func (m *mockExportRepository) ListForDevnet(ctx context.Context, homeDir string) (interface{}, error) {
func (m *mockExportRepository) ListForDevnet(ctx context.Context, homeDir string) ([]*domainExport.Export, error) {
if m.listForDevnetFunc != nil {
return m.listForDevnetFunc(ctx, homeDir)
}
Expand All @@ -110,11 +109,11 @@ func (m *mockExportRepository) Delete(ctx context.Context, exportPath string) er
return nil
}

func (m *mockExportRepository) Validate(ctx context.Context, exportPath string) (interface{}, error) {
func (m *mockExportRepository) Validate(ctx context.Context, exportPath string) (*ports.ExportValidationResult, error) {
if m.validateFunc != nil {
return m.validateFunc(ctx, exportPath)
}
return &infraExport.ValidationResult{
return &ports.ExportValidationResult{
IsComplete: true,
}, nil
}
Expand Down Expand Up @@ -280,7 +279,7 @@ func TestExportUseCase_Execute_DevnetNotRunning(t *testing.T) {

func TestExportUseCase_List_Success(t *testing.T) {
exportRepo := &mockExportRepository{
listForDevnetFunc: func(ctx context.Context, homeDir string) (interface{}, error) {
listForDevnetFunc: func(ctx context.Context, homeDir string) ([]*domainExport.Export, error) {
return []*domainExport.Export{}, nil
},
}
Expand All @@ -304,7 +303,7 @@ func TestExportUseCase_List_Success(t *testing.T) {

func TestExportUseCase_List_RepositoryFailure(t *testing.T) {
exportRepo := &mockExportRepository{
listForDevnetFunc: func(ctx context.Context, homeDir string) (interface{}, error) {
listForDevnetFunc: func(ctx context.Context, homeDir string) ([]*domainExport.Export, error) {
return nil, errors.New("repository error")
},
}
Expand All @@ -318,23 +317,6 @@ func TestExportUseCase_List_RepositoryFailure(t *testing.T) {
}
}

func TestExportUseCase_List_InvalidType(t *testing.T) {
exportRepo := &mockExportRepository{
listForDevnetFunc: func(ctx context.Context, homeDir string) (interface{}, error) {
// Return wrong type
return "invalid", nil
},
}

uc := NewExportUseCase(context.Background(), &mockDevnetRepository{}, &mockNodeRepository{}, exportRepo, &mockNodeLifecycleManager{}, &mockLogger{})

_, err := uc.List(context.Background(), "/tmp/test-devnet")

if err == nil {
t.Fatal("expected error for invalid type")
}
}

func TestExportUseCase_Inspect_Success(t *testing.T) {
// Create valid test export
hash := "a1b2c3d4e5f67890123456789012345678901234567890123456789012345678"
Expand Down Expand Up @@ -365,8 +347,8 @@ func TestExportUseCase_Inspect_Success(t *testing.T) {
)

exportRepo := &mockExportRepository{
validateFunc: func(ctx context.Context, exportPath string) (interface{}, error) {
return &infraExport.ValidationResult{
validateFunc: func(ctx context.Context, exportPath string) (*ports.ExportValidationResult, error) {
return &ports.ExportValidationResult{
Export: export,
IsComplete: true,
MissingFiles: []string{},
Expand All @@ -393,7 +375,7 @@ func TestExportUseCase_Inspect_Success(t *testing.T) {

func TestExportUseCase_Inspect_ValidationFailure(t *testing.T) {
exportRepo := &mockExportRepository{
validateFunc: func(ctx context.Context, exportPath string) (interface{}, error) {
validateFunc: func(ctx context.Context, exportPath string) (*ports.ExportValidationResult, error) {
return nil, errors.New("validation error")
},
}
Expand Down Expand Up @@ -436,8 +418,8 @@ func TestExportUseCase_Inspect_IncompleteExport(t *testing.T) {
)

exportRepo := &mockExportRepository{
validateFunc: func(ctx context.Context, exportPath string) (interface{}, error) {
return &infraExport.ValidationResult{
validateFunc: func(ctx context.Context, exportPath string) (*ports.ExportValidationResult, error) {
return &ports.ExportValidationResult{
Export: export,
IsComplete: false,
MissingFiles: []string{"genesis.json"},
Expand All @@ -463,23 +445,6 @@ func TestExportUseCase_Inspect_IncompleteExport(t *testing.T) {
}
}

func TestExportUseCase_Inspect_InvalidType(t *testing.T) {
exportRepo := &mockExportRepository{
validateFunc: func(ctx context.Context, exportPath string) (interface{}, error) {
// Return wrong type
return "invalid", nil
},
}

uc := NewExportUseCase(context.Background(), &mockDevnetRepository{}, &mockNodeRepository{}, exportRepo, &mockNodeLifecycleManager{}, &mockLogger{})

_, err := uc.Inspect(context.Background(), "/tmp/exports/invalid")

if err == nil {
t.Fatal("expected error for invalid type")
}
}

// Helper functions

func testTimestamp() time.Time {
Expand Down
16 changes: 12 additions & 4 deletions internal/application/ports/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"time"

domainExport "github.com/altuslabsxyz/devnet-builder/internal/domain/export"
"github.com/altuslabsxyz/devnet-builder/types"
)

Expand Down Expand Up @@ -190,17 +191,24 @@ type NodeConfigOptions struct {
// ExportRepository manages export persistence and querying
type ExportRepository interface {
// Save saves export metadata to disk
Save(ctx context.Context, exp interface{}) error
Save(ctx context.Context, exp *domainExport.Export) error

// Load loads export from directory
Load(ctx context.Context, exportPath string) (interface{}, error)
Load(ctx context.Context, exportPath string) (*domainExport.Export, error)

// ListForDevnet lists all exports for a devnet
ListForDevnet(ctx context.Context, devnetHomeDir string) (interface{}, error)
ListForDevnet(ctx context.Context, devnetHomeDir string) ([]*domainExport.Export, error)

// Delete removes an export directory
Delete(ctx context.Context, exportPath string) error

// Validate checks export completeness
Validate(ctx context.Context, exportPath string) (interface{}, error)
Validate(ctx context.Context, exportPath string) (*ExportValidationResult, error)
}

// ExportValidationResult is the typed validation result returned by ExportRepository.
type ExportValidationResult struct {
Export *domainExport.Export
IsComplete bool
MissingFiles []string
}
22 changes: 18 additions & 4 deletions internal/application/ports/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,12 +228,26 @@ const (
StageFailed UpgradeStage = "failed"
)

// ExportExecuteInput is the boundary DTO for export requests.
type ExportExecuteInput struct {
HomeDir string
OutputDir string
Force bool
}

// ExportExecuteOutput is the boundary DTO for export responses.
type ExportExecuteOutput struct {
ExportPath string
BlockHeight int64
GenesisPath string
MetadataPath string
WasRunning bool
Warnings []string
}

// ExportUseCase defines the interface for exporting blockchain state.
// Note: Uses dto.ExportInput and dto.ExportOutput from internal/application/dto
type ExportUseCase interface {
// Execute performs a state export
// Returns export result with paths and metadata
Execute(ctx context.Context, input interface{}) (interface{}, error)
Execute(ctx context.Context, input ExportExecuteInput) (*ExportExecuteOutput, error)
}

// NodeLifecycleManager defines operations for managing node lifecycle.
Expand Down
22 changes: 6 additions & 16 deletions internal/application/upgrade/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,23 +115,18 @@ func (uc *ExecuteUpgradeUseCase) executeWithGov(ctx context.Context, input dto.E
// Pre-upgrade export (if enabled)
if input.WithExport {
uc.logger.Info("Pre-upgrade: Exporting state before upgrade...")
exportInput := dto.ExportInput{
exportInput := ports.ExportExecuteInput{
HomeDir: input.HomeDir,
OutputDir: input.GenesisDir,
Force: false,
}

preExportResultRaw, err := uc.exportUC.Execute(ctx, exportInput)
preExportResult, err := uc.exportUC.Execute(ctx, exportInput)
if err != nil {
uc.logger.Error("Pre-upgrade export failed: %v", err)
output.Error = fmt.Errorf("pre-upgrade export failed: %w", err)
return output, output.Error
}
preExportResult, ok := preExportResultRaw.(*dto.ExportOutput)
if !ok {
output.Error = fmt.Errorf("invalid export result type")
return output, output.Error
}
output.PreGenesisPath = preExportResult.ExportPath
uc.logger.Success("Pre-upgrade export complete: %s", preExportResult.ExportPath)
}
Expand Down Expand Up @@ -214,25 +209,20 @@ func (uc *ExecuteUpgradeUseCase) executeWithGov(ctx context.Context, input dto.E
// Post-upgrade export (if enabled)
if input.WithExport {
uc.logger.Info("Post-upgrade: Exporting state after upgrade...")
exportInput := dto.ExportInput{
exportInput := ports.ExportExecuteInput{
HomeDir: input.HomeDir,
OutputDir: input.GenesisDir,
Force: false,
}

postExportResultRaw, err := uc.exportUC.Execute(ctx, exportInput)
postExportResult, err := uc.exportUC.Execute(ctx, exportInput)
if err != nil {
// Post-upgrade export failure is non-fatal (upgrade already complete)
uc.logger.Warn("Post-upgrade export failed: %v", err)
uc.logger.Warn("Upgrade completed successfully, but post-upgrade state export failed")
} else {
postExportResult, ok := postExportResultRaw.(*dto.ExportOutput)
if ok {
output.PostGenesisPath = postExportResult.ExportPath
uc.logger.Success("Post-upgrade export complete: %s", postExportResult.ExportPath)
} else {
uc.logger.Warn("Invalid export result type for post-upgrade export")
}
output.PostGenesisPath = postExportResult.ExportPath
uc.logger.Success("Post-upgrade export complete: %s", postExportResult.ExportPath)
}
}

Expand Down
16 changes: 7 additions & 9 deletions internal/application/upgrade/resumable_execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,22 +178,20 @@ func (uc *ResumableExecuteUpgradeUseCase) executeWithGovResumable(
// Pre-upgrade export (only if starting fresh and enabled)
if state.Stage == ports.ResumableStageInitialized && input.WithExport {
uc.logger.Info("Pre-upgrade: Exporting state before upgrade...")
exportInput := dto.ExportInput{
exportInput := ports.ExportExecuteInput{
HomeDir: input.HomeDir,
OutputDir: input.GenesisDir,
Force: false,
}

preExportResultRaw, err := uc.exportUC.Execute(ctx, exportInput)
preExportResult, err := uc.exportUC.Execute(ctx, exportInput)
if err != nil {
uc.logger.Error("Pre-upgrade export failed: %v", err)
output.Error = fmt.Errorf("pre-upgrade export failed: %w", err)
return output, output.Error
}
if preExportResult, ok := preExportResultRaw.(*dto.ExportOutput); ok {
output.PreGenesisPath = preExportResult.ExportPath
uc.logger.Success("Pre-upgrade export complete: %s", preExportResult.ExportPath)
}
output.PreGenesisPath = preExportResult.ExportPath
uc.logger.Success("Pre-upgrade export complete: %s", preExportResult.ExportPath)
}

// Resume from current stage
Expand Down Expand Up @@ -345,16 +343,16 @@ func (uc *ResumableExecuteUpgradeUseCase) executeWithGovResumable(
// Post-upgrade export (if enabled)
if input.WithExport {
uc.logger.Info("Post-upgrade: Exporting state after upgrade...")
exportInput := dto.ExportInput{
exportInput := ports.ExportExecuteInput{
HomeDir: input.HomeDir,
OutputDir: input.GenesisDir,
Force: false,
}

postExportResultRaw, err := uc.exportUC.Execute(ctx, exportInput)
postExportResult, err := uc.exportUC.Execute(ctx, exportInput)
if err != nil {
uc.logger.Warn("Post-upgrade export failed: %v", err)
} else if postExportResult, ok := postExportResultRaw.(*dto.ExportOutput); ok {
} else {
output.PostGenesisPath = postExportResult.ExportPath
uc.logger.Success("Post-upgrade export complete: %s", postExportResult.ExportPath)
}
Expand Down
23 changes: 17 additions & 6 deletions internal/di/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package di

import (
"context"
"fmt"
"io"
"sync"
"time"
Expand All @@ -30,12 +29,24 @@ type exportUseCaseAdapter struct {
}
}

func (a *exportUseCaseAdapter) Execute(ctx context.Context, input interface{}) (interface{}, error) {
exportInput, ok := input.(dto.ExportInput)
if !ok {
return nil, fmt.Errorf("invalid input type for export: expected dto.ExportInput, got %T", input)
func (a *exportUseCaseAdapter) Execute(ctx context.Context, input ports.ExportExecuteInput) (*ports.ExportExecuteOutput, error) {
result, err := a.concrete.Execute(ctx, dto.ExportInput{
HomeDir: input.HomeDir,
OutputDir: input.OutputDir,
Force: input.Force,
})
if err != nil {
return nil, err
}
return a.concrete.Execute(ctx, exportInput)

return &ports.ExportExecuteOutput{
ExportPath: result.ExportPath,
BlockHeight: result.BlockHeight,
GenesisPath: result.GenesisPath,
MetadataPath: result.MetadataPath,
WasRunning: result.WasRunning,
Warnings: result.Warnings,
}, nil
}

// Container holds all application dependencies.
Expand Down
Loading
Loading