Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@ devnetd
dvb
*.test
.serena/

devnet-builder
42 changes: 42 additions & 0 deletions cmd/devnet-builder/commands/manage/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"sort"
Expand Down Expand Up @@ -305,6 +306,13 @@ func runDeploy(cmd *cobra.Command, args []string) error {
return fmt.Errorf("invalid mode: %s (must be 'docker' or 'local')", deployMode)
}

// Validate port availability for local mode before proceeding
if deployMode == string(types.ExecutionModeLocal) {
if err := validateLocalModePorts(deployValidators); err != nil {
return err
}
}

// Check for deprecated --binary flag usage
if deployBinary != "" {
return fmt.Errorf(`the --binary flag has been removed in favor of interactive binary selection
Expand Down Expand Up @@ -872,3 +880,37 @@ func validateBinaryPath(binaryPath string) (string, error) {

return absPath, nil
}

// validateLocalModePorts checks if the ports needed for local mode deployment are available.
// Returns an error with conflicting ports if any are already in use.
func validateLocalModePorts(validatorCount int) error {
var conflicts []int

// Check ports for each validator
for i := 0; i < validatorCount; i++ {
portConfig := types.PortConfigForNode(i)
for _, port := range portConfig.AllPorts() {
if isPortInUse(port) {
conflicts = append(conflicts, port)
}
}
}

if len(conflicts) > 0 {
return fmt.Errorf("port conflict detected: ports %v already in use", conflicts)
}

return nil
}

// isPortInUse checks if a TCP port is currently in use by attempting to listen on it.
func isPortInUse(port int) bool {
addr := fmt.Sprintf("127.0.0.1:%d", port)
listener, err := net.Listen("tcp", addr)
if err != nil {
// Port is in use or unavailable
return true
}
listener.Close()
return false
}
12 changes: 11 additions & 1 deletion internal/application/devnet/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,9 +327,19 @@ func (uc *ExportUseCase) Inspect(ctx context.Context, exportPath string) (*dto.E
// 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 err == nil {
genesisChecksum = checksum
}
// If file doesn't exist or can't be read, leave checksum empty
}

output := &dto.ExportInspectOutput{
Metadata: result.Export.Metadata,
GenesisChecksum: "", // TODO: Calculate SHA256 of genesis file
GenesisChecksum: genesisChecksum,
IsComplete: result.IsComplete,
MissingFiles: result.MissingFiles,
SizeBytes: size,
Expand Down
30 changes: 21 additions & 9 deletions internal/daemon/controller/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,17 @@ func (c *TxController) reconcileBuilding(ctx context.Context, tx *types.Transact
return c.setFailed(ctx, tx, fmt.Sprintf("failed to get TxBuilder: %v", err))
}

// Use configured gas limit, defaulting to 200000 if not specified
gasLimit := tx.Spec.GasLimit
if gasLimit == 0 {
gasLimit = 200000
}

unsignedTx, err := builder.BuildTx(ctx, &network.TxBuildRequest{
TxType: network.TxType(tx.Spec.TxType),
Sender: tx.Spec.Signer,
Payload: tx.Spec.Payload,
GasLimit: 200000, // TODO: make configurable
GasLimit: gasLimit,
})
if err != nil {
return c.setFailed(ctx, tx, fmt.Sprintf("failed to build tx: %v", err))
Expand All @@ -110,18 +116,25 @@ func (c *TxController) reconcileBuilding(ctx context.Context, tx *types.Transact
func (c *TxController) reconcileSigning(ctx context.Context, tx *types.Transaction) error {
c.logger.Debug("signing transaction", "name", tx.Metadata.Name)

// Get TxBuilder for signing
builder, err := c.runtime.GetTxBuilder(ctx, tx.Spec.DevnetRef)
if err != nil {
return c.setFailed(ctx, tx, fmt.Sprintf("failed to get TxBuilder: %v", err))
}

// Get cached unsigned tx
unsignedTx, ok := c.unsignedTxCache[tx.Metadata.Name]
if !ok {
// If not in cache, rebuild it
builder, err := c.runtime.GetTxBuilder(ctx, tx.Spec.DevnetRef)
if err != nil {
return c.setFailed(ctx, tx, fmt.Sprintf("failed to get TxBuilder: %v", err))
// If not in cache, rebuild it with same gas limit as building phase
gasLimit := tx.Spec.GasLimit
if gasLimit == 0 {
gasLimit = 200000
}
unsignedTx, err = builder.BuildTx(ctx, &network.TxBuildRequest{
TxType: network.TxType(tx.Spec.TxType),
Sender: tx.Spec.Signer,
Payload: tx.Spec.Payload,
TxType: network.TxType(tx.Spec.TxType),
Sender: tx.Spec.Signer,
Payload: tx.Spec.Payload,
GasLimit: gasLimit,
})
if err != nil {
return c.setFailed(ctx, tx, fmt.Sprintf("failed to rebuild tx: %v", err))
Expand All @@ -135,7 +148,6 @@ func (c *TxController) reconcileSigning(ctx context.Context, tx *types.Transacti
}

// Sign
builder, _ := c.runtime.GetTxBuilder(ctx, tx.Spec.DevnetRef)
signedTx, err := builder.SignTx(ctx, unsignedTx, key)
if err != nil {
return c.setFailed(ctx, tx, fmt.Sprintf("failed to sign tx: %v", err))
Expand Down
105 changes: 100 additions & 5 deletions internal/daemon/server/devnet_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,18 @@ func (s *DevnetService) ListDevnets(ctx context.Context, req *v1.ListDevnetsRequ
return nil, status.Errorf(codes.Internal, "failed to list devnets: %v", err)
}

// TODO: Implement label selector filtering
// For now, return all devnets
// Filter by label selector if provided
// Format: "key1=value1,key2=value2" (all must match)
labelFilter := parseLabelSelector(req.GetLabelSelector())

resp := &v1.ListDevnetsResponse{
Devnets: make([]*v1.Devnet, 0, len(devnets)),
}

for _, d := range devnets {
resp.Devnets = append(resp.Devnets, DevnetToProto(d))
if matchesLabels(d.Metadata.Labels, labelFilter) {
resp.Devnets = append(resp.Devnets, DevnetToProto(d))
}
}

return resp, nil
Expand Down Expand Up @@ -135,7 +138,10 @@ func (s *DevnetService) DeleteDevnet(ctx context.Context, req *v1.DeleteDevnetRe
return nil, status.Errorf(codes.Internal, "failed to delete devnet: %v", err)
}

// TODO: In Phase 3, enqueue for deprovisioning before actual deletion
// Enqueue for deprovisioning (controller will handle cleanup)
if s.manager != nil {
s.manager.Enqueue("devnets", req.Name)
}

return &v1.DeleteDevnetResponse{Deleted: true}, nil
}
Expand Down Expand Up @@ -203,7 +209,96 @@ func (s *DevnetService) StopDevnet(ctx context.Context, req *v1.StopDevnetReques
return nil, status.Errorf(codes.Internal, "failed to update devnet: %v", err)
}

// TODO: In Phase 3, enqueue for actual container stopping
// Enqueue for container stopping (controller will handle the actual stop)
if s.manager != nil {
s.manager.Enqueue("devnets", req.Name)
}

return &v1.StopDevnetResponse{Devnet: DevnetToProto(devnet)}, nil
}

// parseLabelSelector parses a comma-separated label selector string into a map.
// Format: "key1=value1,key2=value2"
// Returns an empty map if the selector is empty.
func parseLabelSelector(selector string) map[string]string {
result := make(map[string]string)
if selector == "" {
return result
}

pairs := splitTrimmed(selector, ",")
for _, pair := range pairs {
if pair == "" {
continue
}
kv := splitTrimmed(pair, "=")
if len(kv) == 2 && kv[0] != "" {
result[kv[0]] = kv[1]
}
}
return result
}

// matchesLabels returns true if the resource labels match all the filter labels.
// An empty filter matches everything.
func matchesLabels(resourceLabels, filter map[string]string) bool {
for k, v := range filter {
if resourceLabels == nil || resourceLabels[k] != v {
return false
}
}
return true
}

// splitTrimmed splits a string and trims whitespace from each part.
func splitTrimmed(s, sep string) []string {
parts := make([]string, 0)
for _, part := range splitString(s, sep) {
trimmed := trimSpace(part)
parts = append(parts, trimmed)
}
return parts
}

// splitString splits a string by separator without using strings package.
func splitString(s, sep string) []string {
var result []string
for {
i := indexOf(s, sep)
if i < 0 {
result = append(result, s)
break
}
result = append(result, s[:i])
s = s[i+len(sep):]
}
return result
}

// indexOf returns the index of sep in s, or -1 if not found.
func indexOf(s, sep string) int {
for i := 0; i <= len(s)-len(sep); i++ {
if s[i:i+len(sep)] == sep {
return i
}
}
return -1
}

// trimSpace removes leading and trailing whitespace.
func trimSpace(s string) string {
start := 0
for start < len(s) && isSpace(s[start]) {
start++
}
end := len(s)
for end > start && isSpace(s[end-1]) {
end--
}
return s[start:end]
}

// isSpace returns true if c is a whitespace character.
func isSpace(c byte) bool {
return c == ' ' || c == '\t' || c == '\n' || c == '\r'
}
4 changes: 4 additions & 0 deletions internal/daemon/types/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ type TransactionSpec struct {

// SDKVersion overrides auto-detected SDK version.
SDKVersion string `json:"sdkVersion,omitempty"`

// GasLimit sets the gas limit for the transaction.
// If not specified, defaults to 200000.
GasLimit uint64 `json:"gasLimit,omitempty"`
}

// TransactionStatus defines the observed state of a Transaction.
Expand Down
6 changes: 6 additions & 0 deletions pkg/network/cosmos/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func TestBuildTx_GovVote(t *testing.T) {
Version: "v0.50.0",
Features: []string{network.FeatureGovV1},
},
txConfig: NewTxConfig(),
}

// Create request with TxTypeGovVote payload
Expand Down Expand Up @@ -93,6 +94,7 @@ func TestBuildTx_GovVote_InvalidOption(t *testing.T) {
Version: "v0.50.0",
Features: []string{network.FeatureGovV1},
},
txConfig: NewTxConfig(),
}

payload, err := json.Marshal(map[string]interface{}{
Expand Down Expand Up @@ -140,6 +142,7 @@ func TestBuildTx_BankSend(t *testing.T) {
Version: "v0.50.0",
Features: []string{network.FeatureGovV1},
},
txConfig: NewTxConfig(),
}

payload, err := json.Marshal(map[string]interface{}{
Expand Down Expand Up @@ -189,6 +192,7 @@ func TestBuildTx_StakingDelegate(t *testing.T) {
Version: "v0.50.0",
Features: []string{network.FeatureGovV1},
},
txConfig: NewTxConfig(),
}

payload, err := json.Marshal(map[string]interface{}{
Expand Down Expand Up @@ -224,6 +228,7 @@ func TestBuildTx_UnsupportedTxType(t *testing.T) {
Version: "v0.50.0",
Features: []string{network.FeatureGovV1},
},
txConfig: NewTxConfig(),
}

payload, err := json.Marshal(map[string]interface{}{})
Expand Down Expand Up @@ -261,6 +266,7 @@ func TestBuildTx_AccountQueryFails(t *testing.T) {
Version: "v0.50.0",
Features: []string{network.FeatureGovV1},
},
txConfig: NewTxConfig(),
}

payload, err := json.Marshal(map[string]interface{}{
Expand Down
Loading
Loading