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 }