diff --git a/chain/ton/provider/ctf_provider.go b/chain/ton/provider/ctf_provider.go index 85adf724..df00c6b0 100644 --- a/chain/ton/provider/ctf_provider.go +++ b/chain/ton/provider/ctf_provider.go @@ -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" @@ -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. @@ -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) - } - p.chain = &cldf_ton.Chain{ ChainMetadata: cldf_ton.ChainMetadata{Selector: p.selector}, Client: nodeClient, Wallet: tonWallet, - WalletAddress: tonWallet.Address(), + WalletAddress: tonWallet.WalletAddress(), URL: url, + Amount: tlb.MustFromTON(defaultTxTONAmount), // default amount for transactions } return *p.chain, nil @@ -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 diff --git a/chain/ton/provider/ctf_provider_test.go b/chain/ton/provider/ctf_provider_test.go index 3be4936c..1d1671b8 100644 --- a/chain/ton/provider/ctf_provider_test.go +++ b/chain/ton/provider/ctf_provider_test.go @@ -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()) } }) } diff --git a/chain/ton/provider/rpc_provider.go b/chain/ton/provider/rpc_provider.go index b36c936f..07a1dc2b 100644 --- a/chain/ton/provider/rpc_provider.go +++ b/chain/ton/provider/rpc_provider.go @@ -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" @@ -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 diff --git a/chain/ton/provider/rpc_provider_test.go b/chain/ton/provider/rpc_provider_test.go index add5c34b..08f3570f 100644 --- a/chain/ton/provider/rpc_provider_test.go +++ b/chain/ton/provider/rpc_provider_test.go @@ -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" ) @@ -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() @@ -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() diff --git a/chain/ton/ton_chain.go b/chain/ton/ton_chain.go index 70a6d069..e74367ab 100644 --- a/chain/ton/ton_chain.go +++ b/chain/ton/ton_chain.go @@ -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" ) @@ -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 } diff --git a/engine/cld/legacy/cli/mcmsv2/mcms_v2.go b/engine/cld/legacy/cli/mcmsv2/mcms_v2.go index ef527bd9..49e84e4d 100644 --- a/engine/cld/legacy/cli/mcmsv2/mcms_v2.go +++ b/engine/cld/legacy/cli/mcmsv2/mcms_v2.go @@ -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" @@ -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) } @@ -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) @@ -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) @@ -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) } @@ -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) } @@ -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) } diff --git a/engine/cld/legacy/cli/mcmsv2/mcms_v2_test.go b/engine/cld/legacy/cli/mcmsv2/mcms_v2_test.go index ff2beada..5a7c7bfc 100644 --- a/engine/cld/legacy/cli/mcmsv2/mcms_v2_test.go +++ b/engine/cld/legacy/cli/mcmsv2/mcms_v2_test.go @@ -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{