Skip to content

Commit e147796

Browse files
authored
Ton Network Support (#1823)
feat: ton support
1 parent 6ba875a commit e147796

File tree

14 files changed

+2171
-271
lines changed

14 files changed

+2171
-271
lines changed

.github/workflows/framework-golden-tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ jobs:
4343
config: smoke_solana.toml
4444
count: 1
4545
timeout: 10m
46+
- name: TestTonSmoke
47+
config: smoke_ton.toml
48+
count: 1
49+
timeout: 10m
4650
- name: TestUpgrade
4751
config: upgrade.toml
4852
count: 1

book/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
- [Sui](framework/components/blockchains/sui.md)
4747
- [TRON](framework/components/blockchains/tron.md)
4848
- [ZKSync](framework/components/blockchains/zksync.md)
49+
- [Ton](framework/components/blockchains/ton.md)
4950
- [Optimism Stack]()
5051
- [Arbitrum Stack]()
5152
- [Chainlink](framework/components/chainlink.md)
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# TON Blockchain Client
2+
3+
TON (The Open Network) support in the framework utilizes MyLocalTon Docker Compose environment to provide a local TON blockchain for testing purposes.
4+
5+
## Configuration
6+
7+
```toml
8+
[blockchain_a]
9+
type = "ton"
10+
# By default uses MyLocalTon Docker Compose file
11+
docker_compose_file_url = "https://raw.githubusercontent.com/neodix42/mylocalton-docker/main/docker-compose.yaml"
12+
# Optional: Specify only core services needed for testing (useful in CI environments)
13+
ton_core_services = [
14+
"genesis",
15+
"tonhttpapi",
16+
"event-cache",
17+
"index-postgres",
18+
"index-worker",
19+
"index-api"
20+
]
21+
```
22+
23+
## Default Ports
24+
25+
The TON implementation exposes several services:
26+
27+
- TON Lite Server: Port 40004
28+
- TON HTTP API: Port 8081
29+
- TON Simple HTTP Server: Port 8000
30+
- TON Explorer: Port 8080
31+
32+
> **Note**: By default, only the lite client service is exposed externally. Other services may need additional configuration to be accessible outside the Docker network.
33+
34+
## Validator Configuration
35+
36+
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.
37+
38+
## Usage
39+
40+
```go
41+
package examples
42+
43+
import (
44+
"strings"
45+
"testing"
46+
47+
"github.com/stretchr/testify/require"
48+
"github.com/xssnick/tonutils-go/liteclient"
49+
"github.com/xssnick/tonutils-go/ton"
50+
"github.com/xssnick/tonutils-go/ton/wallet"
51+
52+
"github.com/smartcontractkit/chainlink-testing-framework/framework"
53+
"github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain"
54+
)
55+
56+
type CfgTon struct {
57+
BlockchainA *blockchain.Input `toml:"blockchain_a" validate:"required"`
58+
}
59+
60+
func TestTonSmoke(t *testing.T) {
61+
in, err := framework.Load[CfgTon](t)
62+
require.NoError(t, err)
63+
64+
bc, err := blockchain.NewBlockchainNetwork(in.BlockchainA)
65+
require.NoError(t, err)
66+
67+
var client ton.APIClientWrapped
68+
69+
t.Run("setup:connect", func(t *testing.T) {
70+
// Create a connection pool
71+
connectionPool := liteclient.NewConnectionPool()
72+
73+
// Get the network configuration from the global config URL
74+
cfg, cferr := liteclient.GetConfigFromUrl(t.Context(), fmt.Sprintf("http://%s/localhost.global.config.json", bc.Nodes[0].ExternalHTTPUrl))
75+
require.NoError(t, cferr, "Failed to get config from URL")
76+
77+
// Add connections from the config
78+
caerr := connectionPool.AddConnectionsFromConfig(t.Context(), cfg)
79+
require.NoError(t, caerr, "Failed to add connections from config")
80+
81+
// Create an API client with retry functionality
82+
client = ton.NewAPIClient(connectionPool).WithRetry()
83+
84+
t.Run("setup:faucet", func(t *testing.T) {
85+
// Create a wallet from the pre-funded high-load wallet seed
86+
rawHlWallet, err := wallet.FromSeed(client, strings.Fields(blockchain.DefaultTonHlWalletMnemonic), wallet.HighloadV2Verified)
87+
require.NoError(t, err, "failed to create highload wallet")
88+
89+
// Create a workchain -1 (masterchain) wallet
90+
mcFunderWallet, err := wallet.FromPrivateKeyWithOptions(client, rawHlWallet.PrivateKey(), wallet.HighloadV2Verified, wallet.WithWorkchain(-1))
91+
require.NoError(t, err, "failed to create highload wallet")
92+
93+
// Get subwallet with ID 42
94+
funder, err := mcFunderWallet.GetSubwallet(uint32(42))
95+
require.NoError(t, err, "failed to get highload subwallet")
96+
97+
// Verify the funder address matches the expected default
98+
require.Equal(t, funder.Address().StringRaw(), blockchain.DefaultTonHlWalletAddress, "funder address mismatch")
99+
100+
// Check the funder balance
101+
master, err := client.GetMasterchainInfo(t.Context())
102+
require.NoError(t, err, "failed to get masterchain info for funder balance check")
103+
funderBalance, err := funder.GetBalance(t.Context(), master)
104+
require.NoError(t, err, "failed to get funder balance")
105+
require.Equal(t, funderBalance.Nano().String(), "1000000000000000", "funder balance mismatch")
106+
})
107+
})
108+
}
109+
```
110+
111+
## Test Private Keys
112+
113+
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.
114+
115+
Default High-Load Wallet:
116+
```
117+
Address: -1:5ee77ced0b7ae6ef88ab3f4350d8872c64667ffbe76073455215d3cdfab3294b
118+
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
119+
```
120+
121+
## Available Pre-funded Wallets
122+
123+
MyLocalTon Docker environment comes with several pre-funded wallets that can be used for testing:
124+
125+
1. Genesis Wallet (V3R2, WalletId: 42)
126+
2. Validator Wallets (1-5) (V3R2, WalletId: 42)
127+
3. Faucet Wallet (V3R2, WalletId: 42, Balance: 1 million TON)
128+
4. Faucet Highload Wallet (Highload V2, QueryId: 0, Balance: 1 million TON)
129+
5. Basechain Faucet Wallet (V3R2, WalletId: 42, Balance: 1 million TON)
130+
6. Basechain Faucet Highload Wallet (Highload V2, QueryId: 0, Balance: 1 million TON)
131+
132+
For the complete list of addresses and mnemonics, refer to the [MyLocalTon Docker documentation](https://github.com/neodix42/mylocalton-docker).

framework/.changeset/v0.7.9.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Add TON network support

framework/components/blockchain/blockchain.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const (
1818
TypeAptos = "aptos"
1919
TypeSui = "sui"
2020
TypeTron = "tron"
21+
TypeTon = "ton"
2122
)
2223

2324
// Blockchain node family
@@ -27,12 +28,13 @@ const (
2728
FamilyAptos = "aptos"
2829
FamilySui = "sui"
2930
FamilyTron = "tron"
31+
FamilyTon = "ton"
3032
)
3133

3234
// Input is a blockchain network configuration params
3335
type Input struct {
3436
// Common EVM fields
35-
Type string `toml:"type" validate:"required,oneof=anvil geth besu solana aptos tron sui" envconfig:"net_type"`
37+
Type string `toml:"type" validate:"required,oneof=anvil geth besu solana aptos tron sui ton" envconfig:"net_type"`
3638
Image string `toml:"image"`
3739
PullImage bool `toml:"pull_image"`
3840
Port string `toml:"port"`
@@ -52,6 +54,10 @@ type Input struct {
5254
SolanaPrograms map[string]string `toml:"solana_programs"`
5355
ContainerResources *framework.ContainerResources `toml:"resources"`
5456
CustomPorts []string `toml:"custom_ports"`
57+
58+
// Ton - MyLocalTon essesntial services to run tests
59+
DockerComposeFileURL string `toml:"docker_compose_file_url"`
60+
TonCoreServices []string `toml:"ton_core_services"`
5561
}
5662

5763
// Output is a blockchain network output, ChainID and one or more nodes that forms the network
@@ -99,6 +105,8 @@ func NewBlockchainNetwork(in *Input) (*Output, error) {
99105
out, err = newTron(in)
100106
case TypeAnvilZKSync:
101107
out, err = newAnvilZksync(in)
108+
case TypeTon:
109+
out, err = newTon(in)
102110
default:
103111
return nil, fmt.Errorf("blockchain type is not supported or empty, must be 'anvil' or 'geth'")
104112
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package blockchain
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"os"
9+
"path/filepath"
10+
"time"
11+
12+
networkTypes "github.com/docker/docker/api/types/network"
13+
"github.com/docker/docker/client"
14+
"github.com/docker/go-connections/nat"
15+
"github.com/testcontainers/testcontainers-go/modules/compose"
16+
"github.com/testcontainers/testcontainers-go/wait"
17+
18+
"github.com/smartcontractkit/chainlink-testing-framework/framework"
19+
)
20+
21+
const (
22+
// default ports from mylocalton-docker
23+
DefaultTonHTTPAPIPort = "8081"
24+
DefaultTonSimpleServerPort = "8000"
25+
DefaultTonTONExplorerPort = "8080"
26+
DefaultTonLiteServerPort = "40004"
27+
28+
// NOTE: Prefunded high-load wallet from MyLocalTon pre-funded wallet, that can send up to 254 messages per 1 external message
29+
// https://docs.ton.org/v3/documentation/smart-contracts/contracts-specs/highload-wallet#highload-wallet-v2
30+
DefaultTonHlWalletAddress = "-1:5ee77ced0b7ae6ef88ab3f4350d8872c64667ffbe76073455215d3cdfab3294b"
31+
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"
32+
)
33+
34+
func defaultTon(in *Input) {
35+
if in.DockerComposeFileURL == "" {
36+
in.DockerComposeFileURL = "https://raw.githubusercontent.com/neodix42/mylocalton-docker/main/docker-compose.yaml"
37+
}
38+
// Note: in local env having all services could be useful(explorer, faucet), in CI we need only core services
39+
if os.Getenv("CI") == "true" && len(in.TonCoreServices) == 0 {
40+
// Note: mylocalton-docker's essential services, excluded explorer, restarter, faucet app,
41+
in.TonCoreServices = []string{
42+
"genesis", "tonhttpapi", "event-cache",
43+
"index-postgres", "index-worker", "index-api",
44+
}
45+
}
46+
}
47+
48+
func newTon(in *Input) (*Output, error) {
49+
defaultTon(in)
50+
containerName := framework.DefaultTCName("blockchain-node")
51+
52+
resp, err := http.Get(in.DockerComposeFileURL)
53+
if err != nil {
54+
return nil, fmt.Errorf("failed to download docker-compose file: %v", err)
55+
}
56+
defer resp.Body.Close()
57+
58+
tempDir, err := os.MkdirTemp(".", "ton-mylocalton-docker")
59+
if err != nil {
60+
return nil, fmt.Errorf("failed to create temp directory: %v", err)
61+
}
62+
63+
defer func() {
64+
// delete the folder whether it was successful or not
65+
_ = os.RemoveAll(tempDir)
66+
}()
67+
68+
composeFile := filepath.Join(tempDir, "docker-compose.yaml")
69+
file, err := os.Create(composeFile)
70+
if err != nil {
71+
return nil, fmt.Errorf("failed to create compose file: %v", err)
72+
}
73+
74+
_, err = io.Copy(file, resp.Body)
75+
if err != nil {
76+
file.Close()
77+
return nil, fmt.Errorf("failed to write compose file: %v", err)
78+
}
79+
file.Close()
80+
81+
ctx := context.Background()
82+
83+
var stack compose.ComposeStack
84+
stack, err = compose.NewDockerComposeWith(
85+
compose.WithStackFiles(composeFile),
86+
compose.StackIdentifier(containerName),
87+
)
88+
if err != nil {
89+
return nil, fmt.Errorf("failed to create compose stack: %v", err)
90+
}
91+
92+
var upOpts []compose.StackUpOption
93+
upOpts = append(upOpts, compose.Wait(true))
94+
95+
if len(in.TonCoreServices) > 0 {
96+
upOpts = append(upOpts, compose.RunServices(in.TonCoreServices...))
97+
}
98+
99+
// always wait for healthy
100+
const genesisBlockID = "E7XwFSQzNkcRepUC23J2nRpASXpnsEKmyyHYV4u/FZY="
101+
execStrat := wait.ForExec([]string{
102+
"/usr/local/bin/lite-client",
103+
"-a", "127.0.0.1:" + DefaultTonLiteServerPort,
104+
"-b", genesisBlockID,
105+
"-t", "3",
106+
"-c", "last",
107+
}).
108+
WithPollInterval(5 * time.Second).
109+
WithStartupTimeout(180 * time.Second)
110+
111+
stack = stack.
112+
WaitForService("genesis", execStrat).
113+
WaitForService("tonhttpapi", wait.ForListeningPort(DefaultTonHTTPAPIPort+"/tcp"))
114+
115+
if err := stack.Up(ctx, upOpts...); err != nil {
116+
return nil, fmt.Errorf("failed to start compose stack: %w", err)
117+
}
118+
119+
// node container is started, now we need to connect it to the network
120+
genesisCtr, _ := stack.ServiceContainer(ctx, "genesis")
121+
122+
// grab and connect to the network
123+
cli, _ := client.NewClientWithOpts(
124+
client.FromEnv,
125+
client.WithAPIVersionNegotiation(),
126+
)
127+
if err := cli.NetworkConnect(
128+
ctx,
129+
framework.DefaultNetworkName,
130+
genesisCtr.ID,
131+
&networkTypes.EndpointSettings{
132+
Aliases: []string{containerName},
133+
},
134+
); err != nil {
135+
return nil, fmt.Errorf("failed to connect to network: %v", err)
136+
}
137+
138+
// verify that the container is connected to the network
139+
inspected, err := cli.ContainerInspect(ctx, genesisCtr.ID)
140+
if err != nil {
141+
return nil, fmt.Errorf("inspect error: %w", err)
142+
}
143+
144+
ns, ok := inspected.NetworkSettings.Networks[framework.DefaultNetworkName]
145+
if !ok {
146+
return nil, fmt.Errorf("container %s is NOT on network %s", genesisCtr.ID, framework.DefaultNetworkName)
147+
}
148+
149+
fmt.Printf("✅ TON genesis '%s' is on network %s with IP %s and Aliases %v\n",
150+
genesisCtr.ID, framework.DefaultNetworkName, ns.IPAddress, ns.Aliases)
151+
152+
httpHost, _ := genesisCtr.Host(ctx)
153+
httpPort, _ := genesisCtr.MappedPort(ctx, nat.Port(fmt.Sprintf("%s/tcp", DefaultTonSimpleServerPort)))
154+
155+
return &Output{
156+
UseCache: true,
157+
ChainID: in.ChainID,
158+
Type: in.Type,
159+
Family: FamilyTon,
160+
ContainerName: containerName,
161+
// Note: in case we need 1+ validators, we need to modify the compose file
162+
Nodes: []*Node{{
163+
// Note: define if we need more access other than the global config(tonutils-go only uses liteclients defined in the config)
164+
ExternalHTTPUrl: fmt.Sprintf("%s:%s", httpHost, httpPort.Port()),
165+
InternalHTTPUrl: fmt.Sprintf("%s:%s", containerName, httpPort.Port()),
166+
}},
167+
}, nil
168+
}

0 commit comments

Comments
 (0)