From f2b8cf17ab615fcbb952f884ab518630574efd2c Mon Sep 17 00:00:00 2001 From: Jade Park Date: Wed, 19 Nov 2025 12:21:09 +0000 Subject: [PATCH 1/5] fix: client type, clean up ctf provider, --- chain/ton/provider/ctf_provider.go | 141 +++++++++++++++--------- chain/ton/provider/rpc_provider.go | 13 +-- chain/ton/provider/rpc_provider_test.go | 38 ------- chain/ton/ton_chain.go | 10 +- 4 files changed, 96 insertions(+), 106 deletions(-) diff --git a/chain/ton/provider/ctf_provider.go b/chain/ton/provider/ctf_provider.go index 0994d95da..480101c49 100644 --- a/chain/ton/provider/ctf_provider.go +++ b/chain/ton/provider/ctf_provider.go @@ -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" @@ -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 @@ -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) { return fmt.Errorf("unsupported image %q: must be from %s", c.Image, supportedTONImageRepository) } @@ -99,9 +95,9 @@ 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 + return p.chain, nil // Already initialized } if err := p.config.validate(); err != nil { @@ -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, @@ -126,10 +136,10 @@ func (p *CTFChainProvider) Initialize(_ context.Context) (chain.BlockChain, erro URL: url, } - return *p.chain, nil + 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 @@ -137,7 +147,9 @@ func (p *CTFChainProvider) startContainer(chainID string) (string, ton.APIClient // 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 @@ -162,7 +174,7 @@ 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), @@ -170,92 +182,119 @@ func (p *CTFChainProvider) startContainer(chainID string) (string, ton.APIClient 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) { 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 } @@ -266,10 +305,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 != "" { diff --git a/chain/ton/provider/rpc_provider.go b/chain/ton/provider/rpc_provider.go index 1a3cebe22..b36c936fa 100644 --- a/chain/ton/provider/rpc_provider.go +++ b/chain/ton/provider/rpc_provider.go @@ -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 @@ -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) @@ -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 @@ -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 } @@ -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 -} diff --git a/chain/ton/provider/rpc_provider_test.go b/chain/ton/provider/rpc_provider_test.go index e2199a87d..add5c34bb 100644 --- a/chain/ton/provider/rpc_provider_test.go +++ b/chain/ton/provider/rpc_provider_test.go @@ -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() @@ -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) @@ -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) } diff --git a/chain/ton/ton_chain.go b/chain/ton/ton_chain.go index 9c7788778..70a6d0690 100644 --- a/chain/ton/ton_chain.go +++ b/chain/ton/ton_chain.go @@ -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 } From d2fdcb36da077c9b06d69da4ec200e6ba7d5d332 Mon Sep 17 00:00:00 2001 From: Jade Park Date: Wed, 19 Nov 2025 13:37:39 +0000 Subject: [PATCH 2/5] fix: chain return type --- chain/ton/provider/ctf_provider.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chain/ton/provider/ctf_provider.go b/chain/ton/provider/ctf_provider.go index 480101c49..23b1ed49e 100644 --- a/chain/ton/provider/ctf_provider.go +++ b/chain/ton/provider/ctf_provider.go @@ -97,7 +97,7 @@ func NewCTFChainProvider( // generating a deployer signer account, and constructing the chain instance. func (p *CTFChainProvider) Initialize(ctx context.Context) (chain.BlockChain, error) { if p.chain != nil { - return p.chain, nil // Already initialized + return *p.chain, nil // Already initialized } if err := p.config.validate(); err != nil { @@ -136,7 +136,7 @@ func (p *CTFChainProvider) Initialize(ctx context.Context) (chain.BlockChain, er URL: url, } - return p.chain, nil + return *p.chain, nil } func (p *CTFChainProvider) startContainer(ctx context.Context, chainID string) (string, *ton.APIClient, error) { From b0e465c88755ef864dc580f4c9e026521bc04ae6 Mon Sep 17 00:00:00 2001 From: Jade Park Date: Wed, 19 Nov 2025 16:29:44 +0000 Subject: [PATCH 3/5] chore: changeset --- .changeset/curvy-berries-film.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/curvy-berries-film.md diff --git a/.changeset/curvy-berries-film.md b/.changeset/curvy-berries-film.md new file mode 100644 index 000000000..0a84842bc --- /dev/null +++ b/.changeset/curvy-berries-film.md @@ -0,0 +1,5 @@ +--- +"chainlink-deployments-framework": patch +--- + +Clean up TON CTF Provider, update test infra methods From de3c3a70b35e8afa6bc503179036fcfcd960506a Mon Sep 17 00:00:00 2001 From: Jade Park Date: Wed, 19 Nov 2025 16:34:32 +0000 Subject: [PATCH 4/5] chore: lint --- chain/ton/provider/ctf_provider.go | 1 + 1 file changed, 1 insertion(+) diff --git a/chain/ton/provider/ctf_provider.go b/chain/ton/provider/ctf_provider.go index 23b1ed49e..74aecec32 100644 --- a/chain/ton/provider/ctf_provider.go +++ b/chain/ton/provider/ctf_provider.go @@ -274,6 +274,7 @@ func getMasterchainBlockID(ctx context.Context, client ton.APIClientWrapped) (*t err := retry.Do(func() error { var err error masterchainBlockID, err = client.GetMasterchainInfo(ctx) + return err }, retry.Context(ctx), From 3364f9ac159560821192ab854307cf682ebe51e3 Mon Sep 17 00:00:00 2001 From: Jade Park Date: Wed, 19 Nov 2025 17:12:04 +0000 Subject: [PATCH 5/5] chore: name --- chain/ton/provider/ctf_provider_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chain/ton/provider/ctf_provider_test.go b/chain/ton/provider/ctf_provider_test.go index 603de9c47..f32e73417 100644 --- a/chain/ton/provider/ctf_provider_test.go +++ b/chain/ton/provider/ctf_provider_test.go @@ -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) {