![JAR search and dependency download from the Maven repository](/logo.png)
io.neow3j.transaction.Transaction Maven / Gradle / Ivy
Show all versions of core Show documentation
package io.neow3j.transaction;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.neow3j.constants.NeoConstants;
import io.neow3j.crypto.Base64;
import io.neow3j.crypto.ECKeyPair;
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.protocol.exceptions.RpcResponseErrorException;
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.ContractParameter;
import io.neow3j.types.ContractParameterType;
import io.neow3j.types.Hash160;
import io.neow3j.types.Hash256;
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.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
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 io.neow3j.utils.Numeric.toHexStringNoPrefix;
import static java.lang.String.format;
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;
}
/**
* @return the version of this transaction.
*/
public byte getVersion() {
return version;
}
/**
* @return the nonce of this transaction.
*/
public long getNonce() {
return nonce;
}
/**
* @return the validity period of this transaction.
*/
public long getValidUntilBlock() {
return validUntilBlock;
}
/**
* @return the signers of this transaction.
*/
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();
}
/**
* @return the system fee of this transaction in GAS fractions.
*/
public long getSystemFee() {
return systemFee;
}
/**
* @return the network fee of this transaction in GAS fractions.
*/
public long getNetworkFee() {
return networkFee;
}
/**
* @return the attributes of this transaction.
*/
public List getAttributes() {
return attributes;
}
/**
* @return the first attribute of this transaction.
*/
public TransactionAttribute getFirstAttribute() {
if (attributes.size() == 0) {
throw new IndexOutOfBoundsException("This transaction has no attributes.");
}
return getAttribute(0);
}
/**
* Gets the attribute at {@code index} of this transaction's attributes list.
*
* @param index the index.
* @return the attribute.
*/
public TransactionAttribute getAttribute(int index) {
if (index >= attributes.size()) {
throw new IndexOutOfBoundsException(format("This transaction has only %s attributes.", attributes.size()));
}
return attributes.get(index);
}
/**
* @return the script of this transaction.
*/
public byte[] getScript() {
return script;
}
/**
* @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 witness to this transaction by signing it with the given account.
*
* Note, that witnesses have to be added in the same order as signers were added.
*
* @param account the account to sign with.
* @return this.
* @throws IOException if an error occurs when fetching the network's magic number.
*/
public Transaction addWitness(Account account) throws IOException {
this.witnesses.add(Witness.create(getHashData(), account.getECKeyPair()));
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 pubKeySigMap a map of participating public keys mapped to the signatures created with their
* corresponding private key.
* @return this.
*/
public Transaction addMultiSigWitness(VerificationScript verificationScript,
Map pubKeySigMap) {
List signatures = pubKeySigMap.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(Map.Entry::getValue)
.collect(Collectors.toList());
Witness multiSigWitness = createMultiSigWitness(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 {
byte[] hashData = getHashData();
List signatures = Arrays.stream(accounts)
.map(Account::getECKeyPair)
.sorted(Comparator.comparing(ECKeyPair::getPublicKey))
.map(a -> signMessage(hashData, a))
.collect(Collectors.toList());
Witness multiSigWitness = createMultiSigWitness(signatures, verificationScript);
this.witnesses.add(multiSigWitness);
return this;
}
/**
* @return this transaction's uniquely identifying ID/hash.
*/
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 TransactionConfigurationException if the number of signers and witnesses on the transaction are not
* equal.
* @throws IOException if a problem in communicating with the Neo node occurs.
*/
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 %s bytes while the transaction has size %s.", MAX_TRANSACTION_SIZE, size));
}
String hex = 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 sent to the Neo node.
*
* If the application log could not be fetched, {@code null} is returned.
*
* @return the application log.
* @throws IOException if something goes wrong in the communication with the neo-node.
* @throws RpcResponseErrorException if the Neo node returns an error.
*/
public NeoApplicationLog getApplicationLog() throws IOException {
if (blockCountWhenSent == null) {
throw new IllegalStateException("Cannot get the application log before transaction has been sent.");
}
return neow3j.getApplicationLog(getTxId()).send().getApplicationLog();
}
@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(
format("A transaction can hold at most %s attributes (including signers). Input data had %s " +
"attributes.", NeoConstants.MAX_TRANSACTION_ATTRIBUTES, nrOfAttributes));
}
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);
}
/**
* Produces a JSON object that can be used in neo-cli for further signing and relaying of this transaction.
*
* @return neo-cli compatible json of this transaction.
* @throws IOException if an error occurs when trying to fetch the network's magic number.
*/
public ContractParametersContext toContractParametersContext() throws IOException {
String hash = getTxId().toString();
String data = Base64.encode(toArrayWithoutWitnesses());
long network = neow3j.getNetworkMagicNumber();
Map items = signers.stream().map(signer -> {
if (signer instanceof ContractSigner) {
throw new UnsupportedOperationException("Cannot handle contract signers");
}
AccountSigner accountSigner = (AccountSigner) signer;
VerificationScript verificationScript = accountSigner.getAccount().getVerificationScript();
// Check if there's a witness for this signer and add all corresponding signatures as parameters.
List params = new ArrayList<>();
witnesses.stream().filter(w -> w.getVerificationScript().equals(verificationScript))
.map(Witness::getInvocationScript).findFirst()
.ifPresent(invocationScript -> invocationScript.getSignatures().stream()
.map(Sign.SignatureData::getConcatenated)
.forEach(s -> params.add(new ContractParameter(ContractParameterType.SIGNATURE, s))));
if (params.isEmpty()) {
// If no witness was found we need to set the parameter without a value.
IntStream.range(0, verificationScript.getSigningThreshold())
.forEach(i -> params.add(new ContractParameter(ContractParameterType.SIGNATURE)));
}
Map pubKeyToSignature = new HashMap<>();
if (verificationScript.isSingleSigScript() && params.get(0).getValue() != null) {
String pubKey = verificationScript.getPublicKeys().get(0).getEncodedCompressedHex();
pubKeyToSignature.put(pubKey, Base64.encode((byte[]) params.get(0).getValue()));
}
String script = Base64.encode(verificationScript.getScript());
return new ContractParametersContext.ContextItem(script, params, pubKeyToSignature);
}).collect(Collectors.toMap(i -> "0x" + Hash160.fromScript(Base64.decode(i.getScript())), Function.identity()));
return new ContractParametersContext(hash, data, items, network);
}
}