Skip to content

Commit

Permalink
feat: FeeBumpTransaction supports transactions that include Soroban…
Browse files Browse the repository at this point in the history
… operations. (#617)
  • Loading branch information
overcat authored Jul 17, 2024
1 parent 734dd74 commit 6a1b8ab
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 31 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ As this project is pre 1.0, breaking changes may happen for minor version bumps.
- refactor!: `Transaction.Builder` has been removed, use `TransactionBuilder` instead.
- refactor!: refactor asset classes. `LiquidityPoolParameters`, `LiquidityPoolConstantProductParameters`, `AssetTypePoolShare`, `LiquidityPoolShareChangeTrustAsset` and `LiquidityPoolShareTrustLineAsset` have been removed. Use `ChangeTrustAsset` and `TrustLineAsset` instead.
- refactor!: `Asset.getType()` returns `org.stellar.sdk.xdr.AssetType` instead of `String`.
- refactor!: `FeeBumpTransaction.Builder` has been removed, use `FeeBumpTransaction#FeeBumpTransaction(String, long, Transaction)` instead.
- refactor!: `FeeBumpTransaction.Builder` has been removed, use `FeeBumpTransaction#createWithBaseFee(String, long, Transaction)` or `FeeBumpTransaction#createWithFee(String, long, Transaction)` instead.
- refactor!: `FeeBumpTransaction.getFeeAccount` has been removed, use `FeeBumpTransaction.getFeeSource` instead.
- refactor!: remove `AccountConverter`, this means that we no longer support disabling support for MuxedAccount.
- refactor!: refactor the way of constructing `Predicate.Or` and `Predicate.And`. The `inner` inside has been removed, and in its place are `left` and `right`, used to represent two predicates.
Expand All @@ -48,6 +48,7 @@ As this project is pre 1.0, breaking changes may happen for minor version bumps.
- feat: add `MuxedAccount` class to represent a multiplexed account on Stellar's network.
- feat: Add `Server.loadAccount` to load the `Account` object used for building transactions, supporting `MuxedAccount`.
- feat: Add support for `MuxedAccount` to `SorobanServer.getAccount`.
- feat: `FeeBumpTransaction` supports transactions that include Soroban operations.

## 0.44.0
### Update
Expand Down
57 changes: 40 additions & 17 deletions src/main/java/org/stellar/sdk/FeeBumpTransaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,29 @@ public class FeeBumpTransaction extends AbstractTransaction {
/** The inner transaction that is being wrapped by this fee bump transaction. */
@NonNull private final Transaction innerTransaction;

private FeeBumpTransaction(
@NonNull String feeSource, long fee, @NonNull Transaction innerTransaction) {
super(innerTransaction.getNetwork());
this.feeSource = feeSource;
this.fee = fee;
this.innerTransaction = innerTransaction;
}

/**
* Creates a new FeeBumpTransaction object, enabling you to resubmit an existing transaction with
* a higher fee.
*
* @param feeSource The account paying for the transaction fee.
* @param fee Max fee willing to pay for this transaction (in stroops)
* @param innerTransaction The inner transaction that is being wrapped by this fee bump
* transaction.
* @return {@link FeeBumpTransaction}
*/
public static FeeBumpTransaction createWithFee(
@NonNull String feeSource, long fee, @NonNull Transaction innerTransaction) {
return new FeeBumpTransaction(feeSource, fee, innerTransaction);
}

/**
* Creates a new FeeBumpTransaction object, enabling you to resubmit an existing transaction with
* a higher fee.
Expand All @@ -37,39 +60,42 @@ public class FeeBumpTransaction extends AbstractTransaction {
* @param baseFee Max fee willing to pay per operation in inner transaction (in stroops)
* @param innerTransaction The inner transaction that is being wrapped by this fee bump
* transaction.
* @return {@link FeeBumpTransaction}
*/
public FeeBumpTransaction(
public static FeeBumpTransaction createWithBaseFee(
@NonNull String feeSource, long baseFee, @NonNull Transaction innerTransaction) {
super(innerTransaction.getNetwork());
this.feeSource = feeSource;

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

long innerBaseFee = innerTransaction.getFee();
long innerSorobanResourceFee = 0;
if (innerTransaction.getSorobanData() != null) {
innerSorobanResourceFee = innerTransaction.getSorobanData().getResourceFee().getInt64();
}

long innerBaseFee =
innerTransaction.getFee() - innerSorobanResourceFee; // dont include soroban resource fee
long numOperations = innerTransaction.getOperations().length;
if (numOperations > 0) {
innerBaseFee = innerBaseFee / numOperations;
innerBaseFee = (long) Math.ceil((double) innerBaseFee / numOperations);
}

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

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

// set inner transaction
Transaction tx;
EnvelopeType txType = innerTransaction.toEnvelopeXdr().getDiscriminant();
if (txType == EnvelopeType.ENVELOPE_TYPE_TX_V0) {
this.innerTransaction =
tx =
new TransactionBuilder(
new Account(
innerTransaction.getSourceAccount(),
Expand All @@ -83,24 +109,21 @@ public FeeBumpTransaction(
.timeBounds(innerTransaction.getTimeBounds())
.build())
.build();
this.innerTransaction.signatures = new ArrayList<>(innerTransaction.signatures);
tx.signatures = new ArrayList<>(innerTransaction.signatures);
} else {
this.innerTransaction = innerTransaction;
tx = innerTransaction;
}
return new FeeBumpTransaction(feeSource, maxFee, tx);
}

public static FeeBumpTransaction fromFeeBumpTransactionEnvelope(
FeeBumpTransactionEnvelope envelope, Network network) {
Transaction inner =
Transaction.fromV1EnvelopeXdr(envelope.getTx().getInnerTx().getV1(), network);
String feeSource = StrKey.encodeMuxedAccount(envelope.getTx().getFeeSource());

long fee = envelope.getTx().getFee().getInt64();
long baseFee = fee / (inner.getOperations().length + 1);

FeeBumpTransaction feeBump = new FeeBumpTransaction(feeSource, baseFee, inner);
FeeBumpTransaction feeBump = new FeeBumpTransaction(feeSource, fee, inner);
feeBump.signatures.addAll(Arrays.asList(envelope.getSignatures()));

return feeBump;
}

Expand Down
1 change: 1 addition & 0 deletions src/main/java/org/stellar/sdk/SorobanServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ private Transaction assembleTransaction(
"unsupported transaction: must contain exactly one InvokeHostFunctionOperation, BumpSequenceOperation, or RestoreFootprintOperation");
}

// TODO: exclude exists soroban resource fee from tx fee
long classicFeeNum = transaction.getFee();
long minResourceFeeNum =
Optional.ofNullable(simulateTransactionResponse.getMinResourceFee()).orElse(0L);
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/stellar/sdk/Transaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
* target="_blank">Transaction</a> in Stellar network.
*/
public class Transaction extends AbstractTransaction {
/** fee paid for transaction in stroops (1 stroop = 0.0000001 XLM). */
/** Max fee paid for transaction in stroops (1 stroop = 0.0000001 XLM). */
@Getter private final long fee;

/** The source account for this transaction. */
Expand Down
150 changes: 140 additions & 10 deletions src/test/java/org/stellar/sdk/FeeBumpTransactionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,30 @@
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.fail;

import java.math.BigInteger;
import java.util.ArrayList;
import org.junit.Test;
import org.stellar.sdk.operations.InvokeHostFunctionOperation;
import org.stellar.sdk.operations.PaymentOperation;
import org.stellar.sdk.xdr.ContractExecutable;
import org.stellar.sdk.xdr.ContractExecutableType;
import org.stellar.sdk.xdr.ContractIDPreimage;
import org.stellar.sdk.xdr.ContractIDPreimageType;
import org.stellar.sdk.xdr.CreateContractArgs;
import org.stellar.sdk.xdr.EnvelopeType;
import org.stellar.sdk.xdr.ExtensionPoint;
import org.stellar.sdk.xdr.HostFunction;
import org.stellar.sdk.xdr.HostFunctionType;
import org.stellar.sdk.xdr.Int64;
import org.stellar.sdk.xdr.LedgerEntryType;
import org.stellar.sdk.xdr.LedgerFootprint;
import org.stellar.sdk.xdr.LedgerKey;
import org.stellar.sdk.xdr.SignerKey;
import org.stellar.sdk.xdr.SorobanResources;
import org.stellar.sdk.xdr.SorobanTransactionData;
import org.stellar.sdk.xdr.Uint256;
import org.stellar.sdk.xdr.Uint32;
import org.stellar.sdk.xdr.XdrUnsignedInteger;

public class FeeBumpTransactionTest {

Expand Down Expand Up @@ -49,7 +70,7 @@ public void testSetBaseFeeBelowNetworkMinimum() {
Transaction inner = createInnerTransaction();

try {
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
Transaction.MIN_BASE_FEE - 1,
inner);
Expand All @@ -64,7 +85,7 @@ public void testSetBaseFeeBelowInner() {
Transaction inner = createInnerTransaction(Transaction.MIN_BASE_FEE + 1);

try {
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
Transaction.MIN_BASE_FEE,
inner);
Expand All @@ -80,7 +101,7 @@ public void testSetBaseFeeOverflowsLong() {
Transaction inner = createInnerTransaction(Transaction.MIN_BASE_FEE + 1);

try {
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3", Long.MAX_VALUE, inner);
fail();
} catch (RuntimeException e) {
Expand All @@ -93,7 +114,7 @@ public void testSetBaseFeeEqualToInner() {
Transaction inner = createInnerTransaction();

FeeBumpTransaction feeBump =
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
Transaction.MIN_BASE_FEE,
inner);
Expand All @@ -108,7 +129,7 @@ public void testHash() {
"2a8ead3351faa7797b284f59027355ddd69c21adb8e4da0b9bb95531f7f32681", inner.hashHex());

FeeBumpTransaction feeBump =
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
Transaction.MIN_BASE_FEE * 2,
inner);
Expand All @@ -121,7 +142,7 @@ public void testRoundTripXdr() {
Transaction inner = createInnerTransaction();

FeeBumpTransaction feeBump =
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
Transaction.MIN_BASE_FEE * 2,
inner);
Expand Down Expand Up @@ -156,7 +177,7 @@ public void testFeeBumpUpgradesInnerToV1() {
innerV0.setEnvelopeType(EnvelopeType.ENVELOPE_TYPE_TX_V0);

FeeBumpTransaction feeBump =
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
Transaction.MIN_BASE_FEE * 2,
innerV0);
Expand Down Expand Up @@ -192,14 +213,14 @@ public void testFeeBumpUpgradesInnerToV1() {
public void testHashCodeAndEquals() {
Transaction inner = createInnerTransaction();
FeeBumpTransaction feeBump0 =
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
Transaction.MIN_BASE_FEE * 2,
inner);

// they get different base fee
FeeBumpTransaction feeBump2 =
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
Transaction.MIN_BASE_FEE * 3,
createInnerTransaction(Network.PUBLIC));
Expand All @@ -208,11 +229,120 @@ public void testHashCodeAndEquals() {

// they get different network
FeeBumpTransaction feeBump3 =
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
Transaction.MIN_BASE_FEE * 2,
createInnerTransaction(Network.PUBLIC));

assertNotEquals(feeBump0, feeBump3);
}

@Test
public void testCreateWithBaseFee() {
Transaction inner = createInnerTransaction(300);
FeeBumpTransaction feeBumpTx =
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3", 400, inner);

assertEquals(
feeBumpTx.getFeeSource(), "GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3");
assertEquals(feeBumpTx.getFee(), 800);
assertEquals(feeBumpTx.getInnerTransaction(), inner);
}

@Test
public void testCreateWithFee() {
Transaction inner = createInnerTransaction(300);
FeeBumpTransaction feeBumpTx =
FeeBumpTransaction.createWithFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3", 1100, inner);

assertEquals(
feeBumpTx.getFeeSource(), "GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3");
assertEquals(feeBumpTx.getFee(), 1100);
assertEquals(feeBumpTx.getInnerTransaction(), inner);
}

@Test
public void testCreateWithBaseFeeWithSorobanOp() {
long sorobanResourceFee = 346546L;
KeyPair source =
KeyPair.fromSecretSeed("SCH27VUZZ6UAKB67BDNF6FA42YMBMQCBKXWGMFD5TZ6S5ZZCZFLRXKHS");

Account account = new Account(source.getAccountId(), 2908908335136768L);
LedgerKey ledgerKey =
LedgerKey.builder()
.discriminant(LedgerEntryType.ACCOUNT)
.account(
LedgerKey.LedgerKeyAccount.builder()
.accountID(
KeyPair.fromAccountId(
"GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO")
.getXdrAccountId())
.build())
.build();
SorobanTransactionData sorobanData =
SorobanTransactionData.builder()
.resources(
SorobanResources.builder()
.footprint(
LedgerFootprint.builder()
.readOnly(new LedgerKey[] {ledgerKey})
.readWrite(new LedgerKey[] {})
.build())
.readBytes(new Uint32(new XdrUnsignedInteger(699)))
.writeBytes(new Uint32(new XdrUnsignedInteger(0)))
.instructions(new Uint32(new XdrUnsignedInteger(34567)))
.build())
.resourceFee(new Int64(sorobanResourceFee))
.ext(ExtensionPoint.builder().discriminant(0).build())
.build();

CreateContractArgs createContractArgs =
CreateContractArgs.builder()
.contractIDPreimage(
ContractIDPreimage.builder()
.discriminant(ContractIDPreimageType.CONTRACT_ID_PREIMAGE_FROM_ADDRESS)
.fromAddress(
ContractIDPreimage.ContractIDPreimageFromAddress.builder()
.address(
new Address(
"GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO")
.toSCAddress())
.salt(new Uint256(new byte[32]))
.build())
.build())
.executable(
ContractExecutable.builder()
.discriminant(ContractExecutableType.CONTRACT_EXECUTABLE_STELLAR_ASSET)
.build())
.build();
HostFunction hostFunction =
HostFunction.builder()
.discriminant(HostFunctionType.HOST_FUNCTION_TYPE_CREATE_CONTRACT)
.createContract(createContractArgs)
.build();
InvokeHostFunctionOperation invokeHostFunctionOperation =
InvokeHostFunctionOperation.builder().hostFunction(hostFunction).build();
Transaction transaction =
new Transaction(
account.getAccountId(),
Transaction.MIN_BASE_FEE,
account.getIncrementedSequenceNumber(),
new org.stellar.sdk.operations.Operation[] {invokeHostFunctionOperation},
null,
new TransactionPreconditions(
null, null, BigInteger.ZERO, 0, new ArrayList<SignerKey>(), null),
sorobanData,
Network.TESTNET);

FeeBumpTransaction feeBumpTransaction =
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3", 200, transaction);
assertEquals(
feeBumpTransaction.getFeeSource(),
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3");
assertEquals(feeBumpTransaction.getFee(), 200 * (1 + 1) + sorobanResourceFee);
assertEquals(feeBumpTransaction.getInnerTransaction(), transaction);
}
}
2 changes: 1 addition & 1 deletion src/test/java/org/stellar/sdk/Sep10ChallengeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1523,7 +1523,7 @@ public void testReadChallengeTransactionRejectFeeBumpTransaction() throws IOExce

transaction.sign(server);
FeeBumpTransaction feeBumpTransaction =
new FeeBumpTransaction(server.getAccountId(), 500, transaction);
FeeBumpTransaction.createWithBaseFee(server.getAccountId(), 500, transaction);
String challenge = feeBumpTransaction.toEnvelopeXdrBase64();
try {
Sep10Challenge.readChallengeTransaction(
Expand Down
3 changes: 2 additions & 1 deletion src/test/java/org/stellar/sdk/ServerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,8 @@ private FeeBumpTransaction feeBump(Transaction inner) {
KeyPair signer =
KeyPair.fromSecretSeed("SA5ZEFDVFZ52GRU7YUGR6EDPBNRU2WLA6IQFQ7S2IH2DG3VFV3DOMV2Q");
FeeBumpTransaction tx =
new FeeBumpTransaction(signer.getAccountId(), FeeBumpTransaction.MIN_BASE_FEE * 10, inner);
FeeBumpTransaction.createWithBaseFee(
signer.getAccountId(), FeeBumpTransaction.MIN_BASE_FEE * 10, inner);
tx.sign(signer);
return tx;
}
Expand Down

0 comments on commit 6a1b8ab

Please sign in to comment.