Skip to content
Open
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
85 changes: 39 additions & 46 deletions framework/components/blockchain/ton.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ package blockchain
import (
"context"
"fmt"
"strconv"
"time"

"github.com/docker/docker/api/types/container"
"github.com/docker/go-connections/nat"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/network"
"github.com/testcontainers/testcontainers-go/wait"

"github.com/smartcontractkit/chainlink-testing-framework/framework"
Expand All @@ -23,14 +22,8 @@ const (
defaultTonHTTPServerPort = "8000"
defaultLiteServerPort = "40000"
defaultLiteServerPublicKey = "E7XwFSQzNkcRepUC23J2nRpASXpnsEKmyyHYV4u/FZY="
liteServerPortOffset = 100 // arbitrary offset for lite server port
)

type portMapping struct {
HTTPServer string
LiteServer string
}

func defaultTon(in *Input) {
if in.Image == "" {
in.Image = "ghcr.io/neodix42/mylocalton-docker:latest"
Expand All @@ -43,15 +36,7 @@ func defaultTon(in *Input) {
func newTon(ctx context.Context, in *Input) (*Output, error) {
defaultTon(in)

base, err := strconv.Atoi(in.Port)
if err != nil {
return nil, fmt.Errorf("invalid base port %s: %w", in.Port, err)
}

ports := &portMapping{
HTTPServer: in.Port,
LiteServer: strconv.Itoa(base + liteServerPortOffset),
}
containerName := framework.DefaultTCName("ton-genesis")

baseEnv := map[string]string{
"GENESIS": "true",
Expand All @@ -72,31 +57,20 @@ func newTon(ctx context.Context, in *Input) (*Output, error) {
}
}

n, err := network.New(ctx,
network.WithAttachable(),
network.WithLabels(framework.DefaultTCLabels()),
)
if err != nil {
return nil, fmt.Errorf("failed to create network: %w", err)
}

networkName := n.Name

req := testcontainers.ContainerRequest{
Image: in.Image,
AlwaysPullImage: in.PullImage,
Name: framework.DefaultTCName("ton-genesis"),
Name: containerName,
ExposedPorts: []string{
fmt.Sprintf("%s:%s/tcp", ports.HTTPServer, defaultTonHTTPServerPort),
fmt.Sprintf("%s:%s/tcp", ports.LiteServer, defaultLiteServerPort),
"40003/udp",
"40002/tcp",
"40001/udp",
fmt.Sprintf("%s/tcp", defaultTonHTTPServerPort),
fmt.Sprintf("%s/tcp", defaultLiteServerPort),
},
Networks: []string{framework.DefaultNetworkName},
NetworkAliases: map[string][]string{
framework.DefaultNetworkName: {containerName},
},
Networks: []string{networkName},
NetworkAliases: map[string][]string{networkName: {"genesis"}},
Labels: framework.DefaultTCLabels(),
Env: finalEnv,
Labels: framework.DefaultTCLabels(),
Env: finalEnv,
WaitingFor: wait.ForExec([]string{
"/usr/local/bin/lite-client",
"-a", fmt.Sprintf("127.0.0.1:%s", defaultLiteServerPort),
Expand All @@ -105,15 +79,29 @@ func newTon(ctx context.Context, in *Input) (*Output, error) {
}).WithStartupTimeout(2 * time.Minute),
Mounts: testcontainers.ContainerMounts{
{
Source: testcontainers.GenericVolumeMountSource{Name: fmt.Sprintf("shared-data-%s", networkName)},
Source: testcontainers.GenericVolumeMountSource{Name: fmt.Sprintf("ton-data-%s", containerName)},
Target: "/usr/share/data",
},
{
Source: testcontainers.GenericVolumeMountSource{Name: fmt.Sprintf("ton-db-%s", networkName)},
Source: testcontainers.GenericVolumeMountSource{Name: fmt.Sprintf("ton-db-%s", containerName)},
Target: "/var/ton-work/db",
},
},
HostConfigModifier: func(h *container.HostConfig) {
h.PortBindings = nat.PortMap{
nat.Port(fmt.Sprintf("%s/tcp", defaultTonHTTPServerPort)): []nat.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: in.Port,
},
},
nat.Port(fmt.Sprintf("%s/tcp", defaultLiteServerPort)): []nat.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: "", // Docker assigns a dynamic available port
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

Setting HostPort to an empty string in Docker port bindings can be rejected by the Docker API (it typically expects a numeric string). To reliably request an ephemeral host port, either omit the LiteServer entry from PortBindings entirely (and rely on ExposedPorts + MappedPort), or set the host port explicitly to "0" (equivalent to -p 0:40000).

Suggested change
HostPort: "", // Docker assigns a dynamic available port
HostPort: "0", // Docker assigns a dynamic available port

Copilot uses AI. Check for mistakes.
},
},
}
Comment on lines +91 to +104
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

The modifier overwrites h.PortBindings wholesale, which can unintentionally discard bindings set elsewhere (e.g., by other modifiers or defaults). Prefer initializing h.PortBindings if nil and then setting/updating only the specific ports you care about, leaving any pre-existing bindings intact.

Suggested change
h.PortBindings = nat.PortMap{
nat.Port(fmt.Sprintf("%s/tcp", defaultTonHTTPServerPort)): []nat.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: in.Port,
},
},
nat.Port(fmt.Sprintf("%s/tcp", defaultLiteServerPort)): []nat.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: "", // Docker assigns a dynamic available port
},
},
}
if h.PortBindings == nil {
h.PortBindings = nat.PortMap{}
}
httpPort := nat.Port(fmt.Sprintf("%s/tcp", defaultTonHTTPServerPort))
lsPort := nat.Port(fmt.Sprintf("%s/tcp", defaultLiteServerPort))
h.PortBindings[httpPort] = []nat.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: in.Port,
},
}
h.PortBindings[lsPort] = []nat.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: "", // Docker assigns a dynamic available port
},
}

Copilot uses AI. Check for mistakes.
framework.ResourceLimitsFunc(h, in.ContainerResources)
},
}
Expand All @@ -126,27 +114,32 @@ func newTon(ctx context.Context, in *Input) (*Output, error) {
return nil, err
}

host, err := c.Host(ctx)
host, err := framework.GetHostWithContext(ctx, c)
if err != nil {
return nil, err
}

name, err := c.Name(ctx)
httpMappedPort, err := c.MappedPort(ctx, nat.Port(fmt.Sprintf("%s/tcp", defaultTonHTTPServerPort)))
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to get mapped HTTP port: %w", err)
}
lsMappedPort, err := c.MappedPort(ctx, nat.Port(fmt.Sprintf("%s/tcp", defaultLiteServerPort)))
if err != nil {
return nil, fmt.Errorf("failed to get mapped LiteServer port: %w", err)
}

return &Output{
UseCache: true,
ChainID: in.ChainID,
Type: in.Type,
Family: FamilyTon,
ContainerName: name,
ContainerName: containerName,
Container: c,
Comment on lines 131 to 137
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

Output.ContainerName previously reflected the actual container name returned by Docker via c.Name(ctx). Returning the requested name (containerName) is a behavior change and can break callers that rely on the real name (including Docker's leading '/' formatting). Consider restoring name, err := c.Name(ctx) for Output.ContainerName, while still using containerName exclusively for network aliasing/internal DNS.

Copilot uses AI. Check for mistakes.
Nodes: []*Node{{
// URLs now contain liteserver://publickey@host:port
ExternalHTTPUrl: fmt.Sprintf("liteserver://%s@%s:%s", defaultLiteServerPublicKey, host, ports.LiteServer),
InternalHTTPUrl: fmt.Sprintf("liteserver://%s@%s:%s", defaultLiteServerPublicKey, name, ports.LiteServer),
ExternalHTTPUrl: fmt.Sprintf("liteserver://%s@%s:%s", defaultLiteServerPublicKey, host, lsMappedPort.Port()),
InternalHTTPUrl: fmt.Sprintf("liteserver://%s@%s:%s", defaultLiteServerPublicKey, containerName, defaultLiteServerPort),
ExternalWSUrl: fmt.Sprintf("http://%s:%s", host, httpMappedPort.Port()),
InternalWSUrl: fmt.Sprintf("http://%s:%s", containerName, defaultTonHTTPServerPort),
}},
}, nil
}
Loading