diff --git a/.github/workflows/framework-golden-tests.yml b/.github/workflows/framework-golden-tests.yml index 505e55626..1ae744165 100644 --- a/.github/workflows/framework-golden-tests.yml +++ b/.github/workflows/framework-golden-tests.yml @@ -30,6 +30,10 @@ jobs: config: smoke_aptos.toml count: 1 timeout: 10m + - name: TestTRONSmoke + config: smoke_tron.toml + count: 1 + timeout: 10m - name: TestSolanaSmoke config: smoke_solana.toml count: 1 diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index f102715b2..e8080913c 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -39,6 +39,7 @@ - [Solana](framework/components/blockchains/solana.md) - [Aptos](framework/components/blockchains/aptos.md) - [Sui](framework/components/blockchains/sui.md) + - [TRON](framework/components/blockchains/tron.md) - [Optimism Stack]() - [Arbitrum Stack]() - [Chainlink](framework/components/chainlink.md) diff --git a/book/src/framework/components/blockchains/tron.md b/book/src/framework/components/blockchains/tron.md new file mode 100644 index 000000000..a1bf16962 --- /dev/null +++ b/book/src/framework/components/blockchains/tron.md @@ -0,0 +1,71 @@ +# TRON Blockchain Client + +## Configuration +```toml +[blockchain_a] + type = "tron" + # image = "tronbox/tre" is default image +``` +Default port is `9090` + +## Usage +```golang +package examples + +import ( + "github.com/smartcontractkit/chainlink-testing-framework/framework" + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" + "github.com/stretchr/testify/require" + "testing" +) + +type CfgTron struct { + BlockchainA *blockchain.Input `toml:"blockchain_a" validate:"required"` +} + +func TestTRONSmoke(t *testing.T) { + in, err := framework.Load[CfgTron](t) + require.NoError(t, err) + + bc, err := blockchain.NewBlockchainNetwork(in.BlockchainA) + require.NoError(t, err) + + // all private keys are funded + _ = blockchain.TRONAccounts.PrivateKeys[0] + + t.Run("test something", func(t *testing.T) { + // use internal URL to connect Chainlink nodes + _ = bc.Nodes[0].DockerInternalHTTPUrl + // use host URL to interact + _ = bc.Nodes[0].HostHTTPUrl + + // use bc.Nodes[0].HostHTTPUrl + "/wallet" to access full node + // use bc.Nodes[0].HostHTTPUrl + "/walletsolidity" to access Solidity node + }) +} +``` + +## More info + +Follow the [guide](https://developers.tron.network/reference/tronbox-quickstart) if you want to work with `TRONBox` environment via JS + +## Golang HTTP Client + +TRON doesn't have any library to interact with it in `Golang` but we maintain our internal fork [here](https://github.com/smartcontractkit/chainlink-internal-integrations/tree/69e35041cdea0bc38ddf642aa93fd3cc3fb5d0d9/tron/relayer/gotron-sdk) + +Check TRON [HTTP API](https://tronprotocol.github.io/documentation-en/api/http/) + +Full node is on `:9090/wallet` +``` +curl -X POST http://127.0.0.1:9090/wallet/createtransaction -d '{ + "owner_address": "TRGhNNfnmgLegT4zHNjEqDSADjgmnHvubJ", + "to_address": "TJCnKsPa7y5okkXvQAidZBzqx3QyQ6sxMW", + "amount": 1000000, + "visible": true +}' +``` + +Solidity node is on `:9090/walletsolidity` +``` +curl -X POST http://127.0.0.1:9090/walletsolidity/getaccount -d '{"address": "41E552F6487585C2B58BC2C9BB4492BC1F17132CD0"}' +``` diff --git a/framework/.changeset/v0.4.5.md b/framework/.changeset/v0.4.5.md new file mode 100644 index 000000000..28d7b17de --- /dev/null +++ b/framework/.changeset/v0.4.5.md @@ -0,0 +1 @@ +- Add TRON network support \ No newline at end of file diff --git a/framework/cmd/observability/compose/docker-compose.yaml b/framework/cmd/observability/compose/docker-compose.yaml index b588b0e62..bf19eb44d 100644 --- a/framework/cmd/observability/compose/docker-compose.yaml +++ b/framework/cmd/observability/compose/docker-compose.yaml @@ -19,7 +19,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock - ./conf/prometheus.yml:/etc/prometheus/prometheus.yml ports: - - "9090:9090" + - "9999:9090" loki: image: grafana/loki:2.5.0 volumes: diff --git a/framework/components/blockchain/blockchain.go b/framework/components/blockchain/blockchain.go index 77004b0e9..87629d447 100644 --- a/framework/components/blockchain/blockchain.go +++ b/framework/components/blockchain/blockchain.go @@ -8,7 +8,7 @@ import ( // 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 sui" envconfig:"net_type"` + Type string `toml:"type" validate:"required,oneof=anvil geth besu solana aptos tron sui" envconfig:"net_type"` Image string `toml:"image"` PullImage bool `toml:"pull_image"` Port string `toml:"port"` @@ -71,6 +71,8 @@ func NewBlockchainNetwork(in *Input) (*Output, error) { out, err = newAptos(in) case "sui": out, err = newSui(in) + case "tron": + out, err = newTron(in) default: return nil, fmt.Errorf("blockchain type is not supported or empty, must be 'anvil' or 'geth'") } diff --git a/framework/components/blockchain/tron.go b/framework/components/blockchain/tron.go new file mode 100644 index 000000000..0d3bce9ec --- /dev/null +++ b/framework/components/blockchain/tron.go @@ -0,0 +1,124 @@ +package blockchain + +import ( + "context" + "encoding/json" + "fmt" + "github.com/docker/docker/api/types/container" + "github.com/smartcontractkit/chainlink-testing-framework/framework" + "os" + "time" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +type Accounts struct { + HDPath string `json:"hdPath"` + Mnemonic string `json:"mnemonic"` + PrivateKeys []string `json:"privateKeys"` + More []string `json:"more"` +} + +var TRONAccounts = Accounts{ + HDPath: "m/44'/195'/0'/0/", + Mnemonic: "resemble birth wool happy sun burger fatal trumpet globe purity health ritual", + PrivateKeys: []string{ + "932a39242805a1b1095638027f26af9664d1d5bf8ab3b7527ee75e7efb2946dd", + "1c17c9c049d36cde7e5ea99df6c86e0474b04f0e258ab619a1e674f397a17152", + "458130a239671674746582184711a6f8d633355df1a491b9f3b323576134c2e9", + "2676fd1427968e07feaa9aff967d4ba7607c5497c499968c098d0517cd75cfbb", + "d26b24a691ff2b03ee6ab65bf164def216f73574996b9ca6299c43a9a63767ac", + "55df6adf3d081944dbe4688205d94f236fb4427ac44f3a286a96d47db0860667", + "8a9a60ddd722a40753c2a38edd6b6fa38e806d681c9b08a520ba4912e62b6458", + "75eb182fb623acf5e53d9885c4e8578f2530533a96c753481cc4277ecc6022de", + "6c4b22b1d9d68ef7a8ecd151cd4ffdd4ecc2a7b3a3f8a9f9f9bbdbcef6671f10", + "e578d66453cb41b6c923b9caa91c375a0545eeb171ccafc60b46fa834ce5c200", + }, + // should not be empty, otherwise TRE will panic + More: []string{}, +} + +const ( + DefaultTronPort = "9090" +) + +func defaultTron(in *Input) { + if in.Image == "" { + in.Image = "tronbox/tre" + } + if in.Port == "" { + in.Port = DefaultTronPort + } +} + +func newTron(in *Input) (*Output, error) { + defaultTron(in) + ctx := context.Background() + + containerName := framework.DefaultTCName("blockchain-node") + bindPort := fmt.Sprintf("%s/tcp", in.Port) + + accounts, err := os.CreateTemp("", "accounts.json") + if err != nil { + return nil, err + } + accountsData, err := json.Marshal(TRONAccounts) + if err != nil { + return nil, err + } + + _, err = accounts.WriteString(string(accountsData)) + if err != nil { + return nil, err + } + + req := testcontainers.ContainerRequest{ + AlwaysPullImage: in.PullImage, + Image: in.Image, + Name: containerName, + ExposedPorts: []string{bindPort}, + Networks: []string{framework.DefaultNetworkName}, + NetworkAliases: map[string][]string{ + framework.DefaultNetworkName: {containerName}, + }, + Labels: framework.DefaultTCLabels(), + HostConfigModifier: func(h *container.HostConfig) { + h.PortBindings = framework.MapTheSamePort(bindPort) + }, + WaitingFor: wait.ForLog("Mnemonic").WithPollInterval(200 * time.Millisecond).WithStartupTimeout(1 * time.Minute), + Files: []testcontainers.ContainerFile{ + { + HostFilePath: accounts.Name(), + ContainerFilePath: "/config/accounts.json", + FileMode: 0644, + }, + }, + } + + c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + return nil, err + } + + host, err := c.Host(ctx) + if err != nil { + return nil, err + } + + return &Output{ + UseCache: true, + ChainID: in.ChainID, + Family: "tron", + ContainerName: containerName, + Nodes: []*Node{ + { + HostHTTPUrl: fmt.Sprintf("http://%s:%s", host, in.Port), + DockerInternalHTTPUrl: fmt.Sprintf("http://%s:%s", containerName, in.Port), + }, + }, + }, nil +} diff --git a/framework/components/blockchain/verify.go b/framework/components/blockchain/verify.go index 85d2f4fce..9120191ac 100644 --- a/framework/components/blockchain/verify.go +++ b/framework/components/blockchain/verify.go @@ -6,13 +6,13 @@ import ( ) // VerifyContract wraps the forge verify-contract command. -func VerifyContract(out *Output, address, foundryDir, contractFile, contractName string) error { +func VerifyContract(out *Output, address, foundryDir, contractFile, contractName, compilerVersion string) error { args := []string{ "verify-contract", "--rpc-url", out.Nodes[0].HostHTTPUrl, "--chain-id", out.ChainID, - "--compiler-version=0.8.24", + fmt.Sprintf("--compiler-version=%s", compilerVersion), address, fmt.Sprintf("%s:%s", contractFile, contractName), "--verifier", "blockscout", diff --git a/framework/examples/myproject/go.mod b/framework/examples/myproject/go.mod index 43e32ff35..520e7afd0 100644 --- a/framework/examples/myproject/go.mod +++ b/framework/examples/myproject/go.mod @@ -14,7 +14,7 @@ require ( github.com/blocto/solana-go-sdk v1.30.0 github.com/ethereum/go-ethereum v1.14.11 github.com/go-resty/resty/v2 v2.15.3 - github.com/smartcontractkit/chainlink-testing-framework/framework v0.4.1 + github.com/smartcontractkit/chainlink-testing-framework/framework v0.4.4 github.com/smartcontractkit/chainlink-testing-framework/seth v1.50.10 github.com/smartcontractkit/chainlink-testing-framework/wasp v1.50.2 github.com/stretchr/testify v1.10.0 diff --git a/framework/examples/myproject/smoke_tron.toml b/framework/examples/myproject/smoke_tron.toml new file mode 100644 index 000000000..38443df69 --- /dev/null +++ b/framework/examples/myproject/smoke_tron.toml @@ -0,0 +1,3 @@ +[blockchain_a] + type = "tron" + image = "tronbox/tre" diff --git a/framework/examples/myproject/smoke_tron_test.go b/framework/examples/myproject/smoke_tron_test.go new file mode 100644 index 000000000..099033956 --- /dev/null +++ b/framework/examples/myproject/smoke_tron_test.go @@ -0,0 +1,33 @@ +package examples + +import ( + "github.com/smartcontractkit/chainlink-testing-framework/framework" + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" + "github.com/stretchr/testify/require" + "testing" +) + +type CfgTron struct { + BlockchainA *blockchain.Input `toml:"blockchain_a" validate:"required"` +} + +func TestTRONSmoke(t *testing.T) { + in, err := framework.Load[CfgTron](t) + require.NoError(t, err) + + bc, err := blockchain.NewBlockchainNetwork(in.BlockchainA) + require.NoError(t, err) + + // all private keys are funded + _ = blockchain.TRONAccounts.PrivateKeys[0] + + t.Run("test something", func(t *testing.T) { + // use internal URL to connect Chainlink nodes + _ = bc.Nodes[0].DockerInternalHTTPUrl + // use host URL to interact + _ = bc.Nodes[0].HostHTTPUrl + + // use bc.Nodes[0].HostHTTPUrl + "/wallet" to access full node + // use bc.Nodes[0].HostHTTPUrl + "/walletsolidity" to access Solidity node + }) +} diff --git a/framework/examples/myproject/verify_test.go b/framework/examples/myproject/verify_test.go index a148d6851..b262707a9 100644 --- a/framework/examples/myproject/verify_test.go +++ b/framework/examples/myproject/verify_test.go @@ -39,6 +39,7 @@ func TestVerify(t *testing.T) { "example_components/onchain", "src/Counter.sol", "Counter", + "0.8.13", ) require.NoError(t, err) })