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
5 changes: 5 additions & 0 deletions .changeset/curvy-berries-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink-deployments-framework": patch
---

Clean up TON CTF Provider, update test infra methods
136 changes: 86 additions & 50 deletions chain/ton/provider/ctf_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (
"time"

"github.com/avast/retry-go/v4"
"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"
Expand Down Expand Up @@ -46,10 +46,6 @@ type CTFChainProviderConfig struct {
// Note: Only images from supportedTONImageRepository are supported.
Image string

// 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
Expand All @@ -61,7 +57,7 @@ func (c CTFChainProviderConfig) validate() error {
return errors.New("sync.Once instance is required")
}

if c.Image != "" && !strings.Contains(c.Image, supportedTONImageRepository) {
if c.Image != "" && !strings.HasPrefix(c.Image, supportedTONImageRepository) {
Comment thread
jadepark-dev marked this conversation as resolved.
return fmt.Errorf("unsupported image %q: must be from %s", c.Image, supportedTONImageRepository)
}

Expand Down Expand Up @@ -99,7 +95,7 @@ func NewCTFChainProvider(

// Initialize sets up the Ton chain by validating the configuration, starting a CTF container,
// generating a deployer signer account, and constructing the chain instance.
func (p *CTFChainProvider) Initialize(_ context.Context) (chain.BlockChain, error) {
func (p *CTFChainProvider) Initialize(ctx context.Context) (chain.BlockChain, error) {
if p.chain != nil {
return *p.chain, nil // Already initialized
}
Expand All @@ -110,14 +106,28 @@ func (p *CTFChainProvider) Initialize(_ context.Context) (chain.BlockChain, erro

// Get the Chain ID
chainID, err := chainsel.GetChainIDFromSelector(p.selector)
require.NoError(p.t, err, "failed to get chain ID from selector")
if err != nil {
return nil, fmt.Errorf("failed to get chain ID from selector: %w", err)
}

url, nodeClient, err := p.startContainer(ctx, chainID)
if err != nil {
return nil, fmt.Errorf("failed to start container: %w", err)
}

url, nodeClient := p.startContainer(chainID)
// mylocalton uses a global_id of -217 by default
// https://github.com/neodix42/mylocalton-docker/blob/8f9c6ea27cd608dc6370c4191554b42b5a797905/docker/scripts/start-genesis.sh#L62
tonWallet := createTonWallet(p.t, nodeClient, wallet.ConfigV5R1Final{NetworkGlobalID: -217}, wallet.WithWorkchain(0))
tonWallet, err := createTonWallet(nodeClient, wallet.ConfigV5R1Final{NetworkGlobalID: -217}, wallet.WithWorkchain(0))
if err != nil {
return nil, fmt.Errorf("failed to create wallet: %w", err)
}

// airdrop the deployer wallet
fundTonWallets(p.t, nodeClient, []*address.Address{tonWallet.Address()}, []tlb.Coins{tlb.MustFromTON("1000")})
ferr := fundTonWallets(ctx, nodeClient, []*address.Address{tonWallet.Address()}, []tlb.Coins{tlb.MustFromTON("1000")})
if ferr != nil {
return nil, fmt.Errorf("failed to fund wallet: %w", ferr)
}

p.chain = &cldf_ton.Chain{
ChainMetadata: cldf_ton.ChainMetadata{Selector: p.selector},
Client: nodeClient,
Expand All @@ -129,15 +139,17 @@ func (p *CTFChainProvider) Initialize(_ context.Context) (chain.BlockChain, erro
return *p.chain, nil
}

func (p *CTFChainProvider) startContainer(chainID string) (string, ton.APIClientWrapped) {
func (p *CTFChainProvider) startContainer(ctx context.Context, chainID string) (string, *ton.APIClient, error) {
var (
attempts = uint(10)
url string
)

// initialize the docker network used by CTF
err := framework.DefaultNetwork(p.config.Once)
require.NoError(p.t, err)
if err != nil {
return "", nil, fmt.Errorf("failed to initialize default network: %w", err)
}

url, err = retry.DoWithData(func() (string, error) {
// Initialize a port for the container
Expand All @@ -162,100 +174,128 @@ func (p *CTFChainProvider) startContainer(chainID string) (string, ton.APIClient

return output.Nodes[0].ExternalHTTPUrl, nil
},
retry.Context(p.t.Context()),
retry.Context(ctx),
retry.Attempts(attempts),
retry.Delay(1*time.Second),
retry.DelayType(retry.FixedDelay),
retry.OnRetry(func(attempt uint, err error) {
p.t.Logf("Attempt %d/%d: Failed to start CTF Ton container: %v", attempt+1, attempts, err)
}),
)
require.NoError(p.t, err, "Failed to start CTF Ton container after %d attempts", attempts)
if err != nil {
return "", nil, fmt.Errorf("failed to start CTF Ton container after %d attempts: %w", attempts, err)
}

connectionPool, err := createLiteclientConnectionPool(p.t.Context(), url)
require.NoError(p.t, err)
connectionPool, err := createLiteclientConnectionPool(ctx, url)
if err != nil {
return "", nil, fmt.Errorf("failed to create liteclient connection pool: %w", err)
}

client := ton.NewAPIClient(connectionPool, ton.ProofCheckPolicyFast)

// check connection, CTFv2 handles the readiness
mb := getMasterchainBlockID(p.t, client)
mb, err := getMasterchainBlockID(ctx, client)
if err != nil {
return "", nil, fmt.Errorf("failed to get masterchain block ID: %w", err)
}

// set starting point to verify master block proofs chain
client.SetTrustedBlock(mb)

retryCount := p.getRetryCount()

return url, client.WithRetry(retryCount)
return url, client, nil
}

// Note: this utility functions can be replaced once we have in the chainlink-ton utils package
func createTonWallet(t *testing.T, client ton.APIClientWrapped, versionConfig wallet.VersionConfig, option wallet.Option) *wallet.Wallet {
t.Helper()

func createTonWallet(client ton.APIClientWrapped, versionConfig wallet.VersionConfig, option wallet.Option) (*wallet.Wallet, error) {

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

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

The helper functions still use ton.APIClientWrapped as the client parameter type, but the PR reverts the chain's Client field to *ton.APIClient. This type mismatch will cause compilation errors when these functions are called with the chain's client. The client parameter types should be changed to *ton.APIClient to match the reverted type.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ton.APIClientWrapped is an interface from the tonutils-go library, *ton.APIClient implements this interface

seed := wallet.NewSeed()
rw, err := wallet.FromSeed(client, seed, versionConfig)
require.NoError(t, err)
if err != nil {
return nil, fmt.Errorf("failed to create wallet from seed: %w", err)
}
pw, perr := wallet.FromPrivateKeyWithOptions(client, rw.PrivateKey(), versionConfig, option)
require.NoError(t, perr)
if perr != nil {
return nil, fmt.Errorf("failed to create wallet from private key: %w", perr)
}

return pw
return pw, nil
}

func fundTonWallets(t *testing.T, client ton.APIClientWrapped, recipients []*address.Address, amounts []tlb.Coins) {
t.Helper()
func fundTonWallets(ctx context.Context, client ton.APIClientWrapped, recipients []*address.Address, amounts []tlb.Coins) error {
if len(amounts) != len(recipients) {
return errors.New("recipients and amounts must have the same length")
}

require.Len(t, amounts, len(recipients), "recipients and amounts must have the same length")
// initialize the prefunded wallet(Highload-V2), for other wallets, see https://github.com/neodix42/mylocalton-docker#pre-installed-wallets
version := wallet.HighloadV2Verified //nolint:staticcheck // SA1019: only available option in mylocalton-docker
rawHlWallet, err := wallet.FromSeed(client, strings.Fields(blockchain.DefaultTonHlWalletMnemonic), version)
require.NoError(t, err)
if err != nil {
return fmt.Errorf("failed to create wallet from seed: %w", err)
}

mcFunderWallet, err := wallet.FromPrivateKeyWithOptions(client, rawHlWallet.PrivateKey(), version, wallet.WithWorkchain(-1))
require.NoError(t, err)
if err != nil {
return fmt.Errorf("failed to create wallet from private key: %w", err)
}

funder, err := mcFunderWallet.GetSubwallet(uint32(42))
require.NoError(t, err)
if err != nil {
return fmt.Errorf("failed to get subwallet: %w", err)
}

// double check funder address
require.Equal(t, blockchain.DefaultTonHlWalletAddress, funder.Address().StringRaw(), "funder address mismatch")
if funder.Address().StringRaw() != blockchain.DefaultTonHlWalletAddress {
return fmt.Errorf("funder address mismatch: %s != %s", funder.Address().StringRaw(), blockchain.DefaultTonHlWalletAddress)
}

// create transfer messages for each recipient
messages := make([]*wallet.Message, len(recipients))
for i, addr := range recipients {
transfer, terr := funder.BuildTransfer(addr, amounts[i], false, "")
require.NoError(t, terr)
if terr != nil {
return fmt.Errorf("failed to build transfer: %w", terr)
}
messages[i] = transfer
}
_, _, txerr := funder.SendManyWaitTransaction(t.Context(), messages)
require.NoError(t, txerr, "airdrop transaction failed")

// we don't wait for the transaction to be confirmed here, as it may take some time
}
// the name SendManyWaitTransaction is misleading, it doesn't wait for the transaction to be confirmed,
// it just sends the transactions(TON has asynchronous transactions)
_, _, txerr := funder.SendManyWaitTransaction(ctx, messages)
if txerr != nil {
return fmt.Errorf("failed to send many wait transaction: %w", txerr)
}

func getMasterchainBlockID(t *testing.T, client *ton.APIClient) *ton.BlockIDExt {
t.Helper()
return nil
}

func getMasterchainBlockID(ctx context.Context, client ton.APIClientWrapped) (*ton.BlockIDExt, error) {
var masterchainBlockID *ton.BlockIDExt
// check connection, CTFv2 handles the readiness
err := retry.Do(func() error {
var err error
masterchainBlockID, err = client.GetMasterchainInfo(t.Context())
masterchainBlockID, err = client.GetMasterchainInfo(ctx)

return err
},
retry.Context(t.Context()),
retry.Context(ctx),
retry.Attempts(30),
retry.Delay(1*time.Second),
retry.DelayType(retry.FixedDelay),
)
require.NoError(t, err, "TON network not ready")
if err != nil {
return nil, fmt.Errorf("failed to get masterchain info: %w", err)
}

// return masterchain block for setting trusted block
return masterchainBlockID
return masterchainBlockID, nil
}

// Name returns the name of the CTFChainProvider.
func (*CTFChainProvider) Name() string {
return "Ton CTF Chain Provider"
return "TON CTF Chain Provider"
}

// ChainSelector returns the chain selector of the Aptos chain managed by this provider.
// ChainSelector returns the chain selector of the TON chain managed by this provider.
func (p *CTFChainProvider) ChainSelector() uint64 {
return p.selector
}
Expand All @@ -266,10 +306,6 @@ func (p *CTFChainProvider) BlockChain() chain.BlockChain {
return *p.chain
}

func (p *CTFChainProvider) getRetryCount() int {
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 != "" {
Expand Down
2 changes: 1 addition & 1 deletion chain/ton/provider/ctf_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func Test_CTFChainProvider_Name(t *testing.T) {
t.Parallel()

p := &CTFChainProvider{}
assert.Equal(t, "Ton CTF Chain Provider", p.Name())
assert.Equal(t, "TON CTF Chain Provider", p.Name())
}

func Test_CTFChainProvider_ChainSelector(t *testing.T) {
Expand Down
13 changes: 3 additions & 10 deletions chain/ton/provider/rpc_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ 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 @@ -113,7 +110,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, retryCount int) (tonlib.APIClientWrapped, error) {
func setupConnection(ctx context.Context, liteserverURL string) (*tonlib.APIClient, error) {
connectionPool, err := createLiteclientConnectionPool(ctx, liteserverURL)
if err != nil {
return nil, fmt.Errorf("failed to connect to liteserver: %w", err)
Expand All @@ -130,7 +127,7 @@ func setupConnection(ctx context.Context, liteserverURL string, retryCount int)
// Set starting point to verify master block proofs chain
api.SetTrustedBlock(mb)

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

// createWallet creates a TON wallet from the given private key and API client
Expand Down Expand Up @@ -159,7 +156,7 @@ func (p *RPCChainProvider) Initialize(ctx context.Context) (chain.BlockChain, er
}

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

func (p *RPCChainProvider) getRetryCount() int {
return p.config.RetryCount
}
38 changes: 0 additions & 38 deletions chain/ton/provider/rpc_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,42 +223,6 @@ 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()

Expand All @@ -267,7 +231,6 @@ func Test_NewRPCChainProvider(t *testing.T) {
HTTPURL: "liteserver://publickey@localhost:8080",
DeployerSignerGen: PrivateKeyRandom(),
WalletVersion: WalletVersionV5R1,
RetryCount: 10,
}

p := NewRPCChainProvider(selector, config)
Expand All @@ -276,7 +239,6 @@ func Test_NewRPCChainProvider(t *testing.T) {
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)
}

Expand Down
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.APIClientWrapped // APIClient for Lite Server connection
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.APIClient // APIClient for Lite Server connection
Wallet *wallet.Wallet // Wallet abstraction (signing, sending)
WalletAddress *address.Address // Address of deployer wallet
URL string // Liteserver URL
}
Loading