All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.neow3j.transaction.Transaction Maven / Gradle / Ivy

package io.neow3j.transaction;

import io.neow3j.constants.NeoConstants;
import io.neow3j.contract.ScriptHash;
import io.neow3j.crypto.Hash;
import io.neow3j.io.BinaryReader;
import io.neow3j.io.BinaryWriter;
import io.neow3j.io.IOUtils;
import io.neow3j.io.NeoSerializable;
import io.neow3j.io.exceptions.DeserializationException;
import io.neow3j.model.NeoConfig;
import io.neow3j.transaction.exceptions.TransactionConfigurationException;
import io.neow3j.utils.ArrayUtils;
import io.neow3j.utils.Numeric;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Transaction extends NeoSerializable {

    public static final int HEADER_SIZE = 1 +  // Version byte
            4 +  // Nonce uint32
            NeoConstants.SCRIPTHASH_SIZE + // Sender script hash
            8 +  // System fee int64
            8 +  // Network fee int64
            4; // Valid until block uint32

    private byte version;
    /**
     * Is a random number added to the transaction to prevent replay attacks. It is an unsigned
     * 32-bit integer in the neo C# implementation. It is represented as a integer here, but when
     * serializing it
     */
    private long nonce;
    /**
     * Defines up to which block this transaction remains valid. If this transaction is not added
     * into a block up to this number it will become invalid and be dropped. It is an unsigned
     * 32-bit integer in the neo C# implementation. Here it is represented as a signed 32-bit
     * integer which offers a smaller but still large enough range.
     */
    private long validUntilBlock;
    private ScriptHash sender;
    private long systemFee;
    private long networkFee;
    private List attributes;
    private byte[] script;
    private List witnesses;

    public Transaction() {
        this.attributes = new ArrayList<>();
        this.witnesses = new ArrayList<>();
    }

    protected Transaction(Builder builder) {
        this.version = builder.version;
        this.nonce = builder.nonce;
        this.validUntilBlock = builder.validUntilBlock;
        this.sender = builder.sender;
        this.systemFee = builder.systemFee;
        this.networkFee = builder.networkFee;
        this.attributes = builder.attributes;
        this.script = builder.script;
        this.witnesses = builder.witnesses;
    }

    public byte getVersion() {
        return version;
    }

    public long getNonce() {
        return nonce;
    }

    public long getValidUntilBlock() {
        return validUntilBlock;
    }

    public ScriptHash getSender() {
        return sender;
    }

    /**
     * Gets the system fee of this transaction.
     *
     * @return the system fee in GAS fractions.
     */
    public long getSystemFee() {
        return systemFee;
    }

    /**
     * Gets the network fee of this transaction.
     *
     * @return the network fee in GAS fractions.
     */
    public long getNetworkFee() {
        return networkFee;
    }

    public List getAttributes() {
        return attributes;
    }

    public List getCosigners() {
        return this.attributes.stream()
                .filter(a -> a.type.equals(TransactionAttributeType.COSIGNER))
                .map(a -> ((Cosigner) a))
                .collect(Collectors.toList());
    }

    public byte[] getScript() {
        return script;
    }

    public List getWitnesses() {
        return witnesses;
    }

    public void addWitness(Witness witness) {
        if (witness.getScriptHash() == null) {
            throw new IllegalArgumentException("The script hash of the given witness must not be "
                    + "null.");
        }
        this.witnesses.add(witness);
    }

    public String getTxId() {
        byte[] hash = Hash.sha256(Hash.sha256(getHashData()));
        return Numeric.toHexStringNoPrefix(ArrayUtils.reverseArray(hash));
    }

    @Override
    public int getSize() {
        return HEADER_SIZE +
                IOUtils.getVarSize(this.attributes) +
                IOUtils.getVarSize(this.script) +
                IOUtils.getVarSize(this.witnesses);
    }

    @Override
    public void deserialize(BinaryReader reader) throws DeserializationException {
        try {
            this.version = reader.readByte();
            this.nonce = reader.readUInt32();
            this.sender = reader.readSerializable(ScriptHash.class);
            this.systemFee = reader.readInt64();
            this.networkFee = reader.readInt64();
            this.validUntilBlock = reader.readUInt32();
            readTransactionAttributes(reader);
            this.script = reader.readVarBytes();
            this.witnesses = reader.readSerializableList(Witness.class);
        } catch (IOException e) {
            throw new DeserializationException(e);
        }
    }

    private void readTransactionAttributes(BinaryReader reader)
            throws IOException, DeserializationException {
        long nrOfAttributes = reader.readVarInt();
        if (nrOfAttributes > NeoConstants.MAX_TRANSACTION_ATTRIBUTES) {
            throw new DeserializationException("A transaction can hold at most "
                    + NeoConstants.MAX_TRANSACTION_ATTRIBUTES + ". Input data had "
                    + nrOfAttributes + " attributes.");
        }
        for (int i = 0; i < nrOfAttributes; i++) {
            this.attributes.add(TransactionAttribute.deserializeAttribute(reader));
        }
    }

    private void serializeWithoutWitnesses(BinaryWriter writer) throws IOException {
        writer.writeByte(this.version);
        writer.writeUInt32(this.nonce);
        writer.writeSerializableFixed(this.sender);
        writer.writeInt64(this.systemFee);
        writer.writeInt64(this.networkFee);
        writer.writeUInt32(this.validUntilBlock);
        writer.writeSerializableVariable(this.attributes);
        writer.writeVarBytes(this.script);
    }

    @Override
    public void serialize(BinaryWriter writer) throws IOException {
        serializeWithoutWitnesses(writer);
        this.witnesses.sort(Comparator.comparing(Witness::getScriptHash));
        writer.writeSerializableVariable(this.witnesses);
    }

    /**
     * Serializes this transaction to a raw byte array without any witnesses.
     * 

* In this form, the transaction byte array can be used for example to create a signature. * * @return the serialized transaction */ public byte[] toArrayWithoutWitnesses() { try (ByteArrayOutputStream ms = new ByteArrayOutputStream()) { try (BinaryWriter writer = new BinaryWriter(ms)) { serializeWithoutWitnesses(writer); writer.flush(); return ms.toByteArray(); } } catch (IOException ex) { throw new UnsupportedOperationException(ex); } } /** * Gets this transaction's data in the format used to produce the transaction's hash. E.g., * for producing the transaction ID or a transaction signature. *

* The returned value depends on the configuration of {@link NeoConfig#magicNumber()}. * * @return the transaction data ready for hashing. */ public byte[] getHashData() { return ArrayUtils.concatenate(NeoConfig.magicNumber(), toArrayWithoutWitnesses()); } /** * Serializes this transaction to a raw byte array including witnesses. * * @return the serialized transaction. */ @Override public byte[] toArray() { return super.toArray(); } public static class Builder { private long nonce; private byte version; private Long validUntilBlock; private ScriptHash sender; private long systemFee; private long networkFee; private byte[] script; private List attributes; private List witnesses; public Builder() { // The random value used to initialize the nonce does not need cryptographic security, // therefore we can use ThreadLocalRandom to generate it. this.nonce = ThreadLocalRandom.current().nextLong((long) Math.pow(2, 32)); this.version = NeoConstants.CURRENT_TX_VERSION; this.networkFee = 0L; this.systemFee = 0L; this.attributes = new ArrayList<>(); this.witnesses = new ArrayList<>(); this.script = new byte[]{}; } /** * Sets the version for this transaction. *

* It is set to {@link NeoConstants#CURRENT_TX_VERSION} by default. * * @param version The transaction version number. * @return this builder. */ public Builder version(byte version) { this.version = version; return this; } /** * Sets the nonce (number used once) for this transaction. The nonce is a number from 0 to * 232. *

* It is set to a random value by default. * * @param nonce The transaction nonce. * @return this builder. * @throws TransactionConfigurationException if the nonce is not in the range [0, 2^32). */ public Builder nonce(Long nonce) { if (nonce < 0 || nonce >= (long) Math.pow(2, 32)) { throw new TransactionConfigurationException("The value of the transaction nonce " + "must be in the interval [0, 2^32)."); } this.nonce = nonce; return this; } /** * Sets the number of the block up to which this transaction can be included. *

* If that block number is reached in the network and this transaction is not yet included * in a block, it becomes invalid. Note that the given block number must not be higher than * the current chain height plus the increment specified in {@link * NeoConstants#MAX_VALID_UNTIL_BLOCK_INCREMENT}. *

* This property is mandatory. * * @param blockNr The block number. * @return this builder. * @throws TransactionConfigurationException if the block number is not in the range [0, * 2^32). */ public Builder validUntilBlock(long blockNr) { if (blockNr < 0 || blockNr >= (long) Math.pow(2, 32)) { throw new TransactionConfigurationException("The block number up to which this " + "transaction can be included cannot be less than zero or more than 2^32."); } this.validUntilBlock = blockNr; return this; } /** * Sets the sender of this transaction. *

* The sender's account will be charged with the network and system fees. *

* This property is mandatory. * * @param sender The sender account's script hash. * @return this builder. */ public Builder sender(ScriptHash sender) { this.sender = sender; return this; } /** * Sets the system fee for this transaction. *

* The system fee is the amount of GAS needed to execute this transaction's script in the * NeoVM. It is distributed to all NEO holders. * * @param systemFee The system fee in fractions of GAS (10^-8) * @return this builder. */ public Builder systemFee(Long systemFee) { this.systemFee = systemFee; return this; } /** * Sets the network fee for this transaction. *

* The network fee is the GAS cost for transaction size and verification. It is distributed * to the consensus nodes. * * @param networkFee The network fee in fractions of GAS (10^-8) * @return this builder. */ public Builder networkFee(Long networkFee) { this.networkFee = networkFee; return this; } /** * Sets the contract script for this transaction. *

* The script defines the actions that this transaction will perform on the blockchain. * * @param script The contract script. * @return this builder. */ public Builder script(byte[] script) { this.script = script; return this; } /** * Adds the given attributes to this transaction. *

* The maximum number of attributes on a transaction is given in {@link * NeoConstants#MAX_TRANSACTION_ATTRIBUTES}. * * @param attributes The attributes. * @return this builder. * @throws TransactionConfigurationException when attempting to add more than {@link * NeoConstants#MAX_TRANSACTION_ATTRIBUTES} * attributes. */ public Builder attributes(TransactionAttribute... attributes) { if (this.attributes.size() + attributes.length > NeoConstants.MAX_TRANSACTION_ATTRIBUTES) { throw new TransactionConfigurationException("A transaction cannot have more " + "than " + NeoConstants.MAX_TRANSACTION_ATTRIBUTES + " attributes."); } if (containsDuplicateCosigners(attributes)) { throw new TransactionConfigurationException("Can't add multiple cosigners" + " concerning the same account."); } this.attributes.addAll(Arrays.asList(attributes)); return this; } private boolean containsDuplicateCosigners(TransactionAttribute... newAttributes) { List newCosignersList = Stream.of(newAttributes) .filter(a -> a.getType().equals(TransactionAttributeType.COSIGNER)) .map(a -> ((Cosigner) a).getScriptHash()) .collect(Collectors.toList()); Set newCosignersSet = new HashSet<>(newCosignersList); if (newCosignersList.size() != newCosignersSet.size()) { // The new cosingers list contains duplicates in itself. return true; } return this.attributes.stream() .filter(a -> a.getType().equals(TransactionAttributeType.COSIGNER)) .map(a -> ((Cosigner) a).getScriptHash()) .anyMatch(newCosignersSet::contains); } /** * Adds the given witnesses to this transaction. *

* Witness data is used to check the transaction validity. It usually consists of the * signature generated by the transacting account but can also be other validating data. * * @param witnesses The witnesses. * @return this builder. */ public Builder witnesses(Witness... witnesses) { for (Witness witness : witnesses) { if (witness.getScriptHash() == null) { throw new IllegalArgumentException("The script hash of the given script is " + "empty. Please set the script hash."); } } this.witnesses.addAll(Arrays.asList(witnesses)); return this; } /** * Builds the transaction. * * @return The transaction. * @throws TransactionConfigurationException if either the sender account or the * "validUntilBlock" property was not set. */ public Transaction build() { if (this.sender == null) { throw new TransactionConfigurationException("A transaction requires a sender " + "account."); } if (this.validUntilBlock == null) { throw new TransactionConfigurationException("A transaction needs to be set up " + "with a block number up to which this it is considered valid."); } if (getCosigners().isEmpty()) { // Add default restrictive witness scope. this.attributes.add(Cosigner.calledByEntry(this.sender)); } return new Transaction(this); } public long getNonce() { return nonce; } public byte getVersion() { return version; } public Long getValidUntilBlock() { return validUntilBlock; } public ScriptHash getSender() { return sender; } public long getSystemFee() { return systemFee; } public long getNetworkFee() { return networkFee; } public List getCosigners() { return this.attributes.stream() .filter(a -> a.type.equals(TransactionAttributeType.COSIGNER)) .map(a -> ((Cosigner) a)) .collect(Collectors.toList()); } public byte[] getScript() { return script; } public List getAttributes() { return attributes; } public List getWitnesses() { return witnesses; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy