How to Build a Transaction Locally

This article will show developers how to build a transaction locally through an example. It is used to build data inside the transaction. If developers have offline-signature, remark transaction and other requirements, you can refer to this article. These data include:

message raw {
    bytes ref_block_bytes = 1;//The bytes from 6 to 8(not included) of the height of the latest block
    int64 ref_block_num = 3;//The height of the latest block.Optional.
    bytes ref_block_hash = 4;//The bytes from the 8 to 16(not included) of the hash of the latest block
    int64 expiration = 8;// Use the latest block time plus N minutes as the value of expiration time('N' can be modified in the configuration file of the local node, if using a public node, the default 'N' is one minute).The network judgment condition is if (expiration > latest block time and expiration < latest block time + 24 hours) means the transaction is in period of validity. Otherwise, it will be an overdue transaction, will not be accepted by the Mainnet.
    repeated authority auths = 9;//Permission setting
    // data not used
    bytes data = 10;//Unused field, can be used for remark information, optional
    //only support size = 1,  repeated list here for extension
    repeated Contract contract = 11;//The contract data
    // scripts not used
    bytes scripts = 12;//Unused field, can be used for remark information, optional
    int64 timestamp = 14;//Create transaction time
    int64 fee_limit = 18;//Threshold for trx consumption due to insufficient resources
  }

📘

Note:

Only supports RPC

Java

The calling environment of the RPC interface can be obtained through wallet-cli

package org.tron.demo;

import com.google.protobuf.Any;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.tron.api.GrpcAPI.Return;
import org.tron.api.GrpcAPI.TransactionExtention;
import org.tron.common.crypto.ECKey;
import org.tron.common.crypto.Sha256Sm3Hash;
import org.tron.common.utils.ByteArray;
import org.tron.core.exception.CancelException;
import org.tron.protos.Contract;
import org.tron.protos.Protocol.Block;
import org.tron.protos.Protocol.Transaction;
import org.tron.walletserver.WalletApi;

import java.util.Arrays;

public class TransactionSignDemo {
/* set reference data*/
  public static Transaction setReference(Transaction transaction, Block newestBlock) {
    long blockHeight = newestBlock.getBlockHeader().getRawData().getNumber();
    byte[] blockHash = getBlockHash(newestBlock).getBytes();
    byte[] refBlockNum = ByteArray.fromLong(blockHeight);
    Transaction.raw rawData = transaction.getRawData().toBuilder()
            .setRefBlockHash(ByteString.copyFrom(ByteArray.subArray(blockHash, 8, 16)))
            .setRefBlockBytes(ByteString.copyFrom(ByteArray.subArray(refBlockNum, 6, 8)))
            .setRefBlockNum(blockHeight)
            .build();
    return transaction.toBuilder().setRawData(rawData).build();
  }

  public static Sha256Sm3Hash getBlockHash(Block block) {
    return Sha256Sm3Hash.of(block.getBlockHeader().getRawData().toByteArray());
  }

  public static String getTransactionHash(Transaction transaction) {
    String txid = ByteArray.toHexString(Sha256Sm3Hash.hash(transaction.getRawData().toByteArray()));
    return txid;
  }

  public static Transaction createTransaction(byte[] from, byte[] to, long amount) {
    Transaction.Builder transactionBuilder = Transaction.newBuilder();
    Block newestBlock = WalletApi.getBlock(-1);
/*set the contract data*/
    Transaction.Contract.Builder contractBuilder = Transaction.Contract.newBuilder();
    Contract.TransferContract.Builder transferContractBuilder =
        Contract.TransferContract.newBuilder();
    transferContractBuilder.setAmount(amount);
    ByteString bsTo = ByteString.copyFrom(to);
    ByteString bsOwner = ByteString.copyFrom(from);
    transferContractBuilder.setToAddress(bsTo);
    transferContractBuilder.setOwnerAddress(bsOwner);
    try {
      Any any = Any.pack(transferContractBuilder.build());
      contractBuilder.setParameter(any);
    } catch (Exception e) {
      return null;
    }
    /*set memo,etc*/
    contractBuilder.setType(Transaction.Contract.ContractType.TransferContract);
    transactionBuilder.getRawDataBuilder().addContract(contractBuilder)
        .setTimestamp(System.currentTimeMillis())
        .setExpiration(newestBlock.getBlockHeader().getRawData().getTimestamp() + 10 * 60 * 60 * 1000)
        .setData(ByteString.copyFromUtf8("memo"))
        .setScripts(ByteString.copyFromUtf8("scripts"));
    Transaction transaction = transactionBuilder.build();
    Transaction refTransaction = setReference(transaction, newestBlock);
    return refTransaction;
  }
/*signature*/
  private static byte[] signTransaction2Byte(byte[] transaction, byte[] privateKey)
      throws InvalidProtocolBufferException {
    ECKey ecKey = ECKey.fromPrivate(privateKey);
    Transaction transaction1 = Transaction.parseFrom(transaction);
    byte[] rawdata = transaction1.getRawData().toByteArray();
    byte[] hash = Sha256Sm3Hash.hash(rawdata);
    byte[] sign = ecKey.sign(hash).toByteArray();
    return transaction1.toBuilder().addSignature(ByteString.copyFrom(sign)).build().toByteArray();
  }

  private static boolean broadcast(byte[] transactionBytes) throws InvalidProtocolBufferException {
    return WalletApi.broadcastTransaction(transactionBytes);
  }

  public static void main(String[] args) throws InvalidProtocolBufferException, CancelException {
    String privateStr = "da146374a75310b9666e834ee4ad0866d6f4035967bfc76217c5a495fff9f0d0";
    byte[] privateBytes = ByteArray.fromHexString(privateStr);
    ECKey ecKey = ECKey.fromPrivate(privateBytes);
    byte[] from = ecKey.getAddress();
    byte[] to = WalletApi.decodeFromBase58Check("TN9RRaXkCFtTXRso2GdTZxSxxwufzxLQPP");
    long amount = 100_000_000L; // 100 TRX, api only receive trx in Sun, and 1 trx = 1000000 Sun
    Transaction transaction = createTransaction(from, to, amount);
    byte[] transactionBytes = transaction.toByteArray();
    byte[] transaction4 = signTransaction2Byte(transactionBytes, privateBytes);
    boolean result = broadcast(transaction4);

    System.out.println(result);
  }
}

After executing the above code, you can get the following information:

{
    "ret": [
        {
            "contractRet": "SUCCESS"
        }
    ],
    "signature": [
        "ced3929af13ca455fca59088c1a98908897640f6dc7c5746f1a21eb850fb266e6d7996e1984464b0d8a262f991342a1c9e4197311ea3562631f411ffde9abcda00"
    ],
    "txID": "a1185aad04cfa78cb66d5eb3780b8801dde1d49d14aee9e21841194b81a64bd6",
    "raw_data": {
        "data": "6d656d6f",
        "contract": [
            {
                "parameter": {
                    "value": {
                        "amount": 100000000,
                        "owner_address": "41bf97a54f4b829c4e9253b26024b1829e1a3b1120",
                        "to_address": "41859009fd225692b11237a6ffd8fdba2eb7140cca"
                    },
                    "type_url": "type.googleapis.com/protocol.TransferContract"
                },
                "type": "TransferContract"
            }
        ],
        "ref_block_bytes": "c75b",
        "ref_block_hash": "7cfc890c6e4d7a15",
        "expiration": 1583304414000,
        "ref_block_num": 2541403,
        "scripts": "73637269707473",
        "timestamp": 1583268416080
    },
    "raw_data_hex": "0a02c75b18db8e9b0122087cfc890c6e4d7a1540b0a6b0a28a2e52046d656d6f5a68080112640a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412330a1541bf97a54f4b829c4e9253b26024b1829e1a3b1120121541859009fd225692b11237a6ffd8fdba2eb7140cca1880c2d72f62077363726970747370d0949b918a2e"
}

📘

Note:

The information in the data and scripts fields can be used as remark information ( data is displayed in TronScan), and the content can be obtained by converting Hex to String.