Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3261478
extend cldf proposal conversion logic
huangzhen1997 Dec 9, 2025
ce292f9
fix goimport
huangzhen1997 Dec 10, 2025
76f7941
rm redundant wallet funds
huangzhen1997 Dec 10, 2025
6b03e06
remove unused fundwallet func
huangzhen1997 Dec 10, 2025
bb6622b
add test coverage
huangzhen1997 Dec 12, 2025
564f880
update test
huangzhen1997 Dec 12, 2025
fa6aa6c
Merge branch 'main' into NONEVM-3069/extend-cldf-proposal-logic-suppo…
huangzhen1997 Dec 12, 2025
580e5c7
update
huangzhen1997 Dec 12, 2025
d139579
bump version
huangzhen1997 Dec 16, 2025
e0493eb
bump mcms version
huangzhen1997 Dec 16, 2025
8c9b9ff
Merge branch 'main' into NONEVM-3069/extend-cldf-proposal-logic-suppo…
huangzhen1997 Dec 16, 2025
b703c08
fix make
huangzhen1997 Dec 16, 2025
a68db89
Merge branch 'NONEVM-3069/extend-cldf-proposal-logic-support-ton-time…
huangzhen1997 Dec 16, 2025
d440c1d
Merge branch 'main' into NONEVM-3069/extend-cldf-proposal-logic-suppo…
huangzhen1997 Jan 22, 2026
060c95e
fix make
huangzhen1997 Jan 22, 2026
4a13173
fix lint
huangzhen1997 Jan 22, 2026
5f800a9
update test
huangzhen1997 Jan 23, 2026
02d0258
fix test
huangzhen1997 Jan 23, 2026
588016c
update test
huangzhen1997 Jan 23, 2026
3d3a6ba
fix test
huangzhen1997 Jan 23, 2026
95337bb
fix test
huangzhen1997 Jan 23, 2026
c434940
remove duplicate test
huangzhen1997 Jan 23, 2026
781a16b
fix
huangzhen1997 Jan 23, 2026
284e77e
more test coverage
huangzhen1997 Jan 23, 2026
2e6c227
update test
huangzhen1997 Jan 23, 2026
06dd941
refactor for test coverage
huangzhen1997 Jan 23, 2026
4168b5e
revert
huangzhen1997 Jan 23, 2026
120f23b
update
huangzhen1997 Jan 23, 2026
d58f919
minor
huangzhen1997 Jan 24, 2026
ad23d0e
lint
huangzhen1997 Jan 24, 2026
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
61 changes: 5 additions & 56 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.25"
)

// CTFChainProviderConfig holds the configuration to initialize the CTFChainProvider.
Expand Down Expand Up @@ -126,18 +128,13 @@ 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 -129 to -133
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:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good, think we removed this as we discussed that the integration tests already funded the deployer wallet


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 Author

Choose a reason for hiding this comment

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

@copilot This is not true, it's tested in rpc_provider_test.go line 276

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

Copy link
Contributor Author

@huangzhen1997 huangzhen1997 Jan 22, 2026

Choose a reason for hiding this comment

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

I think we want to switch to the new WalletAddress() to have non-bounceable, but the underlying address data is identical, just the string rep could be different. Do you see any risk with this change ?

Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The code previously called tonWallet.Address() and now calls tonWallet.WalletAddress(). If these methods return different values or have different behaviors, this could break existing functionality. Verify that WalletAddress() returns the same address format as the previous Address() method.

Suggested change
WalletAddress: tonWallet.WalletAddress(),
WalletAddress: tonWallet.Address(),

Copilot uses AI. Check for mistakes.
URL: url,
Amount: tlb.MustFromTON(defaultTxTONAmount), // default amount for transactions
}

return *p.chain, nil
Expand Down Expand Up @@ -226,54 +223,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
3 changes: 2 additions & 1 deletion chain/ton/provider/ctf_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ func Test_CTFChainProvider_Initialize(t *testing.T) {
assert.Equal(t, tt.giveSelector, gotChain.Selector)
assert.NotEmpty(t, gotChain.Client)
assert.NotEmpty(t, gotChain.Wallet)
assert.NotEmpty(t, gotChain.WalletAddress)
require.False(t, gotChain.WalletAddress.IsAddrNone())
require.Equal(t, defaultTxTONAmount, gotChain.Amount.String())
}
})
}
Expand Down
17 changes: 12 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 Down Expand Up @@ -173,17 +174,23 @@ 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 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,
Amount: tlb.MustFromTON(defaultTxTONAmount),
}

return *p.chain, nil
}

// createLiteclientConnectionPool creates connection pool returning concrete type for production use
Expand Down
43 changes: 43 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,7 @@ import (

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

tonchain "github.com/smartcontractkit/chainlink-deployments-framework/chain/ton"
)
Expand Down Expand Up @@ -89,6 +90,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 @@ -242,6 +259,32 @@ func Test_NewRPCChainProvider(t *testing.T) {
assert.Nil(t, p.chain)
}

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)

assert.Equal(t, defaultTxTONAmount, chain.Amount.String())
}

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

Expand Down
3 changes: 3 additions & 0 deletions chain/ton/ton_chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ 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"
)

Expand All @@ -17,4 +19,5 @@ type Chain struct {
Wallet *wallet.Wallet // Wallet abstraction (signing, sending)
WalletAddress *address.Address // Address of deployer wallet
URL string // Liteserver URL
Amount tlb.Coins // Default amount for msg transfers
}
61 changes: 45 additions & 16 deletions engine/cld/legacy/cli/mcmsv2/mcms_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,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 @@ -1129,6 +1130,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)
default:
return nil, fmt.Errorf("unsupported chain family %s", fam)
}
Expand Down Expand Up @@ -1441,18 +1444,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)]

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 @@ -1463,9 +1466,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 @@ -1476,10 +1479,24 @@ 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("invalid encoder type for TON chain %d: expected *ton.Encoder, got %T", chainSelector, encoder)
}
c := cfg.blockchains.TonChains()[uint64(chainSelector)]
opts := ton.ExecutorOpts{
Encoder: encoder,
Client: c.Client,
Wallet: c.Wallet,
Amount: c.Amount,
}

return ton.NewExecutor(opts)
default:
return nil, fmt.Errorf("unsupported chain family %s", family)
}
Expand Down Expand Up @@ -1509,26 +1526,35 @@ 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.RegistryObj, 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)]
opts := ton.TimelockExecutorOpts{
Client: c.Client,
Wallet: c.Wallet,
Amount: c.Amount,
}

return ton.NewTimelockExecutor(opts)
default:
return nil, fmt.Errorf("unsupported chain family %s", family)
}
Expand Down Expand Up @@ -1583,6 +1609,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)
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