
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;
}
}
}