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

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

There is a newer version: 3.23.0
Show newest version
package io.neow3j.transaction;

import com.fasterxml.jackson.core.JsonProcessingException;
import io.neow3j.constants.NeoConstants;
import io.neow3j.crypto.Sign;
import io.neow3j.protocol.Neow3j;
import io.neow3j.protocol.ObjectMapperFactory;
import io.neow3j.protocol.core.response.NeoApplicationLog;
import io.neow3j.protocol.core.response.NeoGetBlock;
import io.neow3j.protocol.core.response.NeoSendRawTransaction;
import io.neow3j.script.VerificationScript;
import io.neow3j.serialization.BinaryReader;
import io.neow3j.serialization.BinaryWriter;
import io.neow3j.serialization.IOUtils;
import io.neow3j.serialization.NeoSerializable;
import io.neow3j.serialization.exceptions.DeserializationException;
import io.neow3j.transaction.exceptions.TransactionConfigurationException;
import io.neow3j.types.Hash160;
import io.neow3j.types.Hash256;
import io.neow3j.utils.Numeric;
import io.neow3j.wallet.Account;
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.constants.NeoConstants.MAX_TRANSACTION_SIZE;
import static io.neow3j.crypto.Hash.sha256;
import static io.neow3j.crypto.Sign.signMessage;
import static io.neow3j.transaction.Witness.createMultiSigWitness;
import static io.neow3j.utils.ArrayUtils.concatenate;
import static io.neow3j.utils.ArrayUtils.reverseArray;
import static java.lang.String.format;
import static java.util.Arrays.asList;

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

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

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

    public Transaction(Neow3j neow3j, byte version, long nonce, long validUntilBlock,
            List signers, long systemFee, long networkFee,
            List attributes, byte[] script, List witnesses) {
        this.neow3j = neow3j;
        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;
    }

    /**
     * Sets the {@code Neow3j} instance of this transaction.
     *
     * @param neow3j The Neow3j instance.
     */
    public void setNeow3j(Neow3j neow3j) {
        this.neow3j = neow3j;
    }

    /**
     * Gets the version of this transaction.
     *
     * @return the version.
     */
    public byte getVersion() {
        return version;
    }

    /**
     * Gets the nonce of this transaction.
     *
     * @return the nonce.
     */
    public long getNonce() {
        return nonce;
    }

    /**
     * Gets the validity period of this transaction.
     *
     * @return the validity period.
     */
    public long getValidUntilBlock() {
        return validUntilBlock;
    }

    /**
     * Gets the signers of this transaction.
     *
     * @return the transaction signers.
     */
    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;
    }

    /**
     * Get the attributes of this transaction.
     *
     * @return the attributes.
     */
    public List getAttributes() {
        return attributes;
    }

    /**
     * Get the script of this transaction.
     *
     * @return the script of this transaction.
     */
    public byte[] getScript() {
        return script;
    }

    /**
     * Get the witnesses of this transaction.
     *
     * @return the witnesses of this transaction.
     */
    public List getWitnesses() {
        return witnesses;
    }

    /**
     * Adds a witness to this transaction.
     * 

* Note, that witnesses have to be added in the same order as signers were added. * * @param witness The transaction witness. * @return this. */ public Transaction addWitness(Witness witness) { this.witnesses.add(witness); return this; } /** * Adds a multi-sig witness to this transaction. Use this to add a witness of a multi-sig * signer that is part of this transaction. *

* The witness is constructed from the multi-sig account's {@code verificationScript} and the * {@code signatures}. Obviously, the signatures should be derived from this transaction's hash * data (see {@link Transaction#getHashData()}). *

* Note, that witnesses have to be added in the same order as signers were added. * * @param verificationScript The verification script of the multi-sig account. * @param signatures The signatures created with the participating private keys. * @return this. */ public Transaction addMultiSigWitness(VerificationScript verificationScript, Sign.SignatureData... signatures) { Witness multiSigWitness = createMultiSigWitness(asList(signatures), verificationScript); this.witnesses.add(multiSigWitness); return this; } /** * Adds a multi-sig witness to this transaction. Use this to add a witness of a multi-sig * signer that is part of this transaction. *

* The witness is constructed from the multi-sig account's {@code verificationScript} and by * signing this transaction with the given accounts. *

* Note, that witnesses have to be added in the same order as signers were added. * * @param verificationScript The verification script of the multi-sig account. * @param accounts The accounts to use for signing. They need to hold decrypted * private keys. * @return this. * @throws IOException if there was a problem fetching information from the Neo node. */ public Transaction addMultiSigWitness(VerificationScript verificationScript, Account... accounts) throws IOException { ArrayList signatures = new ArrayList<>(); for (Account a : accounts) { signatures.add(signMessage(getHashData(), a.getECKeyPair())); } Witness multiSigWitness = createMultiSigWitness(signatures, verificationScript); this.witnesses.add(multiSigWitness); return this; } /** * Gets this transactions uniquely identifying ID/hash. * * @return the transaction ID. */ public Hash256 getTxId() { return new Hash256(reverseArray(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."); } int size = getSize(); if (size > MAX_TRANSACTION_SIZE) { throw new TransactionConfigurationException(format("The transaction exceeds the " + "maximum transaction size. The maximum size is {} bytes. This transaction " + "has size {}", MAX_TRANSACTION_SIZE, size)); } String hex = Numeric.toHexStringNoPrefix(toArray()); blockCountWhenSent = neow3j.getBlockCount().send().getBlockCount(); return neow3j.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 (blockCountWhenSent == null) { throw new IllegalStateException("Cannot subscribe before transaction has been sent."); } Predicate pred = neoGetBlock -> neoGetBlock.getBlock().getTransactions() != null && neoGetBlock.getBlock().getTransactions().stream() .anyMatch(tx -> tx.getHash().equals(getTxId())); return neow3j.catchUpToLatestAndSubscribeToNewBlocksObservable(blockCountWhenSent, 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 (blockCountWhenSent == null) { throw new IllegalStateException("Cannot get the application log before transaction " + "has been sent."); } NeoApplicationLog applicationLog = null; try { applicationLog = neow3j.getApplicationLog(getTxId()).send().getApplicationLog(); } catch (IOException ignore) { } 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(); if (reader.available() > 0) { 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 + this.signers.size() > NeoConstants.MAX_TRANSACTION_ATTRIBUTES) { throw new DeserializationException("A transaction can hold at most " + NeoConstants.MAX_TRANSACTION_ATTRIBUTES + " attributes (including signers). " + "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 concatenate(neow3j.getNetworkMagicNumberBytes(), sha256(toArrayWithoutWitnesses())); } /** * Serializes this transaction to a raw byte array including witnesses. * * @return the serialized transaction. */ @Override public byte[] toArray() { return super.toArray(); } public String toJson() throws JsonProcessingException { io.neow3j.protocol.core.response.Transaction dtoTx = new io.neow3j.protocol.core.response.Transaction(this); return ObjectMapperFactory.getObjectMapper().writeValueAsString(dtoTx); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy