Skip to content

Commit

Permalink
Implement FeeBumpTransaction builder, update SEP10 and SEP29 (#278)
Browse files Browse the repository at this point in the history
Implement FeeBumpTransaction builder
Update SEP 10 implementation to reject fee bump transactions and muxed accounts
Update SEP 29 implementation to support fee bump transactions
  • Loading branch information
tamirms authored May 4, 2020
1 parent 549ae54 commit 610b567
Show file tree
Hide file tree
Showing 11 changed files with 1,336 additions and 605 deletions.
134 changes: 134 additions & 0 deletions src/main/java/org/stellar/sdk/AbstractTransaction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
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.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static com.google.common.base.Preconditions.checkNotNull;

public abstract class AbstractTransaction {
protected final Network mNetwork;
protected List<DecoratedSignature> mSignatures;
public static final int MIN_BASE_FEE = 100;


AbstractTransaction(Network network) {
this.mNetwork = checkNotNull(network, "network cannot be null");
this.mSignatures = new ArrayList<DecoratedSignature>();
}

/**
* 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 transaction hash encoded as a hexadecimal string.
*/
public String hashHex() {
return BaseEncoding.base16().lowerCase().encode(this.hash());
}

/**
* Returns signature base.
*/
public abstract byte[] signatureBase();

public Network getNetwork() {
return mNetwork;
}

public List<DecoratedSignature> getSignatures() {
return mSignatures;
}

public abstract TransactionEnvelope toEnvelopeXdr();

/**
* Returns base64-encoded TransactionEnvelope XDR object. Transaction need to have at least one signature.
*/
public String toEnvelopeXdrBase64() {
try {
TransactionEnvelope envelope = this.toEnvelopeXdr();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
XdrDataOutputStream xdrOutputStream = new XdrDataOutputStream(outputStream);
TransactionEnvelope.encode(xdrOutputStream, envelope);

BaseEncoding base64Encoding = BaseEncoding.base64();
return base64Encoding.encode(outputStream.toByteArray());
} catch (IOException e) {
throw new AssertionError(e);
}
}

/**
* Creates a <code>AbstractTransaction</code> instance from previously build <code>TransactionEnvelope</code>
* @param envelope
* @return
*/
public static AbstractTransaction fromEnvelopeXdr(TransactionEnvelope envelope, Network network) {
switch (envelope.getDiscriminant()) {
case ENVELOPE_TYPE_TX:
return Transaction.fromV1EnvelopeXdr(envelope.getV1(), network);
case ENVELOPE_TYPE_TX_V0:
return Transaction.fromV0EnvelopeXdr(envelope.getV0(), network);
case ENVELOPE_TYPE_TX_FEE_BUMP:
return FeeBumpTransaction.fromFeeBumpTransactionEnvelope(envelope.getFeeBump(), network);
default:
throw new IllegalArgumentException("transaction type is not supported: "+envelope.getDiscriminant());
}
}

/**
* 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 AbstractTransaction fromEnvelopeXdr(String envelope, Network network) throws IOException {
BaseEncoding base64Encoding = BaseEncoding.base64();
byte[] bytes = base64Encoding.decode(envelope);

TransactionEnvelope transactionEnvelope = TransactionEnvelope.decode(new XdrDataInputStream(new ByteArrayInputStream(bytes)));
return fromEnvelopeXdr(transactionEnvelope, network);
}
}
204 changes: 204 additions & 0 deletions src/main/java/org/stellar/sdk/FeeBumpTransaction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package org.stellar.sdk;

import com.google.common.base.Objects;
import org.stellar.sdk.xdr.*;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;

import static com.google.common.base.Preconditions.checkNotNull;

/**
* Represents <a href="https://github.com/stellar/stellar-protocol/blob/master/core/cap-0015.md" target="_blank">Fee Bump Transaction</a> in Stellar network.
*/
public class FeeBumpTransaction extends AbstractTransaction {
private final long mFee;
private final String mFeeAccount;
private final Transaction mInner;

FeeBumpTransaction(String feeAccount, long fee, Transaction innerTransaction) {
super(innerTransaction.getNetwork());
this.mFeeAccount = checkNotNull(feeAccount, "feeAccount cannot be null");
this.mInner = checkNotNull(innerTransaction, "innerTransaction cannot be null");
this.mFee = fee;
}

public long getFee() {
return mFee;
}

public String getFeeAccount() {
return mFeeAccount;
}


public Transaction getInnerTransaction() {
return mInner;
}

public static FeeBumpTransaction fromFeeBumpTransactionEnvelope(FeeBumpTransactionEnvelope envelope, Network network) {
Transaction inner = Transaction.fromV1EnvelopeXdr(envelope.getTx().getInnerTx().getV1(), network);
String feeAccount = StrKey.encodeStellarMuxedAccount(envelope.getTx().getFeeSource());
long fee = envelope.getTx().getFee().getInt64();

FeeBumpTransaction feeBump = new FeeBumpTransaction(feeAccount, fee, inner);
for (DecoratedSignature signature : envelope.getSignatures()) {
feeBump.mSignatures.add(signature);
}

return feeBump;
}

private org.stellar.sdk.xdr.FeeBumpTransaction toXdr() {
org.stellar.sdk.xdr.FeeBumpTransaction xdr = new org.stellar.sdk.xdr.FeeBumpTransaction();
xdr.setExt(new org.stellar.sdk.xdr.FeeBumpTransaction.FeeBumpTransactionExt());
xdr.getExt().setDiscriminant(0);

Int64 xdrFee = new Int64();
xdrFee.setInt64(mFee);
xdr.setFee(xdrFee);

xdr.setFeeSource(StrKey.encodeToXDRMuxedAccount(this.mFeeAccount));

org.stellar.sdk.xdr.FeeBumpTransaction.FeeBumpTransactionInnerTx innerXDR = new org.stellar.sdk.xdr.FeeBumpTransaction.FeeBumpTransactionInnerTx();
innerXDR.setDiscriminant(EnvelopeType.ENVELOPE_TYPE_TX);
innerXDR.setV1(this.mInner.toEnvelopeXdr().getV1());
xdr.setInnerTx(innerXDR);
return xdr;
}

@Override
public byte[] signatureBase() {
try {
TransactionSignaturePayload payload = new TransactionSignaturePayload();
TransactionSignaturePayload.TransactionSignaturePayloadTaggedTransaction taggedTransaction = new TransactionSignaturePayload.TransactionSignaturePayloadTaggedTransaction();
taggedTransaction.setDiscriminant(EnvelopeType.ENVELOPE_TYPE_TX_FEE_BUMP);
taggedTransaction.setFeeBump(this.toXdr());
Hash hash = new Hash();
hash.setHash(mNetwork.getNetworkId());
payload.setNetworkId(hash);
payload.setTaggedTransaction(taggedTransaction);
ByteArrayOutputStream txOutputStream = new ByteArrayOutputStream();
XdrDataOutputStream xdrOutputStream = new XdrDataOutputStream(txOutputStream);
payload.encode(xdrOutputStream);
return txOutputStream.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

/**
* Generates TransactionEnvelope XDR object.
*/
@Override
public TransactionEnvelope toEnvelopeXdr() {
TransactionEnvelope xdr = new TransactionEnvelope();
FeeBumpTransactionEnvelope feeBumpEnvelope = new FeeBumpTransactionEnvelope();
xdr.setDiscriminant(EnvelopeType.ENVELOPE_TYPE_TX_FEE_BUMP);

feeBumpEnvelope.setTx(this.toXdr());

DecoratedSignature[] signatures = new DecoratedSignature[mSignatures.size()];
signatures = mSignatures.toArray(signatures);
feeBumpEnvelope.setSignatures(signatures);

xdr.setFeeBump(feeBumpEnvelope);
return xdr;
}

/**
* Builds a new FeeBumpTransaction object.
*/
public static class Builder {
private final Transaction mInner;
private Long mBaseFee;
private String mFeeAccount;


/**
* Construct a new fee bump transaction builder.
*
* @param inner The inner transaction which will be fee bumped.
*/
public Builder(Transaction inner) {
this.mInner = checkNotNull(inner, "inner cannot be null");
EnvelopeType txType = this.mInner.toEnvelopeXdr().getDiscriminant();
if (this.mInner.toEnvelopeXdr().getDiscriminant() != EnvelopeType.ENVELOPE_TYPE_TX) {
throw new IllegalArgumentException("invalid transaction type: " + txType);
}
}

public FeeBumpTransaction.Builder setBaseFee(long baseFee) {
if (this.mBaseFee != null) {
throw new RuntimeException("base fee has been already set.");
}

if (baseFee < MIN_BASE_FEE) {
throw new IllegalArgumentException("baseFee cannot be smaller than the BASE_FEE (" + MIN_BASE_FEE + "): " + baseFee);
}

long innerBaseFee = this.mInner.getFee();
long numOperations = this.mInner.getOperations().length;
if (numOperations > 0) {
innerBaseFee = innerBaseFee / numOperations;
}

if (baseFee < innerBaseFee) {
throw new IllegalArgumentException("base fee cannot be lower than provided inner transaction base fee");
}

long maxFee = baseFee * (numOperations + 1);
if (maxFee < 0) {
throw new IllegalArgumentException("fee overflows 64 bit int");
}

this.mBaseFee = maxFee;
return this;
}

public FeeBumpTransaction.Builder setFeeAccount(String feeAccount) {
if (this.mFeeAccount != null) {
throw new RuntimeException("fee account has been already been set.");
}

this.mFeeAccount = checkNotNull(feeAccount, "feeAccount cannot be null");
return this;
}

public FeeBumpTransaction build() {
return new FeeBumpTransaction(
checkNotNull(this.mFeeAccount, "fee account has to be set. you must call setFeeAccount()."),
checkNotNull(this.mBaseFee, "base fee has to be set. you must call setBaseFee()."),
this.mInner
);
}

}

@Override
public int hashCode() {
return Objects.hashCode(
this.mFee,
this.mInner,
this.mNetwork,
this.mFeeAccount,
this.mSignatures
);
}

@Override
public boolean equals(Object object) {
if (object == null || !(object instanceof FeeBumpTransaction)) {
return false;
}

FeeBumpTransaction other = (FeeBumpTransaction) object;
return Objects.equal(this.mFee, other.mFee) &&
Objects.equal(this.mFeeAccount, other.mFeeAccount) &&
Objects.equal(this.mInner, other.mInner) &&
Objects.equal(this.mNetwork, other.mNetwork) &&
Objects.equal(this.mSignatures, other.mSignatures);
}

}
Loading

0 comments on commit 610b567

Please sign in to comment.