Skip to content
Open
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
65 changes: 8 additions & 57 deletions chain/ton/provider/ctf_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"github.com/avast/retry-go/v4"
"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"
Expand All @@ -34,6 +33,9 @@ const (

// supportedTONImageRepository is the only supported Docker image repository for TON localnet.
supportedTONImageRepository = "ghcr.io/neodix42/mylocalton-docker"

// defaultTxTONAmount is the default amount of TON to use for transactions.
defaultTxTONAmount = "0.1"
)

// CTFChainProviderConfig holds the configuration to initialize the CTFChainProvider.
Expand Down Expand Up @@ -122,18 +124,15 @@ func (p *CTFChainProvider) Initialize(ctx context.Context) (chain.BlockChain, er
return nil, fmt.Errorf("failed to create wallet: %w", err)
}

// airdrop the deployer wallet
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)
}
Comment on lines -125 to -129
Copy link
Contributor

Choose a reason for hiding this comment

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

Omitting fund logic from network setup makes sense to me(if I am reading it right), but just a reminder that integration tests in core and chainlink-ton are relying on the deployer wallet to fund transmitters:


p.chain = &cldf_ton.Chain{
ChainMetadata: cldf_ton.ChainMetadata{Selector: p.selector},
Client: nodeClient,
Wallet: tonWallet,
WalletAddress: tonWallet.Address(),
WalletAddress: tonWallet.WalletAddress(),
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The change from tonWallet.Address() to tonWallet.WalletAddress() lacks test coverage. The existing test in ctf_provider_test.go only checks that WalletAddress is not empty but doesn't verify the correct method is being called or that the address value is as expected.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@jadepark-dev jadepark-dev Dec 23, 2025

Choose a reason for hiding this comment

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

Not related to this PR but I wonder if we need to apply this change to the rest of the codebase. Any context on bounceable address?

// Address - returns old (bounce) version of wallet address
// DEPRECATED: because of address reform, use WalletAddress,
// it will return UQ format
func (w *Wallet) Address() *address.Address {
	return w.addr
}

// WalletAddress - returns new standard non bounce address
func (w *Wallet) WalletAddress() *address.Address {
	return w.addr.Bounce(false)
}

URL: url,
TxOps: cldf_ton.TxOps{
Wallet: tonWallet,
Amount: tlb.MustFromTON(defaultTxTONAmount), // default amount for transactions

Choose a reason for hiding this comment

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

Good for now, too simple to scale - we need to track this item.

We will need to be able to load gas configuration (from somewhere) which will need to tell us how much on average something costs to do (per contract call), and something a CLD user should be able to easily override.

cc @patricios-space @vicentevieytes re: per call gas configuration

},
}

return *p.chain, nil
Expand Down Expand Up @@ -220,54 +219,6 @@ func createTonWallet(client ton.APIClientWrapped, versionConfig wallet.VersionCo
return pw, nil
}

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")
}

// 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)
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))
if err != nil {
return fmt.Errorf("failed to create wallet from private key: %w", err)
}

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

// double check funder address
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, "")
if terr != nil {
return fmt.Errorf("failed to build transfer: %w", terr)
}
messages[i] = transfer
}

// 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)
}

return nil
}

func getMasterchainBlockID(ctx context.Context, client ton.APIClientWrapped) (*ton.BlockIDExt, error) {
var masterchainBlockID *ton.BlockIDExt
// check connection, CTFv2 handles the readiness
Expand Down
4 changes: 2 additions & 2 deletions chain/ton/provider/ctf_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ func Test_CTFChainProvider_Initialize(t *testing.T) {
require.True(t, ok, "expected got to be of type ton.Chain")
assert.Equal(t, tt.giveSelector, gotChain.Selector)
assert.NotEmpty(t, gotChain.Client)
assert.NotEmpty(t, gotChain.Wallet)
assert.NotEmpty(t, gotChain.WalletAddress)
assert.NotEmpty(t, gotChain.TxOps)
require.False(t, gotChain.WalletAddress.IsAddrNone())
}
})
}
Expand Down
22 changes: 17 additions & 5 deletions chain/ton/provider/rpc_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/xssnick/tonutils-go/liteclient"
"github.com/xssnick/tonutils-go/tlb"
tonlib "github.com/xssnick/tonutils-go/ton"
"github.com/xssnick/tonutils-go/ton/wallet"

Expand All @@ -22,6 +23,8 @@ const (
WalletVersionV4R2 WalletVersion = "V4R2"
WalletVersionV5R1 WalletVersion = "V5R1"
WalletVersionDefault WalletVersion = ""

defaultAmountTonString = "0.1" // Default amount in TON for transactions
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The default transaction amount should be documented more clearly, explaining why 0.1 TON was chosen and under what circumstances this value might need adjustment.

Suggested change
defaultAmountTonString = "0.1" // Default amount in TON for transactions
// defaultAmountTonString defines the default amount (in TON) used when a caller
// does not explicitly specify a transaction amount.
//
// The value 0.1 TON is intentionally conservative: it is typically sufficient to
// cover standard transaction fees on mainnet and testnet while remaining small
// enough to avoid unintentionally transferring a large balance.
//
// Consumers should override this default if:
// - their workflow requires sending a larger or smaller value per transaction;
// - network fee dynamics change so that 0.1 TON is no longer adequate to cover
// gas and other on-chain costs; or
// - they operate in an environment (e.g., private/test networks) with different
// economic assumptions.
defaultAmountTonString = "0.1"

Copilot uses AI. Check for mistakes.
)

// RPCChainProviderConfig holds the configuration to initialize the RPCChainProvider.
Expand Down Expand Up @@ -173,17 +176,26 @@ func (p *RPCChainProvider) Initialize(ctx context.Context) (chain.BlockChain, er
return nil, err
}

p.chain = &ton.Chain{
p.chain = buildChain(p.selector, api, tonWallet, p.config.HTTPURL)

return *p.chain, nil
}

// buildChain creates a ton.Chain with the given parameters and default TxOps amount.
func buildChain(selector uint64, api *tonlib.APIClient, tonWallet *wallet.Wallet, httpURL string) *ton.Chain {
return &ton.Chain{
ChainMetadata: ton.ChainMetadata{
Selector: p.selector,
Selector: selector,
},
Client: api,
Wallet: tonWallet,
WalletAddress: tonWallet.WalletAddress(),
URL: p.config.HTTPURL,
URL: httpURL,
TxOps: ton.TxOps{
Wallet: tonWallet,
Amount: tlb.MustFromTON(defaultAmountTonString),
},
}

return *p.chain, nil
}

// createLiteclientConnectionPool creates connection pool returning concrete type for production use
Expand Down
47 changes: 47 additions & 0 deletions chain/ton/provider/rpc_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/xssnick/tonutils-go/tlb"
"github.com/xssnick/tonutils-go/ton/wallet"

tonchain "github.com/smartcontractkit/chainlink-deployments-framework/chain/ton"
)
Expand Down Expand Up @@ -89,6 +91,22 @@ func Test_RPCChainProvider_Initialize(t *testing.T) {
assert.Equal(t, existingChain.Selector, gotChain.Selector)
}

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

p := &RPCChainProvider{
selector: 123,
config: RPCChainProviderConfig{
HTTPURL: "", // invalid - missing URL
DeployerSignerGen: PrivateKeyRandom(),
},
}

_, err := p.Initialize(t.Context())
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to validate provider config")
}

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

Expand Down Expand Up @@ -278,6 +296,35 @@ func Test_createWallet(t *testing.T) {
}
}

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

// Create a test wallet using a fixed private key
privateKey := make([]byte, 32)
for i := range privateKey {
privateKey[i] = byte(i)
}
testWallet, err := wallet.FromPrivateKeyWithOptions(nil, privateKey, wallet.V4R2, wallet.WithWorkchain(0))
require.NoError(t, err)

selector := uint64(789)
httpURL := "liteserver://publickey@localhost:8080"

chain := buildChain(selector, nil, testWallet, httpURL)

require.NotNil(t, chain)
assert.Equal(t, selector, chain.Selector)
assert.Equal(t, httpURL, chain.URL)
assert.Nil(t, chain.Client)
assert.Equal(t, testWallet, chain.Wallet)
assert.Equal(t, testWallet.WalletAddress(), chain.WalletAddress)

// Verify TxOps uses the default amount (0.1 TON)
expectedAmount := tlb.MustFromTON("0.1")
assert.Equal(t, expectedAmount, chain.TxOps.Amount)
assert.Equal(t, testWallet, chain.TxOps.Wallet)
}

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

Expand Down
9 changes: 9 additions & 0 deletions chain/ton/ton_chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@ import (
"github.com/xssnick/tonutils-go/ton"
"github.com/xssnick/tonutils-go/ton/wallet"

"github.com/xssnick/tonutils-go/tlb"

"github.com/smartcontractkit/chainlink-deployments-framework/chain/internal/common"
)

type ChainMetadata = common.ChainMetadata

// TxOps holds configuration for transaction operations.
type TxOps struct {
Wallet *wallet.Wallet // Wallet abstraction (signing, sending)
Amount tlb.Coins // Default amount for msg transfers
}

// Chain represents a TON chain.
type Chain struct {
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
TxOps TxOps // Transaction operations configuration
}
49 changes: 33 additions & 16 deletions engine/cld/legacy/cli/mcmsv2/mcms_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/smartcontractkit/mcms/sdk/evm/bindings"
"github.com/smartcontractkit/mcms/sdk/solana"
"github.com/smartcontractkit/mcms/sdk/sui"
"github.com/smartcontractkit/mcms/sdk/ton"
"github.com/smartcontractkit/mcms/types"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
Expand Down Expand Up @@ -1198,6 +1199,8 @@ func newCfgv2(lggr logger.Logger, cmd *cobra.Command, domain cldf_domain.Domain,
if err != nil {
return nil, fmt.Errorf("error creating Sui timelock converter: %w", err)
}
case chainsel.FamilyTon:
converter = ton.NewTimelockConverter(ton.DefaultSendAmount)
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The TON timelock converter initialization is not covered by tests. Add test coverage to verify the converter is created with the correct default send amount.

Copilot uses AI. Check for mistakes.
default:
return nil, fmt.Errorf("unsupported chain family %s", fam)
}
Expand Down Expand Up @@ -1504,18 +1507,18 @@ func getExecutorWithChainOverride(cfg *cfgv2, chainSelector types.ChainSelector)
if !ok {
return nil, fmt.Errorf("invalid encoder type: %T", encoder)
}
chain := cfg.blockchains.EVMChains()[uint64(chainSelector)]
c := cfg.blockchains.EVMChains()[uint64(chainSelector)]
Copy link
Contributor

Choose a reason for hiding this comment

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

what's wrong with the word chain?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's a nit thing, go variables with limited scope should have shorter names


return evm.NewExecutor(evmEncoder, chain.Client, chain.DeployerKey), nil
return evm.NewExecutor(evmEncoder, c.Client, c.DeployerKey), nil

case chainsel.FamilySolana:
solanaEncoder, ok := encoder.(*solana.Encoder)
if !ok {
return nil, fmt.Errorf("invalid encoder type: %T", encoder)
}
chain := cfg.blockchains.SolanaChains()[uint64(chainSelector)]
c := cfg.blockchains.SolanaChains()[uint64(chainSelector)]

return solana.NewExecutor(solanaEncoder, chain.Client, *chain.DeployerKey), nil
return solana.NewExecutor(solanaEncoder, c.Client, *c.DeployerKey), nil

case chainsel.FamilyAptos:
encoder, ok := encoder.(*aptos.Encoder)
Expand All @@ -1526,9 +1529,9 @@ func getExecutorWithChainOverride(cfg *cfgv2, chainSelector types.ChainSelector)
if err != nil {
return nil, fmt.Errorf("error getting aptos role from proposal: %w", err)
}
chain := cfg.blockchains.AptosChains()[uint64(chainSelector)]
c := cfg.blockchains.AptosChains()[uint64(chainSelector)]

return aptos.NewExecutor(chain.Client, chain.DeployerSigner, encoder, *role), nil
return aptos.NewExecutor(c.Client, c.DeployerSigner, encoder, *role), nil

case chainsel.FamilySui:
encoder, ok := encoder.(*sui.Encoder)
Expand All @@ -1539,10 +1542,18 @@ func getExecutorWithChainOverride(cfg *cfgv2, chainSelector types.ChainSelector)
if err != nil {
return nil, fmt.Errorf("error getting sui metadata from proposal: %w", err)
}
chain := cfg.blockchains.SuiChains()[uint64(chainSelector)]
c := cfg.blockchains.SuiChains()[uint64(chainSelector)]
entrypointEncoder := suibindings.NewCCIPEntrypointArgEncoder(metadata.RegistryObj, metadata.DeployerStateObj)

return sui.NewExecutor(chain.Client, chain.Signer, encoder, entrypointEncoder, metadata.McmsPackageID, metadata.Role, cfg.timelockProposal.ChainMetadata[chainSelector].MCMAddress, metadata.AccountObj, metadata.RegistryObj, metadata.TimelockObj)
return sui.NewExecutor(c.Client, c.Signer, encoder, entrypointEncoder, metadata.McmsPackageID, metadata.Role, cfg.timelockProposal.ChainMetadata[chainSelector].MCMAddress, metadata.AccountObj, metadata.RegistryObj, metadata.TimelockObj)
case chainsel.FamilyTon:
encoder, ok := encoder.(*ton.Encoder)
if !ok {
return nil, fmt.Errorf("error getting encoder for chain %d", cfg.chainSelector)
}
c := cfg.blockchains.TonChains()[uint64(chainSelector)]

return ton.NewExecutor(encoder, c.Client, c.TxOps.Wallet, c.TxOps.Amount)
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The newly added TON executor creation logic lacks test coverage. Consider adding tests that verify the executor is correctly created with the proper encoder, client, wallet, and amount parameters.

Copilot uses AI. Check for mistakes.
default:
return nil, fmt.Errorf("unsupported chain family %s", family)
}
Expand Down Expand Up @@ -1572,26 +1583,29 @@ func getTimelockExecutorWithChainOverride(cfg *cfgv2, chainSelector types.ChainS
var executor sdk.TimelockExecutor
switch family {
case chainsel.FamilyEVM:
chain := cfg.blockchains.EVMChains()[uint64(chainSelector)]
c := cfg.blockchains.EVMChains()[uint64(chainSelector)]

executor = evm.NewTimelockExecutor(chain.Client, chain.DeployerKey)
executor = evm.NewTimelockExecutor(c.Client, c.DeployerKey)
case chainsel.FamilySolana:
chain := cfg.blockchains.SolanaChains()[uint64(chainSelector)]
executor = solana.NewTimelockExecutor(chain.Client, *chain.DeployerKey)
c := cfg.blockchains.SolanaChains()[uint64(chainSelector)]
executor = solana.NewTimelockExecutor(c.Client, *c.DeployerKey)
case chainsel.FamilyAptos:
chain := cfg.blockchains.AptosChains()[uint64(chainSelector)]
executor = aptos.NewTimelockExecutor(chain.Client, chain.DeployerSigner)
c := cfg.blockchains.AptosChains()[uint64(chainSelector)]
executor = aptos.NewTimelockExecutor(c.Client, c.DeployerSigner)
case chainsel.FamilySui:
chain := cfg.blockchains.SuiChains()[uint64(chainSelector)]
c := cfg.blockchains.SuiChains()[uint64(chainSelector)]
metadata, err := suiMetadataFromProposal(chainSelector, cfg.timelockProposal)
if err != nil {
return nil, fmt.Errorf("error getting sui metadata from proposal: %w", err)
}
entrypointEncoder := suibindings.NewCCIPEntrypointArgEncoder(metadata.AccountObj, metadata.DeployerStateObj)
executor, err = sui.NewTimelockExecutor(chain.Client, chain.Signer, entrypointEncoder, metadata.McmsPackageID, metadata.RegistryObj, metadata.AccountObj)
executor, err = sui.NewTimelockExecutor(c.Client, c.Signer, entrypointEncoder, metadata.McmsPackageID, metadata.RegistryObj, metadata.AccountObj)
if err != nil {
return nil, fmt.Errorf("error creating sui timelock executor: %w", err)
}
case chainsel.FamilyTon:
c := cfg.blockchains.TonChains()[uint64(chainSelector)]
return ton.NewTimelockExecutor(c.Client, c.TxOps.Wallet, c.TxOps.Amount)
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The newly added TON timelock executor creation logic lacks test coverage. Consider adding tests that verify the timelock executor is correctly instantiated with the appropriate client, wallet, and amount.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The TON timelock executor creation logic lacks test coverage. Consider adding tests to verify correct executor initialization with client, wallet, and amount parameters.

Copilot uses AI. Check for mistakes.
default:
return nil, fmt.Errorf("unsupported chain family %s", family)
}
Expand Down Expand Up @@ -1646,6 +1660,9 @@ var getInspectorFromChainSelector = func(cfg cfgv2) (sdk.Inspector, error) {
if err != nil {
return nil, fmt.Errorf("error creating sui inspector: %w", err)
}
case chainsel.FamilyTon:
chain := cfg.blockchains.TonChains()[cfg.chainSelector]
inspector = ton.NewInspector(chain.Client)
Comment on lines +1664 to +1665
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The newly added TON inspector creation logic lacks test coverage. Consider adding tests that verify the inspector is correctly created from the TON chain client.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The TON inspector creation logic lacks test coverage. Add tests to ensure the inspector is properly initialized with the chain client.

Copilot uses AI. Check for mistakes.
default:
return nil, fmt.Errorf("unsupported chain family %s", fam)
}
Expand Down
9 changes: 9 additions & 0 deletions engine/cld/legacy/cli/mcmsv2/mcms_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,15 @@ func Test_timelockExecuteOptions(t *testing.T) {
require.Empty(t, opts)
},
},
{
name: "empty options for TON",
cfg: &cfgv2{chainSelector: chainsel.TON_MAINNET.Selector},
assert: func(t *testing.T, opts []mcms.Option, err error) {
t.Helper()
require.NoError(t, err)
require.Empty(t, opts)
},
},
{
name: "CallProxy option added for EVM when addresses is in DataStore",
cfg: &cfgv2{
Expand Down
Loading
Loading