-
Notifications
You must be signed in to change notification settings - Fork 159
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement FeeBumpTransaction builder, update SEP10 and SEP29 (#278)
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
Showing
11 changed files
with
1,336 additions
and
605 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
Oops, something went wrong.