tangem-app-android-audited/app/src/main/java/org/stellar/sdk/Transaction.java
2019-01-13 15:11:34 +03:00

381 lines
14 KiB
Java

package org.stellar.sdk;
import com.google.common.io.BaseEncoding;
import org.stellar.sdk.xdr.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Represents <a href="https://www.stellar.org/developers/learn/concepts/transactions.html" target="_blank">Transaction</a> in Stellar network.
*/
public class Transaction {
private static final int BASE_FEE = 100;
protected final int mFee;
protected final KeyPair mSourceAccount;
protected final long mSequenceNumber;
protected final Operation[] mOperations;
protected final Memo mMemo;
protected final TimeBounds mTimeBounds;
protected List<DecoratedSignature> mSignatures;
Transaction(KeyPair sourceAccount, int fee, long sequenceNumber, Operation[] operations, Memo memo, TimeBounds timeBounds) {
mSourceAccount = checkNotNull(sourceAccount, "sourceAccount cannot be null");
mSequenceNumber = checkNotNull(sequenceNumber, "sequenceNumber cannot be null");
mOperations = checkNotNull(operations, "operations cannot be null");
checkArgument(operations.length > 0, "At least one operation required");
mFee = fee;
mSignatures = new ArrayList<DecoratedSignature>();
mMemo = memo != null ? memo : Memo.none();
mTimeBounds = timeBounds;
}
/**
* Adds a new signature ed25519PublicKey to this transaction.
* @param signer {@link KeyPair} object representing a signer
*/
public void sign(KeyPair signer) {
checkNotNull(signer, "signer cannot be null");
byte[] txHash = this.hash();
mSignatures.add(signer.signDecorated(txHash));
}
/**
* Adds a new sha256Hash signature to this transaction by revealing preimage.
* @param preimage the sha256 hash of preimage should be equal to signer hash
*/
public void sign(byte[] preimage) {
checkNotNull(preimage, "preimage cannot be null");
org.stellar.sdk.xdr.Signature signature = new org.stellar.sdk.xdr.Signature();
signature.setSignature(preimage);
byte[] hash = Util.hash(preimage);
byte[] signatureHintBytes = Arrays.copyOfRange(hash, hash.length - 4, hash.length);
SignatureHint signatureHint = new SignatureHint();
signatureHint.setSignatureHint(signatureHintBytes);
DecoratedSignature decoratedSignature = new DecoratedSignature();
decoratedSignature.setHint(signatureHint);
decoratedSignature.setSignature(signature);
mSignatures.add(decoratedSignature);
}
/**
* Returns transaction hash.
*/
public byte[] hash() {
return Util.hash(this.signatureBase());
}
/**
* Returns signature base.
*/
public byte[] signatureBase() {
if (Network.current() == null) {
throw new NoNetworkSelectedException();
}
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// Hashed NetworkID
outputStream.write(Network.current().getNetworkId());
// Envelope Type - 4 bytes
outputStream.write(ByteBuffer.allocate(4).putInt(EnvelopeType.ENVELOPE_TYPE_TX.getValue()).array());
// Transaction XDR bytes
ByteArrayOutputStream txOutputStream = new ByteArrayOutputStream();
XdrDataOutputStream xdrOutputStream = new XdrDataOutputStream(txOutputStream);
org.stellar.sdk.xdr.Transaction.encode(xdrOutputStream, this.toXdr());
outputStream.write(txOutputStream.toByteArray());
return outputStream.toByteArray();
} catch (IOException exception) {
return null;
}
}
public KeyPair getSourceAccount() {
return mSourceAccount;
}
public long getSequenceNumber() {
return mSequenceNumber;
}
public List<DecoratedSignature> getSignatures() {
return mSignatures;
}
public Memo getMemo() {
return mMemo;
}
/**
* @return TimeBounds, or null (representing no time restrictions)
*/
public TimeBounds getTimeBounds() {
return mTimeBounds;
}
/**
* Returns fee paid for transaction in stroops (1 stroop = 0.0000001 XLM).
*/
public int getFee() {
return mFee;
}
/**
* Returns operations in this transaction.
*/
public Operation[] getOperations() {
return mOperations;
}
/**
* Generates Transaction XDR object.
*/
public org.stellar.sdk.xdr.Transaction toXdr() {
// fee
org.stellar.sdk.xdr.Uint32 fee = new org.stellar.sdk.xdr.Uint32();
fee.setUint32(mFee);
// sequenceNumber
org.stellar.sdk.xdr.Int64 sequenceNumberUint = new org.stellar.sdk.xdr.Int64();
sequenceNumberUint.setInt64(mSequenceNumber);
org.stellar.sdk.xdr.SequenceNumber sequenceNumber = new org.stellar.sdk.xdr.SequenceNumber();
sequenceNumber.setSequenceNumber(sequenceNumberUint);
// sourceAccount
org.stellar.sdk.xdr.AccountID sourceAccount = new org.stellar.sdk.xdr.AccountID();
sourceAccount.setAccountID(mSourceAccount.getXdrPublicKey());
// operations
org.stellar.sdk.xdr.Operation[] operations = new org.stellar.sdk.xdr.Operation[mOperations.length];
for (int i = 0; i < mOperations.length; i++) {
operations[i] = mOperations[i].toXdr();
}
// ext
org.stellar.sdk.xdr.Transaction.TransactionExt ext = new org.stellar.sdk.xdr.Transaction.TransactionExt();
ext.setDiscriminant(0);
org.stellar.sdk.xdr.Transaction transaction = new org.stellar.sdk.xdr.Transaction();
transaction.setFee(fee);
transaction.setSeqNum(sequenceNumber);
transaction.setSourceAccount(sourceAccount);
transaction.setOperations(operations);
transaction.setMemo(mMemo.toXdr());
transaction.setTimeBounds(mTimeBounds == null ? null : mTimeBounds.toXdr());
transaction.setExt(ext);
return transaction;
}
/**
* Creates a <code>Transaction</code> instance from previously build <code>TransactionEnvelope</code>
* @param envelope Base-64 encoded <code>TransactionEnvelope</code>
* @return
* @throws IOException
*/
public static Transaction fromEnvelopeXdr(String envelope) throws IOException {
BaseEncoding base64Encoding = BaseEncoding.base64();
byte[] bytes = base64Encoding.decode(envelope);
TransactionEnvelope transactionEnvelope = TransactionEnvelope.decode(new XdrDataInputStream(new ByteArrayInputStream(bytes)));
return fromEnvelopeXdr(transactionEnvelope);
}
/**
* Creates a <code>Transaction</code> instance from previously build <code>TransactionEnvelope</code>
* @param envelope
* @return
*/
public static Transaction fromEnvelopeXdr(TransactionEnvelope envelope) {
org.stellar.sdk.xdr.Transaction tx = envelope.getTx();
int mFee = tx.getFee().getUint32();
KeyPair mSourceAccount = KeyPair.fromXdrPublicKey(tx.getSourceAccount().getAccountID());
Long mSequenceNumber = tx.getSeqNum().getSequenceNumber().getInt64();
Memo mMemo = Memo.fromXdr(tx.getMemo());
TimeBounds mTimeBounds = TimeBounds.fromXdr(tx.getTimeBounds());
Operation[] mOperations = new Operation[tx.getOperations().length];
for (int i = 0; i < tx.getOperations().length; i++) {
mOperations[i] = Operation.fromXdr(tx.getOperations()[i]);
}
Transaction transaction = new Transaction(mSourceAccount, mFee, mSequenceNumber, mOperations, mMemo, mTimeBounds);
for (DecoratedSignature signature : envelope.getSignatures()) {
transaction.mSignatures.add(signature);
}
return transaction;
}
/**
* Generates TransactionEnvelope XDR object. Transaction need to have at least one signature.
*/
public org.stellar.sdk.xdr.TransactionEnvelope toEnvelopeXdr() {
if (mSignatures.size() == 0) {
throw new NotEnoughSignaturesException("Transaction must be signed by at least one signer. Use transaction.sign().");
}
org.stellar.sdk.xdr.TransactionEnvelope xdr = new org.stellar.sdk.xdr.TransactionEnvelope();
org.stellar.sdk.xdr.Transaction transaction = this.toXdr();
xdr.setTx(transaction);
DecoratedSignature[] signatures = new DecoratedSignature[mSignatures.size()];
signatures = mSignatures.toArray(signatures);
xdr.setSignatures(signatures);
return xdr;
}
/**
* Returns base64-encoded TransactionEnvelope XDR object. Transaction need to have at least one signature.
*/
public String toEnvelopeXdrBase64() {
try {
org.stellar.sdk.xdr.TransactionEnvelope envelope = this.toEnvelopeXdr();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
XdrDataOutputStream xdrOutputStream = new XdrDataOutputStream(outputStream);
org.stellar.sdk.xdr.TransactionEnvelope.encode(xdrOutputStream, envelope);
BaseEncoding base64Encoding = BaseEncoding.base64();
return base64Encoding.encode(outputStream.toByteArray());
} catch (IOException e) {
throw new AssertionError(e);
}
}
/**
* Builds a new Transaction object.
*/
public static class Builder {
private final TransactionBuilderAccount mSourceAccount;
private Memo mMemo;
private TimeBounds mTimeBounds;
List<Operation> mOperations;
private boolean timeoutSet;
public static final long TIMEOUT_INFINITE = 0;
/**
* Construct a new transaction builder.
* @param sourceAccount The source account for this transaction. This account is the account
* who will use a sequence number. When build() is called, the account object's sequence number
* will be incremented.
*/
public Builder(TransactionBuilderAccount sourceAccount) {
checkNotNull(sourceAccount, "sourceAccount cannot be null");
mSourceAccount = sourceAccount;
mOperations = Collections.synchronizedList(new ArrayList<Operation>());
}
public int getOperationsCount() {
return mOperations.size();
}
/**
* Adds a new <a href="https://www.stellar.org/developers/learn/concepts/list-of-operations.html" target="_blank">operation</a> to this transaction.
* @param operation
* @return Builder object so you can chain methods.
* @see Operation
*/
public Builder addOperation(Operation operation) {
checkNotNull(operation, "operation cannot be null");
mOperations.add(operation);
return this;
}
/**
* Adds a <a href="https://www.stellar.org/developers/learn/concepts/transactions.html" target="_blank">memo</a> to this transaction.
* @param memo
* @return Builder object so you can chain methods.
* @see Memo
*/
public Builder addMemo(Memo memo) {
if (mMemo != null) {
throw new RuntimeException("Memo has been already added.");
}
checkNotNull(memo, "memo cannot be null");
mMemo = memo;
return this;
}
/**
* Adds a <a href="https://www.stellar.org/developers/learn/concepts/transactions.html" target="_blank">time-bounds</a> to this transaction.
* @param timeBounds
* @return Builder object so you can chain methods.
* @see TimeBounds
*/
public Builder addTimeBounds(TimeBounds timeBounds) {
if (mTimeBounds != null) {
throw new RuntimeException("TimeBounds has been already added.");
}
checkNotNull(timeBounds, "timeBounds cannot be null");
mTimeBounds = timeBounds;
return this;
}
/**
* Because of the distributed nature of the Stellar network it is possible that the status of your transaction
* will be determined after a long time if the network is highly congested.
* If you want to be sure to receive the status of the transaction within a given period you should set the
* {@link TimeBounds} with <code>maxTime</code> on the transaction (this is what <code>setTimeout</code> does
* internally; if there's <code>minTime</code> set but no <code>maxTime</code> it will be added).
* Call to <code>Builder.setTimeout</code> is required if Transaction does not have <code>max_time</code> set.
* If you don't want to set timeout, use <code>TIMEOUT_INFINITE</code>. In general you should set
* <code>TIMEOUT_INFINITE</code> only in smart contracts.
* Please note that Horizon may still return <code>504 Gateway Timeout</code> error, even for short timeouts.
* In such case you need to resubmit the same transaction again without making any changes to receive a status.
* This method is using the machine system time (UTC), make sure it is set correctly.
* @param timeout Timeout in seconds.
* @see TimeBounds
* @return
*/
public Builder setTimeout(long timeout) {
if (mTimeBounds != null && mTimeBounds.getMaxTime() > 0) {
throw new RuntimeException("TimeBounds.max_time has been already set - setting timeout would overwrite it.");
}
if (timeout < 0) {
throw new RuntimeException("timeout cannot be negative");
}
timeoutSet = true;
if (timeout > 0) {
long timeoutTimestamp = System.currentTimeMillis() / 1000L + timeout;
if (mTimeBounds == null) {
mTimeBounds = new TimeBounds(0, timeoutTimestamp);
} else {
mTimeBounds = new TimeBounds(mTimeBounds.getMinTime(), timeoutTimestamp);
}
}
return this;
}
/**
* Builds a transaction. It will increment sequence number of the source account.
*/
public Transaction build() {
// Ensure setTimeout called or maxTime is set
if ((mTimeBounds == null || mTimeBounds != null && mTimeBounds.getMaxTime() == 0) && !timeoutSet) {
throw new RuntimeException("TimeBounds has to be set or you must call setTimeout(TIMEOUT_INFINITE).");
}
Operation[] operations = new Operation[mOperations.size()];
operations = mOperations.toArray(operations);
Transaction transaction = new Transaction(mSourceAccount.getKeypair(), operations.length * BASE_FEE, mSourceAccount.getIncrementedSequenceNumber(), operations, mMemo, mTimeBounds);
// Increment sequence number when there were no exceptions when creating a transaction
mSourceAccount.incrementSequenceNumber();
return transaction;
}
}
}