Skip to content

Commit c797129

Browse files
CLDF Tron Package (#219)
**This PR adds support for the Tron blockchain to the Chainlink Deployments Framework.** ## Key changes: - Introduces a new `chain/tron` module with implementation and integration for Tron chains. - Adds Tron-specific keystore logic (`chain/tron/keystore`) for account management and signing. - Provides utilities for Tron account generation (`chain/tron/provider/account_generator.go`) supporting default generator for CTF, random and private-key-based creation. - Implements a Tron RPC provider (`chain/tron/provider/rpc_provider.go`) to interact with Tron nodes, including support for deploying and triggering smart contracts. - Implements a Tron CTF provider (`chain/tron/provider/ctf_provider.go`) to interact with Tron nodes in testing environment, including support for deploying and triggering smart contracts. - Adds a Tron RPC client wrapper (`chain/tron/provider/rpcclient/client.go`) with transaction signing, broadcasting, and confirmation logic. - Updates core chain management (`chain/blockchain.go`) and tests to include Tron chains, selectors, and filtering. - Adds comprehensive unit and integration tests for all new Tron-related modules. - Updates dependencies and adds `go.mod`/`go.sum` entries for Tron SDKs and required packages. - Use CTF to support running local Tron test nodes for integration testing. ## Purpose: This enables CLD to deploy, manage, and interact with smart contracts on the Tron blockchain in the same unified way as other supported chains (EVM, Solana, Aptos, Sui, TON). --------- Co-authored-by: Graham Goh <graham.goh@smartcontract.com>
1 parent ad6bf23 commit c797129

18 files changed

+2475
-88
lines changed

.changeset/proud-eagles-see.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": minor
3+
---
4+
5+
feat: introduce tron chain provider and ctf provider

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ node_modules/
3333

3434
# OS generated files
3535
.DS_Store
36+
37+
# Tron wallet folder generated
38+
*/tron/provider/wallet/

chain/blockchain.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ import (
1111
"github.com/smartcontractkit/chainlink-deployments-framework/chain/solana"
1212
"github.com/smartcontractkit/chainlink-deployments-framework/chain/sui"
1313
"github.com/smartcontractkit/chainlink-deployments-framework/chain/ton"
14+
"github.com/smartcontractkit/chainlink-deployments-framework/chain/tron"
1415
)
1516

1617
var _ BlockChain = evm.Chain{}
1718
var _ BlockChain = solana.Chain{}
1819
var _ BlockChain = aptos.Chain{}
1920
var _ BlockChain = sui.Chain{}
2021
var _ BlockChain = ton.Chain{}
22+
var _ BlockChain = tron.Chain{}
2123

2224
// BlockChain is an interface that represents a chain.
2325
// A chain can be an EVM chain, Solana chain Aptos chain or others.
@@ -116,6 +118,11 @@ func (b BlockChains) TonChains() map[uint64]ton.Chain {
116118
return getChainsByType[ton.Chain, *ton.Chain](b)
117119
}
118120

121+
// TronChains returns a map of all Tron chains with their selectors.
122+
func (b BlockChains) TronChains() map[uint64]tron.Chain {
123+
return getChainsByType[tron.Chain, *tron.Chain](b)
124+
}
125+
119126
// ChainSelectorsOption defines a function type for configuring ChainSelectors
120127
type ChainSelectorsOption func(*chainSelectorsOptions)
121128

chain/blockchain_test.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/smartcontractkit/chainlink-deployments-framework/chain/solana"
1616
"github.com/smartcontractkit/chainlink-deployments-framework/chain/sui"
1717
"github.com/smartcontractkit/chainlink-deployments-framework/chain/ton"
18+
"github.com/smartcontractkit/chainlink-deployments-framework/chain/tron"
1819
)
1920

2021
var evmChain1 = evm.Chain{Selector: chain_selectors.TEST_90000001.Selector}
@@ -23,6 +24,7 @@ var solanaChain1 = solana.Chain{Selector: chain_selectors.TEST_22222222222222222
2324
var aptosChain1 = aptos.Chain{Selector: chain_selectors.APTOS_LOCALNET.Selector}
2425
var suiChain1 = sui.Chain{ChainMetadata: sui.ChainMetadata{Selector: chain_selectors.SUI_LOCALNET.Selector}}
2526
var tonChain1 = ton.Chain{ChainMetadata: ton.ChainMetadata{Selector: chain_selectors.TON_LOCALNET.Selector}}
27+
var tronChain1 = tron.Chain{ChainMetadata: tron.ChainMetadata{Selector: chain_selectors.TRON_MAINNET.Selector}}
2628

2729
func TestNewBlockChains(t *testing.T) {
2830
t.Parallel()
@@ -145,6 +147,7 @@ func TestBlockChainsAllChains(t *testing.T) {
145147
evmChain1.Selector, evmChain2.Selector,
146148
solanaChain1.Selector, aptosChain1.Selector,
147149
suiChain1.Selector, tonChain1.Selector,
150+
tronChain1.Selector,
148151
}
149152

150153
assert.Len(t, allChains, len(expectedSelectors))
@@ -246,6 +249,22 @@ func TestBlockChainsGetters(t *testing.T) {
246249
},
247250
description: "should return all Ton chains",
248251
},
252+
{
253+
name: "TronChains",
254+
runTest: func(t *testing.T, chains chain.BlockChains) {
255+
t.Helper()
256+
tronChains := chains.TronChains()
257+
expectedSelectors := []uint64{tronChain1.Selector}
258+
259+
assert.Len(t, tronChains, len(expectedSelectors), "unexpected number of Tron chains")
260+
261+
for _, selector := range expectedSelectors {
262+
_, exists := tronChains[selector]
263+
assert.True(t, exists, "expected Tron chain with selector %d", selector)
264+
}
265+
},
266+
description: "should return all Tron chains",
267+
},
249268
}
250269

251270
// Run tests for both value and pointer chains
@@ -285,6 +304,7 @@ func TestBlockChainsListChainSelectors(t *testing.T) {
285304
evmChain1.ChainSelector(), evmChain2.ChainSelector(),
286305
solanaChain1.ChainSelector(), aptosChain1.ChainSelector(),
287306
suiChain1.ChainSelector(), tonChain1.ChainSelector(),
307+
tronChain1.ChainSelector(),
288308
},
289309
description: "expected all chain selectors",
290310
},
@@ -318,6 +338,12 @@ func TestBlockChainsListChainSelectors(t *testing.T) {
318338
expectedIDs: []uint64{tonChain1.Selector},
319339
description: "expected Ton chain selectors",
320340
},
341+
{
342+
name: "with family filter - Tron",
343+
options: []chain.ChainSelectorsOption{chain.WithFamily(chain_selectors.FamilyTron)},
344+
expectedIDs: []uint64{tronChain1.Selector},
345+
description: "expected Tron chain selectors",
346+
},
321347
{
322348
name: "with multiple families",
323349
options: []chain.ChainSelectorsOption{chain.WithFamily(chain_selectors.FamilyEVM), chain.WithFamily(chain_selectors.FamilySolana)},
@@ -329,7 +355,7 @@ func TestBlockChainsListChainSelectors(t *testing.T) {
329355
options: []chain.ChainSelectorsOption{chain.WithChainSelectorsExclusion(
330356
[]uint64{evmChain1.Selector, aptosChain1.Selector}),
331357
},
332-
expectedIDs: []uint64{evmChain2.Selector, solanaChain1.Selector, suiChain1.Selector, tonChain1.Selector},
358+
expectedIDs: []uint64{evmChain2.Selector, solanaChain1.Selector, suiChain1.Selector, tonChain1.Selector, tronChain1.Selector},
333359
description: "expected chain selectors excluding 1 and 4",
334360
},
335361
{
@@ -354,7 +380,7 @@ func TestBlockChainsListChainSelectors(t *testing.T) {
354380
}
355381

356382
// buildBlockChains creates a new BlockChains instance with the test chains.
357-
// 2 evm chains, 1 solana chain, 1 aptos chain, 1 sui chain, 1 ton chain.
383+
// 2 evm chains, 1 solana chain, 1 aptos chain, 1 sui chain, 1 ton chain, 1 tron chain.
358384
func buildBlockChains() chain.BlockChains {
359385
chains := chain.NewBlockChains(map[uint64]chain.BlockChain{
360386
evmChain1.ChainSelector(): evmChain1,
@@ -363,6 +389,7 @@ func buildBlockChains() chain.BlockChains {
363389
aptosChain1.ChainSelector(): aptosChain1,
364390
suiChain1.ChainSelector(): suiChain1,
365391
tonChain1.ChainSelector(): tonChain1,
392+
tronChain1.ChainSelector(): tronChain1,
366393
})
367394

368395
return chains
@@ -384,6 +411,8 @@ func buildBlockChainsPointers() chain.BlockChains {
384411
pointerChains[selector] = &c
385412
case ton.Chain:
386413
pointerChains[selector] = &c
414+
case tron.Chain:
415+
pointerChains[selector] = &c
387416
default:
388417
continue // skip unsupported chains
389418
}

chain/tron/keystore/keystore.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package keystore
2+
3+
import (
4+
"context"
5+
"crypto/ecdsa"
6+
"errors"
7+
8+
"github.com/ethereum/go-ethereum/crypto"
9+
"github.com/fbsobreira/gotron-sdk/pkg/address"
10+
"github.com/smartcontractkit/chainlink-common/pkg/loop"
11+
)
12+
13+
// Keystore is a simple in-memory key store that holds private keys for signing.
14+
// The `Keys` map holds:
15+
// - key (string): the string representation of a Tron address
16+
// - value (*ecdsa.PrivateKey): the private key associated with that address
17+
type Keystore struct {
18+
Keys map[string]*ecdsa.PrivateKey
19+
}
20+
21+
// Assert that *Keystore implements the loop.Keystore interface
22+
var _ loop.Keystore = &Keystore{}
23+
24+
// NewKeystore initializes a new Keystore with a single ECDSA private key.
25+
// The key is stored using the derived Tron address as the map key.
26+
func NewKeystore(privateKey *ecdsa.PrivateKey) (*Keystore, address.Address) {
27+
keys := map[string]*ecdsa.PrivateKey{}
28+
address := address.PubkeyToAddress(privateKey.PublicKey)
29+
30+
keys[address.String()] = privateKey
31+
32+
return &Keystore{Keys: keys}, address
33+
}
34+
35+
// Sign signs the given hash using the private key associated with the provided ID (address string).
36+
// If the key does not exist, it returns an error.
37+
// If the hash is nil (e.g., used as an existence check), it returns nil.
38+
func (ks *Keystore) Sign(ctx context.Context, id string, hash []byte) ([]byte, error) {
39+
privateKey, ok := ks.Keys[id]
40+
if !ok {
41+
return nil, errors.New("no such key")
42+
}
43+
44+
// If hash is nil, don't perform actual signing. This is used to check key existence.
45+
if hash == nil {
46+
return nil, nil
47+
}
48+
49+
return crypto.Sign(hash, privateKey)
50+
}
51+
52+
// ImportECDSA adds a new private key to the Keystore, deriving its Tron address
53+
// and storing it using that address as the map key.
54+
func (ks *Keystore) ImportECDSA(privateKey *ecdsa.PrivateKey) address.Address {
55+
address := address.PubkeyToAddress(privateKey.PublicKey)
56+
ks.Keys[address.String()] = privateKey
57+
58+
return address
59+
}
60+
61+
// Accounts returns a list of all address strings currently stored in the Keystore.
62+
func (ks *Keystore) Accounts(ctx context.Context) ([]string, error) {
63+
accounts := make([]string, 0, len(ks.Keys))
64+
for id := range ks.Keys {
65+
accounts = append(accounts, id)
66+
}
67+
68+
return accounts, nil
69+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package keystore
2+
3+
import (
4+
"context"
5+
"crypto/ecdsa"
6+
"testing"
7+
8+
"github.com/ethereum/go-ethereum/crypto"
9+
"github.com/fbsobreira/gotron-sdk/pkg/address"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func generateKeyPair(t *testing.T) *ecdsa.PrivateKey {
15+
t.Helper()
16+
key, err := crypto.GenerateKey()
17+
require.NoError(t, err)
18+
19+
return key
20+
}
21+
22+
func Test_NewKeystore(t *testing.T) {
23+
t.Parallel()
24+
25+
privKey := generateKeyPair(t)
26+
ks, addr := NewKeystore(privKey)
27+
28+
expectedAddr := address.PubkeyToAddress(privKey.PublicKey)
29+
require.Contains(t, ks.Keys, expectedAddr.String())
30+
assert.Equal(t, privKey, ks.Keys[expectedAddr.String()])
31+
assert.Equal(t, expectedAddr, addr)
32+
}
33+
34+
func Test_Keystore_ImportECDSA(t *testing.T) {
35+
t.Parallel()
36+
37+
ks := &Keystore{Keys: make(map[string]*ecdsa.PrivateKey)}
38+
privKey := generateKeyPair(t)
39+
40+
addr := ks.ImportECDSA(privKey)
41+
assert.Equal(t, address.PubkeyToAddress(privKey.PublicKey), addr)
42+
assert.Equal(t, privKey, ks.Keys[addr.String()])
43+
}
44+
45+
func Test_Keystore_Sign(t *testing.T) {
46+
t.Parallel()
47+
48+
privKey := generateKeyPair(t)
49+
addrStr := address.PubkeyToAddress(privKey.PublicKey).String()
50+
51+
ks := &Keystore{
52+
Keys: map[string]*ecdsa.PrivateKey{
53+
addrStr: privKey,
54+
},
55+
}
56+
57+
t.Run("successful sign", func(t *testing.T) {
58+
t.Parallel()
59+
60+
hash := crypto.Keccak256([]byte("test data"))
61+
62+
sig, err := ks.Sign(context.Background(), addrStr, hash)
63+
require.NoError(t, err)
64+
require.Len(t, sig, 65) // ECDSA signature length
65+
})
66+
67+
t.Run("key not found", func(t *testing.T) {
68+
t.Parallel()
69+
70+
_, err := ks.Sign(context.Background(), "invalid_address", []byte("hash"))
71+
require.Error(t, err)
72+
assert.EqualError(t, err, "no such key")
73+
})
74+
75+
t.Run("nil hash returns nil without error", func(t *testing.T) {
76+
t.Parallel()
77+
78+
sig, err := ks.Sign(context.Background(), addrStr, nil)
79+
require.NoError(t, err)
80+
assert.Nil(t, sig)
81+
})
82+
}
83+
84+
func Test_Keystore_Accounts(t *testing.T) {
85+
t.Parallel()
86+
87+
priv1 := generateKeyPair(t)
88+
priv2 := generateKeyPair(t)
89+
90+
addr1 := address.PubkeyToAddress(priv1.PublicKey).String()
91+
addr2 := address.PubkeyToAddress(priv2.PublicKey).String()
92+
93+
ks := &Keystore{
94+
Keys: map[string]*ecdsa.PrivateKey{
95+
addr1: priv1,
96+
addr2: priv2,
97+
},
98+
}
99+
100+
accounts, err := ks.Accounts(context.Background())
101+
require.NoError(t, err)
102+
assert.ElementsMatch(t, []string{addr1, addr2}, accounts)
103+
}

0 commit comments

Comments
 (0)