diff --git a/relayer/cmd/chainlink-tron/default.nix b/relayer/cmd/chainlink-tron/default.nix index 769b336..396bae5 100644 --- a/relayer/cmd/chainlink-tron/default.nix +++ b/relayer/cmd/chainlink-tron/default.nix @@ -18,7 +18,7 @@ in ]; # pin the vendor hash (update using 'pkgs.lib.fakeHash') - vendorHash = "sha256-SCl9rzV1LAN+0DTVDXCthin/VwR/rCNaz4WdxcpCPJY="; + vendorHash = "sha256-DgTypXNwCZR9e2yMVqh+FSouhf/cDtAUDVp8ttqZgMA="; # postInstall script to write version and rev to share folder postInstall = '' diff --git a/relayer/go.mod b/relayer/go.mod index 8ec43dc..6665236 100644 --- a/relayer/go.mod +++ b/relayer/go.mod @@ -17,6 +17,7 @@ require ( go.uber.org/zap v1.27.0 golang.org/x/crypto v0.40.0 golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc + google.golang.org/protobuf v1.36.10 ) require ( @@ -139,13 +140,12 @@ require ( golang.org/x/sys v0.34.0 // indirect golang.org/x/text v0.27.0 // indirect golang.org/x/time v0.12.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/grpc v1.74.2 // indirect - google.golang.org/protobuf v1.36.7 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251007200510-49b9836ed3ff // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect + google.golang.org/grpc v1.76.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) // original author is not maintaining the repo anymore -replace github.com/fbsobreira/gotron-sdk => github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.4 +replace github.com/fbsobreira/gotron-sdk => github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014120029-d73d15cc23f7 diff --git a/relayer/go.sum b/relayer/go.sum index 8d45ca1..51c2dcc 100644 --- a/relayer/go.sum +++ b/relayer/go.sum @@ -411,8 +411,8 @@ github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.4 h1:hvqATtrZ0 github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.4/go.mod h1:eKGyfTKzr0/PeR7qKN4l2FcW9p+HzyKUwAfGhm/5YZc= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250911124514-5874cc6d62b2 h1:1/KdO5AbUr3CmpLjMPuJXPo2wHMbfB8mldKLsg7D4M8= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250911124514-5874cc6d62b2/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= -github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.4 h1:J4qtAo0ZmgX5pIr8Y5mdC+J2rj2e/6CTUC263t6mGOM= -github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.4/go.mod h1:4WhGgCA0smBbBud5mK+jnDb2wwndMvoqaWBJ3OV/7Bw= +github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014120029-d73d15cc23f7 h1:qPGcryHOEipripujMtsip++fmVbJhyqO6eAGEq85r48= +github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014120029-d73d15cc23f7/go.mod h1:ea1LESxlSSOgc2zZBqf1RTkXTMthHaspdqUHd7W4lF0= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9Mww35LrufCdM9wtS9yVi/rEWGI1UnjHbcKKU0nVY= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= 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 golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210401141331-865547bb08e2/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/api v0.0.0-20251007200510-49b9836ed3ff h1:8Zg5TdmcbU8A7CXGjGXF1Slqu/nIFCRaR3S5gT2plIA= +google.golang.org/genproto/googleapis/api v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:dbWfpVPvW/RqafStmRWBUpMN14puDezDMHxNYiRfQu0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= -google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 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 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= -google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/relayer/txm/seralizer.go b/relayer/txm/seralizer.go new file mode 100644 index 0000000..1fc4137 --- /dev/null +++ b/relayer/txm/seralizer.go @@ -0,0 +1,129 @@ +package txm + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "time" + + "github.com/fbsobreira/gotron-sdk/pkg/abi" + "github.com/fbsobreira/gotron-sdk/pkg/address" + "github.com/fbsobreira/gotron-sdk/pkg/http/common" + "github.com/fbsobreira/gotron-sdk/pkg/proto/core" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" +) + +type Serializer struct { + TransactionType core.Transaction_Contract_ContractType + FromAddress address.Address + ContractAddress address.Address + Method string + Params []any + CallValueSun int64 + FeeLimitSun int64 + RefBlockBytes []byte + RefBlockHash []byte + ExpirationMillis int64 + TimestampMillis int64 +} + +const DefaultExpirationMillis = 30_000 // 30 seconds + +func (p *Serializer) BuildTransaction() (*common.Transaction, error) { + if p.TransactionType != core.Transaction_Contract_TriggerSmartContract { + return nil, fmt.Errorf("invalid transaction type: %d", p.TransactionType) + } + + if len(p.RefBlockBytes) != 2 && len(p.RefBlockHash) != 8 { + return nil, fmt.Errorf("invalid ref block bytes or hash") + } + + callData, err := p.buildCallData() + if err != nil { + return nil, fmt.Errorf("failed to build call data: %+w", err) + } + + smartContractCall := &core.TriggerSmartContract{ + OwnerAddress: p.FromAddress.Bytes(), + ContractAddress: p.ContractAddress.Bytes(), + Data: callData, + CallValue: p.CallValueSun, + } + + callPayload, err := anypb.New(smartContractCall) + if err != nil { + return nil, fmt.Errorf("failed to create call payload: %+w", err) + } + + contract := &core.Transaction_Contract{ + Parameter: callPayload, + Type: core.Transaction_Contract_TriggerSmartContract, + } + + now := time.Now().UnixMilli() + timestamp := p.TimestampMillis + if timestamp == 0 { + timestamp = now + } + + expiration := p.ExpirationMillis + if expiration == 0 { + expiration = now + DefaultExpirationMillis + } + + rawData := &core.TransactionRaw{ + Contract: []*core.Transaction_Contract{contract}, + FeeLimit: p.FeeLimitSun, + Expiration: expiration, + Timestamp: timestamp, + RefBlockBytes: p.RefBlockBytes, + RefBlockHash: p.RefBlockHash, + } + + rawBytes, err := proto.Marshal(rawData) + if err != nil { + return nil, fmt.Errorf("failed to marshal raw data: %+w", err) + } + + hash := sha256.Sum256(rawBytes) + txIDHex := hex.EncodeToString(hash[:]) + rawDataHex := hex.EncodeToString(rawBytes) + + commonRawData := common.RawData{ + Contract: []common.Contract{ + { + Parameter: common.Parameter{ + Value: common.ParameterValue{ + OwnerAddress: p.FromAddress.String(), + ContractAddress: p.ContractAddress.String(), + Data: hex.EncodeToString(smartContractCall.Data), + Amount: p.CallValueSun, + }, + TypeUrl: "type.googleapis.com/protocol.TriggerSmartContract", + }, + Type: "TriggerSmartContract", + }, + }, + RefBlockBytes: hex.EncodeToString(p.RefBlockBytes), + RefBlockHash: hex.EncodeToString(p.RefBlockHash), + Expiration: expiration, + FeeLimit: p.FeeLimitSun, + Timestamp: timestamp, + } + + return &common.Transaction{ + Visible: true, + TxID: txIDHex, + RawData: commonRawData, + RawDataHex: rawDataHex, + }, nil +} + +func (p *Serializer) buildCallData() ([]byte, error) { + parsed, err := abi.Pack(p.Method, p.Params) + if err != nil { + return nil, fmt.Errorf("failed to pack params: %+w", err) + } + return parsed, nil +} diff --git a/relayer/txm/txm.go b/relayer/txm/txm.go index 0510fb9..592cce3 100644 --- a/relayer/txm/txm.go +++ b/relayer/txm/txm.go @@ -12,6 +12,7 @@ import ( "github.com/fbsobreira/gotron-sdk/pkg/http/common" "github.com/fbsobreira/gotron-sdk/pkg/http/fullnode" "github.com/fbsobreira/gotron-sdk/pkg/http/soliditynode" + "github.com/fbsobreira/gotron-sdk/pkg/proto/core" "github.com/google/uuid" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -171,14 +172,37 @@ func (t *TronTxm) broadcastLoop() { for { select { case tx := <-t.BroadcastChan: - triggerResponse, err := t.TriggerSmartContract(ctx, tx) + feeLimit, err := t.calculateFeeLimit(tx) if err != nil { - // TODO: is it ok to leave this transaction unmarked as fatal? - t.Logger.Errorw("failed to trigger smart contract", "error", err, "tx", tx, "txID", tx.ID) + t.Logger.Errorw("failed to calculate fee limit", "error", err, "txID", tx.ID) + continue + } + + // Get the latest block info + refBlockBytes, refBlockHash, err := t.computeRefBlockBytesAndHash() + if err != nil { + t.Logger.Errorw("failed to compute ref block bytes and hash", "error", err, "txID", tx.ID) + continue + } + + txSerializer := Serializer{ + TransactionType: core.Transaction_Contract_TriggerSmartContract, + FromAddress: tx.FromAddress, + ContractAddress: tx.ContractAddress, + Method: tx.Method, + Params: tx.Params, + CallValueSun: 0, + FeeLimitSun: int64(feeLimit), + RefBlockBytes: refBlockBytes, + RefBlockHash: refBlockHash, + } + + coreTx, err := txSerializer.BuildTransaction() + if err != nil { + t.Logger.Errorw("failed to build transaction", "error", err, "txID", tx.ID) continue } - coreTx := triggerResponse.Transaction txHash := coreTx.TxID // RefBlockNum is optional and does not seem in use anymore. @@ -187,7 +211,7 @@ func (t *TronTxm) broadcastLoop() { _, err = t.SignAndBroadcast(ctx, tx.FromAddress, coreTx) if err != nil { - t.Logger.Errorw("transaction failed to broadcast", "txHash", txHash, "error", err, "tx", tx, "triggerResponse", triggerResponse, "txID", tx.ID) + t.Logger.Errorw("transaction failed to broadcast", "txHash", txHash, "error", err, "tx", tx, "coreTx", coreTx, "txID", tx.ID) txStore.OnFatalError(tx.ID) continue } @@ -202,6 +226,47 @@ func (t *TronTxm) broadcastLoop() { } } +func (t *TronTxm) computeRefBlockBytesAndHash() ([]byte, []byte, error) { + nowBlock, err := t.GetClient().GetNowBlockFullNode() + if err != nil { + return nil, nil, fmt.Errorf("failed to get now block: %+w", err) + } + + blockNumber := nowBlock.BlockHeader.RawData.Number + blockId, err := hex.DecodeString(nowBlock.BlockID) + if err != nil { + return nil, nil, fmt.Errorf("failed to decode block id: %+w", err) + } + + refBlockBytes := []byte{byte((blockNumber >> 8) & 0xFF), byte(blockNumber & 0xFF)} // last 2 bytes + refBlockHash := blockId[8:16] + return refBlockBytes, refBlockHash, nil +} + +func (t *TronTxm) calculateFeeLimit(tx *TronTx) (int32, error) { + energyUsed, err := t.estimateEnergy(tx) + if err != nil { + return 0, fmt.Errorf("failed to estimate energy: %+w", err) + } + + energyUnitPrice := DEFAULT_ENERGY_UNIT_PRICE + + if energyPrices, err := t.GetClient().GetEnergyPrices(); err == nil { + if parsedPrice, err := ParseLatestEnergyPrice(energyPrices.Prices); err == nil { + energyUnitPrice = parsedPrice + } else { + t.Logger.Errorw("error parsing energy unit price", "error", err, "txID", tx.ID) + } + } else { + t.Logger.Errorw("failed to get energy unit price", "error", err, "txID", tx.ID) + } + + feeLimit := energyUnitPrice * int32(energyUsed) + paddedFeeLimit := CalculatePaddedFeeLimit(feeLimit, tx.EnergyBumpTimes, t.Config.EnergyMultiplier) + + return paddedFeeLimit, nil +} + func (t *TronTxm) TriggerSmartContract(ctx context.Context, tx *TronTx) (*fullnode.TriggerSmartContractResponse, error) { energyUsed, err := t.estimateEnergy(tx) if err != nil { diff --git a/relayer/txm/txm_test.go b/relayer/txm/txm_test.go index 252a114..56eabae 100644 --- a/relayer/txm/txm_test.go +++ b/relayer/txm/txm_test.go @@ -59,6 +59,7 @@ func createDefaultMockClient(t *testing.T) *mocks.CombinedClient { txid, _ := hex.DecodeString("2a037789237971c1c1d648f7b90b70c68a9aa6b0a2892f947213286346d0210d") combinedClient.On("GetNowBlockFullNode").Maybe().Return(&soliditynode.Block{ + BlockID: "000000000325a7105234af0154beb7fcb0363b809cb469fe7e0e0fd571bbd054", BlockHeader: &soliditynode.BlockHeader{ RawData: &soliditynode.BlockHeaderRaw{ Timestamp: 1000,