Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
53de762
feat: ton support
jadepark-dev May 2, 2025
7ddfc8d
Merge branch 'main' into jade/ton-support
jadepark-dev May 5, 2025
f40ade4
feat: pack default faucet account with blockchain
jadepark-dev May 6, 2025
59137cc
chore: remove temp file
jadepark-dev May 6, 2025
5b2c9eb
chore: clean up
jadepark-dev May 6, 2025
3d7d3d2
chore: clean up
jadepark-dev May 7, 2025
ee4bb23
chore: clean up
jadepark-dev May 7, 2025
c6a826f
chore: doc
jadepark-dev May 7, 2025
9508064
chore: add doc details
jadepark-dev May 7, 2025
0b46172
chore: clean up
jadepark-dev May 7, 2025
448c9fa
Merge branch 'main' into jade/ton-support
jadepark-dev May 7, 2025
a661100
chore: gomods tidy
jadepark-dev May 7, 2025
a68baf0
chore: rename
jadepark-dev May 7, 2025
63882c2
chore: goimports
jadepark-dev May 7, 2025
440cb30
Merge branch 'main' into jade/ton-support
jadepark-dev May 13, 2025
c354cef
fix: only expose necessary ap
jadepark-dev May 13, 2025
e3439f4
chore: update readme
jadepark-dev May 13, 2025
5d2dd53
Merge branch 'main' into jade/ton-support
jadepark-dev May 13, 2025
48fb64b
Merge branch 'main' into jade/ton-support
jadepark-dev May 13, 2025
6727c8d
feat: add input.DockerComposeFileURL
jadepark-dev May 13, 2025
4578737
Merge branch 'jade/ton-support' of github.com:smartcontractkit/chainl…
jadepark-dev May 13, 2025
a121ef7
fat: add genesis node to the network
jadepark-dev May 13, 2025
f24b834
chore: testing nodeset
jadepark-dev May 13, 2025
e21c95e
chore: clean up
jadepark-dev May 13, 2025
270a5a3
add changeset
skudasov May 13, 2025
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
4 changes: 4 additions & 0 deletions .github/workflows/framework-golden-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ jobs:
config: smoke_solana.toml
count: 1
timeout: 10m
- name: TestTonSmoke
config: smoke_ton.toml
count: 1
timeout: 10m
- name: TestUpgrade
config: upgrade.toml
count: 1
Expand Down
1 change: 1 addition & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
- [Sui](framework/components/blockchains/sui.md)
- [TRON](framework/components/blockchains/tron.md)
- [ZKSync](framework/components/blockchains/zksync.md)
- [Ton](framework/components/blockchains/ton.md)
- [Optimism Stack]()
- [Arbitrum Stack]()
- [Chainlink](framework/components/chainlink.md)
Expand Down
132 changes: 132 additions & 0 deletions book/src/framework/components/blockchains/ton.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# TON Blockchain Client

TON (The Open Network) support in the framework utilizes MyLocalTon Docker Compose environment to provide a local TON blockchain for testing purposes.

## Configuration

```toml
[blockchain_a]
type = "ton"
# By default uses MyLocalTon Docker Compose file
docker_compose_file_url = "https://raw.githubusercontent.com/neodix42/mylocalton-docker/main/docker-compose.yaml"
# Optional: Specify only core services needed for testing (useful in CI environments)
ton_core_services = [
"genesis",
"tonhttpapi",
"event-cache",
"index-postgres",
"index-worker",
"index-api"
]
```

## Default Ports

The TON implementation exposes several services:

- TON Lite Server: Port 40004
- TON HTTP API: Port 8081
- TON Simple HTTP Server: Port 8000
- TON Explorer: Port 8080

> **Note**: By default, only the lite client service is exposed externally. Other services may need additional configuration to be accessible outside the Docker network.

## Validator Configuration

By default, the MyLocalTon environment starts with only one validator enabled. If multiple validators are needed (up to 6 are supported), the Docker Compose file must be provided with modified version with corresponding service definition in toml file before starting the environment.

## Usage

```go
package examples

import (
"strings"
"testing"

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

"github.com/smartcontractkit/chainlink-testing-framework/framework"
"github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain"
)

type CfgTon struct {
BlockchainA *blockchain.Input `toml:"blockchain_a" validate:"required"`
}

func TestTonSmoke(t *testing.T) {
in, err := framework.Load[CfgTon](t)
require.NoError(t, err)

bc, err := blockchain.NewBlockchainNetwork(in.BlockchainA)
require.NoError(t, err)

var client ton.APIClientWrapped

t.Run("setup:connect", func(t *testing.T) {
// Create a connection pool
connectionPool := liteclient.NewConnectionPool()

// Get the network configuration from the global config URL
cfg, cferr := liteclient.GetConfigFromUrl(t.Context(), fmt.Sprintf("http://%s/localhost.global.config.json", bc.Nodes[0].ExternalHTTPUrl))
require.NoError(t, cferr, "Failed to get config from URL")

// Add connections from the config
caerr := connectionPool.AddConnectionsFromConfig(t.Context(), cfg)
require.NoError(t, caerr, "Failed to add connections from config")

// Create an API client with retry functionality
client = ton.NewAPIClient(connectionPool).WithRetry()

t.Run("setup:faucet", func(t *testing.T) {
// Create a wallet from the pre-funded high-load wallet seed
rawHlWallet, err := wallet.FromSeed(client, strings.Fields(blockchain.DefaultTonHlWalletMnemonic), wallet.HighloadV2Verified)
require.NoError(t, err, "failed to create highload wallet")

// Create a workchain -1 (masterchain) wallet
mcFunderWallet, err := wallet.FromPrivateKeyWithOptions(client, rawHlWallet.PrivateKey(), wallet.HighloadV2Verified, wallet.WithWorkchain(-1))
require.NoError(t, err, "failed to create highload wallet")

// Get subwallet with ID 42
funder, err := mcFunderWallet.GetSubwallet(uint32(42))
require.NoError(t, err, "failed to get highload subwallet")

// Verify the funder address matches the expected default
require.Equal(t, funder.Address().StringRaw(), blockchain.DefaultTonHlWalletAddress, "funder address mismatch")

// Check the funder balance
master, err := client.GetMasterchainInfo(t.Context())
require.NoError(t, err, "failed to get masterchain info for funder balance check")
funderBalance, err := funder.GetBalance(t.Context(), master)
require.NoError(t, err, "failed to get funder balance")
require.Equal(t, funderBalance.Nano().String(), "1000000000000000", "funder balance mismatch")
})
})
}
```

## Test Private Keys

The framework includes a pre-funded high-load wallet for testing purposes. This wallet type can send up to 254 messages per 1 external message, making it efficient for test scenarios.

Default High-Load Wallet:
```
Address: -1:5ee77ced0b7ae6ef88ab3f4350d8872c64667ffbe76073455215d3cdfab3294b
Mnemonic: twenty unfair stay entry during please water april fabric morning length lumber style tomorrow melody similar forum width ride render void rather custom coin
```

## Available Pre-funded Wallets

MyLocalTon Docker environment comes with several pre-funded wallets that can be used for testing:

1. Genesis Wallet (V3R2, WalletId: 42)
2. Validator Wallets (1-5) (V3R2, WalletId: 42)
3. Faucet Wallet (V3R2, WalletId: 42, Balance: 1 million TON)
4. Faucet Highload Wallet (Highload V2, QueryId: 0, Balance: 1 million TON)
5. Basechain Faucet Wallet (V3R2, WalletId: 42, Balance: 1 million TON)
6. Basechain Faucet Highload Wallet (Highload V2, QueryId: 0, Balance: 1 million TON)

For the complete list of addresses and mnemonics, refer to the [MyLocalTon Docker documentation](https://github.com/neodix42/mylocalton-docker).
1 change: 1 addition & 0 deletions framework/.changeset/v0.7.9.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add TON network support
10 changes: 9 additions & 1 deletion framework/components/blockchain/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
TypeAptos = "aptos"
TypeSui = "sui"
TypeTron = "tron"
TypeTon = "ton"
)

// Blockchain node family
Expand All @@ -27,12 +28,13 @@ const (
FamilyAptos = "aptos"
FamilySui = "sui"
FamilyTron = "tron"
FamilyTon = "ton"
)

// Input is a blockchain network configuration params
type Input struct {
// Common EVM fields
Type string `toml:"type" validate:"required,oneof=anvil geth besu solana aptos tron sui" envconfig:"net_type"`
Type string `toml:"type" validate:"required,oneof=anvil geth besu solana aptos tron sui ton" envconfig:"net_type"`
Image string `toml:"image"`
PullImage bool `toml:"pull_image"`
Port string `toml:"port"`
Expand All @@ -52,6 +54,10 @@ type Input struct {
SolanaPrograms map[string]string `toml:"solana_programs"`
ContainerResources *framework.ContainerResources `toml:"resources"`
CustomPorts []string `toml:"custom_ports"`

// Ton - MyLocalTon essesntial services to run tests
DockerComposeFileURL string `toml:"docker_compose_file_url"`
TonCoreServices []string `toml:"ton_core_services"`
}

// Output is a blockchain network output, ChainID and one or more nodes that forms the network
Expand Down Expand Up @@ -99,6 +105,8 @@ func NewBlockchainNetwork(in *Input) (*Output, error) {
out, err = newTron(in)
case TypeAnvilZKSync:
out, err = newAnvilZksync(in)
case TypeTon:
out, err = newTon(in)
default:
return nil, fmt.Errorf("blockchain type is not supported or empty, must be 'anvil' or 'geth'")
}
Expand Down
168 changes: 168 additions & 0 deletions framework/components/blockchain/ton.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package blockchain

import (
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"

networkTypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"github.com/testcontainers/testcontainers-go/modules/compose"
"github.com/testcontainers/testcontainers-go/wait"

"github.com/smartcontractkit/chainlink-testing-framework/framework"
)

const (
// default ports from mylocalton-docker
DefaultTonHTTPAPIPort = "8081"
DefaultTonSimpleServerPort = "8000"
DefaultTonTONExplorerPort = "8080"
DefaultTonLiteServerPort = "40004"

// NOTE: Prefunded high-load wallet from MyLocalTon pre-funded wallet, that can send up to 254 messages per 1 external message
// https://docs.ton.org/v3/documentation/smart-contracts/contracts-specs/highload-wallet#highload-wallet-v2
DefaultTonHlWalletAddress = "-1:5ee77ced0b7ae6ef88ab3f4350d8872c64667ffbe76073455215d3cdfab3294b"
DefaultTonHlWalletMnemonic = "twenty unfair stay entry during please water april fabric morning length lumber style tomorrow melody similar forum width ride render void rather custom coin"
)

func defaultTon(in *Input) {
if in.DockerComposeFileURL == "" {
in.DockerComposeFileURL = "https://raw.githubusercontent.com/neodix42/mylocalton-docker/main/docker-compose.yaml"
}
// Note: in local env having all services could be useful(explorer, faucet), in CI we need only core services
if os.Getenv("CI") == "true" && len(in.TonCoreServices) == 0 {
// Note: mylocalton-docker's essential services, excluded explorer, restarter, faucet app,
in.TonCoreServices = []string{
"genesis", "tonhttpapi", "event-cache",
"index-postgres", "index-worker", "index-api",
}
}
}

func newTon(in *Input) (*Output, error) {
defaultTon(in)
containerName := framework.DefaultTCName("blockchain-node")

resp, err := http.Get(in.DockerComposeFileURL)
if err != nil {
return nil, fmt.Errorf("failed to download docker-compose file: %v", err)
}
defer resp.Body.Close()

tempDir, err := os.MkdirTemp(".", "ton-mylocalton-docker")
if err != nil {
return nil, fmt.Errorf("failed to create temp directory: %v", err)
}

defer func() {
// delete the folder whether it was successful or not
_ = os.RemoveAll(tempDir)
}()

composeFile := filepath.Join(tempDir, "docker-compose.yaml")
file, err := os.Create(composeFile)
if err != nil {
return nil, fmt.Errorf("failed to create compose file: %v", err)
}

_, err = io.Copy(file, resp.Body)
if err != nil {
file.Close()
return nil, fmt.Errorf("failed to write compose file: %v", err)
}
file.Close()

ctx := context.Background()

var stack compose.ComposeStack
stack, err = compose.NewDockerComposeWith(
compose.WithStackFiles(composeFile),
compose.StackIdentifier(containerName),
)
if err != nil {
return nil, fmt.Errorf("failed to create compose stack: %v", err)
}

var upOpts []compose.StackUpOption
upOpts = append(upOpts, compose.Wait(true))

if len(in.TonCoreServices) > 0 {
upOpts = append(upOpts, compose.RunServices(in.TonCoreServices...))
}

// always wait for healthy
const genesisBlockID = "E7XwFSQzNkcRepUC23J2nRpASXpnsEKmyyHYV4u/FZY="
execStrat := wait.ForExec([]string{
"/usr/local/bin/lite-client",
"-a", "127.0.0.1:" + DefaultTonLiteServerPort,
"-b", genesisBlockID,
"-t", "3",
"-c", "last",
}).
WithPollInterval(5 * time.Second).
WithStartupTimeout(180 * time.Second)

stack = stack.
WaitForService("genesis", execStrat).
WaitForService("tonhttpapi", wait.ForListeningPort(DefaultTonHTTPAPIPort+"/tcp"))

if err := stack.Up(ctx, upOpts...); err != nil {
return nil, fmt.Errorf("failed to start compose stack: %w", err)
}

// node container is started, now we need to connect it to the network
genesisCtr, _ := stack.ServiceContainer(ctx, "genesis")

// grab and connect to the network
cli, _ := client.NewClientWithOpts(
client.FromEnv,
client.WithAPIVersionNegotiation(),
)
if err := cli.NetworkConnect(
ctx,
framework.DefaultNetworkName,
genesisCtr.ID,
&networkTypes.EndpointSettings{
Aliases: []string{containerName},
},
); err != nil {
return nil, fmt.Errorf("failed to connect to network: %v", err)
}

// verify that the container is connected to the network
inspected, err := cli.ContainerInspect(ctx, genesisCtr.ID)
if err != nil {
return nil, fmt.Errorf("inspect error: %w", err)
}

ns, ok := inspected.NetworkSettings.Networks[framework.DefaultNetworkName]
if !ok {
return nil, fmt.Errorf("container %s is NOT on network %s", genesisCtr.ID, framework.DefaultNetworkName)
}

fmt.Printf("✅ TON genesis '%s' is on network %s with IP %s and Aliases %v\n",
genesisCtr.ID, framework.DefaultNetworkName, ns.IPAddress, ns.Aliases)

httpHost, _ := genesisCtr.Host(ctx)
httpPort, _ := genesisCtr.MappedPort(ctx, nat.Port(fmt.Sprintf("%s/tcp", DefaultTonSimpleServerPort)))

return &Output{
UseCache: true,
ChainID: in.ChainID,
Type: in.Type,
Family: FamilyTon,
ContainerName: containerName,
// Note: in case we need 1+ validators, we need to modify the compose file
Nodes: []*Node{{
// Note: define if we need more access other than the global config(tonutils-go only uses liteclients defined in the config)
ExternalHTTPUrl: fmt.Sprintf("%s:%s", httpHost, httpPort.Port()),
InternalHTTPUrl: fmt.Sprintf("%s:%s", containerName, httpPort.Port()),
}},
}, nil
}
Loading
Loading