
io.neow3j.transaction.Transaction Maven / Gradle / Ivy
package io.neow3j.transaction;
import io.neow3j.constants.NeoConstants;
import io.neow3j.contract.Hash160;
import io.neow3j.contract.Hash256;
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.protocol.Neow3j;
import io.neow3j.protocol.core.methods.response.NeoApplicationLog;
import io.neow3j.protocol.core.methods.response.NeoGetBlock;
import io.neow3j.protocol.core.methods.response.NeoSendRawTransaction;
import io.neow3j.transaction.exceptions.TransactionConfigurationException;
import io.neow3j.utils.ArrayUtils;
import io.neow3j.utils.Numeric;
import io.reactivex.Observable;
import io.reactivex.functions.Predicate;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import static io.neow3j.crypto.Hash.sha256;
public class Transaction extends NeoSerializable {
public static final int HEADER_SIZE = 1 + // Version byte
4 + // Nonce uint32
8 + // System fee int64
8 + // Network fee int64
4; // Valid until block uint32
protected Neow3j neow;
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 List signers;
private long systemFee;
private long networkFee;
private List attributes;
private byte[] script;
private List witnesses;
private BigInteger blockIndexWhenSent;
public Transaction() {
signers = new ArrayList<>();
attributes = new ArrayList<>();
witnesses = new ArrayList<>();
}
public Transaction(Neow3j neow, byte version, long nonce, long validUntilBlock,
List signers,
long systemFee, long networkFee, List attributes, byte[] script,
List witnesses) {
this.neow = neow;
this.version = version;
this.nonce = nonce;
this.validUntilBlock = validUntilBlock;
this.signers = signers;
this.systemFee = systemFee;
this.networkFee = networkFee;
this.attributes = attributes;
this.script = script;
this.witnesses = witnesses;
}
public byte getVersion() {
return version;
}
public long getNonce() {
return nonce;
}
public long getValidUntilBlock() {
return validUntilBlock;
}
public List getSigners() {
return signers;
}
/**
* Gets the sender of this transaction. The sender is the account that pays for the
* transaction's fees.
*
* @return the sender account's script hash.
*/
public Hash160 getSender() {
// First we look for a signer that has the fee-only scope. The signer with that scope is
// the sender of the transaction. If there is no such signer then the order of the
// signers defines the sender, i.e., the first signer is the sender of the transaction.
return signers.stream()
.filter(signer -> signer.getScopes().contains(WitnessScope.NONE))
.findFirst()
.orElse(signers.get(0))
.getScriptHash();
}
/**
* 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 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);
}
/**
* Gets this transactions uniquely identifying ID/hash.
*
* @return the transaction ID.
*/
public Hash256 getTxId() {
return new Hash256(sha256(toArrayWithoutWitnesses()));
}
/**
* Sends this invocation transaction to the Neo node via the `sendrawtransaction` RPC.
*
* @return the Neo node's response.
* @throws IOException if a problem in communicating with the Neo node
* occurs.
* @throws TransactionConfigurationException if the number of signers and witnesses on the
* transaction are not equal.
*/
public NeoSendRawTransaction send() throws IOException {
if (getSigners().size() != getWitnesses().size()) {
throw new TransactionConfigurationException("The transaction does not have the same " +
"number of signers and witnesses. For every signer there has to be one " +
"witness, even if that witness is empty.");
}
String hex = Numeric.toHexStringNoPrefix(toArray());
blockIndexWhenSent = neow.getBlockCount().send().getBlockIndex();
return neow.sendRawTransaction(hex).send();
}
/**
* Creates an {@code Observable} that emits the block number containing this transaction as soon
* as it has been integrated in one. The observable completes right after emitting the block
* number.
*
* The observable starts tracking the blocks from the point at which the transaction has been
* sent.
*
* @return The observable.
* @throws IllegalStateException if this transaction has not yet been sent.
*/
public Observable track() {
if (blockIndexWhenSent == null) {
throw new IllegalStateException("Can't subscribe before transaction has been sent.");
}
Predicate pred = neoGetBlock ->
neoGetBlock.getBlock().getTransactions() != null &&
neoGetBlock.getBlock().getTransactions().stream()
.anyMatch(tx -> tx.getHash().equals(getTxId()));
return neow.catchUpToLatestAndSubscribeToNewBlocksObservable(blockIndexWhenSent, true)
.takeUntil(pred)
.filter(pred)
.map(neoGetBlock -> neoGetBlock.getBlock().getIndex());
}
/**
* Gets the application log of this transaction.
*
* The application log is not cached locally. Every time this method is called, requests are
* send to the Neo node.
*
* If the application log could not be fetched, {@code null} is returned.
*
* @return the application log.
*/
public NeoApplicationLog getApplicationLog() {
if (blockIndexWhenSent == null) {
throw new IllegalStateException("Can't get the application log before transaction has" +
" been sent.");
}
NeoApplicationLog applicationLog = null;
try {
applicationLog = neow.getApplicationLog(getTxId()).send().getApplicationLog();
} catch (IOException e) {
e.printStackTrace();
}
return applicationLog;
}
@Override
public int getSize() {
return HEADER_SIZE +
IOUtils.getVarSize(this.signers) +
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.systemFee = reader.readInt64();
this.networkFee = reader.readInt64();
this.validUntilBlock = reader.readUInt32();
this.signers = reader.readSerializableList(Signer.class);
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.writeInt64(this.systemFee);
writer.writeInt64(this.networkFee);
writer.writeUInt32(this.validUntilBlock);
writer.writeSerializableVariable(this.signers);
writer.writeSerializableVariable(this.attributes);
writer.writeVarBytes(this.script);
}
@Override
public void serialize(BinaryWriter writer) throws IOException {
serializeWithoutWitnesses(writer);
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 magic number of the used Neo network, which is retrieved
* from the Neo node via the {@code getversion} RPC method if not already available locally.
*
* @return the transaction data ready for hashing.
* @throws IOException if an error occurs when fetching the network's magic number
*/
public byte[] getHashData() throws IOException {
return ArrayUtils.concatenate(neow.getNetworkMagicNumber(),
sha256(toArrayWithoutWitnesses()));
}
/**
* Serializes this transaction to a raw byte array including witnesses.
*
* @return the serialized transaction.
*/
@Override
public byte[] toArray() {
return super.toArray();
}
}