Updated on 2026-08-14
This commit is contained in:
parent
07bc724ecd
commit
75f19d41f4
278 changed files with 19571 additions and 206 deletions
381
app/src/main/java/org/stellar/sdk/Transaction.java
Normal file
381
app/src/main/java/org/stellar/sdk/Transaction.java
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue