Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 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
8dcbd7a
remove amount from chain
huangzhen1997 Jan 27, 2026
022e0df
Merge branch 'main' into NONEVM-3069/extend-cldf-proposal-logic-suppo…
huangzhen1997 Jan 27, 2026
075de4e
minor
huangzhen1997 Jan 27, 2026
e5efbd0
Merge branch 'NONEVM-3069/extend-cldf-proposal-logic-support-ton-time…
huangzhen1997 Jan 27, 2026
b25e6bc
rm buildChain
huangzhen1997 Jan 27, 2026
42d488c
fix lint
huangzhen1997 Jan 27, 2026
ced4bfd
Merge branch 'main' into NONEVM-3069/extend-cldf-proposal-logic-suppo…
huangzhen1997 Jan 27, 2026
62b9727
Merge branch 'main' into NONEVM-3069/extend-cldf-proposal-logic-suppo…
huangzhen1997 Jan 29, 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
58 changes: 1 addition & 57 deletions chain/ton/provider/ctf_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +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 Down Expand Up @@ -126,17 +124,11 @@ 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,
}

Expand Down Expand Up @@ -226,54 +218,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
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 @@ -89,7 +89,7 @@ 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())
}
})
}
Expand Down
16 changes: 16 additions & 0 deletions chain/ton/provider/rpc_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,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
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

The comment says 'invalid - missing URL' but an empty string is technically a valid Go string value. Consider clarifying that this is testing validation logic for an empty/missing URL configuration.

Suggested change
HTTPURL: "", // invalid - missing URL
HTTPURL: "", // invalid: empty URL to test validation of missing/empty configuration

Copilot uses AI. Check for mistakes.
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
67 changes: 51 additions & 16 deletions engine/cld/legacy/cli/mcmsv2/mcms_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ 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"
"github.com/xssnick/tonutils-go/tlb"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"

Expand All @@ -56,6 +58,11 @@ const (
indexFlag = "index"
forkFlag = "fork"
defaultProposalValidity = 72 * time.Hour

// defaultTONExecutorAmount is the default amount of TON for MCMS/Timelock executor transactions.
// This is a static estimate that should be sufficient for most operations.
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

The comment describes this as a 'static estimate' but doesn't explain why 0.1 TON was chosen or what factors influence whether this amount is sufficient. Consider adding context about what operations this covers and approximate cost considerations.

Suggested change
// This is a static estimate that should be sufficient for most operations.
// This is a conservative static estimate intended to cover the gas and storage fees for a
// typical MCMS/Timelock executor flow on TON (e.g. submitting and executing a proposal with
// a small to medium number of actions) under current mainnet fee conditions, with an extra
// safety buffer. The 0.1 TON value was chosen based on observed costs for common operations
// plus headroom to account for moderate message size growth and fee fluctuations.
//
// IMPORTANT: This may be insufficient for unusually large/complex proposals, for networks
// or environments with significantly higher fees, or if TON fee dynamics change over time.
// Operators who routinely submit high-complexity proposals should consider overriding this
// default with a higher value until a dynamic gas estimation mechanism is available.
//

Copilot uses AI. Check for mistakes.
// TODO: Replace with gas estimator component when implemented. NONEVM-3482
defaultTONExecutorAmount = "0.1"
)

const mcmsv2DeprecatedWarning = `
Expand Down Expand Up @@ -1129,6 +1136,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(tlb.MustFromTON(defaultTONExecutorAmount))
Copy link

Copilot AI Jan 27, 2026

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 lacks test coverage. Consider adding a test case similar to the existing Sui test case at line 1136-1138 to verify the converter is created correctly for TON chains.

Copilot uses AI. Check for mistakes.
default:
return nil, fmt.Errorf("unsupported chain family %s", fam)
}
Expand Down Expand Up @@ -1441,18 +1450,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 +1472,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 +1485,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)
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

The error message for TON encoder type validation is more detailed than the equivalent error messages for EVM (line 1451), Solana (line 1460), Aptos (line 1468), and Sui (line 1481) chains, which only say 'invalid encoder type: %T'. Consider making error messages consistent across all chain families.

Suggested change
return nil, fmt.Errorf("invalid encoder type for TON chain %d: expected *ton.Encoder, got %T", chainSelector, encoder)
return nil, fmt.Errorf("invalid encoder type: %T", encoder)

Copilot uses AI. Check for mistakes.
}
c := cfg.blockchains.TonChains()[uint64(chainSelector)]
opts := ton.ExecutorOpts{
Encoder: encoder,
Client: c.Client,
Wallet: c.Wallet,
Amount: tlb.MustFromTON(defaultTONExecutorAmount),
}

return ton.NewExecutor(opts)
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The TON executor creation in getExecutorWithChainOverride lacks test coverage. Add test cases to verify the executor is created correctly with the expected options, including the Amount field set to defaultTONExecutorAmount.

Copilot uses AI. Check for mistakes.
default:
return nil, fmt.Errorf("unsupported chain family %s", family)
}
Expand Down Expand Up @@ -1509,26 +1532,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: tlb.MustFromTON(defaultTONExecutorAmount),
}

return ton.NewTimelockExecutor(opts)
Copy link

Copilot AI Jan 27, 2026

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 in getTimelockExecutorWithChainOverride lacks test coverage. Add test cases to verify the timelock executor is created correctly with the expected options, similar to other chain family test cases.

Copilot uses AI. Check for mistakes.
default:
return nil, fmt.Errorf("unsupported chain family %s", family)
}
Expand Down Expand Up @@ -1583,6 +1615,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 +1619 to +1620
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

Variable 'chain' should be renamed to 'c' for consistency with the naming pattern used in the same function for other chain families (lines 1535, 1539, 1542, 1545).

Suggested change
chain := cfg.blockchains.TonChains()[cfg.chainSelector]
inspector = ton.NewInspector(chain.Client)
c := cfg.blockchains.TonChains()[cfg.chainSelector]
inspector = ton.NewInspector(c.Client)

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The TON inspector creation in getInspectorFromChainSelector lacks test coverage. Add test cases to verify the inspector is created correctly for TON chains.

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