Skip to content

Commit a0c6328

Browse files
authored
Merge pull request #42 from smartcontractkit/kodey/tron-construct-tx-locally
feat: transaction serializer
2 parents af6b168 + 6a9fb0d commit a0c6328

File tree

6 files changed

+218
-21
lines changed

6 files changed

+218
-21
lines changed

relayer/cmd/chainlink-tron/default.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ in
1818
];
1919

2020
# pin the vendor hash (update using 'pkgs.lib.fakeHash')
21-
vendorHash = "sha256-SCl9rzV1LAN+0DTVDXCthin/VwR/rCNaz4WdxcpCPJY=";
21+
vendorHash = "sha256-DgTypXNwCZR9e2yMVqh+FSouhf/cDtAUDVp8ttqZgMA=";
2222

2323
# postInstall script to write version and rev to share folder
2424
postInstall = ''

relayer/go.mod

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ require (
1717
go.uber.org/zap v1.27.0
1818
golang.org/x/crypto v0.40.0
1919
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc
20+
google.golang.org/protobuf v1.36.10
2021
)
2122

2223
require (
@@ -139,13 +140,12 @@ require (
139140
golang.org/x/sys v0.34.0 // indirect
140141
golang.org/x/text v0.27.0 // indirect
141142
golang.org/x/time v0.12.0 // indirect
142-
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
143-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
144-
google.golang.org/grpc v1.74.2 // indirect
145-
google.golang.org/protobuf v1.36.7 // indirect
143+
google.golang.org/genproto/googleapis/api v0.0.0-20251007200510-49b9836ed3ff // indirect
144+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect
145+
google.golang.org/grpc v1.76.0 // indirect
146146
gopkg.in/yaml.v3 v3.0.1 // indirect
147147
rsc.io/tmplfunc v0.0.3 // indirect
148148
)
149149

150150
// original author is not maintaining the repo anymore
151-
replace github.com/fbsobreira/gotron-sdk => github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.4
151+
replace github.com/fbsobreira/gotron-sdk => github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014120029-d73d15cc23f7

relayer/go.sum

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,8 @@ github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.4 h1:hvqATtrZ0
411411
github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.4/go.mod h1:eKGyfTKzr0/PeR7qKN4l2FcW9p+HzyKUwAfGhm/5YZc=
412412
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250911124514-5874cc6d62b2 h1:1/KdO5AbUr3CmpLjMPuJXPo2wHMbfB8mldKLsg7D4M8=
413413
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250911124514-5874cc6d62b2/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q=
414-
github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.4 h1:J4qtAo0ZmgX5pIr8Y5mdC+J2rj2e/6CTUC263t6mGOM=
415-
github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.4/go.mod h1:4WhGgCA0smBbBud5mK+jnDb2wwndMvoqaWBJ3OV/7Bw=
414+
github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014120029-d73d15cc23f7 h1:qPGcryHOEipripujMtsip++fmVbJhyqO6eAGEq85r48=
415+
github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014120029-d73d15cc23f7/go.mod h1:ea1LESxlSSOgc2zZBqf1RTkXTMthHaspdqUHd7W4lF0=
416416
github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9Mww35LrufCdM9wtS9yVi/rEWGI1UnjHbcKKU0nVY=
417417
github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU=
418418
github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs=
@@ -683,23 +683,25 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
683683
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
684684
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
685685
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
686+
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
687+
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
686688
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
687689
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
688690
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
689691
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
690692
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
691693
google.golang.org/genproto v0.0.0-20210401141331-865547bb08e2/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
692-
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
693-
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
694-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
695-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
694+
google.golang.org/genproto/googleapis/api v0.0.0-20251007200510-49b9836ed3ff h1:8Zg5TdmcbU8A7CXGjGXF1Slqu/nIFCRaR3S5gT2plIA=
695+
google.golang.org/genproto/googleapis/api v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:dbWfpVPvW/RqafStmRWBUpMN14puDezDMHxNYiRfQu0=
696+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk=
697+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
696698
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
697699
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
698700
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
699701
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
700702
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
701-
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
702-
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
703+
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
704+
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
703705
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
704706
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
705707
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -711,8 +713,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
711713
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
712714
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
713715
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
714-
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
715-
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
716+
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
717+
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
716718
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
717719
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
718720
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

relayer/txm/seralizer.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package txm
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/hex"
6+
"fmt"
7+
"time"
8+
9+
"github.com/fbsobreira/gotron-sdk/pkg/abi"
10+
"github.com/fbsobreira/gotron-sdk/pkg/address"
11+
"github.com/fbsobreira/gotron-sdk/pkg/http/common"
12+
"github.com/fbsobreira/gotron-sdk/pkg/proto/core"
13+
"google.golang.org/protobuf/proto"
14+
"google.golang.org/protobuf/types/known/anypb"
15+
)
16+
17+
type Serializer struct {
18+
TransactionType core.Transaction_Contract_ContractType
19+
FromAddress address.Address
20+
ContractAddress address.Address
21+
Method string
22+
Params []any
23+
CallValueSun int64
24+
FeeLimitSun int64
25+
RefBlockBytes []byte
26+
RefBlockHash []byte
27+
ExpirationMillis int64
28+
TimestampMillis int64
29+
}
30+
31+
const DefaultExpirationMillis = 30_000 // 30 seconds
32+
33+
func (p *Serializer) BuildTransaction() (*common.Transaction, error) {
34+
if p.TransactionType != core.Transaction_Contract_TriggerSmartContract {
35+
return nil, fmt.Errorf("invalid transaction type: %d", p.TransactionType)
36+
}
37+
38+
if len(p.RefBlockBytes) != 2 && len(p.RefBlockHash) != 8 {
39+
return nil, fmt.Errorf("invalid ref block bytes or hash")
40+
}
41+
42+
callData, err := p.buildCallData()
43+
if err != nil {
44+
return nil, fmt.Errorf("failed to build call data: %+w", err)
45+
}
46+
47+
smartContractCall := &core.TriggerSmartContract{
48+
OwnerAddress: p.FromAddress.Bytes(),
49+
ContractAddress: p.ContractAddress.Bytes(),
50+
Data: callData,
51+
CallValue: p.CallValueSun,
52+
}
53+
54+
callPayload, err := anypb.New(smartContractCall)
55+
if err != nil {
56+
return nil, fmt.Errorf("failed to create call payload: %+w", err)
57+
}
58+
59+
contract := &core.Transaction_Contract{
60+
Parameter: callPayload,
61+
Type: core.Transaction_Contract_TriggerSmartContract,
62+
}
63+
64+
now := time.Now().UnixMilli()
65+
timestamp := p.TimestampMillis
66+
if timestamp == 0 {
67+
timestamp = now
68+
}
69+
70+
expiration := p.ExpirationMillis
71+
if expiration == 0 {
72+
expiration = now + DefaultExpirationMillis
73+
}
74+
75+
rawData := &core.TransactionRaw{
76+
Contract: []*core.Transaction_Contract{contract},
77+
FeeLimit: p.FeeLimitSun,
78+
Expiration: expiration,
79+
Timestamp: timestamp,
80+
RefBlockBytes: p.RefBlockBytes,
81+
RefBlockHash: p.RefBlockHash,
82+
}
83+
84+
rawBytes, err := proto.Marshal(rawData)
85+
if err != nil {
86+
return nil, fmt.Errorf("failed to marshal raw data: %+w", err)
87+
}
88+
89+
hash := sha256.Sum256(rawBytes)
90+
txIDHex := hex.EncodeToString(hash[:])
91+
rawDataHex := hex.EncodeToString(rawBytes)
92+
93+
commonRawData := common.RawData{
94+
Contract: []common.Contract{
95+
{
96+
Parameter: common.Parameter{
97+
Value: common.ParameterValue{
98+
OwnerAddress: p.FromAddress.String(),
99+
ContractAddress: p.ContractAddress.String(),
100+
Data: hex.EncodeToString(smartContractCall.Data),
101+
Amount: p.CallValueSun,
102+
},
103+
TypeUrl: "type.googleapis.com/protocol.TriggerSmartContract",
104+
},
105+
Type: "TriggerSmartContract",
106+
},
107+
},
108+
RefBlockBytes: hex.EncodeToString(p.RefBlockBytes),
109+
RefBlockHash: hex.EncodeToString(p.RefBlockHash),
110+
Expiration: expiration,
111+
FeeLimit: p.FeeLimitSun,
112+
Timestamp: timestamp,
113+
}
114+
115+
return &common.Transaction{
116+
Visible: true,
117+
TxID: txIDHex,
118+
RawData: commonRawData,
119+
RawDataHex: rawDataHex,
120+
}, nil
121+
}
122+
123+
func (p *Serializer) buildCallData() ([]byte, error) {
124+
parsed, err := abi.Pack(p.Method, p.Params)
125+
if err != nil {
126+
return nil, fmt.Errorf("failed to pack params: %+w", err)
127+
}
128+
return parsed, nil
129+
}

relayer/txm/txm.go

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/fbsobreira/gotron-sdk/pkg/http/common"
1313
"github.com/fbsobreira/gotron-sdk/pkg/http/fullnode"
1414
"github.com/fbsobreira/gotron-sdk/pkg/http/soliditynode"
15+
"github.com/fbsobreira/gotron-sdk/pkg/proto/core"
1516
"github.com/google/uuid"
1617

1718
"github.com/smartcontractkit/chainlink-common/pkg/logger"
@@ -171,14 +172,37 @@ func (t *TronTxm) broadcastLoop() {
171172
for {
172173
select {
173174
case tx := <-t.BroadcastChan:
174-
triggerResponse, err := t.TriggerSmartContract(ctx, tx)
175+
feeLimit, err := t.calculateFeeLimit(tx)
175176
if err != nil {
176-
// TODO: is it ok to leave this transaction unmarked as fatal?
177-
t.Logger.Errorw("failed to trigger smart contract", "error", err, "tx", tx, "txID", tx.ID)
177+
t.Logger.Errorw("failed to calculate fee limit", "error", err, "txID", tx.ID)
178+
continue
179+
}
180+
181+
// Get the latest block info
182+
refBlockBytes, refBlockHash, err := t.computeRefBlockBytesAndHash()
183+
if err != nil {
184+
t.Logger.Errorw("failed to compute ref block bytes and hash", "error", err, "txID", tx.ID)
185+
continue
186+
}
187+
188+
txSerializer := Serializer{
189+
TransactionType: core.Transaction_Contract_TriggerSmartContract,
190+
FromAddress: tx.FromAddress,
191+
ContractAddress: tx.ContractAddress,
192+
Method: tx.Method,
193+
Params: tx.Params,
194+
CallValueSun: 0,
195+
FeeLimitSun: int64(feeLimit),
196+
RefBlockBytes: refBlockBytes,
197+
RefBlockHash: refBlockHash,
198+
}
199+
200+
coreTx, err := txSerializer.BuildTransaction()
201+
if err != nil {
202+
t.Logger.Errorw("failed to build transaction", "error", err, "txID", tx.ID)
178203
continue
179204
}
180205

181-
coreTx := triggerResponse.Transaction
182206
txHash := coreTx.TxID
183207

184208
// RefBlockNum is optional and does not seem in use anymore.
@@ -187,7 +211,7 @@ func (t *TronTxm) broadcastLoop() {
187211

188212
_, err = t.SignAndBroadcast(ctx, tx.FromAddress, coreTx)
189213
if err != nil {
190-
t.Logger.Errorw("transaction failed to broadcast", "txHash", txHash, "error", err, "tx", tx, "triggerResponse", triggerResponse, "txID", tx.ID)
214+
t.Logger.Errorw("transaction failed to broadcast", "txHash", txHash, "error", err, "tx", tx, "coreTx", coreTx, "txID", tx.ID)
191215
txStore.OnFatalError(tx.ID)
192216
continue
193217
}
@@ -202,6 +226,47 @@ func (t *TronTxm) broadcastLoop() {
202226
}
203227
}
204228

229+
func (t *TronTxm) computeRefBlockBytesAndHash() ([]byte, []byte, error) {
230+
nowBlock, err := t.GetClient().GetNowBlockFullNode()
231+
if err != nil {
232+
return nil, nil, fmt.Errorf("failed to get now block: %+w", err)
233+
}
234+
235+
blockNumber := nowBlock.BlockHeader.RawData.Number
236+
blockId, err := hex.DecodeString(nowBlock.BlockID)
237+
if err != nil {
238+
return nil, nil, fmt.Errorf("failed to decode block id: %+w", err)
239+
}
240+
241+
refBlockBytes := []byte{byte((blockNumber >> 8) & 0xFF), byte(blockNumber & 0xFF)} // last 2 bytes
242+
refBlockHash := blockId[8:16]
243+
return refBlockBytes, refBlockHash, nil
244+
}
245+
246+
func (t *TronTxm) calculateFeeLimit(tx *TronTx) (int32, error) {
247+
energyUsed, err := t.estimateEnergy(tx)
248+
if err != nil {
249+
return 0, fmt.Errorf("failed to estimate energy: %+w", err)
250+
}
251+
252+
energyUnitPrice := DEFAULT_ENERGY_UNIT_PRICE
253+
254+
if energyPrices, err := t.GetClient().GetEnergyPrices(); err == nil {
255+
if parsedPrice, err := ParseLatestEnergyPrice(energyPrices.Prices); err == nil {
256+
energyUnitPrice = parsedPrice
257+
} else {
258+
t.Logger.Errorw("error parsing energy unit price", "error", err, "txID", tx.ID)
259+
}
260+
} else {
261+
t.Logger.Errorw("failed to get energy unit price", "error", err, "txID", tx.ID)
262+
}
263+
264+
feeLimit := energyUnitPrice * int32(energyUsed)
265+
paddedFeeLimit := CalculatePaddedFeeLimit(feeLimit, tx.EnergyBumpTimes, t.Config.EnergyMultiplier)
266+
267+
return paddedFeeLimit, nil
268+
}
269+
205270
func (t *TronTxm) TriggerSmartContract(ctx context.Context, tx *TronTx) (*fullnode.TriggerSmartContractResponse, error) {
206271
energyUsed, err := t.estimateEnergy(tx)
207272
if err != nil {

relayer/txm/txm_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ func createDefaultMockClient(t *testing.T) *mocks.CombinedClient {
5959
txid, _ := hex.DecodeString("2a037789237971c1c1d648f7b90b70c68a9aa6b0a2892f947213286346d0210d")
6060

6161
combinedClient.On("GetNowBlockFullNode").Maybe().Return(&soliditynode.Block{
62+
BlockID: "000000000325a7105234af0154beb7fcb0363b809cb469fe7e0e0fd571bbd054",
6263
BlockHeader: &soliditynode.BlockHeader{
6364
RawData: &soliditynode.BlockHeaderRaw{
6465
Timestamp: 1000,

0 commit comments

Comments
 (0)