Skip to content
5 changes: 5 additions & 0 deletions .changeset/good-cups-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink-deployments-framework": minor
---

expose TON CTF configs to caller
62 changes: 52 additions & 10 deletions chain/ton/provider/ctf_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,48 @@ import (
"time"

"github.com/avast/retry-go/v4"
chainsel "github.com/smartcontractkit/chain-selectors"
"github.com/smartcontractkit/chainlink-testing-framework/framework"
"github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain"
"github.com/smartcontractkit/freeport"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
"github.com/xssnick/tonutils-go/ton"
"github.com/xssnick/tonutils-go/ton/wallet"

chainsel "github.com/smartcontractkit/chain-selectors"
"github.com/smartcontractkit/chainlink-testing-framework/framework"
"github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain"
"github.com/smartcontractkit/freeport"

"github.com/smartcontractkit/chainlink-deployments-framework/chain"
cldf_ton "github.com/smartcontractkit/chainlink-deployments-framework/chain/ton"
)

const (
// defaultTONImage is the default Docker image used for TON localnet.
// Only images from this repository are supported.
defaultTONImage = "ghcr.io/neodix42/mylocalton-docker:v3.7"

// supportedTONImageRepository is the only supported Docker image repository for TON localnet.
supportedTONImageRepository = "ghcr.io/neodix42/mylocalton-docker"
)
Comment on lines +32 to +37

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should be private in case it gets referenced outside of this repo

Suggested change
// Only images from this repository are supported.
DefaultTONImage = "ghcr.io/neodix42/mylocalton-docker:v3.7"
// SupportedTONImageRepository is the only supported Docker image repository for TON localnet.
SupportedTONImageRepository = "ghcr.io/neodix42/mylocalton-docker"
)
// Only images from this repository are supported.
defaultTONImage = "ghcr.io/neodix42/mylocalton-docker:v3.7"
// SupportedTONImageRepository is the only supported Docker image repository for TON localnet.
supportedTONImageRepository = "ghcr.io/neodix42/mylocalton-docker"
)


// CTFChainProviderConfig holds the configuration to initialize the CTFChainProvider.
type CTFChainProviderConfig struct {
// Required: A sync.Once instance to ensure that the CTF framework only sets up the new
// DefaultNetwork once
Once *sync.Once

// Optional: Docker image to use for the TON localnet. If empty, defaults to defaultTONImage.
// Note: Only images from supportedTONImageRepository are supported.
Image string
Comment thread
jadepark-dev marked this conversation as resolved.

// Optional: Retry count for APIClient. Default is 0 (unlimited retries).
// Set to positive value for specific retry count.
RetryCount int

// Optional: Custom environment variables to pass to the TON container.
// Example: map[string]string{"NEXT_BLOCK_GENERATION_DELAY": "0.5"}
CustomEnv map[string]string
Comment thread
jadepark-dev marked this conversation as resolved.
}

// validate checks if the CTFChainProviderConfig is valid.
Expand All @@ -39,6 +61,10 @@ func (c CTFChainProviderConfig) validate() error {
return errors.New("sync.Once instance is required")
}

if c.Image != "" && !strings.Contains(c.Image, supportedTONImageRepository) {
return fmt.Errorf("unsupported image %q: must be from %s", c.Image, supportedTONImageRepository)
}

return nil
}

Expand Down Expand Up @@ -103,7 +129,7 @@ func (p *CTFChainProvider) Initialize(_ context.Context) (chain.BlockChain, erro
return *p.chain, nil
}

func (p *CTFChainProvider) startContainer(chainID string) (string, *ton.APIClient) {
func (p *CTFChainProvider) startContainer(chainID string) (string, ton.APIClientWrapped) {
var (
attempts = uint(10)
url string
Expand All @@ -119,10 +145,11 @@ func (p *CTFChainProvider) startContainer(chainID string) (string, *ton.APIClien

// spin up mylocalton with CTFv2
output, rerr := blockchain.NewBlockchainNetwork(&blockchain.Input{
Type: blockchain.TypeTon,
ChainID: chainID,
Port: strconv.Itoa(port),
Image: "ghcr.io/neodix42/mylocalton-docker:v3.7",
Type: blockchain.TypeTon,
ChainID: chainID,
Port: strconv.Itoa(port),
Image: p.getImage(),
CustomEnv: p.config.CustomEnv,
})
if rerr != nil {
// Return the ports to freeport to avoid leaking them during retries
Expand Down Expand Up @@ -155,7 +182,9 @@ func (p *CTFChainProvider) startContainer(chainID string) (string, *ton.APIClien
// set starting point to verify master block proofs chain
client.SetTrustedBlock(mb)

return url, client
retryCount := p.getRetryCount()

return url, client.WithRetry(retryCount)
}

// Note: this utility functions can be replaced once we have in the chainlink-ton utils package
Expand Down Expand Up @@ -236,3 +265,16 @@ func (p *CTFChainProvider) ChainSelector() uint64 {
func (p *CTFChainProvider) BlockChain() chain.BlockChain {
return *p.chain
}

func (p *CTFChainProvider) getRetryCount() int {
Comment thread
jadepark-dev marked this conversation as resolved.
return p.config.RetryCount
}

// getImage returns the configured Docker image, or the default if not specified.
func (p *CTFChainProvider) getImage() string {
if p.config.Image != "" {
return p.config.Image
}

return defaultTONImage
}
12 changes: 12 additions & 0 deletions chain/ton/provider/ctf_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,15 @@ func Test_CTFChainProvider_BlockChain(t *testing.T) {

assert.Equal(t, *chain, p.BlockChain())
}

func Test_CTFChainProvider_getImage(t *testing.T) {
t.Parallel()

// Test default image
p1 := &CTFChainProvider{config: CTFChainProviderConfig{}}
assert.Equal(t, "ghcr.io/neodix42/mylocalton-docker:v3.7", p1.getImage())

// Test custom image
p2 := &CTFChainProvider{config: CTFChainProviderConfig{Image: "ghcr.io/neodix42/mylocalton-docker:latest"}}
assert.Equal(t, "ghcr.io/neodix42/mylocalton-docker:latest", p2.getImage())
}
15 changes: 11 additions & 4 deletions chain/ton/provider/rpc_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ type RPCChainProviderConfig struct {
// Optional: The TON wallet version to use. Supported versions are: V1R1, V1R2, V1R3, V2R1,
// V2R2, V3R1, V3R2, V4R1, V4R2 and V5R1. If no value provided, V5R1 is used as default.
WalletVersion WalletVersion
// Optional: Retry count for APIClient. Default is 0 (unlimited retries).
// Set to positive value for specific retry count.
RetryCount int
}

// validateLiteserverURL validates the format of a liteserver URL
Expand Down Expand Up @@ -110,7 +113,7 @@ func NewRPCChainProvider(selector uint64, config RPCChainProviderConfig) *RPCCha
}

// setupConnection creates and tests a connection to the TON liteserver
func setupConnection(ctx context.Context, liteserverURL string) (*tonlib.APIClient, error) {
func setupConnection(ctx context.Context, liteserverURL string, retryCount int) (tonlib.APIClientWrapped, error) {
connectionPool, err := createLiteclientConnectionPool(ctx, liteserverURL)
if err != nil {
return nil, fmt.Errorf("failed to connect to liteserver: %w", err)
Expand All @@ -127,11 +130,11 @@ func setupConnection(ctx context.Context, liteserverURL string) (*tonlib.APIClie
// Set starting point to verify master block proofs chain
api.SetTrustedBlock(mb)

return api, nil
return api.WithRetry(retryCount), nil
}

// createWallet creates a TON wallet from the given private key and API client
func createWallet(api *tonlib.APIClient, privateKey []byte, version WalletVersion) (*wallet.Wallet, error) {
func createWallet(api tonlib.APIClientWrapped, privateKey []byte, version WalletVersion) (*wallet.Wallet, error) {
walletConfig, err := getWalletVersionConfig(version)
if err != nil {
return nil, fmt.Errorf("unsupported wallet version: %w", err)
Expand All @@ -156,7 +159,7 @@ func (p *RPCChainProvider) Initialize(ctx context.Context) (chain.BlockChain, er
}

// Setup connection to TON network
api, err := setupConnection(ctx, p.config.HTTPURL)
api, err := setupConnection(ctx, p.config.HTTPURL, p.getRetryCount())
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -239,3 +242,7 @@ func (p *RPCChainProvider) ChainSelector() uint64 {
func (p *RPCChainProvider) BlockChain() chain.BlockChain {
return *p.chain
}

func (p *RPCChainProvider) getRetryCount() int {
return p.config.RetryCount
}
131 changes: 131 additions & 0 deletions chain/ton/provider/rpc_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,134 @@ func Test_getWalletVersionConfig(t *testing.T) {
})
}
}

func Test_RPCChainProvider_getRetryCount(t *testing.T) {
t.Parallel()

tests := []struct {
name string
retryCount int
want int
}{
{
name: "returns configured retry count",
retryCount: 10,
want: 10,
},
{
name: "returns zero for unlimited retries",
retryCount: 0,
want: 0,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

p := &RPCChainProvider{
config: RPCChainProviderConfig{
RetryCount: tt.retryCount,
},
}

got := p.getRetryCount()
assert.Equal(t, tt.want, got)
})
}
}

func Test_NewRPCChainProvider(t *testing.T) {
t.Parallel()

selector := uint64(12345)
config := RPCChainProviderConfig{
HTTPURL: "liteserver://publickey@localhost:8080",
DeployerSignerGen: PrivateKeyRandom(),
WalletVersion: WalletVersionV5R1,
RetryCount: 10,
}

p := NewRPCChainProvider(selector, config)

require.NotNil(t, p)
assert.Equal(t, selector, p.selector)
assert.Equal(t, config.HTTPURL, p.config.HTTPURL)
assert.Equal(t, config.WalletVersion, p.config.WalletVersion)
assert.Equal(t, config.RetryCount, p.config.RetryCount)
assert.Nil(t, p.chain)
}

func Test_createWallet(t *testing.T) {
t.Parallel()

tests := []struct {
name string
version WalletVersion
expectError bool
errorContains string
}{
{
name: "unsupported wallet version returns error",
version: "V9R9",
expectError: true,
errorContains: "unsupported wallet version",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

privateKey := make([]byte, 32)
wallet, err := createWallet(nil, privateKey, tt.version)

if tt.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.errorContains)
assert.Nil(t, wallet)
} else {
require.NoError(t, err)
assert.NotNil(t, wallet)
}
})
}
}

func Test_createLiteclientConnectionPool_InvalidURL(t *testing.T) {
t.Parallel()

tests := []struct {
name string
url string
errorContains string
}{
{
name: "empty URL returns error",
url: "",
errorContains: "liteserver url is required",
},
{
name: "invalid prefix returns error",
url: "http://example.com",
errorContains: "invalid liteserver URL format",
},
{
name: "missing @ returns error",
url: "liteserver://invalidurl",
errorContains: "invalid liteserver URL format",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

pool, err := createLiteclientConnectionPool(t.Context(), tt.url)

require.Error(t, err)
assert.Contains(t, err.Error(), tt.errorContains)
assert.Nil(t, pool)
})
}
}
10 changes: 5 additions & 5 deletions chain/ton/ton_chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ type ChainMetadata = common.ChainMetadata

// Chain represents a TON chain.
type Chain struct {
ChainMetadata // Contains canonical chain identifier
Client *ton.APIClient // RPC client via Lite Server
Wallet *wallet.Wallet // Wallet abstraction (signing, sending)
WalletAddress *address.Address // Address of deployer wallet
URL string // Liteserver URL
ChainMetadata // Contains canonical chain identifier
Client ton.APIClientWrapped // APIClient for Lite Server connection
Wallet *wallet.Wallet // Wallet abstraction (signing, sending)
WalletAddress *address.Address // Address of deployer wallet
URL string // Liteserver URL
}
Loading