This commit is contained in:
Luxferre
2026-09-19 18:26:07 +03:00
parent 8ae332e74d
commit e9826b1ae2
28 changed files with 3417 additions and 1 deletions
+206
View File
@@ -0,0 +1,206 @@
package tron
import (
"encoding/hex"
"encoding/json"
"errors"
"time"
)
// Transaction is a signed or unsigned TRON transaction. Its field order matches
// tronpy's Transaction.to_json() dict order so the marshalled JSON is
// byte-compatible with the Python offline output.
type Transaction struct {
TxID string `json:"txID"`
RawData *RawData `json:"raw_data"`
Signature []string `json:"signature"`
Permission any `json:"permission"`
client *Client
}
func currentMillis() int64 {
return time.Now().UnixNano() / int64(time.Millisecond)
}
// TransactionBuilder assembles a transaction before signing.
type TransactionBuilder struct {
client *Client
rawData *RawData
}
// newTransactionBuilder creates a builder for a single contract.
func (c *Client) newTransactionBuilder(typ string, value any) *TransactionBuilder {
now := currentMillis()
rd := &RawData{
Contract: []ContractJSON{{
Parameter: ParameterJSON{
Value: value,
TypeURL: "type.googleapis.com/protocol." + typ,
},
Type: typ,
}},
Timestamp: now,
Expiration: now + 60000,
}
if typ == "TriggerSmartContract" || typ == "CreateSmartContract" {
rd.FeeLimit = c.feeLimit
}
return &TransactionBuilder{client: c, rawData: rd}
}
// Transfer builds a TRX transfer builder. Amount is in SUN.
func (c *Client) Transfer(from, to string, amount int64) (*TransactionBuilder, error) {
fromHex, err := ToHexAddress(from)
if err != nil {
return nil, err
}
toHex, err := ToHexAddress(to)
if err != nil {
return nil, err
}
return c.newTransactionBuilder("TransferContract", TransferValue{
OwnerAddress: fromHex,
ToAddress: toHex,
Amount: amount,
}), nil
}
// FreezeBalance builds a FreezeBalanceV2 builder for the given resource.
func (c *Client) FreezeBalance(owner string, amount int64, resource string) (*TransactionBuilder, error) {
ownerHex, err := ToHexAddress(owner)
if err != nil {
return nil, err
}
return c.newTransactionBuilder("FreezeBalanceV2Contract", FreezeValue{
OwnerAddress: ownerHex,
FrozenBalance: amount,
Resource: resource,
}), nil
}
// UnfreezeBalance builds an UnfreezeBalanceV2 builder for the given resource.
func (c *Client) UnfreezeBalance(owner string, amount int64, resource string) (*TransactionBuilder, error) {
ownerHex, err := ToHexAddress(owner)
if err != nil {
return nil, err
}
return c.newTransactionBuilder("UnfreezeBalanceV2Contract", UnfreezeValue{
OwnerAddress: ownerHex,
UnfreezeBalance: amount,
Resource: resource,
}), nil
}
// Build fills the reference block fields from the chain and computes the txid.
func (b *TransactionBuilder) Build() (*Transaction, error) {
refID, err := b.client.getLatestSolidBlockID()
if err != nil {
return nil, err
}
if len(refID) < 32 {
return nil, ErrBadHash
}
b.rawData.RefBlockBytes = refID[12:16]
b.rawData.RefBlockHash = refID[16:32]
txid, err := b.rawData.TxID()
if err != nil {
return nil, err
}
return &Transaction{
TxID: txid,
RawData: b.rawData,
Signature: []string{},
client: b.client,
}, nil
}
// Sign signs the transaction with the given private key.
func (tx *Transaction) Sign(key *PrivateKey) error {
if tx.TxID == "" {
return ErrTransaction
}
hash, err := hex.DecodeString(tx.TxID)
if err != nil {
return err
}
sig, err := key.Sign(hash)
if err != nil {
return err
}
tx.Signature = append(tx.Signature, hex.EncodeToString(sig))
return nil
}
// Broadcast sends the signed transaction and returns the raw response.
func (tx *Transaction) Broadcast() (map[string]any, error) {
if tx.client == nil {
return nil, ErrTransaction
}
return tx.client.broadcast(tx)
}
// Wait polls for the transaction receipt, matching TransactionRet.wait().
func (tx *Transaction) Wait(timeout, interval time.Duration) (map[string]any, error) {
if tx.client == nil {
return nil, ErrTransaction
}
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
info, err := tx.client.getTransactionInfo(tx.TxID)
if err == nil {
return info, nil
}
if !errors.Is(err, ErrTransactionNotFound) {
return nil, err
}
time.Sleep(interval)
}
return nil, ErrTransactionNotFound
}
// JSON returns the compact transaction JSON used for node requests and offline
// mode output, matching json.dumps(txn.to_json(), separators=(",", ":")).
func (tx *Transaction) JSON() string {
b, _ := json.Marshal(tx)
return string(b)
}
// toJSON returns the transaction as a generic map for node requests.
func (tx *Transaction) toJSON() map[string]any {
var m map[string]any
json.Unmarshal([]byte(tx.JSON()), &m)
return m
}
// TransactionFromJSON parses a transaction previously exported by JSON().
func TransactionFromJSON(s string) (*Transaction, error) {
var decoded struct {
TxID string `json:"txID"`
RawData *RawData `json:"raw_data"`
Signature []string `json:"signature"`
Permission any `json:"permission"`
}
if err := json.Unmarshal([]byte(s), &decoded); err != nil {
return nil, err
}
if decoded.RawData == nil {
return nil, ErrTransaction
}
return &Transaction{
TxID: decoded.TxID,
RawData: decoded.RawData,
Signature: decoded.Signature,
Permission: decoded.Permission,
}, nil
}
// ParseTransaction parses exported transaction JSON and attaches the client so
// the transaction can be broadcast (mirrors Transaction.from_json + _client).
func (c *Client) ParseTransaction(raw string) (*Transaction, error) {
tx, err := TransactionFromJSON(raw)
if err != nil {
return nil, err
}
tx.client = c
return tx, nil
}